From 6dc411fad92f21114478ac59370696ae257e4be3 Mon Sep 17 00:00:00 2001 From: smarcet Date: Tue, 15 Dec 2020 15:41:07 -0300 Subject: [PATCH] Auth Code Flow PKCE support implementation of https://tools.ietf.org/html/rfc7636 Change-Id: Ib88a3b6c9652e6eea9648177ffd0d143ab995ac6 Signed-off-by: smarcet --- app/Http/Kernel.php | 3 +- app/Http/routes.php | 2 +- app/Models/OAuth2/Client.php | 50 +- app/Models/OAuth2/Factories/ClientFactory.php | 36 +- app/Services/OAuth2/ClientService.php | 56 +- app/Services/OAuth2/OAuth2ServiceProvider.php | 2 + app/Services/OAuth2/TokenService.php | 593 ++++++++---------- app/libs/OAuth2/Endpoints/TokenEndpoint.php | 2 +- .../Exceptions/InvalidOAuth2PKCERequest.php | 18 + .../Exceptions/InvalidOAuth2Request.php | 2 +- .../OAuth2AccessTokenResponseFactory.php | 5 +- .../OAuth2PKCEValidationMethodFactory.php | 69 ++ .../GrantTypes/AuthorizationCodeGrantType.php | 84 ++- .../OAuth2/GrantTypes/HybridGrantType.php | 20 +- .../RefreshBearerTokenGrantType.php | 37 +- .../Strategies/PKCEBaseValidator.php | 42 ++ .../Strategies/PKCEPlainValidator.php | 25 + .../Strategies/PKCES256Validator.php | 32 + app/libs/OAuth2/Models/AccessToken.php | 8 + app/libs/OAuth2/Models/AuthorizationCode.php | 244 ++++--- ...ClientCredentialsAuthenticationContext.php | 4 +- app/libs/OAuth2/Models/IClient.php | 5 + app/libs/OAuth2/Models/RefreshToken.php | 8 + app/libs/OAuth2/OAuth2Protocol.php | 21 + .../OAuth2AccessTokenRequestAuthCode.php | 4 + .../Requests/OAuth2AuthenticationRequest.php | 29 - .../Requests/OAuth2AuthorizationRequest.php | 58 +- app/libs/OAuth2/Services/ITokenService.php | 33 +- .../ClientAuthContextValidatorFactory.php | 5 + .../ClientPKCEAuthContextValidator.php | 56 ++ ...ntPlainCredentialsAuthContextValidator.php | 17 +- .../Strategies/IPKCEValidationMethod.php | 22 + .../OAuth2ResponseStrategyFactoryMethod.php | 7 +- app/libs/OpenId/Models/OpenIdNonce.php | 8 + app/libs/Utils/Model/Identifier.php | 5 + database/migrations/Version20190604015804.php | 1 + database/migrations/Version20201214162511.php | 49 ++ database/seeds/TestSeeder.php | 17 +- .../oauth2/profile/edit-client-data.blade.php | 2 +- .../profile/edit-client-scopes.blade.php | 2 +- ...it-client-security-main-settings.blade.php | 16 + .../oauth2/profile/edit-client.blade.php | 2 +- tests/OAuth2ProtocolTest.php | 163 ++++- 43 files changed, 1214 insertions(+), 650 deletions(-) create mode 100644 app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php create mode 100644 app/libs/OAuth2/Factories/OAuth2PKCEValidationMethodFactory.php create mode 100644 app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php create mode 100644 app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php create mode 100644 app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php create mode 100644 app/libs/OAuth2/Strategies/ClientPKCEAuthContextValidator.php create mode 100644 app/libs/OAuth2/Strategies/IPKCEValidationMethod.php create mode 100644 database/migrations/Version20201214162511.php diff --git a/app/Http/Kernel.php b/app/Http/Kernel.php index 3ab3843b..0146ebfd 100644 --- a/app/Http/Kernel.php +++ b/app/Http/Kernel.php @@ -30,6 +30,7 @@ class Kernel extends HttpKernel protected $middleware = [ \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \App\Http\Middleware\SingleAccessPoint::class, + \Spatie\Cors\Cors::class, \App\Http\Middleware\ParseMultipartFormDataInputForNonPostRequests::class, ]; @@ -50,7 +51,6 @@ class Kernel extends HttpKernel 'api' => [ 'ssl', - 'cors', 'oauth2.endpoint', ], ]; @@ -69,7 +69,6 @@ class Kernel extends HttpKernel 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, - 'cors' => \Spatie\Cors\Cors::class, 'oauth2.endpoint' => \App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator::class, 'oauth2.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdmin::class, 'oauth2.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdminJson::class, diff --git a/app/Http/routes.php b/app/Http/routes.php index c883654e..cd09410c 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -91,6 +91,7 @@ Route::group(['namespace' => 'App\Http\Controllers', 'middleware' => 'web' ], fu }); Route::group(['namespace' => 'OAuth2' , 'prefix' => 'oauth2', 'middleware' => ['ssl']], function () { + Route::get('/check-session', "OAuth2ProviderController@checkSessionIFrame"); Route::get('/end-session', "OAuth2ProviderController@endSession"); Route::get('/end-session/cancel', "OAuth2ProviderController@cancelLogout"); @@ -375,7 +376,6 @@ Route::group( 'prefix' => 'api/v1', 'middleware' => [ 'ssl', - 'cors', 'oauth2.endpoint', ] ], function () { diff --git a/app/Models/OAuth2/Client.php b/app/Models/OAuth2/Client.php index fb649050..0b5571f0 100644 --- a/app/Models/OAuth2/Client.php +++ b/app/Models/OAuth2/Client.php @@ -15,10 +15,12 @@ use App\libs\Utils\URLUtils; use Auth\User; use Doctrine\Common\Collections\Criteria; +use Illuminate\Support\Facades\Log; use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry; use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry; use jwa\cryptographic_algorithms\KeyManagementAlgorithms_Registry; use jwa\JSONWebSignatureAndEncryptionAlgorithms; +use models\exceptions\ValidationException; use OAuth2\Models\IClient; use OAuth2\Models\IClientPublicKey; use OAuth2\Models\JWTResponseInfo; @@ -82,6 +84,12 @@ class Client extends BaseEntity implements IClient */ private $active; + /** + * @ORM\Column(name="pkce_enabled", type="boolean") + * @var bool + */ + private $pkce_enabled; + /** * @ORM\Column(name="locked", type="boolean") * @var bool @@ -371,6 +379,7 @@ class Client extends BaseEntity implements IClient $this->active = false; $this->use_refresh_token = false; $this->rotate_refresh_token = false; + $this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic; $this->token_endpoint_auth_signing_alg = JSONWebSignatureAndEncryptionAlgorithms::None; $this->userinfo_signed_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None; $this->userinfo_encrypted_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None; @@ -389,7 +398,7 @@ class Client extends BaseEntity implements IClient $this->max_access_token_issuance_qty = 0; $this->max_refresh_token_issuance_basis = 0; $this->max_refresh_token_issuance_qty = 0; - $this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic; + $this->pkce_enabled = false; } public static $valid_app_types = [ @@ -423,22 +432,25 @@ class Client extends BaseEntity implements IClient throw new \InvalidArgumentException("Invalid application_type"); } $this->application_type = strtoupper($application_type); - $this->client_type = $this->infereClientTypeFromAppType($this->application_type); + $this->client_type = $this->inferClientTypeFromAppType($this->application_type); } /** * @return bool */ public function canRequestRefreshTokens():bool{ - return $this->getApplicationType() == IClient::ApplicationType_Native || - $this->getApplicationType() == IClient::ApplicationType_Web_App; + return + $this->getApplicationType() == IClient::ApplicationType_Native || + $this->getApplicationType() == IClient::ApplicationType_Web_App || + // PCKE + $this->pkce_enabled; } /** * @param string $app_type * @return string */ - private function infereClientTypeFromAppType(string $app_type) + private function inferClientTypeFromAppType(string $app_type) { switch($app_type) { @@ -587,14 +599,20 @@ class Client extends BaseEntity implements IClient ($this->application_type !== IClient::ApplicationType_Native && !URLUtils::isHTTPS($uri)) && (ServerConfigurationService::getConfigValue("SSL.Enable")) ) + { + Log::debug(sprintf("Client::isUriAllowed url %s is not under ssl schema", $uri)); return false; + } $redirect_uris = explode(',',strtolower($this->redirect_uris)); $uri = URLUtils::normalizeUrl($uri); foreach($redirect_uris as $redirect_uri){ + Log::debug(sprintf("Client::isUriAllowed url %s client %s redirect_uri %s", $uri, $this->client_id, $redirect_uri)); if(str_contains($uri, $redirect_uri)) return true; } + + Log::debug(sprintf("Client::isUriAllowed url %s is not allowed as return url for client %s", $uri, $this->client_id)); return false; } @@ -1113,6 +1131,7 @@ class Client extends BaseEntity implements IClient */ public function isOwner(User $user):bool { + if(!$this->hasUser()) return false; return intval($this->user->getId()) === intval($user->getId()); } @@ -1132,7 +1151,7 @@ class Client extends BaseEntity implements IClient */ public function addScope(ApiScope $scope) { - if($this->scopes->contains($scope)) return; + if($this->scopes->contains($scope)) return $this; $this->scopes->add($scope); return $this; } @@ -1565,4 +1584,23 @@ class Client extends BaseEntity implements IClient return $this->getUserId(); return $this->{$name}; } + + public function isPKCEEnabled():bool{ + return $this->pkce_enabled; + } + + public function enablePCKE(){ + if($this->client_type != self::ClientType_Public){ + throw new ValidationException("Only Public Clients could use PCKE."); + } + $this->pkce_enabled = true; + $this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_None; + } + + public function disablePCKE(){ + if($this->client_type != self::ClientType_Public){ + throw new ValidationException("Only Public Clients could use PCKE."); + } + $this->pkce_enabled = false; + } } \ No newline at end of file diff --git a/app/Models/OAuth2/Factories/ClientFactory.php b/app/Models/OAuth2/Factories/ClientFactory.php index 577e6cef..9a54e2e0 100644 --- a/app/Models/OAuth2/Factories/ClientFactory.php +++ b/app/Models/OAuth2/Factories/ClientFactory.php @@ -32,21 +32,8 @@ final class ClientFactory */ public static function build(array $payload):Client { - $scope_repository = App::make(IApiScopeRepository::class); $client = self::populate(new Client, $payload); $client->setActive(true); - //add default scopes - foreach ($scope_repository->getDefaults() as $default_scope) { - if - ( - $default_scope->getName() === OAuth2Protocol::OfflineAccess_Scope - && !$client->canRequestRefreshTokens() - ) { - continue; - } - $client->addScope($default_scope); - } - if ($client->getClientType() !== IClient::ClientType_Confidential) { $client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_None); } @@ -202,6 +189,29 @@ final class ClientFactory $client->setResourceServer($resource_server); } + if(isset($payload['pkce_enabled'])) { + + $pkce_enabled = boolval($payload['pkce_enabled']); + + if($pkce_enabled) + $client->enablePCKE(); + else + $client->disablePCKE(); + } + + $scope_repository = App::make(IApiScopeRepository::class); + //add default scopes + foreach ($scope_repository->getDefaults() as $default_scope) { + if + ( + $default_scope->getName() === OAuth2Protocol::OfflineAccess_Scope + && !$client->canRequestRefreshTokens() + ) { + continue; + } + $client->addScope($default_scope); + } + return $client; } } \ No newline at end of file diff --git a/app/Services/OAuth2/ClientService.php b/app/Services/OAuth2/ClientService.php index ce2e020d..e1aab15e 100644 --- a/app/Services/OAuth2/ClientService.php +++ b/app/Services/OAuth2/ClientService.php @@ -144,38 +144,16 @@ final class ClientService extends AbstractService implements IClientService ); } - if - ( - Input::has(OAuth2Protocol::OAuth2Protocol_ClientId) && - Input::has(OAuth2Protocol::OAuth2Protocol_ClientSecret) - ) - { - Log::debug - ( - sprintf - ( - "ClientService::getCurrentClientAuthInfo params %s - %s present", - OAuth2Protocol::OAuth2Protocol_ClientId, - OAuth2Protocol::OAuth2Protocol_ClientSecret - ) - ); - return new ClientCredentialsAuthenticationContext - ( - urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')), - urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, '')), - OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost - ); - } - - $auth_header = Request::header('Authorization'); - if(!empty($auth_header)) + if(Request::hasHeader('Authorization')) { + Log::debug ( "ClientService::getCurrentClientAuthInfo Authorization Header present" ); + $auth_header = Request::header('Authorization'); $auth_header = trim($auth_header); $auth_header = explode(' ', $auth_header); @@ -211,6 +189,34 @@ final class ClientService extends AbstractService implements IClientService ); } + if(Input::has(OAuth2Protocol::OAuth2Protocol_ClientId)) + { + Log::debug + ( + sprintf + ( + "ClientService::getCurrentClientAuthInfo params %s - %s present", + OAuth2Protocol::OAuth2Protocol_ClientId, + OAuth2Protocol::OAuth2Protocol_ClientSecret + ) + ); + + $client_secret = null; + $auth_type = OAuth2Protocol::TokenEndpoint_AuthMethod_None; + + if(Input::has(OAuth2Protocol::OAuth2Protocol_ClientSecret)){ + $client_secret = urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, '')); + $auth_type = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost; + } + + return new ClientCredentialsAuthenticationContext + ( + urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')), + $client_secret, + $auth_type + ); + } + throw new InvalidClientAuthMethodException; } diff --git a/app/Services/OAuth2/OAuth2ServiceProvider.php b/app/Services/OAuth2/OAuth2ServiceProvider.php index 3a164ad9..a0af026b 100644 --- a/app/Services/OAuth2/OAuth2ServiceProvider.php +++ b/app/Services/OAuth2/OAuth2ServiceProvider.php @@ -16,6 +16,7 @@ use App\Http\Utils\IUserIPHelperProvider; use Illuminate\Support\ServiceProvider; use OAuth2\Services\AccessTokenGenerator; use OAuth2\Services\AuthorizationCodeGenerator; +use OAuth2\Services\IApiScopeService; use OAuth2\Services\OAuth2ServiceCatalog; use OAuth2\Services\RefreshTokenGenerator; use Utils\Services\UtilsServiceCatalog; @@ -83,6 +84,7 @@ final class OAuth2ServiceProvider extends ServiceProvider App::make(\OAuth2\Repositories\IRefreshTokenRepository::class), App::make(\OAuth2\Repositories\IResourceServerRepository::class), App::make(IUserIPHelperProvider::class), + App::make(IApiScopeService::class), App::make(UtilsServiceCatalog::TransactionService) ); }); diff --git a/app/Services/OAuth2/TokenService.php b/app/Services/OAuth2/TokenService.php index 24d5229d..945c09a3 100644 --- a/app/Services/OAuth2/TokenService.php +++ b/app/Services/OAuth2/TokenService.php @@ -11,10 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + use App\Http\Utils\IUserIPHelperProvider; use App\libs\Auth\Models\IGroupSlugs; use App\Services\AbstractService; -use Auth\Group; use Auth\User; use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Log; @@ -46,6 +46,9 @@ use OAuth2\Repositories\IAccessTokenRepository; use OAuth2\Repositories\IClientRepository; use OAuth2\Repositories\IRefreshTokenRepository; use OAuth2\Repositories\IResourceServerRepository; +use OAuth2\Requests\OAuth2AuthenticationRequest; +use OAuth2\Requests\OAuth2AuthorizationRequest; +use OAuth2\Services\IApiScopeService; use OAuth2\Services\ITokenService; use OAuth2\OAuth2Protocol; use OAuth2\Repositories\IServerPrivateKeyRepository; @@ -63,6 +66,7 @@ use Utils\Exceptions\UnacquiredLockException; use utils\json_types\JsonValue; use utils\json_types\NumericDate; use utils\json_types\StringOrURI; +use Utils\Model\Identifier; use Utils\Services\IAuthService; use Utils\Services\ICacheService; use Utils\Services\IdentifierGenerator; @@ -70,6 +74,7 @@ use Utils\Services\ILockManagerService; use Utils\Services\IServerConfigurationService; use Zend\Crypt\Hash; use Exception; + /** * Class TokenService * Provides all Tokens related operations (create, get and revoke) @@ -119,12 +124,10 @@ final class TokenService extends AbstractService implements ITokenService * @var IdentifierGenerator */ private $auth_code_generator; - /** * @var IdentifierGenerator */ private $access_token_generator; - /** * @var IdentifierGenerator */ @@ -174,6 +177,10 @@ final class TokenService extends AbstractService implements ITokenService * @var IResourceServerRepository */ private $resource_server_repository; + /** + * @var IApiScopeService + */ + private $scope_service; /** * @var IUserIPHelperProvider @@ -192,7 +199,7 @@ final class TokenService extends AbstractService implements ITokenService IdentifierGenerator $access_token_generator, IdentifierGenerator $refresh_token_generator, IServerPrivateKeyRepository $server_private_key_repository, - IClientJWKSetReader $jwk_set_reader_service, + IClientJWKSetReader $jwk_set_reader_service, ISecurityContextService $security_context_service, IPrincipalService $principal_service, IdTokenBuilder $id_token_builder, @@ -201,30 +208,32 @@ final class TokenService extends AbstractService implements ITokenService IRefreshTokenRepository $refresh_token_repository, IResourceServerRepository $resource_server_repository, IUserIPHelperProvider $ip_helper, + IApiScopeService $scope_service, ITransactionService $tx_service ) { parent::__construct($tx_service); - $this->client_service = $client_service; - $this->lock_manager_service = $lock_manager_service; - $this->configuration_service = $configuration_service; - $this->cache_service = $cache_service; - $this->auth_service = $auth_service; - $this->user_consent_service = $user_consent_service; - $this->auth_code_generator = $auth_code_generator; - $this->access_token_generator = $access_token_generator; - $this->refresh_token_generator = $refresh_token_generator; + $this->client_service = $client_service; + $this->lock_manager_service = $lock_manager_service; + $this->configuration_service = $configuration_service; + $this->cache_service = $cache_service; + $this->auth_service = $auth_service; + $this->user_consent_service = $user_consent_service; + $this->auth_code_generator = $auth_code_generator; + $this->access_token_generator = $access_token_generator; + $this->refresh_token_generator = $refresh_token_generator; $this->server_private_key_repository = $server_private_key_repository; - $this->jwk_set_reader_service = $jwk_set_reader_service; - $this->security_context_service = $security_context_service; - $this->principal_service = $principal_service; - $this->id_token_builder = $id_token_builder; - $this->client_repository = $client_repository; - $this->access_token_repository = $access_token_repository; - $this->refresh_token_repository = $refresh_token_repository; - $this->resource_server_repository = $resource_server_repository; - $this->ip_helper = $ip_helper; + $this->jwk_set_reader_service = $jwk_set_reader_service; + $this->security_context_service = $security_context_service; + $this->principal_service = $principal_service; + $this->id_token_builder = $id_token_builder; + $this->client_repository = $client_repository; + $this->access_token_repository = $access_token_repository; + $this->refresh_token_repository = $refresh_token_repository; + $this->resource_server_repository = $resource_server_repository; + $this->ip_helper = $ip_helper; + $this->scope_service = $scope_service; Event::listen('oauth2.client.delete', function ($client_id) { $this->revokeClientRelatedTokens($client_id); @@ -237,87 +246,68 @@ final class TokenService extends AbstractService implements ITokenService /** * Creates a brand new authorization code - * @param $user_id - * @param $client_id - * @param $scope - * @param string $audience - * @param null $redirect_uri - * @param string $access_type - * @param string $approval_prompt + * @param OAuth2AuthorizationRequest $request * @param bool $has_previous_user_consent - * @param string|null $state - * @param string|null $nonce - * @param string|null $response_type - * @param string|null $prompt - * @return AuthorizationCode + * @return Identifier */ public function createAuthorizationCode ( - $user_id, - $client_id, - $scope, - $audience = '' , - $redirect_uri = null, - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, - $has_previous_user_consent = false, - $state = null, - $nonce = null, - $response_type = null, - $prompt = null - ) + OAuth2AuthorizationRequest $request, + bool $has_previous_user_consent = false + ): Identifier { - //create model + + $user = $this->auth_service->getCurrentUser(); + // build current audience ... + $audience = $this->scope_service->getStrAudienceByScopeNames + ( + explode + ( + OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, + $request->getScope() + ) + ); + + $nonce = null; + $prompt = null; + + if ($request instanceof OAuth2AuthenticationRequest) { + $nonce = $request->getNonce(); + $prompt = $request->getPrompt(true); + } $code = $this->auth_code_generator->generate ( AuthorizationCode::create ( - $user_id, - $client_id, - $scope, + $user->getId(), + $request->getClientId(), + $request->getScope(), $audience, - $redirect_uri, - $access_type, - $approval_prompt, $has_previous_user_consent, + $request->getRedirectUri(), + $request->getAccessType(), + $request->getApprovalPrompt(), + $has_previous_user_consent, $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'), - $state, + $request->getState(), $nonce, - $response_type, + $request->getResponseType(), $this->security_context_service->get()->isAuthTimeRequired(), $this->principal_service->get()->getAuthTime(), - $prompt + $prompt, + $request->getCodeChallenge(), + $request->getCodeChallengeMethod() ) ); $hashed_value = Hash::compute('sha256', $code->getValue()); //stores on cache - $this->cache_service->storeHash($hashed_value, - array - ( - 'client_id' => $code->getClientId(), - 'scope' => $code->getScope(), - 'audience' => $code->getAudience(), - 'redirect_uri' => $code->getRedirectUri(), - 'issued' => $code->getIssued(), - 'lifetime' => $code->getLifetime(), - 'from_ip' => $code->getFromIp(), - 'user_id' => $code->getUserId(), - 'access_type' => $code->getAccessType(), - 'approval_prompt' => $code->getApprovalPrompt(), - 'has_previous_user_consent' => $code->getHasPreviousUserConsent(), - 'state' => $code->getState(), - 'nonce' => $code->getNonce(), - 'response_type' => $code->getResponseType(), - 'requested_auth_time' => $code->isAuthTimeRequested(), - 'auth_time' => $code->getAuthTime(), - 'prompt' => $code->getPrompt(), - ), intval($code->getLifetime())); + $this->cache_service->storeHash($hashed_value, $code->toArray(), intval($code->getLifetime())); //stores brand new auth code hash value on a set by client id... - $this->cache_service->addMemberSet($client_id . self::ClientAuthCodePrefixList, $hashed_value); + $this->cache_service->addMemberSet($request->getClientId() . self::ClientAuthCodePrefixList, $hashed_value); - $this->cache_service->incCounter($client_id . self::ClientAuthCodeQty, self::ClientAuthCodeQtyLifetime); + $this->cache_service->incCounter($request->getClientId() . self::ClientAuthCodeQty, self::ClientAuthCodeQtyLifetime); return $code; } @@ -333,61 +323,16 @@ final class TokenService extends AbstractService implements ITokenService $hashed_value = Hash::compute('sha256', $value); - if (!$this->cache_service->exists($hashed_value)) - { + if (!$this->cache_service->exists($hashed_value)) { throw new InvalidAuthorizationCodeException(sprintf("auth_code %s ", $value)); } - try - { + try { $this->lock_manager_service->acquireLock('lock.get.authcode.' . $hashed_value); - - $cache_values = $this->cache_service->getHash($hashed_value, [ - 'user_id', - 'client_id', - 'scope', - 'audience', - 'redirect_uri', - 'issued', - 'lifetime', - 'from_ip', - 'access_type', - 'approval_prompt', - 'has_previous_user_consent', - 'state', - 'nonce', - 'response_type', - 'requested_auth_time', - 'auth_time', - 'prompt', - ]); - - $code = AuthorizationCode::load - ( - $value, - $cache_values['user_id'], - $cache_values['client_id'], - $cache_values['scope'], - $cache_values['audience'], - $cache_values['redirect_uri'], - $cache_values['issued'], - $cache_values['lifetime'], - $cache_values['from_ip'], - $cache_values['access_type'], - $cache_values['approval_prompt'], - $cache_values['has_previous_user_consent'], - $cache_values['state'], - $cache_values['nonce'], - $cache_values['response_type'], - $cache_values['requested_auth_time'], - $cache_values['auth_time'], - $cache_values['prompt'] - ); - - return $code; - } - catch (UnacquiredLockException $ex1) - { + $payload = $this->cache_service->getHash($hashed_value, AuthorizationCode::getKeys()); + $payload['value'] = $value; + return AuthorizationCode::load($payload); + } catch (UnacquiredLockException $ex1) { throw new ReplayAttackAuthCodeException ( $value, @@ -413,8 +358,8 @@ final class TokenService extends AbstractService implements ITokenService ( AccessToken::create ( - $auth_code, - $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') + $auth_code, + $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') ) ); @@ -424,13 +369,13 @@ final class TokenService extends AbstractService implements ITokenService $access_token ) { - $value = $access_token->getValue(); + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); //oauth2 client id - $client_id = $access_token->getClientId(); - $user_id = $access_token->getUserId(); - $client = $this->client_repository->getClientById($client_id); - $user = $this->auth_service->getUserById($user_id); + $client_id = $access_token->getClientId(); + $user_id = $access_token->getUserId(); + $client = $this->client_repository->getClientById($client_id); + $user = $this->auth_service->getUserById($user_id); // TODO; move to a factory @@ -451,14 +396,14 @@ final class TokenService extends AbstractService implements ITokenService ( sprintf ( - 'use_refresh_token: %s - app_type: %s - scopes: %s - auth_code_access_type: %s - prompt: %s - approval_prompt: %s', + 'TokenService::createAccessToken use_refresh_token: %s - app_type: %s - scopes: %s - auth_code_access_type: %s - prompt: %s - approval_prompt: %s pkce %s.', $client->useRefreshToken(), $client->getApplicationType(), $auth_code->getScope(), $auth_code->getAccessType(), $auth_code->getPrompt(), - $auth_code->getApprovalPrompt() - + $auth_code->getApprovalPrompt(), + $client->isPKCEEnabled() ) ); @@ -467,26 +412,25 @@ final class TokenService extends AbstractService implements ITokenService $client->useRefreshToken() && ( $client->getApplicationType() == IClient::ApplicationType_Web_App || - $client->getApplicationType() == IClient::ApplicationType_Native + $client->getApplicationType() == IClient::ApplicationType_Native || + $client->isPKCEEnabled() ) && ( $auth_code->getAccessType() == OAuth2Protocol::OAuth2Protocol_AccessType_Offline || //OIDC: http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess str_contains($auth_code->getScope(), OAuth2Protocol::OfflineAccess_Scope) ) - ) - { + ) { //but only the first time (approval_prompt == force || not exists previous consent) if ( !$auth_code->getHasPreviousUserConsent() || - // google oauth2 protocol - strpos($auth_code->getApprovalPrompt(),OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force) !== false || - // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess - strpos($auth_code->getPrompt(), OAuth2Protocol::OAuth2Protocol_Prompt_Consent) !== false - ) - { - Log::debug('creating refresh token ....'); + // google oauth2 protocol + strpos($auth_code->getApprovalPrompt(), OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force) !== false || + // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess + strpos($auth_code->getPrompt(), OAuth2Protocol::OAuth2Protocol_Prompt_Consent) !== false + ) { + Log::debug('TokenService::createAccessToken creating refresh token ....'); $this->createRefreshToken($access_token); } } @@ -518,14 +462,14 @@ final class TokenService extends AbstractService implements ITokenService public function createAccessTokenFromParams($client_id, $scope, $audience, $user_id = null) { - $access_token = $this->access_token_generator->generate(AccessToken::createFromParams - ( - $scope, - $client_id, - $audience, - $user_id, - $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') - ) + $access_token = $this->access_token_generator->generate(AccessToken::createFromParams + ( + $scope, + $client_id, + $audience, + $user_id, + $this->configuration_service->getConfigValue('OAuth2.AccessToken.Lifetime') + ) ); @@ -538,13 +482,13 @@ final class TokenService extends AbstractService implements ITokenService ) { - $value = $access_token->getValue(); + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); $this->storesAccessTokenOnCache($access_token); $client_id = $access_token->getClientId(); - $client = $this->client_repository->getClientById($client_id); + $client = $this->client_repository->getClientById($client_id); // todo: move to a factory @@ -586,21 +530,19 @@ final class TokenService extends AbstractService implements ITokenService $scope ) { - $refresh_token_value = $refresh_token->getValue(); - $refresh_token_hashed_value = Hash::compute('sha256', $refresh_token_value); + $refresh_token_value = $refresh_token->getValue(); + $refresh_token_hashed_value = Hash::compute('sha256', $refresh_token_value); //clear current access tokens as invalid $this->clearAccessTokensForRefreshToken($refresh_token->getValue()); //validate scope if present... - if (!is_null($scope) && empty($scope)) - { - $original_scope = $refresh_token->getScope(); + if (!is_null($scope) && empty($scope)) { + $original_scope = $refresh_token->getScope(); $aux_original_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $original_scope); - $aux_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $scope); + $aux_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $scope); //compare original scope with given one, and validate if its included on original one //or not - if (count(array_diff($aux_scope, $aux_original_scope)) !== 0) - { + if (count(array_diff($aux_scope, $aux_original_scope)) !== 0) { throw new InvalidGrantTypeException ( sprintf @@ -611,9 +553,7 @@ final class TokenService extends AbstractService implements ITokenService ) ); } - } - else - { + } else { //get original scope $scope = $refresh_token->getScope(); } @@ -629,7 +569,7 @@ final class TokenService extends AbstractService implements ITokenService ) ); - $value = $access_token->getValue(); + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); $this->storesAccessTokenOnCache($access_token); @@ -638,7 +578,7 @@ final class TokenService extends AbstractService implements ITokenService $user_id = $access_token->getUserId(); //get current client $client_id = $access_token->getClientId(); - $client = $this->client_repository->getClientById($client_id); + $client = $this->client_repository->getClientById($client_id); //todo : move to a factory @@ -654,8 +594,7 @@ final class TokenService extends AbstractService implements ITokenService $access_token_db->setRefreshToken($refresh_token_db); $access_token_db->setClient($client); - if (!is_null($user_id)) - { + if (!is_null($user_id)) { $user = $this->auth_service->getUserById($user_id); $access_token_db->setOwner($user); } @@ -677,8 +616,9 @@ final class TokenService extends AbstractService implements ITokenService * @param AccessToken $access_token * @return bool */ - private function clearAccessTokenOnCache(AccessToken $access_token){ - $value = $access_token->getValue(); + private function clearAccessTokenOnCache(AccessToken $access_token) + { + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); if ($this->cache_service->exists($hashed_value)) { @@ -696,7 +636,7 @@ final class TokenService extends AbstractService implements ITokenService { //stores in REDIS - $value = $access_token->getValue(); + $value = $access_token->getValue(); $hashed_value = Hash::compute('sha256', $value); if ($this->cache_service->exists($hashed_value)) { @@ -712,14 +652,14 @@ final class TokenService extends AbstractService implements ITokenService $user_id = !is_null($access_token->getUserId()) ? $access_token->getUserId() : 0; $this->cache_service->storeHash($hashed_value, [ - 'user_id' => $user_id, - 'client_id' => $access_token->getClientId(), - 'scope' => $access_token->getScope(), - 'auth_code' => $auth_code, - 'issued' => $access_token->getIssued(), - 'lifetime' => $access_token->getLifetime(), - 'audience' => $access_token->getAudience(), - 'from_ip' => $this->ip_helper->getCurrentUserIpAddress(), + 'user_id' => $user_id, + 'client_id' => $access_token->getClientId(), + 'scope' => $access_token->getScope(), + 'auth_code' => $auth_code, + 'issued' => $access_token->getIssued(), + 'lifetime' => $access_token->getLifetime(), + 'audience' => $access_token->getAudience(), + 'from_ip' => $this->ip_helper->getCurrentUserIpAddress(), 'refresh_token' => $refresh_token_value ], intval($access_token->getLifetime())); } @@ -728,7 +668,8 @@ final class TokenService extends AbstractService implements ITokenService * @param AccessTokenDB $access_token * @return bool */ - private function clearAccessTokenDBOnCache(AccessTokenDB $access_token){ + private function clearAccessTokenDBOnCache(AccessTokenDB $access_token) + { if ($this->cache_service->exists($access_token->getValue())) { $this->cache_service->delete($access_token->getValue()); @@ -750,26 +691,26 @@ final class TokenService extends AbstractService implements ITokenService } $refresh_token_value = ''; - $refresh_token_db = $access_token->getRefreshToken(); + $refresh_token_db = $access_token->getRefreshToken(); if (!is_null($refresh_token_db)) { $refresh_token_value = $refresh_token_db->getValue(); } $user_id = $access_token->getOwnerId(); - $client = $access_token->getClient(); + $client = $access_token->getClient(); $this->cache_service->storeHash($access_token->getValue(), [ - 'user_id' => $user_id, - 'client_id' => $client->getClientId(), - 'scope' => $access_token->getScope(), - 'auth_code' => $access_token->getAssociatedAuthorizationCode(), - 'issued' => $access_token->getCreatedAt()->format("Y-m-d H:i:s"), - 'lifetime' => $access_token->getLifetime(), - 'from_ip' => $access_token->getFromIp(), - 'audience' => $access_token->getAudience(), - 'refresh_token' => $refresh_token_value - ], intval($access_token->getLifetime())); + 'user_id' => $user_id, + 'client_id' => $client->getClientId(), + 'scope' => $access_token->getScope(), + 'auth_code' => $access_token->getAssociatedAuthorizationCode(), + 'issued' => $access_token->getCreatedAt()->format("Y-m-d H:i:s"), + 'lifetime' => $access_token->getLifetime(), + 'from_ip' => $access_token->getFromIp(), + 'audience' => $access_token->getAudience(), + 'refresh_token' => $refresh_token_value + ], intval($access_token->getLifetime())); } /** @@ -790,32 +731,24 @@ final class TokenService extends AbstractService implements ITokenService $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $access_token = null; - try - { + try { // check cache ... - if (!$this->cache_service->exists($hashed_value)) - { - $this->lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function() use($value, $hashed_value){ + if (!$this->cache_service->exists($hashed_value)) { + $this->lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function () use ($value, $hashed_value) { // check on DB... $access_token_db = $this->access_token_repository->getByValue($hashed_value); - if (is_null($access_token_db)) - { - if($this->isAccessTokenRevoked($hashed_value)) - { + if (is_null($access_token_db)) { + if ($this->isAccessTokenRevoked($hashed_value)) { throw new RevokedAccessTokenException(sprintf('Access token %s is revoked!', $value)); - } - else if($this->isAccessTokenVoid($hashed_value)) // check if its marked on cache as expired ... + } else if ($this->isAccessTokenVoid($hashed_value)) // check if its marked on cache as expired ... { throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value)); - } - else - { + } else { throw new InvalidGrantTypeException(sprintf("Access token %s is invalid!", $value)); } } - if ($access_token_db->isVoid()) - { + if ($access_token_db->isVoid()) { // invalid one ... throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value)); } @@ -824,55 +757,45 @@ final class TokenService extends AbstractService implements ITokenService }); } - $cache_values = $this->cache_service->getHash($hashed_value,[ - 'user_id', - 'client_id', - 'scope', - 'auth_code', - 'issued', - 'lifetime', - 'from_ip', - 'audience', - 'refresh_token' + $payload = $this->cache_service->getHash($hashed_value, [ + 'user_id', + 'client_id', + 'scope', + 'auth_code', + 'issued', + 'lifetime', + 'from_ip', + 'audience', + 'refresh_token' ]); // reload auth code ... - $auth_code = AuthorizationCode::load - ( - $cache_values['auth_code'], - intval($cache_values['user_id']) == 0 ? null : intval($cache_values['user_id']), - $cache_values['client_id'], - $cache_values['scope'], - $cache_values['audience'], - null, - null, - $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'), - $cache_values['from_ip'], - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, - $has_previous_user_consent = false, - null, - null, - $is_hashed = true - ); + $payload['value'] = $payload['auth_code']; + + $payload['user_id'] = intval($payload['user_id']) == 0 ? null : intval($payload['user_id']); + $payload['lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'); + $payload['access_type'] = OAuth2Protocol::OAuth2Protocol_AccessType_Online; + $payload['approval_prompt'] = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto; + $payload['has_previous_user_consent'] = false; + $payload['is_hashed'] = true; + $auth_code = AuthorizationCode::load($payload); + // reload access token ... $access_token = AccessToken::load ( $value, $auth_code, - $cache_values['issued'], - $cache_values['lifetime'] + $payload['issued'], + $payload['lifetime'] ); - $refresh_token_value = $cache_values['refresh_token']; + $refresh_token_value = $payload['refresh_token']; if (!empty($refresh_token_value)) { $refresh_token = $this->getRefreshToken($refresh_token_value, true); $access_token->setRefreshToken($refresh_token); } - } - catch (UnacquiredLockException $ex1) - { + } catch (UnacquiredLockException $ex1) { throw new InvalidAccessTokenException(sprintf("access token %s ", $value)); } return $access_token; @@ -919,13 +842,13 @@ final class TokenService extends AbstractService implements ITokenService $access_token, $refresh_cache ) { - $value = $refresh_token->getValue(); + $value = $refresh_token->getValue(); //hash the given value, bc tokens values are stored hashed on DB $hashed_value = Hash::compute('sha256', $value); - $client_id = $refresh_token->getClientId(); - $user_id = $refresh_token->getUserId(); - $client = $this->client_repository->getClientById($client_id); - $user = $this->auth_service->getUserById($user_id); + $client_id = $refresh_token->getClientId(); + $user_id = $refresh_token->getUserId(); + $client = $this->client_repository->getClientById($client_id); + $user = $this->auth_service->getUserById($user_id); // todo: move to a factory $refresh_token_db = new RefreshTokenDB; @@ -944,10 +867,10 @@ final class TokenService extends AbstractService implements ITokenService $access_token->setRefreshToken($refresh_token); // bc refresh token could change - if($refresh_cache) { - if($this->clearAccessTokenOnCache($access_token)) + if ($refresh_cache) { + if ($this->clearAccessTokenOnCache($access_token)) $this->storesAccessTokenOnCache($access_token); - if($this->clearAccessTokenDBOnCache($access_token_db)) + if ($this->clearAccessTokenDBOnCache($access_token_db)) $this->storeAccessTokenDBOnCache($access_token_db); } @@ -973,20 +896,18 @@ final class TokenService extends AbstractService implements ITokenService public function getRefreshToken($value, $is_hashed = false) { //hash the given value, bc tokens values are stored hashed on DB - $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; + $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $refresh_token_db = $this->refresh_token_repository->getByValue($hashed_value); - if (is_null($refresh_token_db)) - { - if($this->isRefreshTokenRevoked($hashed_value)) + if (is_null($refresh_token_db)) { + if ($this->isRefreshTokenRevoked($hashed_value)) throw new RevokedRefreshTokenException(sprintf("revoked refresh token %s !", $value)); throw new InvalidGrantTypeException(sprintf("refresh token %s does not exists!", $value)); } - if ($refresh_token_db->isVoid()) - { + if ($refresh_token_db->isVoid()) { throw new ReplayAttackRefreshTokenException ( $value, @@ -999,8 +920,7 @@ final class TokenService extends AbstractService implements ITokenService } //check is refresh token is stills alive... (ZERO is infinite lifetime) - if ($refresh_token_db->isVoid()) - { + if ($refresh_token_db->isVoid()) { throw new InvalidGrantTypeException(sprintf("refresh token %s is expired!", $value)); } @@ -1009,13 +929,13 @@ final class TokenService extends AbstractService implements ITokenService $refresh_token = RefreshToken::load ( [ - 'value' => $value, - 'scope' => $refresh_token_db->getScope(), + 'value' => $value, + 'scope' => $refresh_token_db->getScope(), 'client_id' => $client->getClientId(), - 'user_id' => $refresh_token_db->getOwnerId(), - 'audience' => $refresh_token_db->getAudience(), - 'from_ip' => $refresh_token_db->getFromIp(), - 'issued' => $refresh_token_db->getCreatedAt()->format("Y-m-d H:i:s"), + 'user_id' => $refresh_token_db->getOwnerId(), + 'audience' => $refresh_token_db->getAudience(), + 'from_ip' => $refresh_token_db->getFromIp(), + 'issued' => $refresh_token_db->getCreatedAt()->format("Y-m-d H:i:s"), 'is_hashed' => $is_hashed ], intval($refresh_token_db->getLifetime()) @@ -1031,26 +951,25 @@ final class TokenService extends AbstractService implements ITokenService */ public function revokeAuthCodeRelatedTokens($auth_code) { - $auth_code_hashed_value = Hash::compute('sha256', $auth_code); + $auth_code_hashed_value = Hash::compute('sha256', $auth_code); - $this->tx_service->transaction(function () use - ( + $this->tx_service->transaction(function () use ( $auth_code_hashed_value ) { //get related access tokens - $db_access_token = $this->access_token_repository->getByAuthCode($auth_code_hashed_value); - if(is_null($db_access_token)) return; + $db_access_token = $this->access_token_repository->getByAuthCode($auth_code_hashed_value); + if (is_null($db_access_token)) return; - $client = $db_access_token->getClient(); + $client = $db_access_token->getClient(); $access_token_value = $db_access_token->getValue(); - $refresh_token_db = $db_access_token->getRefreshToken(); + $refresh_token_db = $db_access_token->getRefreshToken(); //remove auth code from client list on cache $this->cache_service->deleteMemberSet ( $client->getClientId() . TokenService::ClientAuthCodePrefixList, $auth_code_hashed_value ); - //remove access token from client list on cache + //remove access token from client list on cache $this->cache_service->deleteMemberSet ( $client->getClientId() . TokenService::ClientAccessTokenPrefixList, @@ -1078,8 +997,7 @@ final class TokenService extends AbstractService implements ITokenService public function revokeAccessToken($value, $is_hashed = false, ?User $current_user = null) { - return $this->tx_service->transaction(function () use - ( + return $this->tx_service->transaction(function () use ( $value, $is_hashed, $current_user @@ -1090,13 +1008,13 @@ final class TokenService extends AbstractService implements ITokenService $access_token_db = $this->access_token_repository->getByValue($hashed_value); - if(is_null($access_token_db)) return false; + if (is_null($access_token_db)) return false; - if(!is_null($current_user) && !$current_user->belongToGroup(IGroupSlugs::SuperAdminGroup) && $access_token_db->hasOwner() && $access_token_db->getOwnerId() != $current_user->getId()){ - throw new ValidationException(sprintf("access token %s does not belongs to user id %s!.",$value, $current_user->getId())); + if (!is_null($current_user) && !$current_user->belongToGroup(IGroupSlugs::SuperAdminGroup) && $access_token_db->hasOwner() && $access_token_db->getOwnerId() != $current_user->getId()) { + throw new ValidationException(sprintf("access token %s does not belongs to user id %s!.", $value, $current_user->getId())); } - $client = $access_token_db->getClient(); + $client = $access_token_db->getClient(); //delete from cache $this->cache_service->delete($hashed_value); $this->cache_service->deleteMemberSet @@ -1122,8 +1040,7 @@ final class TokenService extends AbstractService implements ITokenService */ public function expireAccessToken($value, $is_hashed = false) { - return $this->tx_service->transaction(function () use - ( + return $this->tx_service->transaction(function () use ( $value, $is_hashed ) { @@ -1132,9 +1049,9 @@ final class TokenService extends AbstractService implements ITokenService $access_token_db = $this->access_token_repository->getByValue($hashed_value); - if(is_null($access_token_db)) return false; + if (is_null($access_token_db)) return false; - $client = $access_token_db->getClient(); + $client = $access_token_db->getClient(); //delete from cache $this->cache_service->delete($hashed_value); $this->cache_service->deleteMemberSet @@ -1163,27 +1080,24 @@ final class TokenService extends AbstractService implements ITokenService $client_id ) { //get client auth codes - $auth_codes = $this->cache_service->getSet($client_id . self::ClientAuthCodePrefixList); + $auth_codes = $this->cache_service->getSet($client_id . self::ClientAuthCodePrefixList); //get client access tokens - $access_tokens = $this->cache_service->getSet($client_id . self::ClientAccessTokenPrefixList); + $access_tokens = $this->cache_service->getSet($client_id . self::ClientAccessTokenPrefixList); $client = $this->client_repository->getClientById($client_id); - if (is_null($client)) - { + if (is_null($client)) { return; } //revoke on cache $this->cache_service->deleteArray($auth_codes); $this->cache_service->deleteArray($access_tokens); //revoke on db - foreach($client->getValidAccessTokens() as $at) - { + foreach ($client->getValidAccessTokens() as $at) { $this->markAccessTokenAsRevoked($at->getValue()); } - foreach($client->getRefreshTokens() as $rt) - { + foreach ($client->getRefreshTokens() as $rt) { $this->markRefreshTokenAsRevoked($rt->getValue()); } @@ -1202,8 +1116,8 @@ final class TokenService extends AbstractService implements ITokenService { $this->cache_service->addSingleValue ( - 'access.token:revoked:'.$at_hash, - 'access.token:revoked:'.$at_hash, + 'access.token:revoked:' . $at_hash, + 'access.token:revoked:' . $at_hash, $this->configuration_service->getConfigValue('OAuth2.AccessToken.Revoked.Lifetime') ); } @@ -1215,8 +1129,8 @@ final class TokenService extends AbstractService implements ITokenService { $this->cache_service->addSingleValue ( - 'access.token:void:'.$at_hash, - 'access.token:void:'.$at_hash, + 'access.token:void:' . $at_hash, + 'access.token:void:' . $at_hash, $this->configuration_service->getConfigValue('OAuth2.AccessToken.Void.Lifetime') ); } @@ -1228,8 +1142,8 @@ final class TokenService extends AbstractService implements ITokenService { $this->cache_service->addSingleValue ( - 'refresh.token:revoked:'.$rt_hash, - 'refresh.token:revoked:'.$rt_hash, + 'refresh.token:revoked:' . $rt_hash, + 'refresh.token:revoked:' . $rt_hash, $this->configuration_service->getConfigValue('OAuth2.RefreshToken.Revoked.Lifetime') ); } @@ -1271,11 +1185,11 @@ final class TokenService extends AbstractService implements ITokenService public function invalidateRefreshToken(string $value, bool $is_hashed = false, ?User $current_user = null) { return $this->tx_service->transaction(function () use ($value, $is_hashed, $current_user) { - $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; + $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $refresh_token = $this->refresh_token_repository->getByValue($hashed_value); - if(is_null($refresh_token)) return false; - if(!is_null($current_user) && !$current_user->belongToGroup(IGroupSlugs::SuperAdminGroup) && $refresh_token->hasOwner() && $refresh_token->getOwnerId() != $current_user->getId()){ - throw new ValidationException(sprintf("refresh token %s does not belongs to user id %s!.",$value, $current_user->getId())); + if (is_null($refresh_token)) return false; + if (!is_null($current_user) && !$current_user->belongToGroup(IGroupSlugs::SuperAdminGroup) && $refresh_token->hasOwner() && $refresh_token->getOwnerId() != $current_user->getId()) { + throw new ValidationException(sprintf("refresh token %s does not belongs to user id %s!.", $value, $current_user->getId())); } $refresh_token->setVoid(); @@ -1311,23 +1225,20 @@ final class TokenService extends AbstractService implements ITokenService public function clearAccessTokensForRefreshToken($value, $is_hashed = false) { - $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; + $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; - return $this->tx_service->transaction(function () use - ( + return $this->tx_service->transaction(function () use ( $hashed_value ) { $refresh_token_db = $this->refresh_token_repository->getByValue($hashed_value); - if (!is_null($refresh_token_db)) - { + if (!is_null($refresh_token_db)) { $access_tokens_db = $this->access_token_repository->getByRefreshToken($refresh_token_db->getId()); if (count($access_tokens_db) == 0) return false; - foreach ($access_tokens_db as $access_token_db) - { + foreach ($access_tokens_db as $access_token_db) { $this->cache_service->delete($access_token_db->getValue()); $client = $access_token_db->getClient(); @@ -1364,18 +1275,17 @@ final class TokenService extends AbstractService implements ITokenService ( $nonce, $client_id, - AccessToken $access_token = null, + AccessToken $access_token = null, AuthorizationCode $auth_code = null ) { - $issuer = $this->configuration_service->getSiteUrl(); - if(empty($issuer)) throw new ConfigurationException('missing idp url'); + $issuer = $this->configuration_service->getSiteUrl(); + if (empty($issuer)) throw new ConfigurationException('missing idp url'); - $client = $this->client_repository->getClientById($client_id); + $client = $this->client_repository->getClientById($client_id); $id_token_lifetime = $this->configuration_service->getConfigValue('OAuth2.IdToken.Lifetime'); - if (is_null($client)) - { + if (is_null($client)) { throw new AbsentClientException ( sprintf @@ -1385,23 +1295,23 @@ final class TokenService extends AbstractService implements ITokenService ) ); } - + $user = $this->auth_service->getCurrentUser(); - if(is_null($user)){ + if (is_null($user)) { $user_id = $this->principal_service->get()->getUserId(); Log::debug(sprintf("user id is %s", $user_id)); $user = $this->auth_service->getUserById($user_id); } - if(is_null($user)) + if (is_null($user)) throw new AbsentCurrentUserException; - if(!$user instanceof User) + if (!$user instanceof User) throw new AbsentCurrentUserException; // build claim set - $epoch_now = time(); + $epoch_now = time(); $jti = $this->auth_service->generateJTI($client_id, $id_token_lifetime); @@ -1428,17 +1338,17 @@ final class TokenService extends AbstractService implements ITokenService UserService::populateAddressClaims($claim_set, $user); UserService::populateEmailClaims($claim_set, $user); - if(!empty($nonce)) + if (!empty($nonce)) $claim_set->addClaim(new JWTClaim(OAuth2Protocol::OAuth2Protocol_Nonce, new StringOrURI($nonce))); $id_token_response_info = $client->getIdTokenResponseInfo(); - $sig_alg = $id_token_response_info->getSigningAlgorithm(); + $sig_alg = $id_token_response_info->getSigningAlgorithm(); - if(!is_null($sig_alg) && !is_null($access_token)) - $this->buildAccessTokenHashClaim($access_token, $sig_alg , $claim_set); + if (!is_null($sig_alg) && !is_null($access_token)) + $this->buildAccessTokenHashClaim($access_token, $sig_alg, $claim_set); - if(!is_null($sig_alg) && !is_null($auth_code)) - $this->buildAuthCodeHashClaim($auth_code, $sig_alg , $claim_set); + if (!is_null($sig_alg) && !is_null($auth_code)) + $this->buildAuthCodeHashClaim($auth_code, $sig_alg, $claim_set); $this->buildAuthTimeClaim($claim_set); @@ -1460,11 +1370,11 @@ final class TokenService extends AbstractService implements ITokenService JWTClaimSet $claim_set ) { - $at = $access_token->getValue(); - $at_len = $hashing_alg->getHashKeyLen() / 2 ; - $encoder = new Base64UrlRepresentation(); + $at = $access_token->getValue(); + $at_len = $hashing_alg->getHashKeyLen() / 2; + $encoder = new Base64UrlRepresentation(); - if($at_len > ByteUtil::bitLength(strlen($at))) + if ($at_len > ByteUtil::bitLength(strlen($at))) throw new InvalidClientCredentials('invalid access token length!.'); $claim_set->addClaim @@ -1511,11 +1421,11 @@ final class TokenService extends AbstractService implements ITokenService ) { - $ac = $auth_code->getValue(); - $ac_len = $hashing_alg->getHashKeyLen() / 2 ; + $ac = $auth_code->getValue(); + $ac_len = $hashing_alg->getHashKeyLen() / 2; $encoder = new Base64UrlRepresentation(); - if($ac_len > ByteUtil::bitLength(strlen($ac))) + if ($ac_len > ByteUtil::bitLength(strlen($ac))) throw new InvalidClientCredentials('invalid auth code length!.'); $claim_set->addClaim @@ -1548,8 +1458,7 @@ final class TokenService extends AbstractService implements ITokenService private function buildAuthTimeClaim(JWTClaimSet $claim_set) { - if($this->security_context_service->get()->isAuthTimeRequired()) - { + if ($this->security_context_service->get()->isAuthTimeRequired()) { $claim_set->addClaim ( new JWTClaim @@ -1572,7 +1481,7 @@ final class TokenService extends AbstractService implements ITokenService { $auth_code_value = Hash::compute('sha256', $auth_code->getValue()); $db_access_token = $this->access_token_repository->getByAuthCode($auth_code_value); - if(is_null($db_access_token)) return null; + if (is_null($db_access_token)) return null; return $this->getAccessToken($db_access_token->getValue(), true); } diff --git a/app/libs/OAuth2/Endpoints/TokenEndpoint.php b/app/libs/OAuth2/Endpoints/TokenEndpoint.php index 55464074..84391235 100644 --- a/app/libs/OAuth2/Endpoints/TokenEndpoint.php +++ b/app/libs/OAuth2/Endpoints/TokenEndpoint.php @@ -20,7 +20,7 @@ use OAuth2\Responses\OAuth2Response; * Class TokenEndpoint * Token Endpoint Implementation * The token endpoint is used by the client to obtain an access token by - * presenting its authorization grant or refresh token. The token + * presenting its authorization grant or refresh token. The token * endpoint is used with every authorization grant except for the * implicit grant type (since an access token is issued directly). * @see http://tools.ietf.org/html/rfc6749#section-3.2 diff --git a/app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php b/app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php new file mode 100644 index 00000000..8823f118 --- /dev/null +++ b/app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php @@ -0,0 +1,18 @@ +getResponseType() ); + $is_hybrid_flow = OAuth2Protocol::responseTypeBelongsToFlow ( $response_type, diff --git a/app/libs/OAuth2/Factories/OAuth2PKCEValidationMethodFactory.php b/app/libs/OAuth2/Factories/OAuth2PKCEValidationMethodFactory.php new file mode 100644 index 00000000..7aba5714 --- /dev/null +++ b/app/libs/OAuth2/Factories/OAuth2PKCEValidationMethodFactory.php @@ -0,0 +1,69 @@ +getCodeChallenge(); + $code_challenge_method = $auth_code->getCodeChallengeMethod(); + + if(empty($code_challenge) || empty($code_challenge_method)){ + throw new InvalidOAuth2PKCERequest(sprintf("%s or %s missing", OAuth2Protocol::PKCE_CodeChallenge, OAuth2Protocol::PKCE_CodeChallengeMethod)); + } + + /** + * code_verifier = high-entropy cryptographic random STRING using the + * unreserved characters [A-Z] / [a-z] / [0-9] / "-" / "." / "_" / "~" + * from Section 2.3 of [RFC3986], with a minimum length of 43 characters + * and a maximum length of 128 characters. + */ + + $code_verifier = $request->getCodeVerifier(); + if(empty($code_verifier)) + throw new InvalidOAuth2PKCERequest(sprintf("%s param required", OAuth2Protocol::PKCE_CodeVerifier)); + $code_verifier_len = strlen($code_verifier); + if( $code_verifier_len < 43 || $code_verifier_len > 128) + throw new InvalidOAuth2PKCERequest(sprintf("%s param should have at least 43 and at most 128 characters.", OAuth2Protocol::PKCE_CodeVerifier)); + + switch ($code_challenge_method){ + case OAuth2Protocol::PKCE_CodeChallengeMethodPlain: + return new PKCEPlainValidator($code_challenge, $code_verifier); + break; + case OAuth2Protocol::PKCE_CodeChallengeMethodSHA256: + return new PKCES256Validator($code_challenge, $code_verifier); + break; + default: + throw new InvalidOAuth2PKCERequest(sprintf("invalid %s param", OAuth2Protocol::PKCE_CodeChallengeMethod)); + break; + } + } +} \ No newline at end of file diff --git a/app/libs/OAuth2/GrantTypes/AuthorizationCodeGrantType.php b/app/libs/OAuth2/GrantTypes/AuthorizationCodeGrantType.php index 4f7758b6..beaba0de 100644 --- a/app/libs/OAuth2/GrantTypes/AuthorizationCodeGrantType.php +++ b/app/libs/OAuth2/GrantTypes/AuthorizationCodeGrantType.php @@ -14,6 +14,7 @@ use App\libs\Utils\URLUtils; use Exception; +use Illuminate\Support\Facades\Log; use Models\OAuth2\Client; use OAuth2\Exceptions\ExpiredAuthorizationCodeException; use OAuth2\Exceptions\InvalidApplicationType; @@ -26,6 +27,7 @@ use OAuth2\Exceptions\OAuth2GenericException; use OAuth2\Exceptions\UnAuthorizedClientException; use OAuth2\Exceptions\UriNotAllowedException; use OAuth2\Factories\OAuth2AccessTokenResponseFactory; +use OAuth2\Factories\OAuth2PKCEValidationMethodFactory; use OAuth2\Models\IClient; use OAuth2\Repositories\IClientRepository; use OAuth2\Services\ITokenService; @@ -187,20 +189,20 @@ class AuthorizationCodeGrantType extends InteractiveGrantType try { parent::completeFlow($request); - - $this->checkClientTypeAccess($this->client_auth_context->getClient()); + $client = $this->client_auth_context->getClient(); + $this->checkClientTypeAccess($client); $current_redirect_uri = $request->getRedirectUri(); //verify redirect uri - if (!$this->current_client->isUriAllowed($current_redirect_uri)) + if (empty($current_redirect_uri) || !$this->current_client->isUriAllowed($current_redirect_uri)) { throw new UriNotAllowedException ( - $current_redirect_uri + empty($current_redirect_uri)? "missing" : $current_redirect_uri ); } - $code = $request->getCode(); + $code = $request->getCode(); // verify that the authorization code is valid // The client MUST NOT use the authorization code // more than once. If an authorization code is used more than @@ -244,12 +246,39 @@ class AuthorizationCodeGrantType extends InteractiveGrantType // "redirect_uri" parameter was included in the initial authorization // and if included ensure that their values are identical. $redirect_uri = $auth_code->getRedirectUri(); - + Log::debug(sprintf("AuthorizationCodeGrantType::completeFlow auth code redirect uri %s current_redirect_uri %s", $redirect_uri, $current_redirect_uri)); if (!empty($redirect_uri) && URLUtils::normalizeUrl($redirect_uri) !== URLUtils::normalizeUrl($current_redirect_uri)) { throw new UriNotAllowedException($current_redirect_uri); } + if($client->isPKCEEnabled()){ + /** + * PKCE Validation + * @see https://tools.ietf.org/html/rfc7636#page-10 + * @see https://oauth.net/2/pkce + * server Verifies code_verifier before Returning the Tokens + * If the "code_challenge_method" from Section 4.3 was "S256", the + * received "code_verifier" is hashed by SHA-256, base64url-encoded, and + * then compared to the "code_challenge", i.e.: + * BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge + * If the "code_challenge_method" from Section 4.3 was "plain", they are + * compared directly, i.e.: + * code_verifier == code_challenge. + * If the values are equal, the token endpoint MUST continue processing + * as normal (as defined by OAuth 2.0 + */ + + if(!$request instanceof OAuth2AccessTokenRequestAuthCode) + throw new InvalidOAuth2Request(); + + $strategy = OAuth2PKCEValidationMethodFactory::build($auth_code, $request); + + if(!$strategy->isValid()){ + throw new InvalidOAuth2Request("PKCE request can not be validated"); + } + } + $this->principal_service->register ( $auth_code->getUserId(), @@ -307,7 +336,8 @@ class AuthorizationCodeGrantType extends InteractiveGrantType ( !( $client->getClientType() === IClient::ClientType_Confidential || - $client->getApplicationType() === IClient::ApplicationType_Native + $client->getApplicationType() === IClient::ApplicationType_Native || + $client->isPKCEEnabled() ) ) { @@ -315,7 +345,7 @@ class AuthorizationCodeGrantType extends InteractiveGrantType ( sprintf ( - "client id %s - Application type must be %s or %s", + "client id %s - Application type must be %s or %s or have PKCE enabled", $client->getClientId(), IClient::ClientType_Confidential, IClient::ApplicationType_Native @@ -332,47 +362,17 @@ class AuthorizationCodeGrantType extends InteractiveGrantType */ protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent) { - $user = $this->auth_service->getCurrentUser(); - - // build current audience ... - $audience = $this->scope_service->getStrAudienceByScopeNames - ( - explode - ( - OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, - $request->getScope() - ) - ); - - $nonce = null; - $prompt = null; - - if($request instanceof OAuth2AuthenticationRequest) - { - $nonce = $request->getNonce(); - $prompt = $request->getPrompt(true); - } - $auth_code = $this->token_service->createAuthorizationCode ( - $user->getId(), - $request->getClientId(), - $request->getScope(), - $audience, - $request->getRedirectUri(), - $request->getAccessType(), - $request->getApprovalPrompt(), - $has_former_consent, - $request->getState(), - $nonce, - $request->getResponseType(), - $prompt + $request, + $has_former_consent ); if (is_null($auth_code)) { throw new OAuth2GenericException("Invalid Auth Code"); } + // http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions $session_state = $this->getSessionState ( @@ -394,6 +394,4 @@ class AuthorizationCodeGrantType extends InteractiveGrantType $session_state ); } - - } \ No newline at end of file diff --git a/app/libs/OAuth2/GrantTypes/HybridGrantType.php b/app/libs/OAuth2/GrantTypes/HybridGrantType.php index 71658712..e0a378d0 100644 --- a/app/libs/OAuth2/GrantTypes/HybridGrantType.php +++ b/app/libs/OAuth2/GrantTypes/HybridGrantType.php @@ -17,6 +17,7 @@ use OAuth2\Exceptions\InvalidApplicationType; use OAuth2\Exceptions\InvalidClientType; use OAuth2\Exceptions\InvalidOAuth2Request; use OAuth2\Exceptions\OAuth2GenericException; +use OAuth2\Models\AuthorizationCode; use OAuth2\Models\IClient; use OAuth2\Repositories\IClientRepository; use OAuth2\Services\ITokenService; @@ -181,28 +182,17 @@ class HybridGrantType extends InteractiveGrantType $auth_code = $this->token_service->createAuthorizationCode ( - $user->getId(), - $request->getClientId(), - $request->getScope(), - $audience, - $request->getRedirectUri(), - $request->getAccessType(), - $request->getApprovalPrompt(), - $has_former_consent, - $request->getState(), - $request->getNonce(), - $request->getResponseType(), - $request->getPrompt(true) + $request, + $has_former_consent ); - if (is_null($auth_code)) { - throw new OAuth2GenericException("Invalid Auth Code"); + if (is_null($auth_code) || !$auth_code instanceof AuthorizationCode) { + throw new OAuth2GenericException("Invalid Auth Code."); } $access_token = null; $id_token = null; - if (in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_Token, $request->getResponseType(false))) { $access_token = $this->token_service->createAccessToken diff --git a/app/libs/OAuth2/GrantTypes/RefreshBearerTokenGrantType.php b/app/libs/OAuth2/GrantTypes/RefreshBearerTokenGrantType.php index 173f112c..536070db 100644 --- a/app/libs/OAuth2/GrantTypes/RefreshBearerTokenGrantType.php +++ b/app/libs/OAuth2/GrantTypes/RefreshBearerTokenGrantType.php @@ -11,6 +11,7 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + use Exception; use OAuth2\Exceptions\InvalidApplicationType; use OAuth2\Exceptions\InvalidGrantTypeException; @@ -27,6 +28,7 @@ use OAuth2\Responses\OAuth2AccessTokenResponse; use OAuth2\Responses\OAuth2Response; use OAuth2\Services\IClientService; use Utils\Services\ILogService; + /** * Class RefreshBearerTokenGrantType * @see http://tools.ietf.org/html/rfc6749#section-6 @@ -59,7 +61,10 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType */ public function canHandle(OAuth2Request $request) { - return $request instanceof OAuth2TokenRequest && $request->isValid() && $request->getGrantType() == $this->getType(); + return + $request instanceof OAuth2TokenRequest && + $request->isValid() && + $request->getGrantType() == $this->getType(); } /** Not implemented , there is no first process phase on this grant type @@ -92,24 +97,18 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType public function completeFlow(OAuth2Request $request) { - if (!($request instanceof OAuth2RefreshAccessTokenRequest)) - { + if (!($request instanceof OAuth2RefreshAccessTokenRequest)) { throw new InvalidOAuth2Request; } parent::completeFlow($request); - if - ( - $this->current_client->getApplicationType() != IClient::ApplicationType_Web_App && - $this->current_client->getApplicationType() != IClient::ApplicationType_Native - ) - { + if (!$this->current_client->canRequestRefreshTokens()) { throw new InvalidApplicationType ( sprintf ( - 'client id %s client type must be %s or ', + 'client id %s client type must be %s or %s or support PKCE', $this->client_auth_context->getId(), IClient::ApplicationType_Web_App, IClient::ApplicationType_Native @@ -117,8 +116,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType ); } - if (!$this->current_client->useRefreshToken()) - { + if (!$this->current_client->useRefreshToken()) { throw new UseRefreshTokenException ( sprintf @@ -130,11 +128,10 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType } $refresh_token_value = $request->getRefreshToken(); - $scope = $request->getScope(); - $refresh_token = $this->token_service->getRefreshToken($refresh_token_value); + $scope = $request->getScope(); + $refresh_token = $this->token_service->getRefreshToken($refresh_token_value); - if (is_null($refresh_token)) - { + if (is_null($refresh_token)) { throw new InvalidGrantTypeException ( sprintf @@ -145,8 +142,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType ); } - if ($refresh_token->getClientId() !== $this->current_client->getClientId()) - { + if ($refresh_token->getClientId() !== $this->current_client->getClientId()) { throw new InvalidGrantTypeException ( sprintf @@ -158,7 +154,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType } $new_refresh_token = null; - $access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope); + $access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope); /* * the authorization server could employ refresh token * rotation in which a new refresh token is issued with every access @@ -168,8 +164,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType * legitimate client, one of them will present an invalidated refresh * token, which will inform the authorization server of the breach. */ - if ($this->current_client->useRotateRefreshTokenPolicy()) - { + if ($this->current_client->useRotateRefreshTokenPolicy()) { $this->token_service->invalidateRefreshToken($refresh_token_value); $new_refresh_token = $this->token_service->createRefreshToken($access_token, true); } diff --git a/app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php b/app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php new file mode 100644 index 00000000..d91e742f --- /dev/null +++ b/app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php @@ -0,0 +1,42 @@ +code_challenge = $code_challenge; + $this->code_verifier = $code_verifier; + } + +} \ No newline at end of file diff --git a/app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php b/app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php new file mode 100644 index 00000000..dfe901d4 --- /dev/null +++ b/app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php @@ -0,0 +1,25 @@ +code_challenge === $this->code_verifier; + } +} \ No newline at end of file diff --git a/app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php b/app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php new file mode 100644 index 00000000..c412c958 --- /dev/null +++ b/app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php @@ -0,0 +1,32 @@ +code_verifier, true)); + $calculate_code_challenge = strtr(rtrim($encoded, '='), '+/', '-_'); + return $this->code_challenge === $calculate_code_challenge; + } +} \ No newline at end of file diff --git a/app/libs/OAuth2/Models/AccessToken.php b/app/libs/OAuth2/Models/AccessToken.php index fcc4d002..b7d53b16 100644 --- a/app/libs/OAuth2/Models/AccessToken.php +++ b/app/libs/OAuth2/Models/AccessToken.php @@ -134,4 +134,12 @@ class AccessToken extends Token { { return 'access_token'; } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return []; + } } \ No newline at end of file diff --git a/app/libs/OAuth2/Models/AuthorizationCode.php b/app/libs/OAuth2/Models/AuthorizationCode.php index be13e814..adc9a9a5 100644 --- a/app/libs/OAuth2/Models/AuthorizationCode.php +++ b/app/libs/OAuth2/Models/AuthorizationCode.php @@ -11,8 +11,10 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ + use Utils\IPHelper; use OAuth2\OAuth2Protocol; + /** * Class AuthorizationCode * http://tools.ietf.org/html/rfc6749#section-1.3.1 @@ -59,6 +61,16 @@ class AuthorizationCode extends Token */ private $requested_auth_time; + /** + * @var string + */ + private $code_challenge; + + /** + * @var string + */ + private $code_challenge_method; + /** * @var string * prompt @@ -111,110 +123,46 @@ class AuthorizationCode extends Token * @param string $approval_prompt * @param bool $has_previous_user_consent * @param int $lifetime - * @param string|null $state - * @param string|null $nonce - * @param string|null $response_type - * @param $requested_auth_time - * @param $auth_time - * @param null|string $prompt + * @param null $state + * @param null $nonce + * @param null $response_type + * @param bool $requested_auth_time + * @param int $auth_time + * @param null $prompt + * @param null $code_challenge + * @param null $code_challenge_method * @return AuthorizationCode */ public static function create( $user_id, $client_id, $scope, - $audience = '', - $redirect_uri = null, - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, + $audience = '', + $redirect_uri = null, + $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, + $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, $has_previous_user_consent = false, - $lifetime = 600, - $state = null, - $nonce = null, - $response_type = null, - $requested_auth_time = false, - $auth_time = -1, - $prompt = null - ) { - $instance = new self(); - $instance->scope = $scope; - $instance->user_id = $user_id; - $instance->redirect_uri = $redirect_uri; - $instance->client_id = $client_id; - $instance->lifetime = intval($lifetime); - $instance->audience = $audience; - $instance->is_hashed = false; - $instance->from_ip = IPHelper::getUserIp(); - $instance->access_type = $access_type; - $instance->approval_prompt = $approval_prompt; - $instance->has_previous_user_consent = $has_previous_user_consent; - $instance->state = $state; - $instance->nonce = $nonce; - $instance->response_type = $response_type; - $instance->requested_auth_time = $requested_auth_time; - $instance->auth_time = $auth_time; - $instance->prompt = $prompt; - - return $instance; - } - - /** - * @param $value - * @param $user_id - * @param $client_id - * @param $scope - * @param string $audience - * @param null $redirect_uri - * @param null $issued - * @param int $lifetime - * @param string $from_ip - * @param string $access_type - * @param string $approval_prompt - * @param bool $has_previous_user_consent - * @param string|null $state - * @param string|null $nonce - * @param string|null $response_type - * @param $requested_auth_time - * @param $auth_time - * @param null|string $prompt - * @param bool $is_hashed - * @return AuthorizationCode - */ - public static function load - ( - $value, - $user_id, - $client_id, - $scope, - $audience = '', - $redirect_uri = null, - $issued = null, - $lifetime = 600, - $from_ip = '127.0.0.1', - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, - $has_previous_user_consent = false, - $state, - $nonce, - $response_type, - $requested_auth_time = false, - $auth_time = -1, - $prompt = null, - $is_hashed = false + $lifetime = 600, + $state = null, + $nonce = null, + $response_type = null, + $requested_auth_time = false, + $auth_time = -1, + $prompt = null, + $code_challenge = null, + $code_challenge_method = null ) { $instance = new self(); - $instance->value = $value; - $instance->user_id = $user_id; - $instance->scope = $scope; - $instance->redirect_uri = $redirect_uri; - $instance->client_id = $client_id; - $instance->audience = $audience; - $instance->issued = $issued; - $instance->lifetime = intval($lifetime); - $instance->from_ip = $from_ip; - $instance->is_hashed = $is_hashed; - $instance->access_type = $access_type; + $instance->scope = $scope; + $instance->user_id = $user_id; + $instance->redirect_uri = $redirect_uri; + $instance->client_id = $client_id; + $instance->lifetime = intval($lifetime); + $instance->audience = $audience; + $instance->is_hashed = false; + $instance->from_ip = IPHelper::getUserIp(); + $instance->access_type = $access_type; $instance->approval_prompt = $approval_prompt; $instance->has_previous_user_consent = $has_previous_user_consent; $instance->state = $state; @@ -222,7 +170,44 @@ class AuthorizationCode extends Token $instance->response_type = $response_type; $instance->requested_auth_time = $requested_auth_time; $instance->auth_time = $auth_time; - $instance->prompt = $prompt; + $instance->prompt = $prompt; + $instance->code_challenge = $code_challenge; + $instance->code_challenge_method = $code_challenge_method; + + return $instance; + } + + /** + * @param array $payload + * @return AuthorizationCode + */ + public static function load + ( + array $payload + ): AuthorizationCode + { + $instance = new self(); + $instance->value = $payload['value']; + $instance->user_id = $payload['user_id'] ?? null; + $instance->scope = $payload['scope'] ?? null; + $instance->redirect_uri = $payload['redirect_uri'] ?? null; + $instance->client_id = $payload['client_id'] ?? null; + $instance->audience = $payload['audience'] ?? null; + $instance->issued = $payload['issued'] ?? null; + $instance->lifetime = intval($payload['lifetime']); + $instance->from_ip = $payload['from_ip'] ?? null; + $instance->is_hashed = isset($payload['is_hashed']) ? boolval($payload['is_hashed']) : false; + $instance->access_type = $payload['access_type'] ?? null; + $instance->approval_prompt = $payload['approval_prompt'] ?? null; + $instance->has_previous_user_consent = $payload['has_previous_user_consent'] ?? false; + $instance->state = $payload['state'] ?? null; + $instance->nonce = $payload['nonce'] ?? null; + $instance->response_type = $payload['response_type'] ?? null; + $instance->requested_auth_time = $payload['requested_auth_time'] ?? null;; + $instance->auth_time = $payload['auth_time'] ?? null; + $instance->prompt = $payload['prompt'] ?? null; + $instance->code_challenge = $payload['code_challenge'] ?? null; + $instance->code_challenge_method = $payload['code_challenge_method'] ?? null; return $instance; } @@ -288,7 +273,7 @@ class AuthorizationCode extends Token public function isAuthTimeRequested() { $res = $this->requested_auth_time; - if (!is_string($res)) return (bool) $res; + if (!is_string($res)) return (bool)$res; switch (strtolower($res)) { case '1': case 'true': @@ -332,4 +317,71 @@ class AuthorizationCode extends Token { return 'auth_code'; } + + public function toArray(): array + { + return [ + 'client_id' => $this->getClientId(), + 'scope' => $this->getScope(), + 'audience' => $this->getAudience(), + 'redirect_uri' => $this->getRedirectUri(), + 'issued' => $this->getIssued(), + 'lifetime' => $this->getLifetime(), + 'from_ip' => $this->getFromIp(), + 'user_id' => $this->getUserId(), + 'access_type' => $this->getAccessType(), + 'approval_prompt' => $this->getApprovalPrompt(), + 'has_previous_user_consent' => $this->getHasPreviousUserConsent(), + 'state' => $this->getState(), + 'nonce' => $this->getNonce(), + 'response_type' => $this->getResponseType(), + 'requested_auth_time' => $this->isAuthTimeRequested(), + 'auth_time' => $this->getAuthTime(), + 'prompt' => $this->getPrompt(), + 'code_challenge' => $this->getCodeChallenge(), + 'code_challenge_method' => $this->getCodeChallengeMethod(), + ]; + } + + public static function getKeys(): array + { + return [ + 'user_id', + 'client_id', + 'scope', + 'audience', + 'redirect_uri', + 'issued', + 'lifetime', + 'from_ip', + 'access_type', + 'approval_prompt', + 'has_previous_user_consent', + 'state', + 'nonce', + 'response_type', + 'requested_auth_time', + 'auth_time', + 'prompt', + 'code_challenge', + 'code_challenge_method', + ]; + } + + /** + * @return string + */ + public function getCodeChallenge(): ?string + { + return $this->code_challenge; + } + + /** + * @return string + */ + public function getCodeChallengeMethod(): ?string + { + return $this->code_challenge_method; + } + } \ No newline at end of file diff --git a/app/libs/OAuth2/Models/ClientCredentialsAuthenticationContext.php b/app/libs/OAuth2/Models/ClientCredentialsAuthenticationContext.php index 6a64a379..f46f9b43 100644 --- a/app/libs/OAuth2/Models/ClientCredentialsAuthenticationContext.php +++ b/app/libs/OAuth2/Models/ClientCredentialsAuthenticationContext.php @@ -11,7 +11,6 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ - use Illuminate\Support\Facades\Log; use OAuth2\Exceptions\InvalidTokenEndpointAuthMethodException; use OAuth2\OAuth2Protocol; @@ -51,7 +50,8 @@ final class ClientCredentialsAuthenticationContext extends ClientAuthenticationC if(!in_array($auth_type, [ OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, - OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost + OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost, + OAuth2Protocol::TokenEndpoint_AuthMethod_None ])) throw new InvalidTokenEndpointAuthMethodException($auth_type); diff --git a/app/libs/OAuth2/Models/IClient.php b/app/libs/OAuth2/Models/IClient.php index ca54ec67..00bd0b45 100644 --- a/app/libs/OAuth2/Models/IClient.php +++ b/app/libs/OAuth2/Models/IClient.php @@ -319,4 +319,9 @@ interface IClient extends IEntity * @return array */ public function getValidAccessTokens(); + + /** + * @return bool + */ + public function isPKCEEnabled():bool; } \ No newline at end of file diff --git a/app/libs/OAuth2/Models/RefreshToken.php b/app/libs/OAuth2/Models/RefreshToken.php index f0200d51..acd1d304 100644 --- a/app/libs/OAuth2/Models/RefreshToken.php +++ b/app/libs/OAuth2/Models/RefreshToken.php @@ -85,4 +85,12 @@ class RefreshToken extends Token { { return 'refresh_token'; } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return []; + } } \ No newline at end of file diff --git a/app/libs/OAuth2/OAuth2Protocol.php b/app/libs/OAuth2/OAuth2Protocol.php index 41655400..4de1e2e8 100644 --- a/app/libs/OAuth2/OAuth2Protocol.php +++ b/app/libs/OAuth2/OAuth2Protocol.php @@ -648,6 +648,8 @@ final class OAuth2Protocol implements IOAuth2Protocol self::TokenEndpoint_AuthMethod_ClientSecretPost, self::TokenEndpoint_AuthMethod_ClientSecretJwt, self::TokenEndpoint_AuthMethod_PrivateKeyJwt, + // PKCE only + self::TokenEndpoint_AuthMethod_None, ); const OpenIdConnect_Scope = 'openid'; @@ -711,6 +713,25 @@ final class OAuth2Protocol implements IOAuth2Protocol */ const VsChar = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_~'; + /** + * PKCE + * @see https://tools.ietf.org/html/rfc7636 + **/ + + // auth request new params + + const PKCE_CodeChallenge = 'code_challenge'; + const PKCE_CodeChallengeMethod = 'code_challenge_method'; + + const PKCE_CodeChallengeMethodPlain = 'plain'; + const PKCE_CodeChallengeMethodSHA256 = 'S256'; + + const PKCE_ValidCodeChallengeMethods = [self::PKCE_CodeChallengeMethodPlain, self::PKCE_CodeChallengeMethodSHA256]; + + // token request new params + + const PKCE_CodeVerifier = 'code_verifier'; + //services /** * @var ILogService diff --git a/app/libs/OAuth2/Requests/OAuth2AccessTokenRequestAuthCode.php b/app/libs/OAuth2/Requests/OAuth2AccessTokenRequestAuthCode.php index 55aee725..088fef92 100644 --- a/app/libs/OAuth2/Requests/OAuth2AccessTokenRequestAuthCode.php +++ b/app/libs/OAuth2/Requests/OAuth2AccessTokenRequestAuthCode.php @@ -74,4 +74,8 @@ class OAuth2AccessTokenRequestAuthCode extends OAuth2TokenRequest { return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType_Code); } + + public function getCodeVerifier():?string{ + return $this->getParam(OAuth2Protocol::PKCE_CodeVerifier); + } } \ No newline at end of file diff --git a/app/libs/OAuth2/Requests/OAuth2AuthenticationRequest.php b/app/libs/OAuth2/Requests/OAuth2AuthenticationRequest.php index 1a67a0d2..454363a6 100644 --- a/app/libs/OAuth2/Requests/OAuth2AuthenticationRequest.php +++ b/app/libs/OAuth2/Requests/OAuth2AuthenticationRequest.php @@ -24,7 +24,6 @@ use OAuth2\ResourceServer\IUserService; */ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest { - /** * @var array */ @@ -120,15 +119,6 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest parent::__construct($auth_request->getMessage()); } - /** - * @see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes - * The Response Mode request parameter response_mode informs the Authorization Server of the mechanism to be used - * for returning Authorization Response parameters from the Authorization Endpoint - */ - public function getResponseMode() - { - return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseMode); - } /** * Validates current request @@ -191,25 +181,6 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest } } - $response_mode = $this->getResponseMode(); - - if(!empty($response_mode)) - { - if(!in_array($response_mode, OAuth2Protocol::$valid_response_modes)) - { - $this->last_validation_error = 'invalid response_mode'; - return false; - } - - $default_response_mode = OAuth2Protocol::getDefaultResponseMode($this->getResponseType(false)); - - if($default_response_mode === $response_mode) - { - $this->last_validation_error = 'invalid response_mode'; - return false; - } - } - // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess // MUST ensure that the prompt parameter contains consent unless other conditions for processing the request // permitting offline access to the requested resources are in place diff --git a/app/libs/OAuth2/Requests/OAuth2AuthorizationRequest.php b/app/libs/OAuth2/Requests/OAuth2AuthorizationRequest.php index 512c011c..945d0a95 100644 --- a/app/libs/OAuth2/Requests/OAuth2AuthorizationRequest.php +++ b/app/libs/OAuth2/Requests/OAuth2AuthorizationRequest.php @@ -33,8 +33,7 @@ class OAuth2AuthorizationRequest extends OAuth2Request parent::__construct($msg); } - public static $params = array - ( + public static $params = [ OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType, OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId, OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri, @@ -42,7 +41,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State, OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt, OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType, - ); + OAuth2Protocol::OAuth2Protocol_ResponseMode => OAuth2Protocol::OAuth2Protocol_ResponseMode, + ]; /** * The Response Type request parameter response_type informs the Authorization Server of the desired authorization @@ -62,6 +62,16 @@ class OAuth2AuthorizationRequest extends OAuth2Request ); } + /** + * @see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes + * The Response Mode request parameter response_mode informs the Authorization Server of the mechanism to be used + * for returning Authorization Response parameters from the Authorization Endpoint + */ + public function getResponseMode() + { + return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseMode); + } + /** * Identifies the client that is making the request. * The value passed in this parameter must exactly match the value shown in the Admin Console. @@ -171,17 +181,43 @@ class OAuth2AuthorizationRequest extends OAuth2Request } //approval_prompt - $valid_approvals = array - ( + $valid_approvals = [ OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force - ); + ]; if (!in_array($this->getApprovalPrompt(), $valid_approvals)) { $this->last_validation_error = 'approval_prompt is not valid'; return false; } + + $response_mode = $this->getResponseMode(); + + if(!empty($response_mode)) + { + if(!in_array($response_mode, OAuth2Protocol::$valid_response_modes)) + { + $this->last_validation_error = 'invalid response_mode'; + return false; + } + + $default_response_mode = OAuth2Protocol::getDefaultResponseMode($this->getResponseType(false)); + + if($default_response_mode === $response_mode) + { + $this->last_validation_error = 'invalid response_mode'; + return false; + } + } + + // PCKE validation + if(!is_null($this->getCodeChallenge())){ + if(!in_array( $this->getCodeChallengeMethod(), OAuth2Protocol::PKCE_ValidCodeChallengeMethods)){ + $this->last_validation_error = sprintf("%s not valid", OAuth2Protocol::PKCE_CodeChallengeMethod); + return false; + } + } return true; } @@ -194,4 +230,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request if(empty($display)) return OAuth2Protocol::OAuth2Protocol_Display_Page; return $display; } + + // PKCE + + public function getCodeChallenge():?string{ + return $this->getParam(OAuth2Protocol::PKCE_CodeChallenge); + } + + public function getCodeChallengeMethod():?string{ + return $this->getParam(OAuth2Protocol::PKCE_CodeChallengeMethod); + } } diff --git a/app/libs/OAuth2/Services/ITokenService.php b/app/libs/OAuth2/Services/ITokenService.php index db9f47b7..724e7007 100644 --- a/app/libs/OAuth2/Services/ITokenService.php +++ b/app/libs/OAuth2/Services/ITokenService.php @@ -22,6 +22,9 @@ use OAuth2\Models\RefreshToken; use OAuth2\OAuth2Protocol; use OAuth2\Exceptions\InvalidAccessTokenException; use OAuth2\Exceptions\InvalidGrantTypeException; +use OAuth2\Requests\OAuth2AuthorizationRequest; +use Utils\Model\Identifier; + /** * Interface ITokenService * Defines the interface for an OAuth2 Token Service @@ -32,35 +35,15 @@ interface ITokenService { /** * Creates a brand new authorization code - * @param $user_id - * @param $client_id - * @param $scope - * @param string $audience - * @param null $redirect_uri - * @param string $access_type - * @param string $approval_prompt + * @param OAuth2AuthorizationRequest $request * @param bool $has_previous_user_consent - * @param string|null $state - * @param string|null $nonce - * @param string|null $response_type - * @param string|null $prompt - * @return AuthorizationCode + * @return Identifier */ public function createAuthorizationCode ( - $user_id, - $client_id, - $scope, - $audience = '' , - $redirect_uri = null, - $access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online, - $approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, - $has_previous_user_consent = false, - $state = null, - $nonce = null, - $response_type = null, - $prompt = null - ); + OAuth2AuthorizationRequest $request, + bool $has_previous_user_consent = false + ):Identifier; /** diff --git a/app/libs/OAuth2/Strategies/ClientAuthContextValidatorFactory.php b/app/libs/OAuth2/Strategies/ClientAuthContextValidatorFactory.php index 0692e61c..838aa23b 100644 --- a/app/libs/OAuth2/Strategies/ClientAuthContextValidatorFactory.php +++ b/app/libs/OAuth2/Strategies/ClientAuthContextValidatorFactory.php @@ -63,6 +63,11 @@ final class ClientAuthContextValidatorFactory return new ClientPlainCredentialsAuthContextValidator; } break; + case OAuth2Protocol::TokenEndpoint_AuthMethod_None: + { + return new ClientPKCEAuthContextValidator; + } + break; case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt: { $validator = new ClientSharedSecretAssertionAuthContextValidator; diff --git a/app/libs/OAuth2/Strategies/ClientPKCEAuthContextValidator.php b/app/libs/OAuth2/Strategies/ClientPKCEAuthContextValidator.php new file mode 100644 index 00000000..1aa0e0fa --- /dev/null +++ b/app/libs/OAuth2/Strategies/ClientPKCEAuthContextValidator.php @@ -0,0 +1,56 @@ +getClient(); + if (is_null($client)) + throw new InvalidClientAuthenticationContextException('client not set!'); + + if ($client->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType()) + throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType())); + + if ($client->getClientType() !== IClient::ClientType_Public) + throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType())); + + $providedClientId = $context->getId(); + + Log::debug(sprintf("ClientPKCEAuthContextValidator::validate client id %s - provide client id %s", $client->getClientId(), $providedClientId)); + + return $client->getClientId() === $providedClientId && $client->isPKCEEnabled(); + } +} \ No newline at end of file diff --git a/app/libs/OAuth2/Strategies/ClientPlainCredentialsAuthContextValidator.php b/app/libs/OAuth2/Strategies/ClientPlainCredentialsAuthContextValidator.php index 66755fd1..0bc665c1 100644 --- a/app/libs/OAuth2/Strategies/ClientPlainCredentialsAuthContextValidator.php +++ b/app/libs/OAuth2/Strategies/ClientPlainCredentialsAuthContextValidator.php @@ -35,21 +35,22 @@ final class ClientPlainCredentialsAuthContextValidator implements IClientAuthCon if(!($context instanceof ClientCredentialsAuthenticationContext)) throw new InvalidClientAuthenticationContextException; - if(is_null($context->getClient())) + $client = $context->getClient(); + if(is_null($client)) throw new InvalidClientAuthenticationContextException('client not set!'); - if($context->getClient()->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType()) + if($client->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType()) throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType())); - if($context->getClient()->getClientType() !== IClient::ClientType_Confidential) - throw new InvalidClientCredentials(sprintf('invalid client type %s', $context->getClient()->getClientType())); + if($client->getClientType() !== IClient::ClientType_Confidential) + throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType())); $providedClientId = $context->getId(); $providedClientSecret = $context->getSecret(); - Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client id %s - provide client id %s", $context->getClient()->getClientId(), $providedClientId)); - Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $context->getClient()->getClientSecret(), $providedClientSecret)); - return $context->getClient()->getClientId() === $providedClientId && - $context->getClient()->getClientSecret() === $providedClientSecret; + Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client id %s - provide client id %s", $client->getClientId(), $providedClientId)); + Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $client->getClientSecret(), $providedClientSecret)); + + return $client->getClientId() === $providedClientId && $client->getClientSecret() === $providedClientSecret; } } \ No newline at end of file diff --git a/app/libs/OAuth2/Strategies/IPKCEValidationMethod.php b/app/libs/OAuth2/Strategies/IPKCEValidationMethod.php new file mode 100644 index 00000000..944c4385 --- /dev/null +++ b/app/libs/OAuth2/Strategies/IPKCEValidationMethod.php @@ -0,0 +1,22 @@ +getType(); - if($request instanceof OAuth2AuthenticationRequest) + if($request instanceof OAuth2AuthorizationRequest) { $response_mode = $request->getResponseMode(); diff --git a/app/libs/OpenId/Models/OpenIdNonce.php b/app/libs/OpenId/Models/OpenIdNonce.php index db8af1e3..2e4349a9 100644 --- a/app/libs/OpenId/Models/OpenIdNonce.php +++ b/app/libs/OpenId/Models/OpenIdNonce.php @@ -143,4 +143,12 @@ final class OpenIdNonce extends Identifier { return 'nonce'; } + + /** + * @inheritDoc + */ + public function toArray(): array + { + return []; + } } \ No newline at end of file diff --git a/app/libs/Utils/Model/Identifier.php b/app/libs/Utils/Model/Identifier.php index 01ca275d..bc9d947e 100644 --- a/app/libs/Utils/Model/Identifier.php +++ b/app/libs/Utils/Model/Identifier.php @@ -90,4 +90,9 @@ abstract class Identifier * @return string */ abstract public function getType(); + + /** + * @return array + */ + abstract public function toArray(): array; } \ No newline at end of file diff --git a/database/migrations/Version20190604015804.php b/database/migrations/Version20190604015804.php index 7549c0bc..392c21e6 100644 --- a/database/migrations/Version20190604015804.php +++ b/database/migrations/Version20190604015804.php @@ -288,6 +288,7 @@ create table if not exists oauth2_client max_refresh_token_issuance_basis smallint(6) not null, use_refresh_token tinyint(1) default '0' not null, rotate_refresh_token tinyint(1) default '0' not null, + pkce_enabled tinyint(1) default '0' not null, resource_server_id bigint unsigned null, website text null, application_type enum('WEB_APPLICATION', 'JS_CLIENT', 'SERVICE', 'NATIVE') default 'WEB_APPLICATION' null, diff --git a/database/migrations/Version20201214162511.php b/database/migrations/Version20201214162511.php new file mode 100644 index 00000000..bc9dc0c0 --- /dev/null +++ b/database/migrations/Version20201214162511.php @@ -0,0 +1,49 @@ +hasTable("oauth2_client") && !$builder->hasColumn("oauth2_client","pkce_enabled") ) { + $builder->table('oauth2_client', function (Table $table) { + $table->boolean('pkce_enabled')->setDefault(0); + }); + } + } + + /** + * @param Schema $schema + */ + public function down(Schema $schema) + { + $builder = new Builder($schema); + if($schema->hasTable("oauth2_client") && $builder->hasColumn("oauth2_client","pkce_enabled") ) { + $builder->table('oauth2_client', function (Table $table) { + $table->dropColumn('pkce_enabled'); + }); + } + } +} diff --git a/database/seeds/TestSeeder.php b/database/seeds/TestSeeder.php index 7ccd2563..c3dc07f5 100644 --- a/database/seeds/TestSeeder.php +++ b/database/seeds/TestSeeder.php @@ -848,6 +848,20 @@ PPK; 'use_refresh_token' => false, 'redirect_uris' => 'https://www.test.com/oauth2', ), + array( + 'app_name' => 'oauth2_test_app_public_pkce', + 'app_description' => 'oauth2_test_app_public_pkce', + 'app_logo' => null, + 'client_id' => '1234/Vcvr6fvQbH4HyNgwKlfSpkce.openstack.client', + 'client_secret' => null, + 'application_type' => IClient::ApplicationType_JS_Client, + 'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_None, + 'owner' => $user, + 'rotate_refresh_token' => true, + 'use_refresh_token' => true, + 'redirect_uris' => 'https://www.test.com/oauth2', + 'pkce_enabled' => true, + ), array( 'app_name' => 'oauth2_native_app', 'app_description' => 'oauth2_native_app', @@ -932,6 +946,7 @@ PPK; $client_confidential2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app2']); $client_confidential3 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app3']); $client_public = $client_repository->findOneBy(['app_name' => 'oauth2_test_app_public']); + $client_public2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app_public_pkce']); $client_service = $client_repository->findOneBy(['app_name' => 'oauth2.service']); $client_native = $client_repository->findOneBy(['app_name' => 'oauth2_native_app']); $client_native2 = $client_repository->findOneBy(['app_name' => 'oauth2_native_app2']); @@ -946,6 +961,7 @@ PPK; $client_confidential2->addScope($scope); $client_confidential3->addScope($scope); $client_public->addScope($scope); + $client_public2->addScope($scope); $client_service->addScope($scope); $client_native->addScope($scope); $client_native2->addScope($scope); @@ -1063,7 +1079,6 @@ PPK; TestKeys::$private_key_pem ); - EntityManager::persist($pkey_2); EntityManager::flush(); diff --git a/resources/views/oauth2/profile/edit-client-data.blade.php b/resources/views/oauth2/profile/edit-client-data.blade.php index 20406b44..020dd4b3 100644 --- a/resources/views/oauth2/profile/edit-client-data.blade.php +++ b/resources/views/oauth2/profile/edit-client-data.blade.php @@ -61,7 +61,7 @@ @endif - @if($client->application_type == OAuth2\Models\IClient::ApplicationType_Web_App || $client->application_type == OAuth2\Models\IClient::ApplicationType_Native) + @if($client->canRequestRefreshTokens())
diff --git a/resources/views/oauth2/profile/edit-client-scopes.blade.php b/resources/views/oauth2/profile/edit-client-scopes.blade.php index f575738a..db30a89e 100644 --- a/resources/views/oauth2/profile/edit-client-scopes.blade.php +++ b/resources/views/oauth2/profile/edit-client-scopes.blade.php @@ -19,7 +19,7 @@