* composer require aws/aws-sdk-php:dev-master * * * Once this is done, instantiate the DynamoDB client * * $storage = new OAuth2\Storage\Dynamodb(array("key" => "YOURKEY", "secret" => "YOURSECRET", "region" => "YOURREGION")); * * * Table : * - oauth_access_tokens (primary hash key : access_token) * - oauth_authorization_codes (primary hash key : authorization_code) * - oauth_clients (primary hash key : client_id) * - oauth_jwt (primary hash key : client_id, primary range key : subject) * - oauth_public_keys (primary hash key : client_id) * - oauth_refresh_tokens (primary hash key : refresh_token) * - oauth_scopes (primary hash key : scope, secondary index : is_default-index hash key is_default) * - oauth_users (primary hash key : username) * * @author Frederic AUGUSTE */ class DynamoDB implements AuthorizationCodeInterface, AccessTokenInterface, ClientCredentialsInterface, UserCredentialsInterface, RefreshTokenInterface, JwtBearerInterface, ScopeInterface, PublicKeyInterface, UserClaimsInterface, OpenIDAuthorizationCodeInterface { protected $client; protected $config; public function __construct($connection, $config = array()) { if (!($connection instanceof DynamoDbClient)) { if (!is_array($connection)) { throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); } if (!array_key_exists("key",$connection) || !array_key_exists("secret",$connection) || !array_key_exists("region",$connection) ) { throw new \InvalidArgumentException('First argument to OAuth2\Storage\Dynamodb must be an instance a configuration array containt key, secret, region'); } $this->client = DynamoDbClient::factory(array( 'key' => $connection["key"], 'secret' => $connection["secret"], 'region' =>$connection["region"] )); } else { $this->client = $connection; } $this->config = array_merge(array( 'client_table' => 'oauth_clients', 'access_token_table' => 'oauth_access_tokens', 'refresh_token_table' => 'oauth_refresh_tokens', 'code_table' => 'oauth_authorization_codes', 'user_table' => 'oauth_users', 'jwt_table' => 'oauth_jwt', 'scope_table' => 'oauth_scopes', 'public_key_table' => 'oauth_public_keys', ), $config); } /* OAuth2\Storage\ClientCredentialsInterface */ public function checkClientCredentials($client_id, $client_secret = null) { $result = $this->client->getItem(array( "TableName"=> $this->config['client_table'], "Key" => array('client_id' => array('S' => $client_id)) )); return $result->count()==1 && $result["Item"]["client_secret"]["S"] == $client_secret; } public function isPublicClient($client_id) { $result = $this->client->getItem(array( "TableName"=> $this->config['client_table'], "Key" => array('client_id' => array('S' => $client_id)) )); if ($result->count()==0) { return false ; } return empty($result["Item"]["client_secret"]); } /* OAuth2\Storage\ClientInterface */ public function getClientDetails($client_id) { $result = $this->client->getItem(array( "TableName"=> $this->config['client_table'], "Key" => array('client_id' => array('S' => $client_id)) )); if ($result->count()==0) { return false ; } $result = $this->dynamo2array($result); foreach (array('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id') as $key => $val) { if (!array_key_exists ($val, $result)) { $result[$val] = null; } } return $result; } public function setClientDetails($client_id, $client_secret = null, $redirect_uri = null, $grant_types = null, $scope = null, $user_id = null) { $clientData = compact('client_id', 'client_secret', 'redirect_uri', 'grant_types', 'scope', 'user_id'); $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); $result = $this->client->putItem(array( 'TableName' => $this->config['client_table'], 'Item' => $this->client->formatAttributes($clientData) )); return true; } public function checkRestrictedGrantType($client_id, $grant_type) { $details = $this->getClientDetails($client_id); if (isset($details['grant_types'])) { $grant_types = explode(' ', $details['grant_types']); return in_array($grant_type, (array) $grant_types); } // if grant_types are not defined, then none are restricted return true; } /* OAuth2\Storage\AccessTokenInterface */ public function getAccessToken($access_token) { $result = $this->client->getItem(array( "TableName"=> $this->config['access_token_table'], "Key" => array('access_token' => array('S' => $access_token)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); if (array_key_exists ('expires', $token)) { $token['expires'] = strtotime($token['expires']); } return $token; } public function setAccessToken($access_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('access_token', 'client_id', 'user_id', 'expires', 'scope'); $clientData = array_filter($clientData, function ($value) { return !empty($value); }); $result = $this->client->putItem(array( 'TableName' => $this->config['access_token_table'], 'Item' => $this->client->formatAttributes($clientData) )); return true; } public function unsetAccessToken($access_token) { $result = $this->client->deleteItem(array( 'TableName' => $this->config['access_token_table'], 'Key' => $this->client->formatAttributes(array("access_token" => $access_token)) )); return true; } /* OAuth2\Storage\AuthorizationCodeInterface */ public function getAuthorizationCode($code) { $result = $this->client->getItem(array( "TableName"=> $this->config['code_table'], "Key" => array('authorization_code' => array('S' => $code)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); if (!array_key_exists("id_token", $token )) { $token['id_token'] = null; } $token['expires'] = strtotime($token['expires']); return $token; } public function setAuthorizationCode($authorization_code, $client_id, $user_id, $redirect_uri, $expires, $scope = null, $id_token = null) { // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('authorization_code', 'client_id', 'user_id', 'redirect_uri', 'expires', 'id_token', 'scope'); $clientData = array_filter($clientData, function ($value) { return !empty($value); }); $result = $this->client->putItem(array( 'TableName' => $this->config['code_table'], 'Item' => $this->client->formatAttributes($clientData) )); return true; } public function expireAuthorizationCode($code) { $result = $this->client->deleteItem(array( 'TableName' => $this->config['code_table'], 'Key' => $this->client->formatAttributes(array("authorization_code" => $code)) )); return true; } /* OAuth2\Storage\UserCredentialsInterface */ public function checkUserCredentials($username, $password) { if ($user = $this->getUser($username)) { return $this->checkPassword($user, $password); } return false; } public function getUserDetails($username) { return $this->getUser($username); } /* UserClaimsInterface */ public function getUserClaims($user_id, $claims) { if (!$userDetails = $this->getUserDetails($user_id)) { return false; } $claims = explode(' ', trim($claims)); $userClaims = array(); // for each requested claim, if the user has the claim, set it in the response $validClaims = explode(' ', self::VALID_CLAIMS); foreach ($validClaims as $validClaim) { if (in_array($validClaim, $claims)) { if ($validClaim == 'address') { // address is an object with subfields $userClaims['address'] = $this->getUserClaim($validClaim, $userDetails['address'] ?: $userDetails); } else { $userClaims = array_merge($userClaims, $this->getUserClaim($validClaim, $userDetails)); } } } return $userClaims; } protected function getUserClaim($claim, $userDetails) { $userClaims = array(); $claimValuesString = constant(sprintf('self::%s_CLAIM_VALUES', strtoupper($claim))); $claimValues = explode(' ', $claimValuesString); foreach ($claimValues as $value) { if ($value == 'email_verified') { $userClaims[$value] = $userDetails[$value]=='true' ? true : false; } else { $userClaims[$value] = isset($userDetails[$value]) ? $userDetails[$value] : null; } } return $userClaims; } /* OAuth2\Storage\RefreshTokenInterface */ public function getRefreshToken($refresh_token) { $result = $this->client->getItem(array( "TableName"=> $this->config['refresh_token_table'], "Key" => array('refresh_token' => array('S' => $refresh_token)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); $token['expires'] = strtotime($token['expires']); return $token; } public function setRefreshToken($refresh_token, $client_id, $user_id, $expires, $scope = null) { // convert expires to datestring $expires = date('Y-m-d H:i:s', $expires); $clientData = compact('refresh_token', 'client_id', 'user_id', 'expires', 'scope'); $clientData = array_filter($clientData, function ($value) { return !empty($value); }); $result = $this->client->putItem(array( 'TableName' => $this->config['refresh_token_table'], 'Item' => $this->client->formatAttributes($clientData) )); return true; } public function unsetRefreshToken($refresh_token) { $result = $this->client->deleteItem(array( 'TableName' => $this->config['refresh_token_table'], 'Key' => $this->client->formatAttributes(array("refresh_token" => $refresh_token)) )); return true; } // plaintext passwords are bad! Override this for your application protected function checkPassword($user, $password) { return $user['password'] == sha1($password); } public function getUser($username) { $result = $this->client->getItem(array( "TableName"=> $this->config['user_table'], "Key" => array('username' => array('S' => $username)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); $token['user_id'] = $username; return $token; } public function setUser($username, $password, $first_name = null, $last_name = null) { // do not store in plaintext $password = sha1($password); $clientData = compact('username', 'password', 'first_name', 'last_name'); $clientData = array_filter($clientData, function ($value) { return !is_null($value); }); $result = $this->client->putItem(array( 'TableName' => $this->config['user_table'], 'Item' => $this->client->formatAttributes($clientData) )); return true; } /* ScopeInterface */ public function scopeExists($scope) { $scope = explode(' ', $scope); $scope_query = array(); $count = 0; foreach ($scope as $key => $val) { $result = $this->client->query(array( 'TableName' => $this->config['scope_table'], 'Select' => 'COUNT', 'KeyConditions' => array( 'scope' => array( 'AttributeValueList' => array(array('S' => $val)), 'ComparisonOperator' => 'EQ' ) ) )); $count += $result['Count']; } return $count == count($scope); } public function getDefaultScope($client_id = null) { $result = $this->client->query(array( 'TableName' => $this->config['scope_table'], 'IndexName' => 'is_default-index', 'Select' => 'ALL_ATTRIBUTES', 'KeyConditions' => array( 'is_default' => array( 'AttributeValueList' => array(array('S' => 'true')), 'ComparisonOperator' => 'EQ', ), ) )); $defaultScope = array(); if ($result->count() > 0) { $array = $result->toArray(); foreach ($array["Items"] as $item) { $defaultScope[] = $item['scope']['S']; } return empty($defaultScope) ? null : implode(' ', $defaultScope); } return null; } /* JWTBearerInterface */ public function getClientKey($client_id, $subject) { $result = $this->client->getItem(array( "TableName"=> $this->config['jwt_table'], "Key" => array('client_id' => array('S' => $client_id), 'subject' => array('S' => $subject)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); return $token['public_key']; } public function getClientScope($client_id) { if (!$clientDetails = $this->getClientDetails($client_id)) { return false; } if (isset($clientDetails['scope'])) { return $clientDetails['scope']; } return null; } public function getJti($client_id, $subject, $audience, $expires, $jti) { //TODO not use. } public function setJti($client_id, $subject, $audience, $expires, $jti) { //TODO not use. } /* PublicKeyInterface */ public function getPublicKey($client_id = '0') { $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); return $token['public_key']; } public function getPrivateKey($client_id = '0') { $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); if ($result->count()==0) { return false ; } $token = $this->dynamo2array($result); return $token['private_key']; } public function getEncryptionAlgorithm($client_id = null) { $result = $this->client->getItem(array( "TableName"=> $this->config['public_key_table'], "Key" => array('client_id' => array('S' => $client_id)) )); if ($result->count()==0) { return 'RS256' ; } $token = $this->dynamo2array($result); return $token['encryption_algorithm']; } /** * Transform dynamodb resultset to an array. * @param $dynamodbResult * @return $array */ private function dynamo2array($dynamodbResult) { $result = array(); foreach ($dynamodbResult["Item"] as $key => $val) { $result[$key] = $val["S"]; $result[] = $val["S"]; } return $result; } }