Loading classes/IdTokenBuilder.php +4 −4 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ class IdTokenBuilder { $this->locator = $locator; } public function getIdToken(AccessToken $accessToken, $nonce = null): string { public function getIdToken(AccessToken $accessToken, string $nonce = null): string { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); Loading @@ -21,15 +21,15 @@ class IdTokenBuilder { return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } private function createPayloadArray(AccessToken $accessToken, $nonce = null) { private function createPayloadArray(AccessToken $accessToken, string $nonce = null) { $user = $this->locator->getUserDAO()->findUserById($accessToken->userId); $payloadArr = array( 'iss' => $this->locator->config->jwtIssuer, 'sub' => $user->id, 'iat' => time(), 'exp' => time() + 3600, 'iat' => $accessToken->creationTime, 'exp' => $accessToken->expirationTime, 'name' => $user->getCompleteName(), 'aud' => $accessToken->clientId ); Loading classes/Locator.php +10 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,16 @@ class Locator { } } public function getRefreshTokenDAO(): RefreshTokenDAO { $databaseConfig = $this->config->databaseConfig; switch ($databaseConfig->dbtype) { case 'MySQL': return new MySQLRefreshTokenDAO($this); default: throw new \Exception($databaseConfig->dbtype . ' not supported yet'); } } public function getCallbackHandler(): CallbackHandler { return new CallbackHandler($this); } Loading classes/OAuth2RequestHandler.php +82 −17 Original line number Diff line number Diff line Loading @@ -90,7 +90,16 @@ class OAuth2RequestHandler { public function handleAccessTokenRequest($params): array { $this->validateAccessTokenRequest($params); if ($params['code'] === null) { throw new BadRequestException("code id is required"); } if ($params['redirect_uri'] === null) { throw new BadRequestException("Redirect URI is required"); } // Note: theorically the standard wants also the client_id here, // however some clients don't send it $accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($params['code']); Loading @@ -102,10 +111,73 @@ class OAuth2RequestHandler { throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']); } return $this->getAccessTokenResponse($accessToken); } public function handleRefreshTokenRequest($params): array { if ($params['refresh_token'] === null) { throw new BadRequestException("refresh_token is required"); } $refreshToken = $this->locator->getRefreshTokenDAO()->getRefreshToken($params['refresh_token']); if ($refreshToken === null || $refreshToken->isExpired()) { throw new UnauthorizedException("Invalid refresh token"); } $scope = $this->getScope($params, $refreshToken); // Generating a new access token $accessToken = new AccessToken(); $accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $accessToken->clientId = $refreshToken->clientId; $accessToken->userId = $refreshToken->userId; $accessToken->scope = $scope; $accessToken = $this->locator->getAccessTokenDAO()->createAccessToken($accessToken); return $this->getAccessTokenResponse($accessToken); } /** * We can request a new access token with a scope that is a subset (or the * same set) of the scope defined for the refresh token. */ private function getScope(array $params, RefreshToken $refreshToken): ?array { $scope = $refreshToken->scope; if ($params['scope'] !== null) { $newScopeValues = explode(' ', $params['scope']); foreach ($newScopeValues as $newScopeValue) { $found = false; foreach ($scope as $oldScopeValue) { if ($oldScopeValue === $newScopeValue) { $found = true; break; } } if (!$found) { throw new BadRequestException("Scope " . $newScopeValue . " was not defined for the given refresh token"); } } $scope = $newScopeValues; } return $scope; } private function getAccessTokenResponse(AccessToken $accessToken) { $result = []; $result['access_token'] = $accessToken->token; $result['token_type'] = 'Bearer'; $result['expires_in'] = $accessToken->expirationTime - time(); $result['refresh_token'] = $this->getNewRefreshToken($accessToken); if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) { $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken); Loading @@ -114,24 +186,17 @@ class OAuth2RequestHandler { return $result; } private function validateAccessTokenRequest($params) { if ($params['grant_type'] === null) { throw new BadRequestException("grant_type is required"); } else if ($params['grant_type'] !== 'authorization_code') { throw new BadRequestException("grant_type must be authorization_code"); } private function getNewRefreshToken(AccessToken $accessToken): string { if ($params['code'] === null) { throw new BadRequestException("code id is required"); } $refreshToken = new RefreshToken(); $refreshToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshToken->clientId = $accessToken->clientId; $refreshToken->userId = $accessToken->userId; $refreshToken->scope = $accessToken->scope; if ($params['redirect_uri'] === null) { throw new BadRequestException("Redirect URI is required"); } $this->locator->getRefreshTokenDAO()->createRefreshToken($refreshToken); // Note: theorically the standard wants also the client_id here, // however some clients don't send it return $refreshToken->token; } public function handleCheckTokenRequest($token): array { Loading Loading @@ -171,7 +236,7 @@ class OAuth2RequestHandler { $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token); if ($accessToken === null) { $this->attemptJWTTokenValidation($bearer_token); } else if ($accessToken->expired) { } else if ($accessToken->isExpired()) { throw new UnauthorizedException("Access token is expired"); } } Loading classes/datalayer/AccessTokenDAO.php +0 −6 Original line number Diff line number Diff line Loading @@ -15,11 +15,5 @@ interface AccessTokenDAO { function getAccessToken(string $token): ?AccessToken; /** * Delete an access token from the database. This happens when the caller * application has received the token and used it for retrieving user * information from the token using the RAP REST web service. * @param type $token login token */ function deleteAccessToken(string $token): void; } classes/datalayer/RefreshTokenDAO.php 0 → 100644 +12 −0 Original line number Diff line number Diff line <?php namespace RAP; interface RefreshTokenDAO { function createRefreshToken(RefreshToken $refreshToken): RefreshToken; function getRefreshToken(string $token): ?RefreshToken; function deleteRefreshToken(string $token): void; } Loading
classes/IdTokenBuilder.php +4 −4 Original line number Diff line number Diff line Loading @@ -12,7 +12,7 @@ class IdTokenBuilder { $this->locator = $locator; } public function getIdToken(AccessToken $accessToken, $nonce = null): string { public function getIdToken(AccessToken $accessToken, string $nonce = null): string { $keyPair = $this->locator->getJWKSDAO()->getNewestKeyPair(); Loading @@ -21,15 +21,15 @@ class IdTokenBuilder { return JWT::encode($payload, $keyPair->privateKey, $keyPair->alg, $keyPair->keyId); } private function createPayloadArray(AccessToken $accessToken, $nonce = null) { private function createPayloadArray(AccessToken $accessToken, string $nonce = null) { $user = $this->locator->getUserDAO()->findUserById($accessToken->userId); $payloadArr = array( 'iss' => $this->locator->config->jwtIssuer, 'sub' => $user->id, 'iat' => time(), 'exp' => time() + 3600, 'iat' => $accessToken->creationTime, 'exp' => $accessToken->expirationTime, 'name' => $user->getCompleteName(), 'aud' => $accessToken->clientId ); Loading
classes/Locator.php +10 −0 Original line number Diff line number Diff line Loading @@ -72,6 +72,16 @@ class Locator { } } public function getRefreshTokenDAO(): RefreshTokenDAO { $databaseConfig = $this->config->databaseConfig; switch ($databaseConfig->dbtype) { case 'MySQL': return new MySQLRefreshTokenDAO($this); default: throw new \Exception($databaseConfig->dbtype . ' not supported yet'); } } public function getCallbackHandler(): CallbackHandler { return new CallbackHandler($this); } Loading
classes/OAuth2RequestHandler.php +82 −17 Original line number Diff line number Diff line Loading @@ -90,7 +90,16 @@ class OAuth2RequestHandler { public function handleAccessTokenRequest($params): array { $this->validateAccessTokenRequest($params); if ($params['code'] === null) { throw new BadRequestException("code id is required"); } if ($params['redirect_uri'] === null) { throw new BadRequestException("Redirect URI is required"); } // Note: theorically the standard wants also the client_id here, // however some clients don't send it $accessToken = $this->locator->getAccessTokenDAO()->retrieveAccessTokenFromCode($params['code']); Loading @@ -102,10 +111,73 @@ class OAuth2RequestHandler { throw new BadRequestException("Invalid redirect URI: " . $params['redirect_uri']); } return $this->getAccessTokenResponse($accessToken); } public function handleRefreshTokenRequest($params): array { if ($params['refresh_token'] === null) { throw new BadRequestException("refresh_token is required"); } $refreshToken = $this->locator->getRefreshTokenDAO()->getRefreshToken($params['refresh_token']); if ($refreshToken === null || $refreshToken->isExpired()) { throw new UnauthorizedException("Invalid refresh token"); } $scope = $this->getScope($params, $refreshToken); // Generating a new access token $accessToken = new AccessToken(); $accessToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $accessToken->clientId = $refreshToken->clientId; $accessToken->userId = $refreshToken->userId; $accessToken->scope = $scope; $accessToken = $this->locator->getAccessTokenDAO()->createAccessToken($accessToken); return $this->getAccessTokenResponse($accessToken); } /** * We can request a new access token with a scope that is a subset (or the * same set) of the scope defined for the refresh token. */ private function getScope(array $params, RefreshToken $refreshToken): ?array { $scope = $refreshToken->scope; if ($params['scope'] !== null) { $newScopeValues = explode(' ', $params['scope']); foreach ($newScopeValues as $newScopeValue) { $found = false; foreach ($scope as $oldScopeValue) { if ($oldScopeValue === $newScopeValue) { $found = true; break; } } if (!$found) { throw new BadRequestException("Scope " . $newScopeValue . " was not defined for the given refresh token"); } } $scope = $newScopeValues; } return $scope; } private function getAccessTokenResponse(AccessToken $accessToken) { $result = []; $result['access_token'] = $accessToken->token; $result['token_type'] = 'Bearer'; $result['expires_in'] = $accessToken->expirationTime - time(); $result['refresh_token'] = $this->getNewRefreshToken($accessToken); if ($accessToken->scope !== null && in_array('openid', $accessToken->scope)) { $result['id_token'] = $this->locator->getIdTokenBuilder()->getIdToken($accessToken); Loading @@ -114,24 +186,17 @@ class OAuth2RequestHandler { return $result; } private function validateAccessTokenRequest($params) { if ($params['grant_type'] === null) { throw new BadRequestException("grant_type is required"); } else if ($params['grant_type'] !== 'authorization_code') { throw new BadRequestException("grant_type must be authorization_code"); } private function getNewRefreshToken(AccessToken $accessToken): string { if ($params['code'] === null) { throw new BadRequestException("code id is required"); } $refreshToken = new RefreshToken(); $refreshToken->token = base64_encode(bin2hex(openssl_random_pseudo_bytes(128))); $refreshToken->clientId = $accessToken->clientId; $refreshToken->userId = $accessToken->userId; $refreshToken->scope = $accessToken->scope; if ($params['redirect_uri'] === null) { throw new BadRequestException("Redirect URI is required"); } $this->locator->getRefreshTokenDAO()->createRefreshToken($refreshToken); // Note: theorically the standard wants also the client_id here, // however some clients don't send it return $refreshToken->token; } public function handleCheckTokenRequest($token): array { Loading Loading @@ -171,7 +236,7 @@ class OAuth2RequestHandler { $accessToken = $this->locator->getAccessTokenDAO()->getAccessToken($bearer_token); if ($accessToken === null) { $this->attemptJWTTokenValidation($bearer_token); } else if ($accessToken->expired) { } else if ($accessToken->isExpired()) { throw new UnauthorizedException("Access token is expired"); } } Loading
classes/datalayer/AccessTokenDAO.php +0 −6 Original line number Diff line number Diff line Loading @@ -15,11 +15,5 @@ interface AccessTokenDAO { function getAccessToken(string $token): ?AccessToken; /** * Delete an access token from the database. This happens when the caller * application has received the token and used it for retrieving user * information from the token using the RAP REST web service. * @param type $token login token */ function deleteAccessToken(string $token): void; }
classes/datalayer/RefreshTokenDAO.php 0 → 100644 +12 −0 Original line number Diff line number Diff line <?php namespace RAP; interface RefreshTokenDAO { function createRefreshToken(RefreshToken $refreshToken): RefreshToken; function getRefreshToken(string $token): ?RefreshToken; function deleteRefreshToken(string $token): void; }