Auth Code Flow PKCE support

WIP

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 92d57b1737
commit b7d146c056
33 changed files with 905 additions and 596 deletions

View File

@ -19,6 +19,7 @@ use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry;
use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry;
use jwa\cryptographic_algorithms\KeyManagementAlgorithms_Registry;
use jwa\JSONWebSignatureAndEncryptionAlgorithms;
use models\exceptions\ValidationException;
use OAuth2\Models\IClient;
use OAuth2\Models\IClientPublicKey;
use OAuth2\Models\JWTResponseInfo;
@ -82,6 +83,12 @@ class Client extends BaseEntity implements IClient
*/
private $active;
/**
* @ORM\Column(name="pkce_enabled", type="boolean")
* @var bool
*/
private $pkce_enabled;
/**
* @ORM\Column(name="locked", type="boolean")
* @var bool
@ -390,6 +397,7 @@ class Client extends BaseEntity implements IClient
$this->max_refresh_token_issuance_basis = 0;
$this->max_refresh_token_issuance_qty = 0;
$this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic;
$this->pkce_enabled = false;
}
public static $valid_app_types = [
@ -423,22 +431,25 @@ class Client extends BaseEntity implements IClient
throw new \InvalidArgumentException("Invalid application_type");
}
$this->application_type = strtoupper($application_type);
$this->client_type = $this->infereClientTypeFromAppType($this->application_type);
$this->client_type = $this->inferClientTypeFromAppType($this->application_type);
}
/**
* @return bool
*/
public function canRequestRefreshTokens():bool{
return $this->getApplicationType() == IClient::ApplicationType_Native ||
$this->getApplicationType() == IClient::ApplicationType_Web_App;
return
$this->getApplicationType() == IClient::ApplicationType_Native ||
$this->getApplicationType() == IClient::ApplicationType_Web_App ||
// PCKE
$this->pkce_enabled;
}
/**
* @param string $app_type
* @return string
*/
private function infereClientTypeFromAppType(string $app_type)
private function inferClientTypeFromAppType(string $app_type)
{
switch($app_type)
{
@ -1113,6 +1124,7 @@ class Client extends BaseEntity implements IClient
*/
public function isOwner(User $user):bool
{
if(!$this->hasUser()) return false;
return intval($this->user->getId()) === intval($user->getId());
}
@ -1132,7 +1144,7 @@ class Client extends BaseEntity implements IClient
*/
public function addScope(ApiScope $scope)
{
if($this->scopes->contains($scope)) return;
if($this->scopes->contains($scope)) return $this;
$this->scopes->add($scope);
return $this;
}
@ -1565,4 +1577,22 @@ class Client extends BaseEntity implements IClient
return $this->getUserId();
return $this->{$name};
}
public function isPKCEEnabled():bool{
return $this->pkce_enabled;
}
public function enablePCKE(){
if($this->client_type != self::ClientType_Public){
throw new ValidationException("Only Public Clients could use PCKE.");
}
$this->pkce_enabled = true;
}
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
{
$scope_repository = App::make(IApiScopeRepository::class);
$client = self::populate(new Client, $payload);
$client->setActive(true);
//add default scopes
foreach ($scope_repository->getDefaults() as $default_scope) {
if
(
$default_scope->getName() === OAuth2Protocol::OfflineAccess_Scope
&& !$client->canRequestRefreshTokens()
) {
continue;
}
$client->addScope($default_scope);
}
if ($client->getClientType() !== IClient::ClientType_Confidential) {
$client->setTokenEndpointAuthMethod(OAuth2Protocol::TokenEndpoint_AuthMethod_None);
}
@ -202,6 +189,29 @@ final class ClientFactory
$client->setResourceServer($resource_server);
}
if(isset($payload['pkce_enabled'])) {
$pkce_enabled = boolval($payload['pkce_enabled']);
if($pkce_enabled)
$client->enablePCKE();
else
$client->disablePCKE();
}
$scope_repository = App::make(IApiScopeRepository::class);
//add default scopes
foreach ($scope_repository->getDefaults() as $default_scope) {
if
(
$default_scope->getName() === OAuth2Protocol::OfflineAccess_Scope
&& !$client->canRequestRefreshTokens()
) {
continue;
}
$client->addScope($default_scope);
}
return $client;
}
}

