diff --git a/src/OAuth2/OpenID/ResponseType/IdToken.php b/src/OAuth2/OpenID/ResponseType/IdToken.php index 55e446074..3f7ac27ea 100644 --- a/src/OAuth2/OpenID/ResponseType/IdToken.php +++ b/src/OAuth2/OpenID/ResponseType/IdToken.php @@ -7,6 +7,7 @@ use OAuth2\Storage\PublicKeyInterface; use OAuth2\OpenID\Storage\UserClaimsInterface; use LogicException; +use OAuth2\OpenID\Storage\OpenIDConnectInterface; class IdToken implements IdTokenInterface { @@ -30,18 +31,28 @@ class IdToken implements IdTokenInterface protected $encryptionUtil; /** - * Constructor * + * @var OpenIDConnectInterface + */ + protected $openIDStorage; + + protected $subjectIdentifierType; + /** + * Constructor + * * @param UserClaimsInterface $userClaimsStorage * @param PublicKeyInterface $publicKeyStorage + * @param OpenIDConnectInterface $openIDStorage * @param array $config * @param EncryptionInterface $encryptionUtil + * @param type $subjectIdentifierType * @throws LogicException */ - public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, array $config = array(), EncryptionInterface $encryptionUtil = null) - { + public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInterface $publicKeyStorage, OpenIDConnectInterface $openIDStorage, array $config = array(), EncryptionInterface $encryptionUtil = null, $subjectIdentifierType = self::SUBJECT_IDENTIFIER_PUBLIC) { $this->userClaimsStorage = $userClaimsStorage; $this->publicKeyStorage = $publicKeyStorage; + $this->openIDStorage = $openIDStorage; + $this->subjectIdentifierType = $subjectIdentifierType; if (is_null($encryptionUtil)) { $encryptionUtil = new Jwt(); } @@ -55,6 +66,9 @@ public function __construct(UserClaimsInterface $userClaimsStorage, PublicKeyInt ), $config); } + public function setSubjectIdentifierType($type){ + $this->subjectIdentifierType = $type; + } /** * @param array $params * @param null $userInfo @@ -96,7 +110,7 @@ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims $token = array( 'iss' => $this->config['issuer'], - 'sub' => $user_id, + 'sub' => $this->openIDStorage->getOpenID($user_id, $client_id,$this->subjectIdentifierType), 'aud' => $client_id, 'iat' => time(), 'exp' => time() + $this->config['id_lifetime'], @@ -117,7 +131,7 @@ public function createIdToken($client_id, $userInfo, $nonce = null, $userClaims return $this->encodeToken($token, $client_id); } - + /** * @param $access_token * @param null $client_id diff --git a/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php b/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php index 226a3bcbb..a093d0b7b 100644 --- a/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php +++ b/src/OAuth2/OpenID/ResponseType/IdTokenInterface.php @@ -4,8 +4,11 @@ use OAuth2\ResponseType\ResponseTypeInterface; -interface IdTokenInterface extends ResponseTypeInterface -{ +interface IdTokenInterface extends ResponseTypeInterface { + + const SUBJECT_IDENTIFIER_PUBLIC = 'public'; + const SUBJECT_IDENTIFIER_PAIRWISE = 'pairwise'; + /** * Create the id token. * diff --git a/src/OAuth2/OpenID/Storage/OpenIDConnectInterface.php b/src/OAuth2/OpenID/Storage/OpenIDConnectInterface.php new file mode 100644 index 000000000..887865451 --- /dev/null +++ b/src/OAuth2/OpenID/Storage/OpenIDConnectInterface.php @@ -0,0 +1,20 @@ + 'openid' + * 'user_id' => 'user id', + * 'client_id' => 'client_id' + * ) + */ + public function getOpenID($userId, $clientId, $type); + + public function setOpenID($openid, $userId, $clientId); +} \ No newline at end of file diff --git a/src/OAuth2/Server.php b/src/OAuth2/Server.php index cf040c2bc..46966d587 100644 --- a/src/OAuth2/Server.php +++ b/src/OAuth2/Server.php @@ -124,6 +124,7 @@ class Server implements ResourceControllerInterface, 'public_key' => 'OAuth2\Storage\PublicKeyInterface', 'jwt_bearer' => 'OAuth2\Storage\JWTBearerInterface', 'scope' => 'OAuth2\Storage\ScopeInterface', + 'openid_connect' => 'OAuth2\OpenID\Storage\OpenIDConnectInterface', ); /** @@ -176,6 +177,7 @@ public function __construct($storage = array(), array $config = array(), array $ 'allow_public_clients' => true, 'always_issue_new_refresh_token' => false, 'unset_refresh_token_after_use' => true, + 'subject_identifier_type' => IdToken::SUBJECT_IDENTIFIER_PUBLIC, ), $config); foreach ($grantTypes as $key => $grantType) { @@ -879,10 +881,14 @@ protected function createDefaultIdTokenResponseType() if (!isset($this->storages['public_key'])) { throw new LogicException("You must supply a storage object implementing OAuth2\Storage\PublicKeyInterface to use openid connect"); } + + if (!isset($this->storages['openid_connect'])) { + throw new LogicException("You must supply a storage object implementing OAuth2\OpenID\Storage\OpenIDConnectInterface to use openid connect"); + } $config = array_intersect_key($this->config, array_flip(explode(' ', 'issuer id_lifetime'))); - return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $config); + return new IdToken($this->storages['user_claims'], $this->storages['public_key'], $this->storages['openid_connect'], $config, null, $this->config['subject_identifier_type']); } /** diff --git a/src/OAuth2/Storage/Memory.php b/src/OAuth2/Storage/Memory.php index 2c60b71ce..5d8580ae5 100644 --- a/src/OAuth2/Storage/Memory.php +++ b/src/OAuth2/Storage/Memory.php @@ -4,6 +4,8 @@ use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; +use OAuth2\OpenID\Storage\OpenIDConnectInterface; +use OAuth2\OpenID\ResponseType\IdTokenInterface; /** * Simple in-memory storage for all storage types @@ -22,7 +24,8 @@ class Memory implements AuthorizationCodeInterface, JwtBearerInterface, ScopeInterface, PublicKeyInterface, - OpenIDAuthorizationCodeInterface + OpenIDAuthorizationCodeInterface, + OpenIDConnectInterface { public $authorizationCodes; public $userCredentials; @@ -34,6 +37,7 @@ class Memory implements AuthorizationCodeInterface, public $supportedScopes; public $defaultScope; public $keys; + public $openId; public function __construct($params = array()) { @@ -74,6 +78,22 @@ public function getAuthorizationCode($code) ), $this->authorizationCodes[$code]); } + public function getOpenID($userId, $clientId, $type) { + + if ($type === IdTokenInterface::SUBJECT_IDENTIFIER_PUBLIC) { + $this->setOpenID($userId, $userId, $clientId); + return $userId; + } + + $openid = hash_hmac('sha256', $userId, $clientId); + $this->setOpenID($openid, $userId, $clientId); + + return $openid; + } + + public function setOpenID($openid,$userId,$clientId) { + $this->openId = $openid; + } public function setAuthorizationCode($code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { $this->authorizationCodes[$code] = compact('code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'scope', 'id_token'); diff --git a/src/OAuth2/Storage/Pdo.php b/src/OAuth2/Storage/Pdo.php index 074cee447..204fde2c5 100644 --- a/src/OAuth2/Storage/Pdo.php +++ b/src/OAuth2/Storage/Pdo.php @@ -5,7 +5,8 @@ use OAuth2\OpenID\Storage\UserClaimsInterface; use OAuth2\OpenID\Storage\AuthorizationCodeInterface as OpenIDAuthorizationCodeInterface; use InvalidArgumentException; - +use OAuth2\OpenID\Storage\OpenIDConnectInterface; +use OAuth2\OpenID\ResponseType\IdTokenInterface; /** * Simple PDO storage for all storage types * @@ -28,7 +29,8 @@ class Pdo implements ScopeInterface, PublicKeyInterface, UserClaimsInterface, - OpenIDAuthorizationCodeInterface + OpenIDAuthorizationCodeInterface, + OpenIDConnectInterface { /** * @var \PDO @@ -81,6 +83,7 @@ public function __construct($connection, $config = array()) 'jti_table' => 'oauth_jti', 'scope_table' => 'oauth_scopes', 'public_key_table' => 'oauth_public_keys', + 'openid_connect_table' => 'oauth_openid_connect', ), $config); } @@ -115,6 +118,27 @@ public function isPublicClient($client_id) return empty($result['client_secret']); } + public function getOpenID($user_id, $client_id, $type) { + + if ($type === IdTokenInterface::SUBJECT_IDENTIFIER_PUBLIC) { + return $user_id; + } + $stmt = $this->db->prepare(sprintf('SELECT * from %s where client_id = :client_id and user_id = :user_id', $this->config['openid_connect_table'])); + $stmt->execute(compact('client_id', 'user_id')); + $tmp = $stmt->fetch(\PDO::FETCH_ASSOC); + + if ($tmp === false) { + $openid = hash_hmac('sha256', $user_id, $client_id); + $this->setOpenID($openid, $user_id, $client_id); + return $openid; + } + return $tmp['openid']; + } + + public function setOpenID($openid, $user_id, $client_id) { + $stmt = $this->db->prepare(sprintf('INSERT INTO %s (openid, client_id, user_id) VALUES (:openid, :client_id, :user_id)', $this->config['openid_connect_table'])); + return $stmt->execute(compact('openid', 'client_id', 'user_id')); + } /** * @param string $client_id * @return array|mixed @@ -723,6 +747,13 @@ public function getBuildSql($dbName = 'oauth2_server_php') public_key VARCHAR(2000), private_key VARCHAR(2000), encryption_algorithm VARCHAR(100) DEFAULT 'RS256' + ); + + CREATE TABLE {$this->config['openid_connect_table']} ( + openid VARCHAR(255), + user_id VARCHAR(80), + client_id VARCHAR(80), + PRIMARY KEY (client_id,user_id) ) "; diff --git a/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php b/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php index 7b892c946..c195b909e 100644 --- a/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php +++ b/test/OAuth2/OpenID/ResponseType/CodeIdTokenTest.php @@ -171,7 +171,7 @@ private function getTestServer($config = array()) $memoryStorage->supportedScopes[] = 'email'; $responseTypes = array( 'code' => $code = new AuthorizationCode($memoryStorage), - 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $memoryStorage, $config), 'code id_token' => new CodeIdToken($code, $idToken), ); diff --git a/test/OAuth2/OpenID/ResponseType/IdTokenTest.php b/test/OAuth2/OpenID/ResponseType/IdTokenTest.php index a0df3a936..bfd7fe9b0 100644 --- a/test/OAuth2/OpenID/ResponseType/IdTokenTest.php +++ b/test/OAuth2/OpenID/ResponseType/IdTokenTest.php @@ -172,9 +172,10 @@ private function getTestServer($config = array()) $storage = array( 'client' => $memoryStorage, 'scope' => $memoryStorage, + 'openid_connect' => $memoryStorage ); $responseTypes = array( - 'id_token' => new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token' => new IdToken($memoryStorage, $memoryStorage, $memoryStorage, $config), ); $server = new Server($storage, $config, array(), $responseTypes); @@ -182,4 +183,36 @@ private function getTestServer($config = array()) return $server; } + + public function testOpenIDConnectWithSubjectIdentifierTypePairwise(){ + // add the test parameters in memory + $server = $this->getTestServer(array('allow_implicit' => true)); + $server->getResponseType('id_token')->setSubjectIdentifierType(IdToken::SUBJECT_IDENTIFIER_PAIRWISE); + $request = new Request(array( + 'response_type' => 'id_token', + 'redirect_uri' => 'http://adobe.com', + 'client_id' => 'Test Client ID', + 'scope' => 'openid email', + 'state' => 'test', + 'nonce' => 'test', + )); + + $user_id = 'testuser'; + $server->handleAuthorizeRequest($request, $response = new Response(), true, $user_id); + + $this->assertEquals($response->getStatusCode(), 302); + $location = $response->getHttpHeader('Location'); + $this->assertNotContains('error', $location); + + $parts = parse_url($location); + $this->assertArrayHasKey('fragment', $parts); + $this->assertFalse(isset($parts['query'])); + + // assert fragment is in "application/x-www-form-urlencoded" format + parse_str($parts['fragment'], $params); + $this->assertNotNull($params); + $this->assertArrayHasKey('id_token', $params); + $this->assertArrayNotHasKey('access_token', $params); + $this->validateIdToken($params['id_token']); + } } diff --git a/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php b/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php index 0573a9866..6013548ca 100644 --- a/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php +++ b/test/OAuth2/OpenID/ResponseType/IdTokenTokenTest.php @@ -80,7 +80,7 @@ private function getTestServer($config = array()) $memoryStorage = Bootstrap::getInstance()->getMemoryStorage(); $responseTypes = array( 'token' => $token = new AccessToken($memoryStorage, $memoryStorage), - 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $config), + 'id_token' => $idToken = new IdToken($memoryStorage, $memoryStorage, $memoryStorage, $config), 'id_token token' => new IdTokenToken($token, $idToken), ); diff --git a/test/OAuth2/ServerTest.php b/test/OAuth2/ServerTest.php index 3106961e2..c971f01b4 100644 --- a/test/OAuth2/ServerTest.php +++ b/test/OAuth2/ServerTest.php @@ -487,7 +487,21 @@ public function testUsingJwtAccessTokenAndClientStorageWithAuthorizeControllerIs public function testUsingOpenIDConnectWithoutUserClaimsThrowsException() { $client = $this->getMock('OAuth2\Storage\ClientInterface'); - $server = new Server($client, array('use_openid_connect' => true)); + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); + $server = new Server(array($client, $openidConnect), array('use_openid_connect' => true)); + + $server->getAuthorizeController(); + } + + /** + * @expectedException LogicException OpenIDConnectInterface + **/ + public function testUsingOpenIDConnectWithoutOpenIDConnectStorageThrowsException() + { + $client = $this->getMock('OAuth2\Storage\ClientInterface'); + $userClaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); + $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $server = new Server(array($client, $userClaims, $pubkey), array('use_openid_connect' => true)); $server->getAuthorizeController(); } @@ -512,7 +526,8 @@ public function testUsingOpenIDConnectWithoutIssuerThrowsException() $client = $this->getMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($client, $userclaims, $pubkey), array('use_openid_connect' => true)); + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $openidConnect), array('use_openid_connect' => true)); $server->getAuthorizeController(); } @@ -522,7 +537,8 @@ public function testUsingOpenIDConnectWithIssuerPublicKeyAndUserClaimsIsOkay() $client = $this->getMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($client, $userclaims, $pubkey), array( + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $openidConnect), array( 'use_openid_connect' => true, 'issuer' => 'someguy', )); @@ -549,13 +565,14 @@ public function testUsingOpenIDConnectWithAllowImplicitWithoutTokenStorageThrows $server->getAuthorizeController(); } - + public function testUsingOpenIDConnectWithAllowImplicitAndUseJwtAccessTokensIsOkay() { $client = $this->getMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); - $server = new Server(array($client, $userclaims, $pubkey), array( + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $openidConnect), array( 'use_openid_connect' => true, 'issuer' => 'someguy', 'allow_implicit' => true, @@ -574,7 +591,8 @@ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenStorageIsOk $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); - $server = new Server(array($client, $userclaims, $pubkey, $token), array( + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); + $server = new Server(array($client, $userclaims, $pubkey, $token, $openidConnect), array( 'use_openid_connect' => true, 'issuer' => 'someguy', 'allow_implicit' => true, @@ -591,8 +609,9 @@ public function testUsingOpenIDConnectWithAllowImplicitAndAccessTokenResponseTyp $client = $this->getMock('OAuth2\Storage\ClientInterface'); $userclaims = $this->getMock('OAuth2\OpenID\Storage\UserClaimsInterface'); $pubkey = $this->getMock('OAuth2\Storage\PublicKeyInterface'); + $openidConnect = $this->getMock('OAuth2\OpenID\Storage\OpenIDConnectInterface'); // $token = $this->getMock('OAuth2\Storage\AccessTokenInterface'); - $server = new Server(array($client, $userclaims, $pubkey), array( + $server = new Server(array($client, $userclaims, $pubkey, $openidConnect), array( 'use_openid_connect' => true, 'issuer' => 'someguy', 'allow_implicit' => true, diff --git a/test/lib/OAuth2/Storage/Bootstrap.php b/test/lib/OAuth2/Storage/Bootstrap.php index 8e428f9b5..f1774bddc 100755 --- a/test/lib/OAuth2/Storage/Bootstrap.php +++ b/test/lib/OAuth2/Storage/Bootstrap.php @@ -390,8 +390,8 @@ public function runPdoSql(\PDO $pdo) } // set up clients - $sql = 'INSERT INTO oauth_clients (client_id, client_secret, scope, grant_types) VALUES (?, ?, ?, ?)'; - $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); + $sql = 'INSERT INTO oauth_clients (client_id, client_secret, grant_types, scope) VALUES (?, ?, ?, ?)'; + $pdo->prepare($sql)->execute(array('Test Client ID', 'TestSecret', 'implicit authorization_code', 'openid email')); $pdo->prepare($sql)->execute(array('Test Client ID 2', 'TestSecret', 'clientscope1 clientscope2 clientscope3', null)); $pdo->prepare($sql)->execute(array('Test Default Scope Client ID', 'TestSecret', 'clientscope1 clientscope2', null)); $pdo->prepare($sql)->execute(array('oauth_test_client', 'testpass', null, 'implicit password')); @@ -627,6 +627,7 @@ public function getDynamoDbStorage() 'jwt_table' => $prefix.'oauth_jwt', 'scope_table' => $prefix.'oauth_scopes', 'public_key_table' => $prefix.'oauth_public_keys', + 'openid_connect_table' => $prefix.'oauth_openid_connect', ); $this->dynamodb = new DynamoDB($client, $config); } elseif (!$this->dynamodb) { @@ -668,7 +669,7 @@ private function getDynamoDbClient() private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null, $waitForDeletion = false) { - $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users oauth_openid_connect'); $nbTables = count($tablesList); // Delete all table. @@ -711,7 +712,7 @@ private function deleteDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = null) { - $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users'); + $tablesList = explode(' ', 'oauth_access_tokens oauth_authorization_codes oauth_clients oauth_jwt oauth_public_keys oauth_refresh_tokens oauth_scopes oauth_users oauth_openid_connect'); $nbTables = count($tablesList); $client->createTable(array( 'TableName' => $prefix.'oauth_access_tokens', @@ -722,6 +723,17 @@ private function createDynamoDb(\Aws\DynamoDb\DynamoDbClient $client, $prefix = 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) )); + $client->createTable(array( + 'TableName' => $prefix.'oauth_openid_connect', + 'AttributeDefinitions' => array( + array('AttributeName' => 'openid','AttributeType' => 'S'), + array('AttributeName' => 'client_id','AttributeType' => 'S'), + array('AttributeName' => 'user_id','AttributeType' => 'S'), + ), + 'KeySchema' => array(array('AttributeName' => 'access_token','KeyType' => 'HASH')), + 'ProvisionedThroughput' => array('ReadCapacityUnits' => 1,'WriteCapacityUnits' => 1) + )); + $client->createTable(array( 'TableName' => $prefix.'oauth_authorization_codes', 'AttributeDefinitions' => array(