Auth Code Flow PKCE support

implementation of https://tools.ietf.org/html/rfc7636

Change-Id: Ib88a3b6c9652e6eea9648177ffd0d143ab995ac6
Signed-off-by: smarcet <smarcet@gmail.com>
This commit is contained in:
smarcet 2020-12-15 15:41:07 -03:00
parent e3b8987704
commit 6dc411fad9
43 changed files with 1214 additions and 650 deletions

View File

@ -30,6 +30,7 @@ class Kernel extends HttpKernel
protected $middleware = [ protected $middleware = [
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class, \Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
\App\Http\Middleware\SingleAccessPoint::class, \App\Http\Middleware\SingleAccessPoint::class,
\Spatie\Cors\Cors::class,
\App\Http\Middleware\ParseMultipartFormDataInputForNonPostRequests::class, \App\Http\Middleware\ParseMultipartFormDataInputForNonPostRequests::class,
]; ];
@ -50,7 +51,6 @@ class Kernel extends HttpKernel
'api' => [ 'api' => [
'ssl', 'ssl',
'cors',
'oauth2.endpoint', 'oauth2.endpoint',
], ],
]; ];
@ -69,7 +69,6 @@ class Kernel extends HttpKernel
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class, 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class, 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class, 'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
'cors' => \Spatie\Cors\Cors::class,
'oauth2.endpoint' => \App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator::class, 'oauth2.endpoint' => \App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator::class,
'oauth2.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdmin::class, 'oauth2.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdmin::class,
'oauth2.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdminJson::class, 'oauth2.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdminJson::class,

View File

@ -91,6 +91,7 @@ Route::group(['namespace' => 'App\Http\Controllers', 'middleware' => 'web' ], fu
}); });
Route::group(['namespace' => 'OAuth2' , 'prefix' => 'oauth2', 'middleware' => ['ssl']], function () { Route::group(['namespace' => 'OAuth2' , 'prefix' => 'oauth2', 'middleware' => ['ssl']], function () {
Route::get('/check-session', "OAuth2ProviderController@checkSessionIFrame"); Route::get('/check-session', "OAuth2ProviderController@checkSessionIFrame");
Route::get('/end-session', "OAuth2ProviderController@endSession"); Route::get('/end-session', "OAuth2ProviderController@endSession");
Route::get('/end-session/cancel', "OAuth2ProviderController@cancelLogout"); Route::get('/end-session/cancel', "OAuth2ProviderController@cancelLogout");
@ -375,7 +376,6 @@ Route::group(
'prefix' => 'api/v1', 'prefix' => 'api/v1',
'middleware' => [ 'middleware' => [
'ssl', 'ssl',
'cors',
'oauth2.endpoint', 'oauth2.endpoint',
] ]
], function () { ], function () {

View File

@ -15,10 +15,12 @@
use App\libs\Utils\URLUtils; use App\libs\Utils\URLUtils;
use Auth\User; use Auth\User;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Illuminate\Support\Facades\Log;
use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry; use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry;
use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry; use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry;
use jwa\cryptographic_algorithms\KeyManagementAlgorithms_Registry; use jwa\cryptographic_algorithms\KeyManagementAlgorithms_Registry;
use jwa\JSONWebSignatureAndEncryptionAlgorithms; use jwa\JSONWebSignatureAndEncryptionAlgorithms;
use models\exceptions\ValidationException;
use OAuth2\Models\IClient; use OAuth2\Models\IClient;
use OAuth2\Models\IClientPublicKey; use OAuth2\Models\IClientPublicKey;
use OAuth2\Models\JWTResponseInfo; use OAuth2\Models\JWTResponseInfo;
@ -82,6 +84,12 @@ class Client extends BaseEntity implements IClient
*/ */
private $active; private $active;
/**
* @ORM\Column(name="pkce_enabled", type="boolean")
* @var bool
*/
private $pkce_enabled;
/** /**
* @ORM\Column(name="locked", type="boolean") * @ORM\Column(name="locked", type="boolean")
* @var bool * @var bool
@ -371,6 +379,7 @@ class Client extends BaseEntity implements IClient
$this->active = false; $this->active = false;
$this->use_refresh_token = false; $this->use_refresh_token = false;
$this->rotate_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->token_endpoint_auth_signing_alg = JSONWebSignatureAndEncryptionAlgorithms::None;
$this->userinfo_signed_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None; $this->userinfo_signed_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None;
$this->userinfo_encrypted_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_access_token_issuance_qty = 0;
$this->max_refresh_token_issuance_basis = 0; $this->max_refresh_token_issuance_basis = 0;
$this->max_refresh_token_issuance_qty = 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 = [ public static $valid_app_types = [
@ -423,22 +432,25 @@ class Client extends BaseEntity implements IClient
throw new \InvalidArgumentException("Invalid application_type"); throw new \InvalidArgumentException("Invalid application_type");
} }
$this->application_type = strtoupper($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 * @return bool
*/ */
public function canRequestRefreshTokens():bool{ public function canRequestRefreshTokens():bool{
return $this->getApplicationType() == IClient::ApplicationType_Native || return
$this->getApplicationType() == IClient::ApplicationType_Web_App; $this->getApplicationType() == IClient::ApplicationType_Native ||
$this->getApplicationType() == IClient::ApplicationType_Web_App ||
// PCKE
$this->pkce_enabled;
} }
/** /**
* @param string $app_type * @param string $app_type
* @return string * @return string
*/ */
private function infereClientTypeFromAppType(string $app_type) private function inferClientTypeFromAppType(string $app_type)
{ {
switch($app_type) switch($app_type)
{ {
@ -587,14 +599,20 @@ class Client extends BaseEntity implements IClient
($this->application_type !== IClient::ApplicationType_Native && !URLUtils::isHTTPS($uri)) ($this->application_type !== IClient::ApplicationType_Native && !URLUtils::isHTTPS($uri))
&& (ServerConfigurationService::getConfigValue("SSL.Enable")) && (ServerConfigurationService::getConfigValue("SSL.Enable"))
) )
{
Log::debug(sprintf("Client::isUriAllowed url %s is not under ssl schema", $uri));
return false; return false;
}
$redirect_uris = explode(',',strtolower($this->redirect_uris)); $redirect_uris = explode(',',strtolower($this->redirect_uris));
$uri = URLUtils::normalizeUrl($uri); $uri = URLUtils::normalizeUrl($uri);
foreach($redirect_uris as $redirect_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)) if(str_contains($uri, $redirect_uri))
return true; return true;
} }
Log::debug(sprintf("Client::isUriAllowed url %s is not allowed as return url for client %s", $uri, $this->client_id));
return false; return false;
} }
@ -1113,6 +1131,7 @@ class Client extends BaseEntity implements IClient
*/ */
public function isOwner(User $user):bool public function isOwner(User $user):bool
{ {
if(!$this->hasUser()) return false;
return intval($this->user->getId()) === intval($user->getId()); return intval($this->user->getId()) === intval($user->getId());
} }
@ -1132,7 +1151,7 @@ class Client extends BaseEntity implements IClient
*/ */
public function addScope(ApiScope $scope) public function addScope(ApiScope $scope)
{ {
if($this->scopes->contains($scope)) return; if($this->scopes->contains($scope)) return $this;
$this->scopes->add($scope); $this->scopes->add($scope);
return $this; return $this;
} }
@ -1565,4 +1584,23 @@ class Client extends BaseEntity implements IClient
return $this->getUserId(); return $this->getUserId();
return $this->{$name}; 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;
}
} }

View File

@ -32,21 +32,8 @@ final class ClientFactory
*/ */
public static function build(array $payload):Client public static function build(array $payload):Client
{ {
$scope_repository = App::make(IApiScopeRepository::class);
$client = self::populate(new Client, $payload); $client = self::populate(new Client, $payload);
$client->setActive(true); $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) { if ($client->getClientType() !== IClient::ClientType_Confidential) {
$client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_None); $client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_None);
} }
@ -202,6 +189,29 @@ final class ClientFactory
$client->setResourceServer($resource_server); $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; return $client;
} }
} }

View File

@ -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 if(Request::hasHeader('Authorization'))
(
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))
{ {
Log::debug Log::debug
( (
"ClientService::getCurrentClientAuthInfo Authorization Header present" "ClientService::getCurrentClientAuthInfo Authorization Header present"
); );
$auth_header = Request::header('Authorization');
$auth_header = trim($auth_header); $auth_header = trim($auth_header);
$auth_header = explode(' ', $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; throw new InvalidClientAuthMethodException;
} }

View File

@ -16,6 +16,7 @@ use App\Http\Utils\IUserIPHelperProvider;
use Illuminate\Support\ServiceProvider; use Illuminate\Support\ServiceProvider;
use OAuth2\Services\AccessTokenGenerator; use OAuth2\Services\AccessTokenGenerator;
use OAuth2\Services\AuthorizationCodeGenerator; use OAuth2\Services\AuthorizationCodeGenerator;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\OAuth2ServiceCatalog; use OAuth2\Services\OAuth2ServiceCatalog;
use OAuth2\Services\RefreshTokenGenerator; use OAuth2\Services\RefreshTokenGenerator;
use Utils\Services\UtilsServiceCatalog; use Utils\Services\UtilsServiceCatalog;
@ -83,6 +84,7 @@ final class OAuth2ServiceProvider extends ServiceProvider
App::make(\OAuth2\Repositories\IRefreshTokenRepository::class), App::make(\OAuth2\Repositories\IRefreshTokenRepository::class),
App::make(\OAuth2\Repositories\IResourceServerRepository::class), App::make(\OAuth2\Repositories\IResourceServerRepository::class),
App::make(IUserIPHelperProvider::class), App::make(IUserIPHelperProvider::class),
App::make(IApiScopeService::class),
App::make(UtilsServiceCatalog::TransactionService) App::make(UtilsServiceCatalog::TransactionService)
); );
}); });

