diff --git a/README.md b/README.md
index 7ef2886..7317cfd 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,14 @@ p_rayno_cas_auth:
```
Note : the xml_namespace and options parameters are optionals
+If you want any to access user attributes from the CAS response, inject the session into the user-provider in services.yml :
+```yaml
+services:
+ prayno.cas_user_provider:
+ class: PRayno\CasAuthBundle\Security\User\CasUserProvider
+ arguments: ['@session']
+```
+
Modify your security.xml with the following values (the provider in the following settings should not be used as it's just a very basic example) :
```yaml
security:
diff --git a/Security/CasAuthenticator.php b/Security/CasAuthenticator.php
index 7a6ff43..3f0d24f 100644
--- a/Security/CasAuthenticator.php
+++ b/Security/CasAuthenticator.php
@@ -11,6 +11,7 @@
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
+use PRayno\CasAuthBundle\Security\User\CasUserCredentialStoreInterface;
class CasAuthenticator extends AbstractGuardAuthenticator
{
@@ -21,12 +22,14 @@ class CasAuthenticator extends AbstractGuardAuthenticator
protected $query_ticket_parameter;
protected $query_service_parameter;
protected $options;
+ protected $client;
/**
* Process configuration
* @param array $config
+ * @param optional Client $client
*/
- public function __construct($config)
+ public function __construct($config, Client $client = null)
{
$this->server_login_url = $config['server_login_url'];
$this->server_validation_url = $config['server_validation_url'];
@@ -35,6 +38,11 @@ public function __construct($config)
$this->query_service_parameter = $config['query_service_parameter'];
$this->query_ticket_parameter = $config['query_ticket_parameter'];
$this->options = $config['options'];
+ if (is_null($client)) {
+ $this->client = new Client();
+ } else {
+ $this->client = $client;
+ }
}
/**
@@ -49,8 +57,7 @@ public function getCredentials(Request $request)
$request->get($this->query_ticket_parameter).'&'.
$this->query_service_parameter.'='.urlencode($this->removeCasTicket($request->getUri()));
- $client = new Client();
- $response = $client->request('GET', $url, $this->options);
+ $response = $this->client->request('GET', $url, $this->options);
$string = $response->getBody()->getContents();
@@ -73,6 +80,9 @@ public function getCredentials(Request $request)
public function getUser($credentials, UserProviderInterface $userProvider)
{
if (isset($credentials[$this->username_attribute])) {
+ if ($userProvider instanceof CasUserCredentialStoreInterface) {
+ $userProvider->storeUserCredentials($credentials);
+ }
return $userProvider->loadUserByUsername($credentials[$this->username_attribute]);
} else {
return null;
diff --git a/Security/User/CasUser.php b/Security/User/CasUser.php
index 2533f7f..2fbcdef 100644
--- a/Security/User/CasUser.php
+++ b/Security/User/CasUser.php
@@ -4,13 +4,15 @@
namespace PRayno\CasAuthBundle\Security\User;
use Symfony\Component\Security\Core\User\UserInterface;
+use PRayno\CasAuthBundle\Security\User\CasUserAttributesInterface;
-class CasUser implements UserInterface
+class CasUser implements UserInterface, CasUserAttributesInterface
{
private $username;
private $password;
private $salt;
private $roles;
+ private $attributes;
/**
* @param $username
@@ -18,12 +20,13 @@ class CasUser implements UserInterface
* @param $salt
* @param array $roles
*/
- public function __construct($username, $password, $salt, array $roles)
+ public function __construct($username, $password, $salt, array $roles, array $attributes = array())
{
$this->username = $username;
$this->password = $password;
$this->salt = $salt;
$this->roles = $roles;
+ $this->attributes = $attributes;
}
/**
@@ -65,4 +68,25 @@ public function eraseCredentials()
{
}
-}
\ No newline at end of file
+
+ /**
+ * @return array
+ */
+ public function getAttributes()
+ {
+ return $this->attributes;
+ }
+
+ /**
+ * @return mixed string, array, or null if not found.
+ */
+ public function getAttribute($name)
+ {
+ if (isset($this->attributes[$name])) {
+ return $this->attributes[$name];
+ } else {
+ return null;
+ }
+
+ }
+}
diff --git a/Security/User/CasUserAttributesInterface.php b/Security/User/CasUserAttributesInterface.php
new file mode 100644
index 0000000..e6a6ab0
--- /dev/null
+++ b/Security/User/CasUserAttributesInterface.php
@@ -0,0 +1,18 @@
+user_attribute_bag = new AttributeBag('PRayno_CasAuthBundle_UserAttributes');
+ $session->registerBag($this->user_attribute_bag);
+ }
+ }
+
+ /**
+ * @param array $credentials
+ */
+ public function storeUserCredentials(array $credentials) {
+ if (isset($this->user_attribute_bag)) {
+ if ($credentials['user']) {
+ $this->user_attribute_bag->set($credentials['user'], $this->getCasAttributes($credentials));
+ } else {
+ throw new \InvalidArgumentException('Credentials must contain a user property');
+ }
+ }
+ }
+
/**
* Provides the authenticated user a ROLE_USER
* @param $username
@@ -22,7 +51,14 @@ public function loadUserByUsername($username)
$salt = "";
$roles = ["ROLE_USER"];
- return new CasUser($username, $password, $salt, $roles);
+ if (isset($this->user_attribute_bag) && $this->user_attribute_bag->has($username)) {
+ $attributes = $this->user_attribute_bag->get($username);
+ } else {
+ $attributes = array();
+ }
+ $user = new CasUser($username, $password, $salt, $roles, $attributes);
+
+ return $user;
}
throw new UsernameNotFoundException(
@@ -55,4 +91,115 @@ public function supportsClass($class)
{
return $class === 'PRayno\CasAuthBundle\Security\User\CasUser';
}
-}
\ No newline at end of file
+
+ /**
+ * @param $credentials
+ * @retun array
+ */
+ protected function getCasAttributes($credentials) {
+ $attras = array();
+
+ // "Jasig Style" & CAS 3.0 Attributes:
+ //
+ //
+ //
+ // jsmith
+ //
+ // Jasig
+ // Smith
+ // John
+ // CN=Staff,OU=Groups,DC=example,DC=edu
+ // CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
+ //
+ // PGTIOU-84678-8a9d2sfa23casd
+ //
+ //
+ //
+ if (isset($credentials['attributes'])) {
+ foreach ($credentials['attributes'] as $attribute) {
+ $name = $attribute->getName();
+ $value = $attribute->__toString();
+ // Make an attribute multi-valued on the second one seen.
+ if (isset($attras[$name]) && !is_array($attras[$name])) {
+ $tmp = $attras[$name];
+ $attras[$name] = array($tmp);
+ }
+ // Add multi-valued attributes.
+ if (isset($attras[$name])) {
+ $attras[$name][] = $value;
+ }
+ // Single-valued attributes.
+ else {
+ $attras[$name] = $value;
+ }
+ }
+ } else {
+ // "Name-Value" attributes.
+ //
+ // Attribute format from these mailing list thread:
+ // http://jasig.275507.n4.nabble.com/CAS-attributes-and-how-they-appear-in-the-CAS-response-td264272.html
+ // Note: This is a less widely used format, but in use by at least two institutions.
+ //
+ //
+ //
+ // jsmith
+ //
+ //
+ //
+ //
+ //
+ //
+ //
+ // PGTIOU-84678-8a9d2sfa23casd
+ //
+ //
+ //
+ if (isset($credentials['attribute']) && isset($credentials['attribute'][0]->attributes()['name']) && isset($credentials['attribute'][0]->attributes()['value'])) {
+ foreach ($credentials['attribute'] as $attribute) {
+ $name = (string)$attribute->attributes()['name'];
+ $value = (string)$attribute->attributes()['value'];
+ // Make an attribute multi-valued on the second one seen.
+ if (isset($attras[$name]) && !is_array($attras[$name])) {
+ $tmp = $attras[$name];
+ $attras[$name] = array($tmp);
+ }
+ // Add multi-valued attributes.
+ if (isset($attras[$name])) {
+ $attras[$name][] = $value;
+ }
+ // Single-valued attributes.
+ else {
+ $attras[$name] = $value;
+ }
+ }
+ }
+ // "RubyCAS Style" attributes
+ //
+ //
+ //
+ // jsmith
+ //
+ // RubyCAS
+ // Smith
+ // John
+ // CN=Staff,OU=Groups,DC=example,DC=edu
+ // CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
+ //
+ // PGTIOU-84678-8a9d2sfa23casd
+ //
+ //
+ //
+ else {
+ // Test for elements other than our allowed list know to the protocol.
+ $skip = array('user', 'proxyGrantingTicket');
+ foreach ($credentials as $name => $value) {
+ if (!in_array($name, $skip)) {
+ $attras[$name] = $value;
+ }
+ }
+ }
+ }
+
+ return $attras;
+ }
+}
diff --git a/autoload.php b/autoload.php
new file mode 100644
index 0000000..c26cd50
--- /dev/null
+++ b/autoload.php
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
+
+
+ tests
+
+
+
+
+
+
diff --git a/tests/PRayno/CasAuthBundle/Security/CasAuthenticatorTest.php b/tests/PRayno/CasAuthBundle/Security/CasAuthenticatorTest.php
new file mode 100644
index 0000000..aefd88b
--- /dev/null
+++ b/tests/PRayno/CasAuthBundle/Security/CasAuthenticatorTest.php
@@ -0,0 +1,170 @@
+mockCasServer = new MockHandler();
+ $handler = HandlerStack::create($this->mockCasServer);
+ $client = new Client(['handler' => $handler]);
+
+ $this->authenticator = new CasAuthenticator(array(
+ 'server_login_url' => 'https://cas.example.com/cas/',
+ 'server_validation_url' => 'https://cas.example.com/cas/serviceValidate',
+ 'server_logout_url' => 'https://cas.example.com/cas/logout',
+ 'xml_namespace' => 'cas',
+ 'options' => array(),
+ 'username_attribute' => 'user',
+ 'query_ticket_parameter' => 'ticket',
+ 'query_service_parameter' => 'service'
+ ),
+ $client);
+
+ $this->provider = new CasUserProvider(new Session(new MockArraySessionStorage()));
+ }
+
+ public function test_get_user_with_name_only() {
+ // Create a mock response.
+ $response = new Response(
+ 200,
+ array('Content-Type' => 'text/xml'),
+ '
+
+
+ testuser
+
+'
+ );
+ $this->mockCasServer->append($response);
+
+ $request = Request::create('http://app.example.com/?ticket=ABC123-1', 'GET');
+
+ // Get the credentials.
+ $credentials = $this->authenticator->getCredentials($request);
+ $this->assertNotEmpty($credentials);
+ $this->assertEquals('testuser', $credentials['user']);
+
+ // Get the user object for the credentials.
+ $user = $this->authenticator->getUser($credentials, $this->provider);
+ $this->assertEquals('testuser', $user->getUsername());
+ $this->assertEquals(array('ROLE_USER'), $user->getRoles());
+ }
+
+ public function test_get_user_with_jasig_attributes() {
+ // Create a mock response.
+ $response = new Response(
+ 200,
+ array('Content-Type' => 'text/xml'),
+ '
+
+
+ testuser
+
+ Smith
+ John
+ CN=Staff,OU=Groups,DC=example,DC=edu
+ CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
+
+
+'
+ );
+ $this->mockCasServer->append($response);
+
+ $request = Request::create('http://app.example.com/?ticket=ABC123-1', 'GET');
+
+ // Get the credentials.
+ $credentials = $this->authenticator->getCredentials($request);
+ $this->assertNotEmpty($credentials);
+ $this->assertEquals('testuser', $credentials['user']);
+
+ // Get the user object for the credentials.
+ $user = $this->authenticator->getUser($credentials, $this->provider);
+ $this->assertEquals('testuser', $user->getUsername());
+ $this->assertEquals(array('ROLE_USER'), $user->getRoles());
+ $this->assertEquals('Smith', $user->getAttribute('surname'));
+ $this->assertEquals('John', $user->getAttribute('givenName'));
+ $this->assertEquals(array('CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'), $user->getAttribute('memberOf'));
+ }
+
+ public function test_get_user_with_name_value_attributes() {
+ // Create a mock response.
+ $response = new Response(
+ 200,
+ array('Content-Type' => 'text/xml'),
+ '
+
+
+ testuser
+
+
+
+
+
+'
+ );
+ $this->mockCasServer->append($response);
+
+ $request = Request::create('http://app.example.com/?ticket=ABC123-1', 'GET');
+
+ // Get the credentials.
+ $credentials = $this->authenticator->getCredentials($request);
+ $this->assertNotEmpty($credentials);
+ $this->assertEquals('testuser', $credentials['user']);
+
+ // Get the user object for the credentials.
+ $user = $this->authenticator->getUser($credentials, $this->provider);
+ $this->assertEquals('testuser', $user->getUsername());
+ $this->assertEquals(array('ROLE_USER'), $user->getRoles());
+ $this->assertEquals('Smith', $user->getAttribute('surname'));
+ $this->assertEquals('John', $user->getAttribute('givenName'));
+ $this->assertEquals(array('CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'), $user->getAttribute('memberOf'));
+ }
+
+ public function test_get_user_with_rubycas_attributes() {
+ // Create a mock response.
+ $response = new Response(
+ 200,
+ array('Content-Type' => 'text/xml'),
+ '
+
+
+ testuser
+ Smith
+ John
+ CN=Staff,OU=Groups,DC=example,DC=edu
+ CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu
+
+'
+ );
+ $this->mockCasServer->append($response);
+
+ $request = Request::create('http://app.example.com/?ticket=ABC123-1', 'GET');
+
+ // Get the credentials.
+ $credentials = $this->authenticator->getCredentials($request);
+ $this->assertNotEmpty($credentials);
+ $this->assertEquals('testuser', $credentials['user']);
+
+ // Get the user object for the credentials.
+ $user = $this->authenticator->getUser($credentials, $this->provider);
+ $this->assertEquals('testuser', $user->getUsername());
+ $this->assertEquals(array('ROLE_USER'), $user->getRoles());
+ $this->assertEquals('Smith', $user->getAttribute('surname'));
+ $this->assertEquals('John', $user->getAttribute('givenName'));
+ $this->assertEquals(array('CN=Staff,OU=Groups,DC=example,DC=edu', 'CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu'), $user->getAttribute('memberOf'));
+ }
+
+
+}