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')); + } + + +}