View File

@ -11,10 +11,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use App\Http\Utils\IUserIPHelperProvider; use App\Http\Utils\IUserIPHelperProvider;
use App\libs\Auth\Models\IGroupSlugs; use App\libs\Auth\Models\IGroupSlugs;
use App\Services\AbstractService; use App\Services\AbstractService;
use Auth\Group;
use Auth\User; use Auth\User;
use Illuminate\Support\Facades\Event; use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
@ -46,6 +46,9 @@ use OAuth2\Repositories\IAccessTokenRepository;
use OAuth2\Repositories\IClientRepository; use OAuth2\Repositories\IClientRepository;
use OAuth2\Repositories\IRefreshTokenRepository; use OAuth2\Repositories\IRefreshTokenRepository;
use OAuth2\Repositories\IResourceServerRepository; use OAuth2\Repositories\IResourceServerRepository;
use OAuth2\Requests\OAuth2AuthenticationRequest;
use OAuth2\Requests\OAuth2AuthorizationRequest;
use OAuth2\Services\IApiScopeService;
use OAuth2\Services\ITokenService; use OAuth2\Services\ITokenService;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
use OAuth2\Repositories\IServerPrivateKeyRepository; use OAuth2\Repositories\IServerPrivateKeyRepository;
@ -63,6 +66,7 @@ use Utils\Exceptions\UnacquiredLockException;
use utils\json_types\JsonValue; use utils\json_types\JsonValue;
use utils\json_types\NumericDate; use utils\json_types\NumericDate;
use utils\json_types\StringOrURI; use utils\json_types\StringOrURI;
use Utils\Model\Identifier;
use Utils\Services\IAuthService; use Utils\Services\IAuthService;
use Utils\Services\ICacheService; use Utils\Services\ICacheService;
use Utils\Services\IdentifierGenerator; use Utils\Services\IdentifierGenerator;
@ -70,6 +74,7 @@ use Utils\Services\ILockManagerService;
use Utils\Services\IServerConfigurationService; use Utils\Services\IServerConfigurationService;
use Zend\Crypt\Hash; use Zend\Crypt\Hash;
use Exception; use Exception;
/** /**
* Class TokenService * Class TokenService
* Provides all Tokens related operations (create, get and revoke) * Provides all Tokens related operations (create, get and revoke)
@ -119,12 +124,10 @@ final class TokenService extends AbstractService implements ITokenService
* @var IdentifierGenerator * @var IdentifierGenerator
*/ */
private $auth_code_generator; private $auth_code_generator;
/** /**
* @var IdentifierGenerator * @var IdentifierGenerator
*/ */
private $access_token_generator; private $access_token_generator;
/** /**
* @var IdentifierGenerator * @var IdentifierGenerator
*/ */
@ -174,6 +177,10 @@ final class TokenService extends AbstractService implements ITokenService
* @var IResourceServerRepository * @var IResourceServerRepository
*/ */
private $resource_server_repository; private $resource_server_repository;
/**
* @var IApiScopeService
*/
private $scope_service;
/** /**
* @var IUserIPHelperProvider * @var IUserIPHelperProvider
@ -201,6 +208,7 @@ final class TokenService extends AbstractService implements ITokenService
IRefreshTokenRepository $refresh_token_repository, IRefreshTokenRepository $refresh_token_repository,
IResourceServerRepository $resource_server_repository, IResourceServerRepository $resource_server_repository,
IUserIPHelperProvider $ip_helper, IUserIPHelperProvider $ip_helper,
IApiScopeService $scope_service,
ITransactionService $tx_service ITransactionService $tx_service
) )
{ {
@ -225,6 +233,7 @@ final class TokenService extends AbstractService implements ITokenService
$this->refresh_token_repository = $refresh_token_repository; $this->refresh_token_repository = $refresh_token_repository;
$this->resource_server_repository = $resource_server_repository; $this->resource_server_repository = $resource_server_repository;
$this->ip_helper = $ip_helper; $this->ip_helper = $ip_helper;
$this->scope_service = $scope_service;
Event::listen('oauth2.client.delete', function ($client_id) { Event::listen('oauth2.client.delete', function ($client_id) {
$this->revokeClientRelatedTokens($client_id); $this->revokeClientRelatedTokens($client_id);
@ -237,87 +246,68 @@ final class TokenService extends AbstractService implements ITokenService
/** /**
* Creates a brand new authorization code * Creates a brand new authorization code
* @param $user_id * @param OAuth2AuthorizationRequest $request
* @param $client_id
* @param $scope
* @param string $audience
* @param null $redirect_uri
* @param string $access_type
* @param string $approval_prompt
* @param bool $has_previous_user_consent * @param bool $has_previous_user_consent
* @param string|null $state * @return Identifier
* @param string|null $nonce
* @param string|null $response_type
* @param string|null $prompt
* @return AuthorizationCode
*/ */
public function createAuthorizationCode public function createAuthorizationCode
( (
$user_id, OAuth2AuthorizationRequest $request,
$client_id, bool $has_previous_user_consent = false
$scope, ): Identifier
$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
)
{ {
//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 $code = $this->auth_code_generator->generate
( (
AuthorizationCode::create AuthorizationCode::create
( (
$user_id, $user->getId(),
$client_id, $request->getClientId(),
$scope, $request->getScope(),
$audience, $audience,
$redirect_uri, $request->getRedirectUri(),
$access_type, $request->getAccessType(),
$approval_prompt, $has_previous_user_consent, $request->getApprovalPrompt(),
$has_previous_user_consent,
$this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'), $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime'),
$state, $request->getState(),
$nonce, $nonce,
$response_type, $request->getResponseType(),
$this->security_context_service->get()->isAuthTimeRequired(), $this->security_context_service->get()->isAuthTimeRequired(),
$this->principal_service->get()->getAuthTime(), $this->principal_service->get()->getAuthTime(),
$prompt $prompt,
$request->getCodeChallenge(),
$request->getCodeChallengeMethod()
) )
); );
$hashed_value = Hash::compute('sha256', $code->getValue()); $hashed_value = Hash::compute('sha256', $code->getValue());
//stores on cache //stores on cache
$this->cache_service->storeHash($hashed_value, $this->cache_service->storeHash($hashed_value, $code->toArray(), intval($code->getLifetime()));
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()));
//stores brand new auth code hash value on a set by client id... //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; return $code;
} }
@ -333,61 +323,16 @@ final class TokenService extends AbstractService implements ITokenService
$hashed_value = Hash::compute('sha256', $value); $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)); throw new InvalidAuthorizationCodeException(sprintf("auth_code %s ", $value));
} }
try try {
{
$this->lock_manager_service->acquireLock('lock.get.authcode.' . $hashed_value); $this->lock_manager_service->acquireLock('lock.get.authcode.' . $hashed_value);
$payload = $this->cache_service->getHash($hashed_value, AuthorizationCode::getKeys());
$cache_values = $this->cache_service->getHash($hashed_value, [ $payload['value'] = $value;
'user_id', return AuthorizationCode::load($payload);
'client_id', } catch (UnacquiredLockException $ex1) {
'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)
{
throw new ReplayAttackAuthCodeException throw new ReplayAttackAuthCodeException
( (
$value, $value,
@ -451,14 +396,14 @@ final class TokenService extends AbstractService implements ITokenService
( (
sprintf 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->useRefreshToken(),
$client->getApplicationType(), $client->getApplicationType(),
$auth_code->getScope(), $auth_code->getScope(),
$auth_code->getAccessType(), $auth_code->getAccessType(),
$auth_code->getPrompt(), $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->useRefreshToken() &&
( (
$client->getApplicationType() == IClient::ApplicationType_Web_App || $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 || $auth_code->getAccessType() == OAuth2Protocol::OAuth2Protocol_AccessType_Offline ||
//OIDC: http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess //OIDC: http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
str_contains($auth_code->getScope(), OAuth2Protocol::OfflineAccess_Scope) str_contains($auth_code->getScope(), OAuth2Protocol::OfflineAccess_Scope)
) )
) ) {
{
//but only the first time (approval_prompt == force || not exists previous consent) //but only the first time (approval_prompt == force || not exists previous consent)
if if
( (
!$auth_code->getHasPreviousUserConsent() || !$auth_code->getHasPreviousUserConsent() ||
// google oauth2 protocol // google oauth2 protocol
strpos($auth_code->getApprovalPrompt(),OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force) !== false || strpos($auth_code->getApprovalPrompt(), OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force) !== false ||
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess // http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
strpos($auth_code->getPrompt(), OAuth2Protocol::OAuth2Protocol_Prompt_Consent) !== false strpos($auth_code->getPrompt(), OAuth2Protocol::OAuth2Protocol_Prompt_Consent) !== false
) ) {
{ Log::debug('TokenService::createAccessToken creating refresh token ....');
Log::debug('creating refresh token ....');
$this->createRefreshToken($access_token); $this->createRefreshToken($access_token);
} }
} }
@ -592,15 +536,13 @@ final class TokenService extends AbstractService implements ITokenService
$this->clearAccessTokensForRefreshToken($refresh_token->getValue()); $this->clearAccessTokensForRefreshToken($refresh_token->getValue());
//validate scope if present... //validate scope if present...
if (!is_null($scope) && empty($scope)) if (!is_null($scope) && empty($scope)) {
{
$original_scope = $refresh_token->getScope(); $original_scope = $refresh_token->getScope();
$aux_original_scope = explode(OAuth2Protocol::OAuth2Protocol_Scope_Delimiter, $original_scope); $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 //compare original scope with given one, and validate if its included on original one
//or not //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 throw new InvalidGrantTypeException
( (
sprintf sprintf
@ -611,9 +553,7 @@ final class TokenService extends AbstractService implements ITokenService
) )
); );
} }
} } else {
else
{
//get original scope //get original scope
$scope = $refresh_token->getScope(); $scope = $refresh_token->getScope();
} }
@ -654,8 +594,7 @@ final class TokenService extends AbstractService implements ITokenService
$access_token_db->setRefreshToken($refresh_token_db); $access_token_db->setRefreshToken($refresh_token_db);
$access_token_db->setClient($client); $access_token_db->setClient($client);
if (!is_null($user_id)) if (!is_null($user_id)) {
{
$user = $this->auth_service->getUserById($user_id); $user = $this->auth_service->getUserById($user_id);
$access_token_db->setOwner($user); $access_token_db->setOwner($user);
} }
@ -677,7 +616,8 @@ final class TokenService extends AbstractService implements ITokenService
* @param AccessToken $access_token * @param AccessToken $access_token
* @return bool * @return bool
*/ */
private function clearAccessTokenOnCache(AccessToken $access_token){ private function clearAccessTokenOnCache(AccessToken $access_token)
{
$value = $access_token->getValue(); $value = $access_token->getValue();
$hashed_value = Hash::compute('sha256', $value); $hashed_value = Hash::compute('sha256', $value);
@ -728,7 +668,8 @@ final class TokenService extends AbstractService implements ITokenService
* @param AccessTokenDB $access_token * @param AccessTokenDB $access_token
* @return bool * @return bool
*/ */
private function clearAccessTokenDBOnCache(AccessTokenDB $access_token){ private function clearAccessTokenDBOnCache(AccessTokenDB $access_token)
{
if ($this->cache_service->exists($access_token->getValue())) { if ($this->cache_service->exists($access_token->getValue())) {
$this->cache_service->delete($access_token->getValue()); $this->cache_service->delete($access_token->getValue());
@ -790,32 +731,24 @@ final class TokenService extends AbstractService implements ITokenService
$hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value; $hashed_value = !$is_hashed ? Hash::compute('sha256', $value) : $value;
$access_token = null; $access_token = null;
try try {
{
// check cache ... // check cache ...
if (!$this->cache_service->exists($hashed_value)) if (!$this->cache_service->exists($hashed_value)) {
{ $this->lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function () use ($value, $hashed_value) {
$this->lock_manager_service->lock('lock.get.accesstoken.' . $hashed_value, function() use($value, $hashed_value){
// check on DB... // check on DB...
$access_token_db = $this->access_token_repository->getByValue($hashed_value); $access_token_db = $this->access_token_repository->getByValue($hashed_value);
if (is_null($access_token_db)) if (is_null($access_token_db)) {
{ if ($this->isAccessTokenRevoked($hashed_value)) {
if($this->isAccessTokenRevoked($hashed_value))
{
throw new RevokedAccessTokenException(sprintf('Access token %s is revoked!', $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)); throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value));
} } else {
else
{
throw new InvalidGrantTypeException(sprintf("Access token %s is invalid!", $value)); throw new InvalidGrantTypeException(sprintf("Access token %s is invalid!", $value));
} }
} }
if ($access_token_db->isVoid()) if ($access_token_db->isVoid()) {
{
// invalid one ... // invalid one ...
throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value)); throw new ExpiredAccessTokenException(sprintf('Access token %s is expired!', $value));
} }
@ -824,7 +757,7 @@ final class TokenService extends AbstractService implements ITokenService
}); });
} }
$cache_values = $this->cache_service->getHash($hashed_value,[ $payload = $this->cache_service->getHash($hashed_value, [
'user_id', 'user_id',
'client_id', 'client_id',
'scope', 'scope',
@ -837,42 +770,32 @@ final class TokenService extends AbstractService implements ITokenService
]); ]);
// reload auth code ... // reload auth code ...
$auth_code = AuthorizationCode::load $payload['value'] = $payload['auth_code'];
(
$cache_values['auth_code'], $payload['user_id'] = intval($payload['user_id']) == 0 ? null : intval($payload['user_id']);
intval($cache_values['user_id']) == 0 ? null : intval($cache_values['user_id']), $payload['lifetime'] = $this->configuration_service->getConfigValue('OAuth2.AuthorizationCode.Lifetime');
$cache_values['client_id'], $payload['access_type'] = OAuth2Protocol::OAuth2Protocol_AccessType_Online;
$cache_values['scope'], $payload['approval_prompt'] = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto;
$cache_values['audience'], $payload['has_previous_user_consent'] = false;
null, $payload['is_hashed'] = true;
null, $auth_code = AuthorizationCode::load($payload);
$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
);
// reload access token ... // reload access token ...
$access_token = AccessToken::load $access_token = AccessToken::load
( (
$value, $value,
$auth_code, $auth_code,
$cache_values['issued'], $payload['issued'],
$cache_values['lifetime'] $payload['lifetime']
); );
$refresh_token_value = $cache_values['refresh_token']; $refresh_token_value = $payload['refresh_token'];
if (!empty($refresh_token_value)) { if (!empty($refresh_token_value)) {
$refresh_token = $this->getRefreshToken($refresh_token_value, true); $refresh_token = $this->getRefreshToken($refresh_token_value, true);
$access_token->setRefreshToken($refresh_token); $access_token->setRefreshToken($refresh_token);
} }
} } catch (UnacquiredLockException $ex1) {
catch (UnacquiredLockException $ex1)
{
throw new InvalidAccessTokenException(sprintf("access token %s ", $value)); throw new InvalidAccessTokenException(sprintf("access token %s ", $value));
} }
return $access_token; return $access_token;
@ -944,10 +867,10 @@ final class TokenService extends AbstractService implements ITokenService
$access_token->setRefreshToken($refresh_token); $access_token->setRefreshToken($refresh_token);
// bc refresh token could change // bc refresh token could change
if($refresh_cache) { if ($refresh_cache) {
if($this->clearAccessTokenOnCache($access_token)) if ($this->clearAccessTokenOnCache($access_token))
$this->storesAccessTokenOnCache($access_token); $this->storesAccessTokenOnCache($access_token);
if($this->clearAccessTokenDBOnCache($access_token_db)) if ($this->clearAccessTokenDBOnCache($access_token_db))
$this->storeAccessTokenDBOnCache($access_token_db); $this->storeAccessTokenDBOnCache($access_token_db);
} }
@ -977,16 +900,14 @@ final class TokenService extends AbstractService implements ITokenService
$refresh_token_db = $this->refresh_token_repository->getByValue($hashed_value); $refresh_token_db = $this->refresh_token_repository->getByValue($hashed_value);
if (is_null($refresh_token_db)) if (is_null($refresh_token_db)) {
{ if ($this->isRefreshTokenRevoked($hashed_value))
if($this->isRefreshTokenRevoked($hashed_value))
throw new RevokedRefreshTokenException(sprintf("revoked refresh token %s !", $value)); throw new RevokedRefreshTokenException(sprintf("revoked refresh token %s !", $value));
throw new InvalidGrantTypeException(sprintf("refresh token %s does not exists!", $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 throw new ReplayAttackRefreshTokenException
( (
$value, $value,
@ -999,8 +920,7 @@ final class TokenService extends AbstractService implements ITokenService
} }
//check is refresh token is stills alive... (ZERO is infinite lifetime) //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)); throw new InvalidGrantTypeException(sprintf("refresh token %s is expired!", $value));
} }
@ -1033,13 +953,12 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$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 $auth_code_hashed_value
) { ) {
//get related access tokens //get related access tokens
$db_access_token = $this->access_token_repository->getByAuthCode($auth_code_hashed_value); $db_access_token = $this->access_token_repository->getByAuthCode($auth_code_hashed_value);
if(is_null($db_access_token)) return; if (is_null($db_access_token)) return;
$client = $db_access_token->getClient(); $client = $db_access_token->getClient();
$access_token_value = $db_access_token->getValue(); $access_token_value = $db_access_token->getValue();
@ -1078,8 +997,7 @@ final class TokenService extends AbstractService implements ITokenService
public function revokeAccessToken($value, $is_hashed = false, ?User $current_user = null) 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, $value,
$is_hashed, $is_hashed,
$current_user $current_user
@ -1090,10 +1008,10 @@ final class TokenService extends AbstractService implements ITokenService
$access_token_db = $this->access_token_repository->getByValue($hashed_value); $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()){ 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())); 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();
@ -1122,8 +1040,7 @@ final class TokenService extends AbstractService implements ITokenService
*/ */
public function expireAccessToken($value, $is_hashed = false) public function expireAccessToken($value, $is_hashed = false)
{ {
return $this->tx_service->transaction(function () use return $this->tx_service->transaction(function () use (
(
$value, $value,
$is_hashed $is_hashed
) { ) {
@ -1132,7 +1049,7 @@ final class TokenService extends AbstractService implements ITokenService
$access_token_db = $this->access_token_repository->getByValue($hashed_value); $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 //delete from cache
@ -1169,21 +1086,18 @@ final class TokenService extends AbstractService implements ITokenService
$client = $this->client_repository->getClientById($client_id); $client = $this->client_repository->getClientById($client_id);
if (is_null($client)) if (is_null($client)) {
{
return; return;
} }
//revoke on cache //revoke on cache
$this->cache_service->deleteArray($auth_codes); $this->cache_service->deleteArray($auth_codes);
$this->cache_service->deleteArray($access_tokens); $this->cache_service->deleteArray($access_tokens);
//revoke on db //revoke on db
foreach($client->getValidAccessTokens() as $at) foreach ($client->getValidAccessTokens() as $at) {
{
$this->markAccessTokenAsRevoked($at->getValue()); $this->markAccessTokenAsRevoked($at->getValue());
} }
foreach($client->getRefreshTokens() as $rt) foreach ($client->getRefreshTokens() as $rt) {
{
$this->markRefreshTokenAsRevoked($rt->getValue()); $this->markRefreshTokenAsRevoked($rt->getValue());
} }
@ -1202,8 +1116,8 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$this->cache_service->addSingleValue $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') $this->configuration_service->getConfigValue('OAuth2.AccessToken.Revoked.Lifetime')
); );
} }
@ -1215,8 +1129,8 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$this->cache_service->addSingleValue $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') $this->configuration_service->getConfigValue('OAuth2.AccessToken.Void.Lifetime')
); );
} }
@ -1228,8 +1142,8 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$this->cache_service->addSingleValue $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') $this->configuration_service->getConfigValue('OAuth2.RefreshToken.Revoked.Lifetime')
); );
} }
@ -1273,9 +1187,9 @@ final class TokenService extends AbstractService implements ITokenService
return $this->tx_service->transaction(function () use ($value, $is_hashed, $current_user) { 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); $refresh_token = $this->refresh_token_repository->getByValue($hashed_value);
if(is_null($refresh_token)) return false; 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()){ 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())); throw new ValidationException(sprintf("refresh token %s does not belongs to user id %s!.", $value, $current_user->getId()));
} }
$refresh_token->setVoid(); $refresh_token->setVoid();
@ -1313,21 +1227,18 @@ final class TokenService extends AbstractService implements ITokenService
$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 $hashed_value
) { ) {
$refresh_token_db = $this->refresh_token_repository->getByValue($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()); $access_tokens_db = $this->access_token_repository->getByRefreshToken($refresh_token_db->getId());
if (count($access_tokens_db) == 0) return false; 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()); $this->cache_service->delete($access_token_db->getValue());
$client = $access_token_db->getClient(); $client = $access_token_db->getClient();
@ -1369,13 +1280,12 @@ final class TokenService extends AbstractService implements ITokenService
) )
{ {
$issuer = $this->configuration_service->getSiteUrl(); $issuer = $this->configuration_service->getSiteUrl();
if(empty($issuer)) throw new ConfigurationException('missing idp url'); 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'); $id_token_lifetime = $this->configuration_service->getConfigValue('OAuth2.IdToken.Lifetime');
if (is_null($client)) if (is_null($client)) {
{
throw new AbsentClientException throw new AbsentClientException
( (
sprintf sprintf
@ -1388,16 +1298,16 @@ final class TokenService extends AbstractService implements ITokenService
$user = $this->auth_service->getCurrentUser(); $user = $this->auth_service->getCurrentUser();
if(is_null($user)){ if (is_null($user)) {
$user_id = $this->principal_service->get()->getUserId(); $user_id = $this->principal_service->get()->getUserId();
Log::debug(sprintf("user id is %s", $user_id)); Log::debug(sprintf("user id is %s", $user_id));
$user = $this->auth_service->getUserById($user_id); $user = $this->auth_service->getUserById($user_id);
} }
if(is_null($user)) if (is_null($user))
throw new AbsentCurrentUserException; throw new AbsentCurrentUserException;
if(!$user instanceof User) if (!$user instanceof User)
throw new AbsentCurrentUserException; throw new AbsentCurrentUserException;
// build claim set // build claim set
@ -1428,17 +1338,17 @@ final class TokenService extends AbstractService implements ITokenService
UserService::populateAddressClaims($claim_set, $user); UserService::populateAddressClaims($claim_set, $user);
UserService::populateEmailClaims($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))); $claim_set->addClaim(new JWTClaim(OAuth2Protocol::OAuth2Protocol_Nonce, new StringOrURI($nonce)));
$id_token_response_info = $client->getIdTokenResponseInfo(); $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)) if (!is_null($sig_alg) && !is_null($access_token))
$this->buildAccessTokenHashClaim($access_token, $sig_alg , $claim_set); $this->buildAccessTokenHashClaim($access_token, $sig_alg, $claim_set);
if(!is_null($sig_alg) && !is_null($auth_code)) if (!is_null($sig_alg) && !is_null($auth_code))
$this->buildAuthCodeHashClaim($auth_code, $sig_alg , $claim_set); $this->buildAuthCodeHashClaim($auth_code, $sig_alg, $claim_set);
$this->buildAuthTimeClaim($claim_set); $this->buildAuthTimeClaim($claim_set);
@ -1461,10 +1371,10 @@ final class TokenService extends AbstractService implements ITokenService
) )
{ {
$at = $access_token->getValue(); $at = $access_token->getValue();
$at_len = $hashing_alg->getHashKeyLen() / 2 ; $at_len = $hashing_alg->getHashKeyLen() / 2;
$encoder = new Base64UrlRepresentation(); $encoder = new Base64UrlRepresentation();
if($at_len > ByteUtil::bitLength(strlen($at))) if ($at_len > ByteUtil::bitLength(strlen($at)))
throw new InvalidClientCredentials('invalid access token length!.'); throw new InvalidClientCredentials('invalid access token length!.');
$claim_set->addClaim $claim_set->addClaim
@ -1512,10 +1422,10 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$ac = $auth_code->getValue(); $ac = $auth_code->getValue();
$ac_len = $hashing_alg->getHashKeyLen() / 2 ; $ac_len = $hashing_alg->getHashKeyLen() / 2;
$encoder = new Base64UrlRepresentation(); $encoder = new Base64UrlRepresentation();
if($ac_len > ByteUtil::bitLength(strlen($ac))) if ($ac_len > ByteUtil::bitLength(strlen($ac)))
throw new InvalidClientCredentials('invalid auth code length!.'); throw new InvalidClientCredentials('invalid auth code length!.');
$claim_set->addClaim $claim_set->addClaim
@ -1548,8 +1458,7 @@ final class TokenService extends AbstractService implements ITokenService
private function buildAuthTimeClaim(JWTClaimSet $claim_set) private function buildAuthTimeClaim(JWTClaimSet $claim_set)
{ {
if($this->security_context_service->get()->isAuthTimeRequired()) if ($this->security_context_service->get()->isAuthTimeRequired()) {
{
$claim_set->addClaim $claim_set->addClaim
( (
new JWTClaim new JWTClaim
@ -1572,7 +1481,7 @@ final class TokenService extends AbstractService implements ITokenService
{ {
$auth_code_value = Hash::compute('sha256', $auth_code->getValue()); $auth_code_value = Hash::compute('sha256', $auth_code->getValue());
$db_access_token = $this->access_token_repository->getByAuthCode($auth_code_value); $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); return $this->getAccessToken($db_access_token->getValue(), true);
} }

View File

@ -0,0 +1,18 @@
<?php namespace OAuth2\Exceptions;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
class InvalidOAuth2PKCERequest extends InvalidOAuth2Request
{
}

View File

@ -16,7 +16,7 @@ use OAuth2\OAuth2Protocol;
* Class InvalidOAuth2Request * Class InvalidOAuth2Request
* @package OAuth2\Exceptions * @package OAuth2\Exceptions
*/ */
final class InvalidOAuth2Request extends OAuth2BaseException class InvalidOAuth2Request extends OAuth2BaseException
{ {
/** /**

View File

@ -1,5 +1,4 @@
<?php namespace OAuth2\Factories; <?php namespace OAuth2\Factories;
/** /**
* Copyright 2015 OpenStack Foundation * Copyright 2015 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -12,7 +11,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use OAuth2\Exceptions\InvalidOAuth2Request; use OAuth2\Exceptions\InvalidOAuth2Request;
use OAuth2\Models\AuthorizationCode; use OAuth2\Models\AuthorizationCode;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
@ -20,7 +18,6 @@ use OAuth2\Requests\OAuth2AccessTokenRequestAuthCode;
use OAuth2\Responses\OAuth2AccessTokenResponse; use OAuth2\Responses\OAuth2AccessTokenResponse;
use OAuth2\Responses\OAuth2IdTokenResponse; use OAuth2\Responses\OAuth2IdTokenResponse;
use OAuth2\Services\ITokenService; use OAuth2\Services\ITokenService;
/** /**
* Class OAuth2AccessTokenResponseFactory * Class OAuth2AccessTokenResponseFactory
* @package OAuth2\Factories * @package OAuth2\Factories
@ -48,12 +45,14 @@ final class OAuth2AccessTokenResponseFactory
$access_token = null; $access_token = null;
$id_token = null; $id_token = null;
$refresh_token = null; $refresh_token = null;
$response_type = explode $response_type = explode
( (
OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter, OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter,
$auth_code->getResponseType() $auth_code->getResponseType()
); );
$is_hybrid_flow = OAuth2Protocol::responseTypeBelongsToFlow $is_hybrid_flow = OAuth2Protocol::responseTypeBelongsToFlow
( (
$response_type, $response_type,

View File

@ -0,0 +1,69 @@
<?php namespace OAuth2\Factories;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\Exceptions\InvalidOAuth2PKCERequest;
use OAuth2\GrantTypes\Strategies\PKCEPlainValidator;
use OAuth2\GrantTypes\Strategies\PKCES256Validator;
use OAuth2\Models\AuthorizationCode;
use OAuth2\OAuth2Protocol;
use OAuth2\Requests\OAuth2AccessTokenRequestAuthCode;
use OAuth2\Strategies\IPKCEValidationMethod;
/**
* Class OAuth2PKCEValidationMethodFactory
* @package OAuth2\Factories
*/
final class OAuth2PKCEValidationMethodFactory
{
/**
* @param AuthorizationCode $auth_code
* @param OAuth2AccessTokenRequestAuthCode $request
* @return IPKCEValidationMethod
* @throws InvalidOAuth2PKCERequest
*/
static public function build(AuthorizationCode $auth_code, OAuth2AccessTokenRequestAuthCode $request)
:IPKCEValidationMethod {
$code_challenge = $auth_code->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;
}
}
}

View File

@ -14,6 +14,7 @@
use App\libs\Utils\URLUtils; use App\libs\Utils\URLUtils;
use Exception; use Exception;
use Illuminate\Support\Facades\Log;
use Models\OAuth2\Client; use Models\OAuth2\Client;
use OAuth2\Exceptions\ExpiredAuthorizationCodeException; use OAuth2\Exceptions\ExpiredAuthorizationCodeException;
use OAuth2\Exceptions\InvalidApplicationType; use OAuth2\Exceptions\InvalidApplicationType;
@ -26,6 +27,7 @@ use OAuth2\Exceptions\OAuth2GenericException;
use OAuth2\Exceptions\UnAuthorizedClientException; use OAuth2\Exceptions\UnAuthorizedClientException;
use OAuth2\Exceptions\UriNotAllowedException; use OAuth2\Exceptions\UriNotAllowedException;
use OAuth2\Factories\OAuth2AccessTokenResponseFactory; use OAuth2\Factories\OAuth2AccessTokenResponseFactory;
use OAuth2\Factories\OAuth2PKCEValidationMethodFactory;
use OAuth2\Models\IClient; use OAuth2\Models\IClient;
use OAuth2\Repositories\IClientRepository; use OAuth2\Repositories\IClientRepository;
use OAuth2\Services\ITokenService; use OAuth2\Services\ITokenService;
@ -187,16 +189,16 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
try try
{ {
parent::completeFlow($request); parent::completeFlow($request);
$client = $this->client_auth_context->getClient();
$this->checkClientTypeAccess($this->client_auth_context->getClient()); $this->checkClientTypeAccess($client);
$current_redirect_uri = $request->getRedirectUri(); $current_redirect_uri = $request->getRedirectUri();
//verify redirect uri //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 throw new UriNotAllowedException
( (
$current_redirect_uri empty($current_redirect_uri)? "missing" : $current_redirect_uri
); );
} }
@ -244,12 +246,39 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
// "redirect_uri" parameter was included in the initial authorization // "redirect_uri" parameter was included in the initial authorization
// and if included ensure that their values are identical. // and if included ensure that their values are identical.
$redirect_uri = $auth_code->getRedirectUri(); $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)) if (!empty($redirect_uri) && URLUtils::normalizeUrl($redirect_uri) !== URLUtils::normalizeUrl($current_redirect_uri))
{ {
throw new UriNotAllowedException($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 $this->principal_service->register
( (
$auth_code->getUserId(), $auth_code->getUserId(),
@ -307,7 +336,8 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
( (
!( !(
$client->getClientType() === IClient::ClientType_Confidential || $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 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(), $client->getClientId(),
IClient::ClientType_Confidential, IClient::ClientType_Confidential,
IClient::ApplicationType_Native IClient::ApplicationType_Native
@ -332,47 +362,17 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
*/ */
protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent) 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 $auth_code = $this->token_service->createAuthorizationCode
( (
$user->getId(), $request,
$request->getClientId(), $has_former_consent
$request->getScope(),
$audience,
$request->getRedirectUri(),
$request->getAccessType(),
$request->getApprovalPrompt(),
$has_former_consent,
$request->getState(),
$nonce,
$request->getResponseType(),
$prompt
); );
if (is_null($auth_code)) if (is_null($auth_code))
{ {
throw new OAuth2GenericException("Invalid Auth Code"); throw new OAuth2GenericException("Invalid Auth Code");
} }
// http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions // http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
$session_state = $this->getSessionState $session_state = $this->getSessionState
( (
@ -394,6 +394,4 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
$session_state $session_state
); );
} }
} }

View File

@ -17,6 +17,7 @@ use OAuth2\Exceptions\InvalidApplicationType;
use OAuth2\Exceptions\InvalidClientType; use OAuth2\Exceptions\InvalidClientType;
use OAuth2\Exceptions\InvalidOAuth2Request; use OAuth2\Exceptions\InvalidOAuth2Request;
use OAuth2\Exceptions\OAuth2GenericException; use OAuth2\Exceptions\OAuth2GenericException;
use OAuth2\Models\AuthorizationCode;
use OAuth2\Models\IClient; use OAuth2\Models\IClient;
use OAuth2\Repositories\IClientRepository; use OAuth2\Repositories\IClientRepository;
use OAuth2\Services\ITokenService; use OAuth2\Services\ITokenService;
@ -181,28 +182,17 @@ class HybridGrantType extends InteractiveGrantType
$auth_code = $this->token_service->createAuthorizationCode $auth_code = $this->token_service->createAuthorizationCode
( (
$user->getId(), $request,
$request->getClientId(), $has_former_consent
$request->getScope(),
$audience,
$request->getRedirectUri(),
$request->getAccessType(),
$request->getApprovalPrompt(),
$has_former_consent,
$request->getState(),
$request->getNonce(),
$request->getResponseType(),
$request->getPrompt(true)
); );
if (is_null($auth_code)) { if (is_null($auth_code) || !$auth_code instanceof AuthorizationCode) {
throw new OAuth2GenericException("Invalid Auth Code"); throw new OAuth2GenericException("Invalid Auth Code.");
} }
$access_token = null; $access_token = null;
$id_token = null; $id_token = null;
if (in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_Token, $request->getResponseType(false))) if (in_array(OAuth2Protocol::OAuth2Protocol_ResponseType_Token, $request->getResponseType(false)))
{ {
$access_token = $this->token_service->createAccessToken $access_token = $this->token_service->createAccessToken

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use Exception; use Exception;
use OAuth2\Exceptions\InvalidApplicationType; use OAuth2\Exceptions\InvalidApplicationType;
use OAuth2\Exceptions\InvalidGrantTypeException; use OAuth2\Exceptions\InvalidGrantTypeException;
@ -27,6 +28,7 @@ use OAuth2\Responses\OAuth2AccessTokenResponse;
use OAuth2\Responses\OAuth2Response; use OAuth2\Responses\OAuth2Response;
use OAuth2\Services\IClientService; use OAuth2\Services\IClientService;
use Utils\Services\ILogService; use Utils\Services\ILogService;
/** /**
* Class RefreshBearerTokenGrantType * Class RefreshBearerTokenGrantType
* @see http://tools.ietf.org/html/rfc6749#section-6 * @see http://tools.ietf.org/html/rfc6749#section-6
@ -59,7 +61,10 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
*/ */
public function canHandle(OAuth2Request $request) 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 /** 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) public function completeFlow(OAuth2Request $request)
{ {
if (!($request instanceof OAuth2RefreshAccessTokenRequest)) if (!($request instanceof OAuth2RefreshAccessTokenRequest)) {
{
throw new InvalidOAuth2Request; throw new InvalidOAuth2Request;
} }
parent::completeFlow($request); parent::completeFlow($request);
if if (!$this->current_client->canRequestRefreshTokens()) {
(
$this->current_client->getApplicationType() != IClient::ApplicationType_Web_App &&
$this->current_client->getApplicationType() != IClient::ApplicationType_Native
)
{
throw new InvalidApplicationType throw new InvalidApplicationType
( (
sprintf 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(), $this->client_auth_context->getId(),
IClient::ApplicationType_Web_App, IClient::ApplicationType_Web_App,
IClient::ApplicationType_Native 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 throw new UseRefreshTokenException
( (
sprintf sprintf
@ -133,8 +131,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
$scope = $request->getScope(); $scope = $request->getScope();
$refresh_token = $this->token_service->getRefreshToken($refresh_token_value); $refresh_token = $this->token_service->getRefreshToken($refresh_token_value);
if (is_null($refresh_token)) if (is_null($refresh_token)) {
{
throw new InvalidGrantTypeException throw new InvalidGrantTypeException
( (
sprintf 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 throw new InvalidGrantTypeException
( (
sprintf sprintf
@ -168,8 +164,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
* legitimate client, one of them will present an invalidated refresh * legitimate client, one of them will present an invalidated refresh
* token, which will inform the authorization server of the breach. * 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); $this->token_service->invalidateRefreshToken($refresh_token_value);
$new_refresh_token = $this->token_service->createRefreshToken($access_token, true); $new_refresh_token = $this->token_service->createRefreshToken($access_token, true);
} }

View File

@ -0,0 +1,42 @@
<?php namespace OAuth2\GrantTypes\Strategies;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Class PKCEBaseValidator
* @package OAuth2\GrantTypes\Strategies
*/
abstract class PKCEBaseValidator
{
/**
* @var string
*/
protected $code_challenge;
/**
* @var string
*/
protected $code_verifier;
/**
* PKCEBaseValidator constructor.
* @param string $code_challenge
* @param string $code_verifier
*/
public function __construct(string $code_challenge, string $code_verifier)
{
$this->code_challenge = $code_challenge;
$this->code_verifier = $code_verifier;
}
}

View File

@ -0,0 +1,25 @@
<?php namespace OAuth2\GrantTypes\Strategies;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\Strategies\IPKCEValidationMethod;
/**
* Class PKCEPlainValidator
* @package OAuth2\GrantTypes\Strategies
*/
final class PKCEPlainValidator extends PKCEBaseValidator implements IPKCEValidationMethod
{
public function isValid(): bool
{
return $this->code_challenge === $this->code_verifier;
}
}

View File

@ -0,0 +1,32 @@
<?php namespace OAuth2\GrantTypes\Strategies;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use OAuth2\Strategies\IPKCEValidationMethod;
/**
* Class PKCES256Validator
* @package OAuth2\GrantTypes\Strategies
*/
final class PKCES256Validator extends PKCEBaseValidator implements IPKCEValidationMethod
{
public function isValid(): bool
{
/**
* The code challenge should be a Base64 encoded string with URL and filename-safe characters. The trailing '='
* characters should be removed and no line breaks, whitespace, or other additional characters should be present.
*/
$encoded = base64_encode(hash('sha256', $this->code_verifier, true));
$calculate_code_challenge = strtr(rtrim($encoded, '='), '+/', '-_');
return $this->code_challenge === $calculate_code_challenge;
}
}

View File

@ -134,4 +134,12 @@ class AccessToken extends Token {
{ {
return 'access_token'; return 'access_token';
} }
/**
* @inheritDoc
*/
public function toArray(): array
{
return [];
}
} }

View File

@ -11,8 +11,10 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use Utils\IPHelper; use Utils\IPHelper;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
/** /**
* Class AuthorizationCode * Class AuthorizationCode
* http://tools.ietf.org/html/rfc6749#section-1.3.1 * http://tools.ietf.org/html/rfc6749#section-1.3.1
@ -59,6 +61,16 @@ class AuthorizationCode extends Token
*/ */
private $requested_auth_time; private $requested_auth_time;
/**
* @var string
*/
private $code_challenge;
/**
* @var string
*/
private $code_challenge_method;
/** /**
* @var string * @var string
* prompt * prompt
@ -111,12 +123,14 @@ class AuthorizationCode extends Token
* @param string $approval_prompt * @param string $approval_prompt
* @param bool $has_previous_user_consent * @param bool $has_previous_user_consent
* @param int $lifetime * @param int $lifetime
* @param string|null $state * @param null $state
* @param string|null $nonce * @param null $nonce
* @param string|null $response_type * @param null $response_type
* @param $requested_auth_time * @param bool $requested_auth_time
* @param $auth_time * @param int $auth_time
* @param null|string $prompt * @param null $prompt
* @param null $code_challenge
* @param null $code_challenge_method
* @return AuthorizationCode * @return AuthorizationCode
*/ */
public static function create( public static function create(
@ -134,8 +148,11 @@ class AuthorizationCode extends Token
$response_type = null, $response_type = null,
$requested_auth_time = false, $requested_auth_time = false,
$auth_time = -1, $auth_time = -1,
$prompt = null $prompt = null,
) { $code_challenge = null,
$code_challenge_method = null
)
{
$instance = new self(); $instance = new self();
$instance->scope = $scope; $instance->scope = $scope;
$instance->user_id = $user_id; $instance->user_id = $user_id;
@ -154,75 +171,43 @@ class AuthorizationCode extends Token
$instance->requested_auth_time = $requested_auth_time; $instance->requested_auth_time = $requested_auth_time;
$instance->auth_time = $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; return $instance;
} }
/** /**
* @param $value * @param array $payload
* @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 * @return AuthorizationCode
*/ */
public static function load public static function load
( (
$value, array $payload
$user_id, ): AuthorizationCode
$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
)
{ {
$instance = new self(); $instance = new self();
$instance->value = $value; $instance->value = $payload['value'];
$instance->user_id = $user_id; $instance->user_id = $payload['user_id'] ?? null;
$instance->scope = $scope; $instance->scope = $payload['scope'] ?? null;
$instance->redirect_uri = $redirect_uri; $instance->redirect_uri = $payload['redirect_uri'] ?? null;
$instance->client_id = $client_id; $instance->client_id = $payload['client_id'] ?? null;
$instance->audience = $audience; $instance->audience = $payload['audience'] ?? null;
$instance->issued = $issued; $instance->issued = $payload['issued'] ?? null;
$instance->lifetime = intval($lifetime); $instance->lifetime = intval($payload['lifetime']);
$instance->from_ip = $from_ip; $instance->from_ip = $payload['from_ip'] ?? null;
$instance->is_hashed = $is_hashed; $instance->is_hashed = isset($payload['is_hashed']) ? boolval($payload['is_hashed']) : false;
$instance->access_type = $access_type; $instance->access_type = $payload['access_type'] ?? null;
$instance->approval_prompt = $approval_prompt; $instance->approval_prompt = $payload['approval_prompt'] ?? null;
$instance->has_previous_user_consent = $has_previous_user_consent; $instance->has_previous_user_consent = $payload['has_previous_user_consent'] ?? false;
$instance->state = $state; $instance->state = $payload['state'] ?? null;
$instance->nonce = $nonce; $instance->nonce = $payload['nonce'] ?? null;
$instance->response_type = $response_type; $instance->response_type = $payload['response_type'] ?? null;
$instance->requested_auth_time = $requested_auth_time; $instance->requested_auth_time = $payload['requested_auth_time'] ?? null;;
$instance->auth_time = $auth_time; $instance->auth_time = $payload['auth_time'] ?? null;
$instance->prompt = $prompt; $instance->prompt = $payload['prompt'] ?? null;
$instance->code_challenge = $payload['code_challenge'] ?? null;
$instance->code_challenge_method = $payload['code_challenge_method'] ?? null;
return $instance; return $instance;
} }
@ -288,7 +273,7 @@ class AuthorizationCode extends Token
public function isAuthTimeRequested() public function isAuthTimeRequested()
{ {
$res = $this->requested_auth_time; $res = $this->requested_auth_time;
if (!is_string($res)) return (bool) $res; if (!is_string($res)) return (bool)$res;
switch (strtolower($res)) { switch (strtolower($res)) {
case '1': case '1':
case 'true': case 'true':
@ -332,4 +317,71 @@ class AuthorizationCode extends Token
{ {
return 'auth_code'; 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;
}
} }

View File

@ -11,7 +11,6 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Log;
use OAuth2\Exceptions\InvalidTokenEndpointAuthMethodException; use OAuth2\Exceptions\InvalidTokenEndpointAuthMethodException;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
@ -51,7 +50,8 @@ final class ClientCredentialsAuthenticationContext extends ClientAuthenticationC
if(!in_array($auth_type, [ if(!in_array($auth_type, [
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic, OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic,
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost,
OAuth2Protocol::TokenEndpoint_AuthMethod_None
])) ]))
throw new InvalidTokenEndpointAuthMethodException($auth_type); throw new InvalidTokenEndpointAuthMethodException($auth_type);

View File

@ -319,4 +319,9 @@ interface IClient extends IEntity
* @return array * @return array
*/ */
public function getValidAccessTokens(); public function getValidAccessTokens();
/**
* @return bool
*/
public function isPKCEEnabled():bool;
} }

View File

@ -85,4 +85,12 @@ class RefreshToken extends Token {
{ {
return 'refresh_token'; return 'refresh_token';
} }
/**
* @inheritDoc
*/
public function toArray(): array
{
return [];
}
} }

View File

@ -648,6 +648,8 @@ final class OAuth2Protocol implements IOAuth2Protocol
self::TokenEndpoint_AuthMethod_ClientSecretPost, self::TokenEndpoint_AuthMethod_ClientSecretPost,
self::TokenEndpoint_AuthMethod_ClientSecretJwt, self::TokenEndpoint_AuthMethod_ClientSecretJwt,
self::TokenEndpoint_AuthMethod_PrivateKeyJwt, self::TokenEndpoint_AuthMethod_PrivateKeyJwt,
// PKCE only
self::TokenEndpoint_AuthMethod_None,
); );
const OpenIdConnect_Scope = 'openid'; const OpenIdConnect_Scope = 'openid';
@ -711,6 +713,25 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/ */
const VsChar = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_~'; 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 //services
/** /**
* @var ILogService * @var ILogService

View File

@ -74,4 +74,8 @@ class OAuth2AccessTokenRequestAuthCode extends OAuth2TokenRequest
{ {
return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType_Code); return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseType_Code);
} }
public function getCodeVerifier():?string{
return $this->getParam(OAuth2Protocol::PKCE_CodeVerifier);
}
} }

View File

@ -24,7 +24,6 @@ use OAuth2\ResourceServer\IUserService;
*/ */
class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
{ {
/** /**
* @var array * @var array
*/ */
@ -120,15 +119,6 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
parent::__construct($auth_request->getMessage()); 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 * 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 // 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 // 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 // permitting offline access to the requested resources are in place

View File

@ -33,8 +33,7 @@ class OAuth2AuthorizationRequest extends OAuth2Request
parent::__construct($msg); parent::__construct($msg);
} }
public static $params = array public static $params = [
(
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType, OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType,
OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId, OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId,
OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri, OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri,
@ -42,7 +41,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request
OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State, OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State,
OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt, OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt,
OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType, 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 * 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. * Identifies the client that is making the request.
* The value passed in this parameter must exactly match the value shown in the Admin Console. * 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 //approval_prompt
$valid_approvals = array $valid_approvals = [
(
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto, OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force
); ];
if (!in_array($this->getApprovalPrompt(), $valid_approvals)) if (!in_array($this->getApprovalPrompt(), $valid_approvals))
{ {
$this->last_validation_error = 'approval_prompt is not valid'; $this->last_validation_error = 'approval_prompt is not valid';
return false; 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; return true;
} }
@ -194,4 +230,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request
if(empty($display)) return OAuth2Protocol::OAuth2Protocol_Display_Page; if(empty($display)) return OAuth2Protocol::OAuth2Protocol_Display_Page;
return $display; return $display;
} }
// PKCE
public function getCodeChallenge():?string{
return $this->getParam(OAuth2Protocol::PKCE_CodeChallenge);
}
public function getCodeChallengeMethod():?string{
return $this->getParam(OAuth2Protocol::PKCE_CodeChallengeMethod);
}
} }

View File

@ -22,6 +22,9 @@ use OAuth2\Models\RefreshToken;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
use OAuth2\Exceptions\InvalidAccessTokenException; use OAuth2\Exceptions\InvalidAccessTokenException;
use OAuth2\Exceptions\InvalidGrantTypeException; use OAuth2\Exceptions\InvalidGrantTypeException;
use OAuth2\Requests\OAuth2AuthorizationRequest;
use Utils\Model\Identifier;
/** /**
* Interface ITokenService * Interface ITokenService
* Defines the interface for an OAuth2 Token Service * Defines the interface for an OAuth2 Token Service
@ -32,35 +35,15 @@ interface ITokenService {
/** /**
* Creates a brand new authorization code * Creates a brand new authorization code
* @param $user_id * @param OAuth2AuthorizationRequest $request
* @param $client_id
* @param $scope
* @param string $audience
* @param null $redirect_uri
* @param string $access_type
* @param string $approval_prompt
* @param bool $has_previous_user_consent * @param bool $has_previous_user_consent
* @param string|null $state * @return Identifier
* @param string|null $nonce
* @param string|null $response_type
* @param string|null $prompt
* @return AuthorizationCode
*/ */
public function createAuthorizationCode public function createAuthorizationCode
( (
$user_id, OAuth2AuthorizationRequest $request,
$client_id, bool $has_previous_user_consent = false
$scope, ):Identifier;
$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
);
/** /**

View File

@ -63,6 +63,11 @@ final class ClientAuthContextValidatorFactory
return new ClientPlainCredentialsAuthContextValidator; return new ClientPlainCredentialsAuthContextValidator;
} }
break; break;
case OAuth2Protocol::TokenEndpoint_AuthMethod_None:
{
return new ClientPKCEAuthContextValidator;
}
break;
case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt: case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt:
{ {
$validator = new ClientSharedSecretAssertionAuthContextValidator; $validator = new ClientSharedSecretAssertionAuthContextValidator;

View File

@ -0,0 +1,56 @@
<?php namespace OAuth2\Strategies;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Illuminate\Support\Facades\Log;
use OAuth2\Exceptions\InvalidClientAuthenticationContextException;
use OAuth2\Exceptions\InvalidClientCredentials;
use OAuth2\Models\ClientAuthenticationContext;
use OAuth2\Models\ClientCredentialsAuthenticationContext;
use OAuth2\Models\IClient;
/**
* Class ClientPKCEAuthContextValidator
* @package OAuth2\Strategies
*/
final class ClientPKCEAuthContextValidator implements IClientAuthContextValidator
{
/**
* @param ClientAuthenticationContext $context
* @return bool
* @throws InvalidClientAuthenticationContextException
* @throws InvalidClientCredentials
*/
public function validate(ClientAuthenticationContext $context)
{
if (!($context instanceof ClientCredentialsAuthenticationContext))
throw new InvalidClientAuthenticationContextException;
$client = $context->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();
}
}

View File

@ -35,21 +35,22 @@ final class ClientPlainCredentialsAuthContextValidator implements IClientAuthCon
if(!($context instanceof ClientCredentialsAuthenticationContext)) if(!($context instanceof ClientCredentialsAuthenticationContext))
throw new InvalidClientAuthenticationContextException; throw new InvalidClientAuthenticationContextException;
if(is_null($context->getClient())) $client = $context->getClient();
if(is_null($client))
throw new InvalidClientAuthenticationContextException('client not set!'); 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())); throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType()));
if($context->getClient()->getClientType() !== IClient::ClientType_Confidential) if($client->getClientType() !== IClient::ClientType_Confidential)
throw new InvalidClientCredentials(sprintf('invalid client type %s', $context->getClient()->getClientType())); throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType()));
$providedClientId = $context->getId(); $providedClientId = $context->getId();
$providedClientSecret = $context->getSecret(); $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 id %s - provide client id %s", $client->getClientId(), $providedClientId));
Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $context->getClient()->getClientSecret(), $providedClientSecret)); Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $client->getClientSecret(), $providedClientSecret));
return $context->getClient()->getClientId() === $providedClientId &&
$context->getClient()->getClientSecret() === $providedClientSecret; return $client->getClientId() === $providedClientId && $client->getClientSecret() === $providedClientSecret;
} }
} }