View File

@ -144,31 +144,8 @@ final class ClientService extends AbstractService implements IClientService
);
}
if
(
Input::has(OAuth2Protocol::OAuth2Protocol_ClientId) &&
Input::has(OAuth2Protocol::OAuth2Protocol_ClientSecret)
)
{
Log::debug
(
sprintf
(
"ClientService::getCurrentClientAuthInfo params %s - %s present",
OAuth2Protocol::OAuth2Protocol_ClientId,
OAuth2Protocol::OAuth2Protocol_ClientSecret
)
);
return new ClientCredentialsAuthenticationContext
(
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')),
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, '')),
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost
);
}
$auth_header = Request::header('Authorization');
if(!empty($auth_header))
{
Log::debug
@ -211,6 +188,26 @@ 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
)
);
return new ClientCredentialsAuthenticationContext
(
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')),
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, '')),
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost
);
}
throw new InvalidClientAuthMethodException;
}

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@ use OAuth2\Responses\OAuth2Response;
* Class TokenEndpoint
* Token Endpoint Implementation
* The token endpoint is used by the client to obtain an access token by
* presenting its authorization grant or refresh token. The token
* presenting its authorization grant or refresh token. The token
* endpoint is used with every authorization grant except for the
* implicit grant type (since an access token is issued directly).
* @see http://tools.ietf.org/html/rfc6749#section-3.2

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
* @package OAuth2\Exceptions
*/
final class InvalidOAuth2Request extends OAuth2BaseException
class InvalidOAuth2Request extends OAuth2BaseException
{
/**

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

@ -26,6 +26,7 @@ use OAuth2\Exceptions\OAuth2GenericException;
use OAuth2\Exceptions\UnAuthorizedClientException;
use OAuth2\Exceptions\UriNotAllowedException;
use OAuth2\Factories\OAuth2AccessTokenResponseFactory;
use OAuth2\Factories\OAuth2PKCEValidationMethodFactory;
use OAuth2\Models\IClient;
use OAuth2\Repositories\IClientRepository;
use OAuth2\Services\ITokenService;
@ -187,8 +188,8 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
try
{
parent::completeFlow($request);
$this->checkClientTypeAccess($this->client_auth_context->getClient());
$client = $this->client_auth_context->getClient();
$this->checkClientTypeAccess($client);
$current_redirect_uri = $request->getRedirectUri();
//verify redirect uri
@ -200,7 +201,7 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
);
}
$code = $request->getCode();
$code = $request->getCode();
// verify that the authorization code is valid
// The client MUST NOT use the authorization code
// more than once. If an authorization code is used more than
@ -250,6 +251,32 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
throw new UriNotAllowedException($current_redirect_uri);
}
if($client->isPKCEEnabled()){
/**
* PKCE Validation
* @see https://tools.ietf.org/html/rfc7636#page-10
* @see https://oauth.net/2/pkce
* server Verifies code_verifier before Returning the Tokens
* If the "code_challenge_method" from Section 4.3 was "S256", the
* received "code_verifier" is hashed by SHA-256, base64url-encoded, and
* then compared to the "code_challenge", i.e.:
* BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
* If the "code_challenge_method" from Section 4.3 was "plain", they are
* compared directly, i.e.:
* code_verifier == code_challenge.
* If the values are equal, the token endpoint MUST continue processing
* as normal (as defined by OAuth 2.0
*/
if(!$request instanceof OAuth2AccessTokenRequestAuthCode)
throw new InvalidOAuth2Request();
$strategy = OAuth2PKCEValidationMethodFactory::build($auth_code, $request);
if(!$strategy->isValid()){
throw new InvalidOAuth2Request("PKCE request can not be validated");
}
}
$this->principal_service->register
(
$auth_code->getUserId(),
@ -307,7 +334,8 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
(
!(
$client->getClientType() === IClient::ClientType_Confidential ||
$client->getApplicationType() === IClient::ApplicationType_Native
$client->getApplicationType() === IClient::ApplicationType_Native ||
$client->isPKCEEnabled()
)
)
{
@ -315,7 +343,7 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
(
sprintf
(
"client id %s - Application type must be %s or %s",
"client id %s - Application type must be %s or %s or have PKCE enabled",
$client->getClientId(),
IClient::ClientType_Confidential,
IClient::ApplicationType_Native
@ -332,47 +360,17 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
*/
protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent)
{
$user = $this->auth_service->getCurrentUser();
// build current audience ...
$audience = $this->scope_service->getStrAudienceByScopeNames
(
explode
(
OAuth2Protocol::OAuth2Protocol_Scope_Delimiter,
$request->getScope()
)
);
$nonce = null;
$prompt = null;
if($request instanceof OAuth2AuthenticationRequest)
{
$nonce = $request->getNonce();
$prompt = $request->getPrompt(true);
}
$auth_code = $this->token_service->createAuthorizationCode
(
$user->getId(),
$request->getClientId(),
$request->getScope(),
$audience,
$request->getRedirectUri(),
$request->getAccessType(),
$request->getApprovalPrompt(),
$has_former_consent,
$request->getState(),
$nonce,
$request->getResponseType(),
$prompt
$request,
$has_former_consent
);
if (is_null($auth_code))
{
throw new OAuth2GenericException("Invalid Auth Code");
}
// http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
$session_state = $this->getSessionState
(

View File

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

View File

@ -11,6 +11,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Exception;
use OAuth2\Exceptions\InvalidApplicationType;
use OAuth2\Exceptions\InvalidGrantTypeException;
@ -27,6 +28,7 @@ use OAuth2\Responses\OAuth2AccessTokenResponse;
use OAuth2\Responses\OAuth2Response;
use OAuth2\Services\IClientService;
use Utils\Services\ILogService;
/**
* Class RefreshBearerTokenGrantType
* @see http://tools.ietf.org/html/rfc6749#section-6
@ -59,7 +61,10 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
*/
public function canHandle(OAuth2Request $request)
{
return $request instanceof OAuth2TokenRequest && $request->isValid() && $request->getGrantType() == $this->getType();
return
$request instanceof OAuth2TokenRequest &&
$request->isValid() &&
$request->getGrantType() == $this->getType();
}
/** Not implemented , there is no first process phase on this grant type
@ -92,24 +97,18 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
public function completeFlow(OAuth2Request $request)
{
if (!($request instanceof OAuth2RefreshAccessTokenRequest))
{
if (!($request instanceof OAuth2RefreshAccessTokenRequest)) {
throw new InvalidOAuth2Request;
}
parent::completeFlow($request);
if
(
$this->current_client->getApplicationType() != IClient::ApplicationType_Web_App &&
$this->current_client->getApplicationType() != IClient::ApplicationType_Native
)
{
if (!$this->current_client->canRequestRefreshTokens()) {
throw new InvalidApplicationType
(
sprintf
(
'client id %s client type must be %s or ',
'client id %s client type must be %s or %s or support PKCE',
$this->client_auth_context->getId(),
IClient::ApplicationType_Web_App,
IClient::ApplicationType_Native
@ -117,8 +116,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
);
}
if (!$this->current_client->useRefreshToken())
{
if (!$this->current_client->useRefreshToken()) {
throw new UseRefreshTokenException
(
sprintf
@ -130,11 +128,10 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
}
$refresh_token_value = $request->getRefreshToken();
$scope = $request->getScope();
$refresh_token = $this->token_service->getRefreshToken($refresh_token_value);
$scope = $request->getScope();
$refresh_token = $this->token_service->getRefreshToken($refresh_token_value);
if (is_null($refresh_token))
{
if (is_null($refresh_token)) {
throw new InvalidGrantTypeException
(
sprintf
@ -145,8 +142,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
);
}
if ($refresh_token->getClientId() !== $this->current_client->getClientId())
{
if ($refresh_token->getClientId() !== $this->current_client->getClientId()) {
throw new InvalidGrantTypeException
(
sprintf
@ -158,7 +154,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
}
$new_refresh_token = null;
$access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope);
$access_token = $this->token_service->createAccessTokenFromRefreshToken($refresh_token, $scope);
/*
* the authorization server could employ refresh token
* rotation in which a new refresh token is issued with every access
@ -168,8 +164,7 @@ final class RefreshBearerTokenGrantType extends AbstractGrantType
* legitimate client, one of them will present an invalidated refresh
* token, which will inform the authorization server of the breach.
*/
if ($this->current_client->useRotateRefreshTokenPolicy())
{
if ($this->current_client->useRotateRefreshTokenPolicy()) {
$this->token_service->invalidateRefreshToken($refresh_token_value);
$new_refresh_token = $this->token_service->createRefreshToken($access_token, true);
}

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';
}
/**
* @inheritDoc
*/
public function toArray(): array
{
return [];
}
}

View File

@ -11,8 +11,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
**/
use Utils\IPHelper;
use OAuth2\OAuth2Protocol;
/**
* Class AuthorizationCode
* http://tools.ietf.org/html/rfc6749#section-1.3.1
@ -59,6 +61,16 @@ class AuthorizationCode extends Token
*/
private $requested_auth_time;
/**
* @var string
*/
private $code_challenge;
/**
* @var string
*/
private $code_challenge_method;
/**
* @var string
* prompt
@ -111,110 +123,46 @@ class AuthorizationCode extends Token
* @param string $approval_prompt
* @param bool $has_previous_user_consent
* @param int $lifetime
* @param string|null $state
* @param string|null $nonce
* @param string|null $response_type
* @param $requested_auth_time
* @param $auth_time
* @param null|string $prompt
* @param null $state
* @param null $nonce
* @param null $response_type
* @param bool $requested_auth_time
* @param int $auth_time
* @param null $prompt
* @param null $code_challenge
* @param null $code_challenge_method
* @return AuthorizationCode
*/
public static function create(
$user_id,
$client_id,
$scope,
$audience = '',
$redirect_uri = null,
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
$audience = '',
$redirect_uri = null,
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
$has_previous_user_consent = false,
$lifetime = 600,
$state = null,
$nonce = null,
$response_type = null,
$requested_auth_time = false,
$auth_time = -1,
$prompt = null
) {
$instance = new self();
$instance->scope = $scope;
$instance->user_id = $user_id;
$instance->redirect_uri = $redirect_uri;
$instance->client_id = $client_id;
$instance->lifetime = intval($lifetime);
$instance->audience = $audience;
$instance->is_hashed = false;
$instance->from_ip = IPHelper::getUserIp();
$instance->access_type = $access_type;
$instance->approval_prompt = $approval_prompt;
$instance->has_previous_user_consent = $has_previous_user_consent;
$instance->state = $state;
$instance->nonce = $nonce;
$instance->response_type = $response_type;
$instance->requested_auth_time = $requested_auth_time;
$instance->auth_time = $auth_time;
$instance->prompt = $prompt;
return $instance;
}
/**
* @param $value
* @param $user_id
* @param $client_id
* @param $scope
* @param string $audience
* @param null $redirect_uri
* @param null $issued
* @param int $lifetime
* @param string $from_ip
* @param string $access_type
* @param string $approval_prompt
* @param bool $has_previous_user_consent
* @param string|null $state
* @param string|null $nonce
* @param string|null $response_type
* @param $requested_auth_time
* @param $auth_time
* @param null|string $prompt
* @param bool $is_hashed
* @return AuthorizationCode
*/
public static function load
(
$value,
$user_id,
$client_id,
$scope,
$audience = '',
$redirect_uri = null,
$issued = null,
$lifetime = 600,
$from_ip = '127.0.0.1',
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
$has_previous_user_consent = false,
$state,
$nonce,
$response_type,
$requested_auth_time = false,
$auth_time = -1,
$prompt = null,
$is_hashed = false
$lifetime = 600,
$state = null,
$nonce = null,
$response_type = null,
$requested_auth_time = false,
$auth_time = -1,
$prompt = null,
$code_challenge = null,
$code_challenge_method = null
)
{
$instance = new self();
$instance->value = $value;
$instance->user_id = $user_id;
$instance->scope = $scope;
$instance->redirect_uri = $redirect_uri;
$instance->client_id = $client_id;
$instance->audience = $audience;
$instance->issued = $issued;
$instance->lifetime = intval($lifetime);
$instance->from_ip = $from_ip;
$instance->is_hashed = $is_hashed;
$instance->access_type = $access_type;
$instance->scope = $scope;
$instance->user_id = $user_id;
$instance->redirect_uri = $redirect_uri;
$instance->client_id = $client_id;
$instance->lifetime = intval($lifetime);
$instance->audience = $audience;
$instance->is_hashed = false;
$instance->from_ip = IPHelper::getUserIp();
$instance->access_type = $access_type;
$instance->approval_prompt = $approval_prompt;
$instance->has_previous_user_consent = $has_previous_user_consent;
$instance->state = $state;
@ -222,7 +170,44 @@ class AuthorizationCode extends Token
$instance->response_type = $response_type;
$instance->requested_auth_time = $requested_auth_time;
$instance->auth_time = $auth_time;
$instance->prompt = $prompt;
$instance->prompt = $prompt;
$instance->code_challenge = $code_challenge;
$instance->code_challenge_method = $code_challenge_method;
return $instance;
}
/**
* @param array $payload
* @return AuthorizationCode
*/
public static function load
(
array $payload
): AuthorizationCode
{
$instance = new self();
$instance->value = $payload['value'];
$instance->user_id = $payload['user_id'] ?? null;
$instance->scope = $payload['scope'] ?? null;
$instance->redirect_uri = $payload['redirect_uri'] ?? null;
$instance->client_id = $payload['client_id'] ?? null;
$instance->audience = $payload['audience'] ?? null;
$instance->issued = $payload['issued'] ?? null;
$instance->lifetime = intval($payload['lifetime']);
$instance->from_ip = $payload['from_ip'] ?? null;
$instance->is_hashed = isset($payload['is_hashed']) ? boolval($payload['is_hashed']) : false;
$instance->access_type = $payload['access_type'] ?? null;
$instance->approval_prompt = $payload['approval_prompt'] ?? null;
$instance->has_previous_user_consent = $payload['has_previous_user_consent'] ?? false;
$instance->state = $payload['state'] ?? null;
$instance->nonce = $payload['nonce'] ?? null;
$instance->response_type = $payload['response_type'] ?? null;
$instance->requested_auth_time = $payload['requested_auth_time'] ?? null;;
$instance->auth_time = $payload['auth_time'] ?? null;
$instance->prompt = $payload['prompt'] ?? null;
$instance->code_challenge = $payload['code_challenge'] ?? null;
$instance->code_challenge_method = $payload['code_challenge_method'] ?? null;
return $instance;
}
@ -288,7 +273,7 @@ class AuthorizationCode extends Token
public function isAuthTimeRequested()
{
$res = $this->requested_auth_time;
if (!is_string($res)) return (bool) $res;
if (!is_string($res)) return (bool)$res;
switch (strtolower($res)) {
case '1':
case 'true':
@ -332,4 +317,69 @@ class AuthorizationCode extends Token
{
return 'auth_code';
}
public function toArray(): array
{
return [
'client_id' => $this->getClientId(),
'scope' => $this->getScope(),
'audience' => $this->getAudience(),
'redirect_uri' => $this->getRedirectUri(),
'issued' => $this->getIssued(),
'lifetime' => $this->getLifetime(),
'from_ip' => $this->getFromIp(),
'user_id' => $this->getUserId(),
'access_type' => $this->getAccessType(),
'approval_prompt' => $this->getApprovalPrompt(),
'has_previous_user_consent' => $this->getHasPreviousUserConsent(),
'state' => $this->getState(),
'nonce' => $this->getNonce(),
'response_type' => $this->getResponseType(),
'requested_auth_time' => $this->isAuthTimeRequested(),
'auth_time' => $this->getAuthTime(),
'prompt' => $this->getPrompt(),
];
}
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
* limitations under the License.
**/
use Illuminate\Support\Facades\Log;
use OAuth2\Exceptions\InvalidTokenEndpointAuthMethodException;
use OAuth2\OAuth2Protocol;

View File

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

View File

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

View File

@ -711,6 +711,25 @@ final class OAuth2Protocol implements IOAuth2Protocol
*/
const VsChar = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz.-_~';
/**
* PKCE
* @see https://tools.ietf.org/html/rfc7636
**/
// auth request new params
const PKCE_CodeChallenge = 'code_challenge';
const PKCE_CodeChallengeMethod = 'code_challenge_method';
const PKCE_CodeChallengeMethodPlain = 'plain';
const PKCE_CodeChallengeMethodSHA256 = 'S256';
const PKCE_ValidCodeChallengeMethods = [self::PKCE_CodeChallengeMethodPlain, self::PKCE_CodeChallengeMethodSHA256];
// token request new params
const PKCE_CodeVerifier = 'code_verifier';
//services
/**
* @var ILogService

View File

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

View File

@ -33,8 +33,7 @@ class OAuth2AuthorizationRequest extends OAuth2Request
parent::__construct($msg);
}
public static $params = array
(
public static $params = [
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType,
OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId,
OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri,
@ -42,7 +41,7 @@ class OAuth2AuthorizationRequest extends OAuth2Request
OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State,
OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt,
OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType,
);
];
/**
* The Response Type request parameter response_type informs the Authorization Server of the desired authorization
@ -171,17 +170,23 @@ class OAuth2AuthorizationRequest extends OAuth2Request
}
//approval_prompt
$valid_approvals = array
(
$valid_approvals = [
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force
);
];
if (!in_array($this->getApprovalPrompt(), $valid_approvals))
{
$this->last_validation_error = 'approval_prompt is not valid';
return false;
}
// PCKE validation
if(!is_null($this->getCodeChallenge())){
if(!in_array( $this->getCodeChallengeMethod(), OAuth2Protocol::PKCE_ValidCodeChallengeMethods)){
$this->last_validation_error = sprintf("%s not valid", OAuth2Protocol::PKCE_CodeChallengeMethod);
return false;
}
}
return true;
}
@ -194,4 +199,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request
if(empty($display)) return OAuth2Protocol::OAuth2Protocol_Display_Page;
return $display;
}
// PKCE
public function getCodeChallenge():?string{
return $this->getParam(OAuth2Protocol::PKCE_CodeChallenge);
}
public function getCodeChallengeMethod():?string{
return $this->getParam(OAuth2Protocol::PKCE_CodeChallengeMethod);
}
}

View File

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

View File

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

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

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

View File

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

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

@ -61,7 +61,7 @@
</div>
</div>
@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="col-md-12">
<label class="label-client-secret">Client Settings</label>

View File

@ -19,7 +19,7 @@
<div class="checkbox">
<label>
<input type="checkbox" class="scope-checkbox" id="scope[]"
@if ( in_array($scope->id,$selected_scopes))
@if ( in_array($scope->id, $selected_scopes))
checked
@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>

View File

@ -1,4 +1,20 @@
<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">
<label for="default_max_age">Default Max. Age (optional)&nbsp;<span class="glyphicon glyphicon-info-sign accordion-toggle"
aria-hidden="true"

View File

@ -95,7 +95,7 @@
</div>
<div id="main_data" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="main_data_heading">
<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>