View File

@ -0,0 +1,22 @@
<?php namespace OAuth2\Strategies;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* Interface IPKCEValidationMethod
* @package OAuth2\Strategies
*/
interface IPKCEValidationMethod
{
public function isValid():bool;
}

View File

@ -1,5 +1,4 @@
<?php namespace OAuth2\Strategies; <?php namespace OAuth2\Strategies;
/** /**
* Copyright 2016 OpenStack Foundation * Copyright 2016 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
@ -12,8 +11,7 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
**/ **/
use OAuth2\Requests\OAuth2AuthorizationRequest;
use OAuth2\Requests\OAuth2AuthenticationRequest;
use OAuth2\Requests\OAuth2Request; use OAuth2\Requests\OAuth2Request;
use OAuth2\Responses\OAuth2DirectResponse; use OAuth2\Responses\OAuth2DirectResponse;
use OAuth2\Responses\OAuth2IndirectFragmentResponse; use OAuth2\Responses\OAuth2IndirectFragmentResponse;
@ -24,7 +22,6 @@ use Utils\IHttpResponseStrategy;
use Utils\Services\ServiceLocator; use Utils\Services\ServiceLocator;
use OAuth2\OAuth2Protocol; use OAuth2\OAuth2Protocol;
use Exception; use Exception;
/** /**
* Class OAuth2ResponseStrategyFactoryMethod * Class OAuth2ResponseStrategyFactoryMethod
* @package OAuth2\Strategies * @package OAuth2\Strategies
@ -42,7 +39,7 @@ final class OAuth2ResponseStrategyFactoryMethod
{ {
$type = $response->getType(); $type = $response->getType();
if($request instanceof OAuth2AuthenticationRequest) if($request instanceof OAuth2AuthorizationRequest)
{ {
$response_mode = $request->getResponseMode(); $response_mode = $request->getResponseMode();

View File

@ -143,4 +143,12 @@ final class OpenIdNonce extends Identifier
{ {
return 'nonce'; return 'nonce';
} }
/**
* @inheritDoc
*/
public function toArray(): array
{
return [];
}
} }

View File

@ -90,4 +90,9 @@ abstract class Identifier
* @return string * @return string
*/ */
abstract public function getType(); abstract public function getType();
/**
* @return array
*/
abstract public function toArray(): array;
} }

View File

@ -288,6 +288,7 @@ create table if not exists oauth2_client
max_refresh_token_issuance_basis smallint(6) not null, max_refresh_token_issuance_basis smallint(6) not null,
use_refresh_token tinyint(1) default '0' not null, use_refresh_token tinyint(1) default '0' not null,
rotate_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, resource_server_id bigint unsigned null,
website text null, website text null,
application_type enum('WEB_APPLICATION', 'JS_CLIENT', 'SERVICE', 'NATIVE') default 'WEB_APPLICATION' null, application_type enum('WEB_APPLICATION', 'JS_CLIENT', 'SERVICE', 'NATIVE') default 'WEB_APPLICATION' null,

View File

@ -0,0 +1,49 @@
<?php namespace Database\Migrations;
/**
* Copyright 2020 OpenStack Foundation
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Doctrine\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema as Schema;
use LaravelDoctrine\Migrations\Schema\Builder;
use LaravelDoctrine\Migrations\Schema\Table;
/**
* Class Version20201214162511
* @package Database\Migrations
*/
class Version20201214162511 extends AbstractMigration
{
/**
* @param Schema $schema
*/
public function up(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->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');
});
}
}
}

View File

@ -848,6 +848,20 @@ PPK;
'use_refresh_token' => false, 'use_refresh_token' => false,
'redirect_uris' => 'https://www.test.com/oauth2', '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( array(
'app_name' => 'oauth2_native_app', 'app_name' => 'oauth2_native_app',
'app_description' => 'oauth2_native_app', 'app_description' => 'oauth2_native_app',
@ -932,6 +946,7 @@ PPK;
$client_confidential2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app2']); $client_confidential2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app2']);
$client_confidential3 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app3']); $client_confidential3 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app3']);
$client_public = $client_repository->findOneBy(['app_name' => 'oauth2_test_app_public']); $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_service = $client_repository->findOneBy(['app_name' => 'oauth2.service']);
$client_native = $client_repository->findOneBy(['app_name' => 'oauth2_native_app']); $client_native = $client_repository->findOneBy(['app_name' => 'oauth2_native_app']);
$client_native2 = $client_repository->findOneBy(['app_name' => 'oauth2_native_app2']); $client_native2 = $client_repository->findOneBy(['app_name' => 'oauth2_native_app2']);
@ -946,6 +961,7 @@ PPK;
$client_confidential2->addScope($scope); $client_confidential2->addScope($scope);
$client_confidential3->addScope($scope); $client_confidential3->addScope($scope);
$client_public->addScope($scope); $client_public->addScope($scope);
$client_public2->addScope($scope);
$client_service->addScope($scope); $client_service->addScope($scope);
$client_native->addScope($scope); $client_native->addScope($scope);
$client_native2->addScope($scope); $client_native2->addScope($scope);
@ -1063,7 +1079,6 @@ PPK;
TestKeys::$private_key_pem TestKeys::$private_key_pem
); );
EntityManager::persist($pkey_2); EntityManager::persist($pkey_2);
EntityManager::flush(); EntityManager::flush();

View File

@ -61,7 +61,7 @@
</div> </div>
</div> </div>
@endif @endif
@if($client->application_type == OAuth2\Models\IClient::ApplicationType_Web_App || $client->application_type == OAuth2\Models\IClient::ApplicationType_Native) @if($client->canRequestRefreshTokens())
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<label class="label-client-secret">Client Settings</label> <label class="label-client-secret">Client Settings</label>

View File

@ -19,7 +19,7 @@
<div class="checkbox"> <div class="checkbox">
<label> <label>
<input type="checkbox" class="scope-checkbox" id="scope[]" <input type="checkbox" class="scope-checkbox" id="scope[]"
@if ( in_array($scope->id,$selected_scopes)) @if ( in_array($scope->id, $selected_scopes))
checked checked
@endif @endif
value="{!!$scope->id!!}"/><span>{!!trim($scope->name)!!}</span>&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle" aria-hidden="true" title="{!!$scope->description!!}"></span> value="{!!$scope->id!!}"/><span>{!!trim($scope->name)!!}</span>&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle" aria-hidden="true" title="{!!$scope->description!!}"></span>

View File

@ -1,4 +1,20 @@
<form id="form-application-security" name="form-application-security"> <form id="form-application-security" name="form-application-security">
@if($client->getClientType() == \OAuth2\Models\IClient::ClientType_Public)
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox"
@if ($client->pkce_enabled)
checked
@endif
id="pkce_enabled">
Use PCKE?
&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true" title="Use Proof Key for Code Exchange instead of a Client Secret ( Public Clients)"></span>
</label>
</div>
</div>
@endif
<div class="form-group"> <div class="form-group">
<label for="default_max_age">Default Max. Age (optional)&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle" <label for="default_max_age">Default Max. Age (optional)&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true" aria-hidden="true"

View File

@ -95,7 +95,7 @@
</div> </div>
<div id="main_data" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="main_data_heading"> <div id="main_data" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="main_data_heading">
<div class="panel-body"> <div class="panel-body">
@include('oauth2.profile.edit-client-data',array('access_tokens' => $access_tokens, 'refresh_tokens' => $refresh_tokens,'client' => $client)) @include('oauth2.profile.edit-client-data', array('access_tokens' => $access_tokens, 'refresh_tokens' => $refresh_tokens,'client' => $client))
</div> </div>
</div> </div>
</div> </div>

View File

@ -223,7 +223,6 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode, 'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
); );
$response = $this->action $response = $this->action
( (
"POST", "POST",
@ -251,7 +250,6 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
$refresh_token = $response->refresh_token; $refresh_token = $response->refresh_token;
$this->assertTrue(!empty($refresh_token)); $this->assertTrue(!empty($refresh_token));
} }
} }
public function testTokenNTimes($n = 100){ public function testTokenNTimes($n = 100){
@ -1221,4 +1219,165 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
$this->assertTrue(isset($comps["query"])); $this->assertTrue(isset($comps["query"]));
$this->assertTrue($comps["query"] == "error=invalid_scope&error_description=missing+scope+param"); $this->assertTrue($comps["query"] == "error=invalid_scope&error_description=missing+scope+param");
} }
public function testAuthCodePKCEPublicClient(){
$client_id = '1234/Vcvr6fvQbH4HyNgwKlfSpkce.openstack.client';
Session::put("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce);
$code_verifier = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik8ik9ol1qaz2wsx3edc4rfv5tgb6yhn~";
$encoded = base64_encode(hash('sha256', $code_verifier, true));
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');
$params = [
'client_id' => $client_id,
'redirect_uri' => 'https://www.test.com/oauth2',
'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Code,
'response_mode' => 'fragment',
'scope' => sprintf('openid %s/resource-server/read', $this->current_realm),
'state' => '123456',
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType_Offline,
];
$response = $this->action("GET", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(302);
$url = $response->getTargetUrl();
// get auth code ...
$comps = @parse_url($url);
$fragment = $comps['fragment'];
$response = [];
parse_str($fragment, $response);
$this->assertTrue(isset($response['code']));
$this->assertTrue(isset($response['state']));
$this->assertTrue($response['state'] === '123456');
$params = [
'code' => $response['code'],
'redirect_uri' => 'https://www.test.com/oauth2',
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
'code_verifier' => $code_verifier,
'client_id' => $client_id,
];
$response = $this->action
(
"POST",
"OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
[]
);
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
$access_token = $response->access_token;
$this->assertTrue(!empty($access_token));
$refresh_token = $response->refresh_token;
$this->assertTrue(!empty($refresh_token));
$params = [
'refresh_token' => $refresh_token,
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_RefreshToken,
'client_id' => $client_id
];
$response = $this->action("POST", "OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
[]);
$this->assertResponseStatus(200);
$this->assertEquals('application/json;charset=UTF-8', $response->headers->get('Content-Type'));
$content = $response->getContent();
$response = json_decode($content);
//get new access token and new refresh token...
$new_access_token = $response->access_token;
$new_refresh_token = $response->refresh_token;
$this->assertTrue(!empty($new_access_token));
$this->assertTrue(!empty($new_refresh_token));
}
public function testAuthCodeInvalidPKCEPublicClient(){
$client_id = '1234/Vcvr6fvQbH4HyNgwKlfSpkce.openstack.client';
Session::put("openid.authorization.response", IAuthService::AuthorizationResponse_AllowOnce);
$code_verifier = "1qaz2wsx3edc4rfv5tgb6yhn7ujm8ik8ik9ol1qaz2wsx3edc4rfv5tgb6yhn~";
$encoded = base64_encode(hash('sha256', $code_verifier, true));
$codeChallenge = strtr(rtrim($encoded, '='), '+/', '-_');
$params = [
'client_id' => $client_id,
'redirect_uri' => 'https://www.test.com/oauth2',
'response_type' => OAuth2Protocol::OAuth2Protocol_ResponseType_Code,
'response_mode' => 'fragment',
'scope' => sprintf('%s/resource-server/read', $this->current_realm),
'state' => '123456',
'code_challenge' => $codeChallenge,
'code_challenge_method' => 'S256',
OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType_Offline,
];
$response = $this->action("GET", "OAuth2\OAuth2ProviderController@auth",
$params,
[],
[],
[]);
$this->assertResponseStatus(302);
$url = $response->getTargetUrl();
// get auth code ...
$comps = @parse_url($url);
$fragment = $comps['fragment'];
$response = [];
parse_str($fragment, $response);
$this->assertTrue(isset($response['code']));
$this->assertTrue(isset($response['state']));
$this->assertTrue($response['state'] === '123456');
$params = [
'code' => $response['code'],
'redirect_uri' => 'https://www.test.com/oauth2',
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
'code_verifier' => "missmatch",
'client_id' => $client_id,
];
$response = $this->action
(
"POST",
"OAuth2\OAuth2ProviderController@token",
$params,
[],
[],
[],
[]
);
$this->assertResponseStatus(400);
}
} }