Merge "Auth Code Flow PKCE support"
This commit is contained in:
commit
0d20134f7f
@ -30,6 +30,7 @@ class Kernel extends HttpKernel
|
||||
protected $middleware = [
|
||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||
\App\Http\Middleware\SingleAccessPoint::class,
|
||||
\Spatie\Cors\Cors::class,
|
||||
\App\Http\Middleware\ParseMultipartFormDataInputForNonPostRequests::class,
|
||||
];
|
||||
|
||||
@ -50,7 +51,6 @@ class Kernel extends HttpKernel
|
||||
|
||||
'api' => [
|
||||
'ssl',
|
||||
'cors',
|
||||
'oauth2.endpoint',
|
||||
],
|
||||
];
|
||||
@ -69,7 +69,6 @@ class Kernel extends HttpKernel
|
||||
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
|
||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
||||
'csrf' => \App\Http\Middleware\VerifyCsrfToken::class,
|
||||
'cors' => \Spatie\Cors\Cors::class,
|
||||
'oauth2.endpoint' => \App\Http\Middleware\OAuth2BearerAccessTokenRequestValidator::class,
|
||||
'oauth2.currentuser.serveradmin' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdmin::class,
|
||||
'oauth2.currentuser.serveradmin.json' => \App\Http\Middleware\CurrentUserIsOAuth2ServerAdminJson::class,
|
||||
|
@ -91,6 +91,7 @@ Route::group(['namespace' => 'App\Http\Controllers', 'middleware' => 'web' ], fu
|
||||
});
|
||||
|
||||
Route::group(['namespace' => 'OAuth2' , 'prefix' => 'oauth2', 'middleware' => ['ssl']], function () {
|
||||
|
||||
Route::get('/check-session', "OAuth2ProviderController@checkSessionIFrame");
|
||||
Route::get('/end-session', "OAuth2ProviderController@endSession");
|
||||
Route::get('/end-session/cancel', "OAuth2ProviderController@cancelLogout");
|
||||
@ -375,7 +376,6 @@ Route::group(
|
||||
'prefix' => 'api/v1',
|
||||
'middleware' => [
|
||||
'ssl',
|
||||
'cors',
|
||||
'oauth2.endpoint',
|
||||
]
|
||||
], function () {
|
||||
|
@ -15,10 +15,12 @@
|
||||
use App\libs\Utils\URLUtils;
|
||||
use Auth\User;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use jwa\cryptographic_algorithms\ContentEncryptionAlgorithms_Registry;
|
||||
use jwa\cryptographic_algorithms\DigitalSignatures_MACs_Registry;
|
||||
use jwa\cryptographic_algorithms\KeyManagementAlgorithms_Registry;
|
||||
use jwa\JSONWebSignatureAndEncryptionAlgorithms;
|
||||
use models\exceptions\ValidationException;
|
||||
use OAuth2\Models\IClient;
|
||||
use OAuth2\Models\IClientPublicKey;
|
||||
use OAuth2\Models\JWTResponseInfo;
|
||||
@ -82,6 +84,12 @@ class Client extends BaseEntity implements IClient
|
||||
*/
|
||||
private $active;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="pkce_enabled", type="boolean")
|
||||
* @var bool
|
||||
*/
|
||||
private $pkce_enabled;
|
||||
|
||||
/**
|
||||
* @ORM\Column(name="locked", type="boolean")
|
||||
* @var bool
|
||||
@ -371,6 +379,7 @@ class Client extends BaseEntity implements IClient
|
||||
$this->active = false;
|
||||
$this->use_refresh_token = false;
|
||||
$this->rotate_refresh_token = false;
|
||||
$this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic;
|
||||
$this->token_endpoint_auth_signing_alg = JSONWebSignatureAndEncryptionAlgorithms::None;
|
||||
$this->userinfo_signed_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None;
|
||||
$this->userinfo_encrypted_response_alg = JSONWebSignatureAndEncryptionAlgorithms::None;
|
||||
@ -389,7 +398,7 @@ class Client extends BaseEntity implements IClient
|
||||
$this->max_access_token_issuance_qty = 0;
|
||||
$this->max_refresh_token_issuance_basis = 0;
|
||||
$this->max_refresh_token_issuance_qty = 0;
|
||||
$this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic;
|
||||
$this->pkce_enabled = false;
|
||||
}
|
||||
|
||||
public static $valid_app_types = [
|
||||
@ -423,22 +432,25 @@ class Client extends BaseEntity implements IClient
|
||||
throw new \InvalidArgumentException("Invalid application_type");
|
||||
}
|
||||
$this->application_type = strtoupper($application_type);
|
||||
$this->client_type = $this->infereClientTypeFromAppType($this->application_type);
|
||||
$this->client_type = $this->inferClientTypeFromAppType($this->application_type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function canRequestRefreshTokens():bool{
|
||||
return $this->getApplicationType() == IClient::ApplicationType_Native ||
|
||||
$this->getApplicationType() == IClient::ApplicationType_Web_App;
|
||||
return
|
||||
$this->getApplicationType() == IClient::ApplicationType_Native ||
|
||||
$this->getApplicationType() == IClient::ApplicationType_Web_App ||
|
||||
// PCKE
|
||||
$this->pkce_enabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $app_type
|
||||
* @return string
|
||||
*/
|
||||
private function infereClientTypeFromAppType(string $app_type)
|
||||
private function inferClientTypeFromAppType(string $app_type)
|
||||
{
|
||||
switch($app_type)
|
||||
{
|
||||
@ -587,14 +599,20 @@ class Client extends BaseEntity implements IClient
|
||||
($this->application_type !== IClient::ApplicationType_Native && !URLUtils::isHTTPS($uri))
|
||||
&& (ServerConfigurationService::getConfigValue("SSL.Enable"))
|
||||
)
|
||||
{
|
||||
Log::debug(sprintf("Client::isUriAllowed url %s is not under ssl schema", $uri));
|
||||
return false;
|
||||
}
|
||||
|
||||
$redirect_uris = explode(',',strtolower($this->redirect_uris));
|
||||
$uri = URLUtils::normalizeUrl($uri);
|
||||
foreach($redirect_uris as $redirect_uri){
|
||||
Log::debug(sprintf("Client::isUriAllowed url %s client %s redirect_uri %s", $uri, $this->client_id, $redirect_uri));
|
||||
if(str_contains($uri, $redirect_uri))
|
||||
return true;
|
||||
}
|
||||
|
||||
Log::debug(sprintf("Client::isUriAllowed url %s is not allowed as return url for client %s", $uri, $this->client_id));
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -1113,6 +1131,7 @@ class Client extends BaseEntity implements IClient
|
||||
*/
|
||||
public function isOwner(User $user):bool
|
||||
{
|
||||
if(!$this->hasUser()) return false;
|
||||
return intval($this->user->getId()) === intval($user->getId());
|
||||
}
|
||||
|
||||
@ -1132,7 +1151,7 @@ class Client extends BaseEntity implements IClient
|
||||
*/
|
||||
public function addScope(ApiScope $scope)
|
||||
{
|
||||
if($this->scopes->contains($scope)) return;
|
||||
if($this->scopes->contains($scope)) return $this;
|
||||
$this->scopes->add($scope);
|
||||
return $this;
|
||||
}
|
||||
@ -1565,4 +1584,23 @@ class Client extends BaseEntity implements IClient
|
||||
return $this->getUserId();
|
||||
return $this->{$name};
|
||||
}
|
||||
|
||||
public function isPKCEEnabled():bool{
|
||||
return $this->pkce_enabled;
|
||||
}
|
||||
|
||||
public function enablePCKE(){
|
||||
if($this->client_type != self::ClientType_Public){
|
||||
throw new ValidationException("Only Public Clients could use PCKE.");
|
||||
}
|
||||
$this->pkce_enabled = true;
|
||||
$this->token_endpoint_auth_method = OAuth2Protocol::TokenEndpoint_AuthMethod_None;
|
||||
}
|
||||
|
||||
public function disablePCKE(){
|
||||
if($this->client_type != self::ClientType_Public){
|
||||
throw new ValidationException("Only Public Clients could use PCKE.");
|
||||
}
|
||||
$this->pkce_enabled = false;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -144,38 +144,16 @@ final class ClientService extends AbstractService implements IClientService
|
||||
);
|
||||
}
|
||||
|
||||
if
|
||||
(
|
||||
Input::has(OAuth2Protocol::OAuth2Protocol_ClientId) &&
|
||||
Input::has(OAuth2Protocol::OAuth2Protocol_ClientSecret)
|
||||
)
|
||||
{
|
||||
Log::debug
|
||||
(
|
||||
sprintf
|
||||
(
|
||||
"ClientService::getCurrentClientAuthInfo params %s - %s present",
|
||||
OAuth2Protocol::OAuth2Protocol_ClientId,
|
||||
OAuth2Protocol::OAuth2Protocol_ClientSecret
|
||||
)
|
||||
);
|
||||
|
||||
return new ClientCredentialsAuthenticationContext
|
||||
(
|
||||
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')),
|
||||
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, '')),
|
||||
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost
|
||||
);
|
||||
}
|
||||
|
||||
$auth_header = Request::header('Authorization');
|
||||
if(!empty($auth_header))
|
||||
if(Request::hasHeader('Authorization'))
|
||||
{
|
||||
|
||||
Log::debug
|
||||
(
|
||||
"ClientService::getCurrentClientAuthInfo Authorization Header present"
|
||||
);
|
||||
|
||||
$auth_header = Request::header('Authorization');
|
||||
$auth_header = trim($auth_header);
|
||||
$auth_header = explode(' ', $auth_header);
|
||||
|
||||
@ -211,6 +189,34 @@ final class ClientService extends AbstractService implements IClientService
|
||||
);
|
||||
}
|
||||
|
||||
if(Input::has(OAuth2Protocol::OAuth2Protocol_ClientId))
|
||||
{
|
||||
Log::debug
|
||||
(
|
||||
sprintf
|
||||
(
|
||||
"ClientService::getCurrentClientAuthInfo params %s - %s present",
|
||||
OAuth2Protocol::OAuth2Protocol_ClientId,
|
||||
OAuth2Protocol::OAuth2Protocol_ClientSecret
|
||||
)
|
||||
);
|
||||
|
||||
$client_secret = null;
|
||||
$auth_type = OAuth2Protocol::TokenEndpoint_AuthMethod_None;
|
||||
|
||||
if(Input::has(OAuth2Protocol::OAuth2Protocol_ClientSecret)){
|
||||
$client_secret = urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientSecret, ''));
|
||||
$auth_type = OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost;
|
||||
}
|
||||
|
||||
return new ClientCredentialsAuthenticationContext
|
||||
(
|
||||
urldecode(Input::get(OAuth2Protocol::OAuth2Protocol_ClientId, '')),
|
||||
$client_secret,
|
||||
$auth_type
|
||||
);
|
||||
}
|
||||
|
||||
throw new InvalidClientAuthMethodException;
|
||||
}
|
||||
|
||||
|
@ -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
@ -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
|
||||
|
18
app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php
Normal file
18
app/libs/OAuth2/Exceptions/InvalidOAuth2PKCERequest.php
Normal 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
|
||||
{
|
||||
|
||||
}
|
@ -16,7 +16,7 @@ use OAuth2\OAuth2Protocol;
|
||||
* Class InvalidOAuth2Request
|
||||
* @package OAuth2\Exceptions
|
||||
*/
|
||||
final class InvalidOAuth2Request extends OAuth2BaseException
|
||||
class InvalidOAuth2Request extends OAuth2BaseException
|
||||
{
|
||||
|
||||
/**
|
||||
|
@ -1,5 +1,4 @@
|
||||
<?php namespace OAuth2\Factories;
|
||||
|
||||
/**
|
||||
* Copyright 2015 OpenStack Foundation
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -12,7 +11,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
use OAuth2\Exceptions\InvalidOAuth2Request;
|
||||
use OAuth2\Models\AuthorizationCode;
|
||||
use OAuth2\OAuth2Protocol;
|
||||
@ -20,7 +18,6 @@ use OAuth2\Requests\OAuth2AccessTokenRequestAuthCode;
|
||||
use OAuth2\Responses\OAuth2AccessTokenResponse;
|
||||
use OAuth2\Responses\OAuth2IdTokenResponse;
|
||||
use OAuth2\Services\ITokenService;
|
||||
|
||||
/**
|
||||
* Class OAuth2AccessTokenResponseFactory
|
||||
* @package OAuth2\Factories
|
||||
@ -48,12 +45,14 @@ final class OAuth2AccessTokenResponseFactory
|
||||
$access_token = null;
|
||||
$id_token = null;
|
||||
$refresh_token = null;
|
||||
|
||||
$response_type = explode
|
||||
(
|
||||
OAuth2Protocol::OAuth2Protocol_ResponseType_Delimiter,
|
||||
$auth_code->getResponseType()
|
||||
);
|
||||
|
||||
|
||||
$is_hybrid_flow = OAuth2Protocol::responseTypeBelongsToFlow
|
||||
(
|
||||
$response_type,
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -14,6 +14,7 @@
|
||||
|
||||
use App\libs\Utils\URLUtils;
|
||||
use Exception;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Models\OAuth2\Client;
|
||||
use OAuth2\Exceptions\ExpiredAuthorizationCodeException;
|
||||
use OAuth2\Exceptions\InvalidApplicationType;
|
||||
@ -26,6 +27,7 @@ use OAuth2\Exceptions\OAuth2GenericException;
|
||||
use OAuth2\Exceptions\UnAuthorizedClientException;
|
||||
use OAuth2\Exceptions\UriNotAllowedException;
|
||||
use OAuth2\Factories\OAuth2AccessTokenResponseFactory;
|
||||
use OAuth2\Factories\OAuth2PKCEValidationMethodFactory;
|
||||
use OAuth2\Models\IClient;
|
||||
use OAuth2\Repositories\IClientRepository;
|
||||
use OAuth2\Services\ITokenService;
|
||||
@ -187,20 +189,20 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
try
|
||||
{
|
||||
parent::completeFlow($request);
|
||||
|
||||
$this->checkClientTypeAccess($this->client_auth_context->getClient());
|
||||
$client = $this->client_auth_context->getClient();
|
||||
$this->checkClientTypeAccess($client);
|
||||
|
||||
$current_redirect_uri = $request->getRedirectUri();
|
||||
//verify redirect uri
|
||||
if (!$this->current_client->isUriAllowed($current_redirect_uri))
|
||||
if (empty($current_redirect_uri) || !$this->current_client->isUriAllowed($current_redirect_uri))
|
||||
{
|
||||
throw new UriNotAllowedException
|
||||
(
|
||||
$current_redirect_uri
|
||||
empty($current_redirect_uri)? "missing" : $current_redirect_uri
|
||||
);
|
||||
}
|
||||
|
||||
$code = $request->getCode();
|
||||
$code = $request->getCode();
|
||||
// verify that the authorization code is valid
|
||||
// The client MUST NOT use the authorization code
|
||||
// more than once. If an authorization code is used more than
|
||||
@ -244,12 +246,39 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
// "redirect_uri" parameter was included in the initial authorization
|
||||
// and if included ensure that their values are identical.
|
||||
$redirect_uri = $auth_code->getRedirectUri();
|
||||
|
||||
Log::debug(sprintf("AuthorizationCodeGrantType::completeFlow auth code redirect uri %s current_redirect_uri %s", $redirect_uri, $current_redirect_uri));
|
||||
if (!empty($redirect_uri) && URLUtils::normalizeUrl($redirect_uri) !== URLUtils::normalizeUrl($current_redirect_uri))
|
||||
{
|
||||
throw new UriNotAllowedException($current_redirect_uri);
|
||||
}
|
||||
|
||||
if($client->isPKCEEnabled()){
|
||||
/**
|
||||
* PKCE Validation
|
||||
* @see https://tools.ietf.org/html/rfc7636#page-10
|
||||
* @see https://oauth.net/2/pkce
|
||||
* server Verifies code_verifier before Returning the Tokens
|
||||
* If the "code_challenge_method" from Section 4.3 was "S256", the
|
||||
* received "code_verifier" is hashed by SHA-256, base64url-encoded, and
|
||||
* then compared to the "code_challenge", i.e.:
|
||||
* BASE64URL-ENCODE(SHA256(ASCII(code_verifier))) == code_challenge
|
||||
* If the "code_challenge_method" from Section 4.3 was "plain", they are
|
||||
* compared directly, i.e.:
|
||||
* code_verifier == code_challenge.
|
||||
* If the values are equal, the token endpoint MUST continue processing
|
||||
* as normal (as defined by OAuth 2.0
|
||||
*/
|
||||
|
||||
if(!$request instanceof OAuth2AccessTokenRequestAuthCode)
|
||||
throw new InvalidOAuth2Request();
|
||||
|
||||
$strategy = OAuth2PKCEValidationMethodFactory::build($auth_code, $request);
|
||||
|
||||
if(!$strategy->isValid()){
|
||||
throw new InvalidOAuth2Request("PKCE request can not be validated");
|
||||
}
|
||||
}
|
||||
|
||||
$this->principal_service->register
|
||||
(
|
||||
$auth_code->getUserId(),
|
||||
@ -307,7 +336,8 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
(
|
||||
!(
|
||||
$client->getClientType() === IClient::ClientType_Confidential ||
|
||||
$client->getApplicationType() === IClient::ApplicationType_Native
|
||||
$client->getApplicationType() === IClient::ApplicationType_Native ||
|
||||
$client->isPKCEEnabled()
|
||||
)
|
||||
)
|
||||
{
|
||||
@ -315,7 +345,7 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
(
|
||||
sprintf
|
||||
(
|
||||
"client id %s - Application type must be %s or %s",
|
||||
"client id %s - Application type must be %s or %s or have PKCE enabled",
|
||||
$client->getClientId(),
|
||||
IClient::ClientType_Confidential,
|
||||
IClient::ApplicationType_Native
|
||||
@ -332,47 +362,17 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
*/
|
||||
protected function buildResponse(OAuth2AuthorizationRequest $request, $has_former_consent)
|
||||
{
|
||||
$user = $this->auth_service->getCurrentUser();
|
||||
|
||||
// build current audience ...
|
||||
$audience = $this->scope_service->getStrAudienceByScopeNames
|
||||
(
|
||||
explode
|
||||
(
|
||||
OAuth2Protocol::OAuth2Protocol_Scope_Delimiter,
|
||||
$request->getScope()
|
||||
)
|
||||
);
|
||||
|
||||
$nonce = null;
|
||||
$prompt = null;
|
||||
|
||||
if($request instanceof OAuth2AuthenticationRequest)
|
||||
{
|
||||
$nonce = $request->getNonce();
|
||||
$prompt = $request->getPrompt(true);
|
||||
}
|
||||
|
||||
$auth_code = $this->token_service->createAuthorizationCode
|
||||
(
|
||||
$user->getId(),
|
||||
$request->getClientId(),
|
||||
$request->getScope(),
|
||||
$audience,
|
||||
$request->getRedirectUri(),
|
||||
$request->getAccessType(),
|
||||
$request->getApprovalPrompt(),
|
||||
$has_former_consent,
|
||||
$request->getState(),
|
||||
$nonce,
|
||||
$request->getResponseType(),
|
||||
$prompt
|
||||
$request,
|
||||
$has_former_consent
|
||||
);
|
||||
|
||||
if (is_null($auth_code))
|
||||
{
|
||||
throw new OAuth2GenericException("Invalid Auth Code");
|
||||
}
|
||||
|
||||
// http://openid.net/specs/openid-connect-session-1_0.html#CreatingUpdatingSessions
|
||||
$session_state = $this->getSessionState
|
||||
(
|
||||
@ -394,6 +394,4 @@ class AuthorizationCodeGrantType extends InteractiveGrantType
|
||||
$session_state
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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
|
||||
|
@ -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);
|
||||
}
|
||||
|
42
app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php
Normal file
42
app/libs/OAuth2/GrantTypes/Strategies/PKCEBaseValidator.php
Normal 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;
|
||||
}
|
||||
|
||||
}
|
25
app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php
Normal file
25
app/libs/OAuth2/GrantTypes/Strategies/PKCEPlainValidator.php
Normal 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;
|
||||
}
|
||||
}
|
32
app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php
Normal file
32
app/libs/OAuth2/GrantTypes/Strategies/PKCES256Validator.php
Normal 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;
|
||||
}
|
||||
}
|
@ -134,4 +134,12 @@ class AccessToken extends Token {
|
||||
{
|
||||
return 'access_token';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -11,8 +11,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
use Utils\IPHelper;
|
||||
use OAuth2\OAuth2Protocol;
|
||||
|
||||
/**
|
||||
* Class AuthorizationCode
|
||||
* http://tools.ietf.org/html/rfc6749#section-1.3.1
|
||||
@ -59,6 +61,16 @@ class AuthorizationCode extends Token
|
||||
*/
|
||||
private $requested_auth_time;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $code_challenge;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $code_challenge_method;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
* prompt
|
||||
@ -111,110 +123,46 @@ class AuthorizationCode extends Token
|
||||
* @param string $approval_prompt
|
||||
* @param bool $has_previous_user_consent
|
||||
* @param int $lifetime
|
||||
* @param string|null $state
|
||||
* @param string|null $nonce
|
||||
* @param string|null $response_type
|
||||
* @param $requested_auth_time
|
||||
* @param $auth_time
|
||||
* @param null|string $prompt
|
||||
* @param null $state
|
||||
* @param null $nonce
|
||||
* @param null $response_type
|
||||
* @param bool $requested_auth_time
|
||||
* @param int $auth_time
|
||||
* @param null $prompt
|
||||
* @param null $code_challenge
|
||||
* @param null $code_challenge_method
|
||||
* @return AuthorizationCode
|
||||
*/
|
||||
public static function create(
|
||||
$user_id,
|
||||
$client_id,
|
||||
$scope,
|
||||
$audience = '',
|
||||
$redirect_uri = null,
|
||||
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
|
||||
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
|
||||
$audience = '',
|
||||
$redirect_uri = null,
|
||||
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
|
||||
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
|
||||
$has_previous_user_consent = false,
|
||||
$lifetime = 600,
|
||||
$state = null,
|
||||
$nonce = null,
|
||||
$response_type = null,
|
||||
$requested_auth_time = false,
|
||||
$auth_time = -1,
|
||||
$prompt = null
|
||||
) {
|
||||
$instance = new self();
|
||||
$instance->scope = $scope;
|
||||
$instance->user_id = $user_id;
|
||||
$instance->redirect_uri = $redirect_uri;
|
||||
$instance->client_id = $client_id;
|
||||
$instance->lifetime = intval($lifetime);
|
||||
$instance->audience = $audience;
|
||||
$instance->is_hashed = false;
|
||||
$instance->from_ip = IPHelper::getUserIp();
|
||||
$instance->access_type = $access_type;
|
||||
$instance->approval_prompt = $approval_prompt;
|
||||
$instance->has_previous_user_consent = $has_previous_user_consent;
|
||||
$instance->state = $state;
|
||||
$instance->nonce = $nonce;
|
||||
$instance->response_type = $response_type;
|
||||
$instance->requested_auth_time = $requested_auth_time;
|
||||
$instance->auth_time = $auth_time;
|
||||
$instance->prompt = $prompt;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $value
|
||||
* @param $user_id
|
||||
* @param $client_id
|
||||
* @param $scope
|
||||
* @param string $audience
|
||||
* @param null $redirect_uri
|
||||
* @param null $issued
|
||||
* @param int $lifetime
|
||||
* @param string $from_ip
|
||||
* @param string $access_type
|
||||
* @param string $approval_prompt
|
||||
* @param bool $has_previous_user_consent
|
||||
* @param string|null $state
|
||||
* @param string|null $nonce
|
||||
* @param string|null $response_type
|
||||
* @param $requested_auth_time
|
||||
* @param $auth_time
|
||||
* @param null|string $prompt
|
||||
* @param bool $is_hashed
|
||||
* @return AuthorizationCode
|
||||
*/
|
||||
public static function load
|
||||
(
|
||||
$value,
|
||||
$user_id,
|
||||
$client_id,
|
||||
$scope,
|
||||
$audience = '',
|
||||
$redirect_uri = null,
|
||||
$issued = null,
|
||||
$lifetime = 600,
|
||||
$from_ip = '127.0.0.1',
|
||||
$access_type = OAuth2Protocol::OAuth2Protocol_AccessType_Online,
|
||||
$approval_prompt = OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
|
||||
$has_previous_user_consent = false,
|
||||
$state,
|
||||
$nonce,
|
||||
$response_type,
|
||||
$requested_auth_time = false,
|
||||
$auth_time = -1,
|
||||
$prompt = null,
|
||||
$is_hashed = false
|
||||
$lifetime = 600,
|
||||
$state = null,
|
||||
$nonce = null,
|
||||
$response_type = null,
|
||||
$requested_auth_time = false,
|
||||
$auth_time = -1,
|
||||
$prompt = null,
|
||||
$code_challenge = null,
|
||||
$code_challenge_method = null
|
||||
)
|
||||
{
|
||||
$instance = new self();
|
||||
$instance->value = $value;
|
||||
$instance->user_id = $user_id;
|
||||
$instance->scope = $scope;
|
||||
$instance->redirect_uri = $redirect_uri;
|
||||
$instance->client_id = $client_id;
|
||||
$instance->audience = $audience;
|
||||
$instance->issued = $issued;
|
||||
$instance->lifetime = intval($lifetime);
|
||||
$instance->from_ip = $from_ip;
|
||||
$instance->is_hashed = $is_hashed;
|
||||
$instance->access_type = $access_type;
|
||||
$instance->scope = $scope;
|
||||
$instance->user_id = $user_id;
|
||||
$instance->redirect_uri = $redirect_uri;
|
||||
$instance->client_id = $client_id;
|
||||
$instance->lifetime = intval($lifetime);
|
||||
$instance->audience = $audience;
|
||||
$instance->is_hashed = false;
|
||||
$instance->from_ip = IPHelper::getUserIp();
|
||||
$instance->access_type = $access_type;
|
||||
$instance->approval_prompt = $approval_prompt;
|
||||
$instance->has_previous_user_consent = $has_previous_user_consent;
|
||||
$instance->state = $state;
|
||||
@ -222,7 +170,44 @@ class AuthorizationCode extends Token
|
||||
$instance->response_type = $response_type;
|
||||
$instance->requested_auth_time = $requested_auth_time;
|
||||
$instance->auth_time = $auth_time;
|
||||
$instance->prompt = $prompt;
|
||||
$instance->prompt = $prompt;
|
||||
$instance->code_challenge = $code_challenge;
|
||||
$instance->code_challenge_method = $code_challenge_method;
|
||||
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $payload
|
||||
* @return AuthorizationCode
|
||||
*/
|
||||
public static function load
|
||||
(
|
||||
array $payload
|
||||
): AuthorizationCode
|
||||
{
|
||||
$instance = new self();
|
||||
$instance->value = $payload['value'];
|
||||
$instance->user_id = $payload['user_id'] ?? null;
|
||||
$instance->scope = $payload['scope'] ?? null;
|
||||
$instance->redirect_uri = $payload['redirect_uri'] ?? null;
|
||||
$instance->client_id = $payload['client_id'] ?? null;
|
||||
$instance->audience = $payload['audience'] ?? null;
|
||||
$instance->issued = $payload['issued'] ?? null;
|
||||
$instance->lifetime = intval($payload['lifetime']);
|
||||
$instance->from_ip = $payload['from_ip'] ?? null;
|
||||
$instance->is_hashed = isset($payload['is_hashed']) ? boolval($payload['is_hashed']) : false;
|
||||
$instance->access_type = $payload['access_type'] ?? null;
|
||||
$instance->approval_prompt = $payload['approval_prompt'] ?? null;
|
||||
$instance->has_previous_user_consent = $payload['has_previous_user_consent'] ?? false;
|
||||
$instance->state = $payload['state'] ?? null;
|
||||
$instance->nonce = $payload['nonce'] ?? null;
|
||||
$instance->response_type = $payload['response_type'] ?? null;
|
||||
$instance->requested_auth_time = $payload['requested_auth_time'] ?? null;;
|
||||
$instance->auth_time = $payload['auth_time'] ?? null;
|
||||
$instance->prompt = $payload['prompt'] ?? null;
|
||||
$instance->code_challenge = $payload['code_challenge'] ?? null;
|
||||
$instance->code_challenge_method = $payload['code_challenge_method'] ?? null;
|
||||
return $instance;
|
||||
}
|
||||
|
||||
@ -288,7 +273,7 @@ class AuthorizationCode extends Token
|
||||
public function isAuthTimeRequested()
|
||||
{
|
||||
$res = $this->requested_auth_time;
|
||||
if (!is_string($res)) return (bool) $res;
|
||||
if (!is_string($res)) return (bool)$res;
|
||||
switch (strtolower($res)) {
|
||||
case '1':
|
||||
case 'true':
|
||||
@ -332,4 +317,71 @@ class AuthorizationCode extends Token
|
||||
{
|
||||
return 'auth_code';
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'client_id' => $this->getClientId(),
|
||||
'scope' => $this->getScope(),
|
||||
'audience' => $this->getAudience(),
|
||||
'redirect_uri' => $this->getRedirectUri(),
|
||||
'issued' => $this->getIssued(),
|
||||
'lifetime' => $this->getLifetime(),
|
||||
'from_ip' => $this->getFromIp(),
|
||||
'user_id' => $this->getUserId(),
|
||||
'access_type' => $this->getAccessType(),
|
||||
'approval_prompt' => $this->getApprovalPrompt(),
|
||||
'has_previous_user_consent' => $this->getHasPreviousUserConsent(),
|
||||
'state' => $this->getState(),
|
||||
'nonce' => $this->getNonce(),
|
||||
'response_type' => $this->getResponseType(),
|
||||
'requested_auth_time' => $this->isAuthTimeRequested(),
|
||||
'auth_time' => $this->getAuthTime(),
|
||||
'prompt' => $this->getPrompt(),
|
||||
'code_challenge' => $this->getCodeChallenge(),
|
||||
'code_challenge_method' => $this->getCodeChallengeMethod(),
|
||||
];
|
||||
}
|
||||
|
||||
public static function getKeys(): array
|
||||
{
|
||||
return [
|
||||
'user_id',
|
||||
'client_id',
|
||||
'scope',
|
||||
'audience',
|
||||
'redirect_uri',
|
||||
'issued',
|
||||
'lifetime',
|
||||
'from_ip',
|
||||
'access_type',
|
||||
'approval_prompt',
|
||||
'has_previous_user_consent',
|
||||
'state',
|
||||
'nonce',
|
||||
'response_type',
|
||||
'requested_auth_time',
|
||||
'auth_time',
|
||||
'prompt',
|
||||
'code_challenge',
|
||||
'code_challenge_method',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCodeChallenge(): ?string
|
||||
{
|
||||
return $this->code_challenge;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getCodeChallengeMethod(): ?string
|
||||
{
|
||||
return $this->code_challenge_method;
|
||||
}
|
||||
|
||||
}
|
@ -11,7 +11,6 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use OAuth2\Exceptions\InvalidTokenEndpointAuthMethodException;
|
||||
use OAuth2\OAuth2Protocol;
|
||||
@ -51,7 +50,8 @@ final class ClientCredentialsAuthenticationContext extends ClientAuthenticationC
|
||||
|
||||
if(!in_array($auth_type, [
|
||||
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretBasic,
|
||||
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost
|
||||
OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretPost,
|
||||
OAuth2Protocol::TokenEndpoint_AuthMethod_None
|
||||
]))
|
||||
throw new InvalidTokenEndpointAuthMethodException($auth_type);
|
||||
|
||||
|
@ -319,4 +319,9 @@ interface IClient extends IEntity
|
||||
* @return array
|
||||
*/
|
||||
public function getValidAccessTokens();
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isPKCEEnabled():bool;
|
||||
}
|
@ -85,4 +85,12 @@ class RefreshToken extends Token {
|
||||
{
|
||||
return 'refresh_token';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -649,6 +649,8 @@ final class OAuth2Protocol implements IOAuth2Protocol
|
||||
self::TokenEndpoint_AuthMethod_ClientSecretPost,
|
||||
self::TokenEndpoint_AuthMethod_ClientSecretJwt,
|
||||
self::TokenEndpoint_AuthMethod_PrivateKeyJwt,
|
||||
// PKCE only
|
||||
self::TokenEndpoint_AuthMethod_None,
|
||||
);
|
||||
|
||||
const OpenIdConnect_Scope = 'openid';
|
||||
@ -712,6 +714,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
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -24,7 +24,6 @@ use OAuth2\ResourceServer\IUserService;
|
||||
*/
|
||||
class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
|
||||
{
|
||||
|
||||
/**
|
||||
* @var array
|
||||
*/
|
||||
@ -120,15 +119,6 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
|
||||
parent::__construct($auth_request->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
|
||||
* The Response Mode request parameter response_mode informs the Authorization Server of the mechanism to be used
|
||||
* for returning Authorization Response parameters from the Authorization Endpoint
|
||||
*/
|
||||
public function getResponseMode()
|
||||
{
|
||||
return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates current request
|
||||
@ -191,25 +181,6 @@ class OAuth2AuthenticationRequest extends OAuth2AuthorizationRequest
|
||||
}
|
||||
}
|
||||
|
||||
$response_mode = $this->getResponseMode();
|
||||
|
||||
if(!empty($response_mode))
|
||||
{
|
||||
if(!in_array($response_mode, OAuth2Protocol::$valid_response_modes))
|
||||
{
|
||||
$this->last_validation_error = 'invalid response_mode';
|
||||
return false;
|
||||
}
|
||||
|
||||
$default_response_mode = OAuth2Protocol::getDefaultResponseMode($this->getResponseType(false));
|
||||
|
||||
if($default_response_mode === $response_mode)
|
||||
{
|
||||
$this->last_validation_error = 'invalid response_mode';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// http://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
// MUST ensure that the prompt parameter contains consent unless other conditions for processing the request
|
||||
// permitting offline access to the requested resources are in place
|
||||
|
@ -33,8 +33,7 @@ class OAuth2AuthorizationRequest extends OAuth2Request
|
||||
parent::__construct($msg);
|
||||
}
|
||||
|
||||
public static $params = array
|
||||
(
|
||||
public static $params = [
|
||||
OAuth2Protocol::OAuth2Protocol_ResponseType => OAuth2Protocol::OAuth2Protocol_ResponseType,
|
||||
OAuth2Protocol::OAuth2Protocol_ClientId => OAuth2Protocol::OAuth2Protocol_ClientId,
|
||||
OAuth2Protocol::OAuth2Protocol_RedirectUri => OAuth2Protocol::OAuth2Protocol_RedirectUri,
|
||||
@ -42,7 +41,8 @@ class OAuth2AuthorizationRequest extends OAuth2Request
|
||||
OAuth2Protocol::OAuth2Protocol_State => OAuth2Protocol::OAuth2Protocol_State,
|
||||
OAuth2Protocol::OAuth2Protocol_Approval_Prompt => OAuth2Protocol::OAuth2Protocol_Approval_Prompt,
|
||||
OAuth2Protocol::OAuth2Protocol_AccessType => OAuth2Protocol::OAuth2Protocol_AccessType,
|
||||
);
|
||||
OAuth2Protocol::OAuth2Protocol_ResponseMode => OAuth2Protocol::OAuth2Protocol_ResponseMode,
|
||||
];
|
||||
|
||||
/**
|
||||
* The Response Type request parameter response_type informs the Authorization Server of the desired authorization
|
||||
@ -62,6 +62,16 @@ class OAuth2AuthorizationRequest extends OAuth2Request
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @see http://openid.net/specs/oauth-v2-multiple-response-types-1_0.html#ResponseModes
|
||||
* The Response Mode request parameter response_mode informs the Authorization Server of the mechanism to be used
|
||||
* for returning Authorization Response parameters from the Authorization Endpoint
|
||||
*/
|
||||
public function getResponseMode()
|
||||
{
|
||||
return $this->getParam(OAuth2Protocol::OAuth2Protocol_ResponseMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies the client that is making the request.
|
||||
* The value passed in this parameter must exactly match the value shown in the Admin Console.
|
||||
@ -171,17 +181,43 @@ class OAuth2AuthorizationRequest extends OAuth2Request
|
||||
}
|
||||
|
||||
//approval_prompt
|
||||
$valid_approvals = array
|
||||
(
|
||||
$valid_approvals = [
|
||||
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Auto,
|
||||
OAuth2Protocol::OAuth2Protocol_Approval_Prompt_Force
|
||||
);
|
||||
];
|
||||
|
||||
if (!in_array($this->getApprovalPrompt(), $valid_approvals))
|
||||
{
|
||||
$this->last_validation_error = 'approval_prompt is not valid';
|
||||
return false;
|
||||
}
|
||||
|
||||
$response_mode = $this->getResponseMode();
|
||||
|
||||
if(!empty($response_mode))
|
||||
{
|
||||
if(!in_array($response_mode, OAuth2Protocol::$valid_response_modes))
|
||||
{
|
||||
$this->last_validation_error = 'invalid response_mode';
|
||||
return false;
|
||||
}
|
||||
|
||||
$default_response_mode = OAuth2Protocol::getDefaultResponseMode($this->getResponseType(false));
|
||||
|
||||
if($default_response_mode === $response_mode)
|
||||
{
|
||||
$this->last_validation_error = 'invalid response_mode';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// PCKE validation
|
||||
if(!is_null($this->getCodeChallenge())){
|
||||
if(!in_array( $this->getCodeChallengeMethod(), OAuth2Protocol::PKCE_ValidCodeChallengeMethods)){
|
||||
$this->last_validation_error = sprintf("%s not valid", OAuth2Protocol::PKCE_CodeChallengeMethod);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -194,4 +230,14 @@ class OAuth2AuthorizationRequest extends OAuth2Request
|
||||
if(empty($display)) return OAuth2Protocol::OAuth2Protocol_Display_Page;
|
||||
return $display;
|
||||
}
|
||||
|
||||
// PKCE
|
||||
|
||||
public function getCodeChallenge():?string{
|
||||
return $this->getParam(OAuth2Protocol::PKCE_CodeChallenge);
|
||||
}
|
||||
|
||||
public function getCodeChallengeMethod():?string{
|
||||
return $this->getParam(OAuth2Protocol::PKCE_CodeChallengeMethod);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -63,6 +63,11 @@ final class ClientAuthContextValidatorFactory
|
||||
return new ClientPlainCredentialsAuthContextValidator;
|
||||
}
|
||||
break;
|
||||
case OAuth2Protocol::TokenEndpoint_AuthMethod_None:
|
||||
{
|
||||
return new ClientPKCEAuthContextValidator;
|
||||
}
|
||||
break;
|
||||
case OAuth2Protocol::TokenEndpoint_AuthMethod_ClientSecretJwt:
|
||||
{
|
||||
$validator = new ClientSharedSecretAssertionAuthContextValidator;
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
@ -35,21 +35,22 @@ final class ClientPlainCredentialsAuthContextValidator implements IClientAuthCon
|
||||
if(!($context instanceof ClientCredentialsAuthenticationContext))
|
||||
throw new InvalidClientAuthenticationContextException;
|
||||
|
||||
if(is_null($context->getClient()))
|
||||
$client = $context->getClient();
|
||||
if(is_null($client))
|
||||
throw new InvalidClientAuthenticationContextException('client not set!');
|
||||
|
||||
if($context->getClient()->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType())
|
||||
if($client->getTokenEndpointAuthInfo()->getAuthenticationMethod() !== $context->getAuthType())
|
||||
throw new InvalidClientCredentials(sprintf('invalid token endpoint auth method %s', $context->getAuthType()));
|
||||
|
||||
if($context->getClient()->getClientType() !== IClient::ClientType_Confidential)
|
||||
throw new InvalidClientCredentials(sprintf('invalid client type %s', $context->getClient()->getClientType()));
|
||||
if($client->getClientType() !== IClient::ClientType_Confidential)
|
||||
throw new InvalidClientCredentials(sprintf('invalid client type %s', $client->getClientType()));
|
||||
|
||||
$providedClientId = $context->getId();
|
||||
$providedClientSecret = $context->getSecret();
|
||||
|
||||
Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client id %s - provide client id %s", $context->getClient()->getClientId(), $providedClientId));
|
||||
Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $context->getClient()->getClientSecret(), $providedClientSecret));
|
||||
return $context->getClient()->getClientId() === $providedClientId &&
|
||||
$context->getClient()->getClientSecret() === $providedClientSecret;
|
||||
Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client id %s - provide client id %s", $client->getClientId(), $providedClientId));
|
||||
Log::debug(sprintf("ClientPlainCredentialsAuthContextValidator::validate client secret %s - provide client secret %s", $client->getClientSecret(), $providedClientSecret));
|
||||
|
||||
return $client->getClientId() === $providedClientId && $client->getClientSecret() === $providedClientSecret;
|
||||
}
|
||||
}
|
22
app/libs/OAuth2/Strategies/IPKCEValidationMethod.php
Normal file
22
app/libs/OAuth2/Strategies/IPKCEValidationMethod.php
Normal 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;
|
||||
}
|
@ -1,5 +1,4 @@
|
||||
<?php namespace OAuth2\Strategies;
|
||||
|
||||
/**
|
||||
* Copyright 2016 OpenStack Foundation
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
@ -12,8 +11,7 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
**/
|
||||
|
||||
use OAuth2\Requests\OAuth2AuthenticationRequest;
|
||||
use OAuth2\Requests\OAuth2AuthorizationRequest;
|
||||
use OAuth2\Requests\OAuth2Request;
|
||||
use OAuth2\Responses\OAuth2DirectResponse;
|
||||
use OAuth2\Responses\OAuth2IndirectFragmentResponse;
|
||||
@ -24,7 +22,6 @@ use Utils\IHttpResponseStrategy;
|
||||
use Utils\Services\ServiceLocator;
|
||||
use OAuth2\OAuth2Protocol;
|
||||
use Exception;
|
||||
|
||||
/**
|
||||
* Class OAuth2ResponseStrategyFactoryMethod
|
||||
* @package OAuth2\Strategies
|
||||
@ -42,7 +39,7 @@ final class OAuth2ResponseStrategyFactoryMethod
|
||||
{
|
||||
$type = $response->getType();
|
||||
|
||||
if($request instanceof OAuth2AuthenticationRequest)
|
||||
if($request instanceof OAuth2AuthorizationRequest)
|
||||
{
|
||||
$response_mode = $request->getResponseMode();
|
||||
|
||||
|
@ -143,4 +143,12 @@ final class OpenIdNonce extends Identifier
|
||||
{
|
||||
return 'nonce';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
@ -90,4 +90,9 @@ abstract class Identifier
|
||||
* @return string
|
||||
*/
|
||||
abstract public function getType();
|
||||
|
||||
/**
|
||||
* @return array
|
||||
*/
|
||||
abstract public function toArray(): array;
|
||||
}
|
@ -288,6 +288,7 @@ create table if not exists oauth2_client
|
||||
max_refresh_token_issuance_basis smallint(6) not null,
|
||||
use_refresh_token tinyint(1) default '0' not null,
|
||||
rotate_refresh_token tinyint(1) default '0' not null,
|
||||
pkce_enabled tinyint(1) default '0' not null,
|
||||
resource_server_id bigint unsigned null,
|
||||
website text null,
|
||||
application_type enum('WEB_APPLICATION', 'JS_CLIENT', 'SERVICE', 'NATIVE') default 'WEB_APPLICATION' null,
|
||||
|
49
database/migrations/Version20201214162511.php
Normal file
49
database/migrations/Version20201214162511.php
Normal 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');
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -848,6 +848,20 @@ PPK;
|
||||
'use_refresh_token' => false,
|
||||
'redirect_uris' => 'https://www.test.com/oauth2',
|
||||
),
|
||||
array(
|
||||
'app_name' => 'oauth2_test_app_public_pkce',
|
||||
'app_description' => 'oauth2_test_app_public_pkce',
|
||||
'app_logo' => null,
|
||||
'client_id' => '1234/Vcvr6fvQbH4HyNgwKlfSpkce.openstack.client',
|
||||
'client_secret' => null,
|
||||
'application_type' => IClient::ApplicationType_JS_Client,
|
||||
'token_endpoint_auth_method' => OAuth2Protocol::TokenEndpoint_AuthMethod_None,
|
||||
'owner' => $user,
|
||||
'rotate_refresh_token' => true,
|
||||
'use_refresh_token' => true,
|
||||
'redirect_uris' => 'https://www.test.com/oauth2',
|
||||
'pkce_enabled' => true,
|
||||
),
|
||||
array(
|
||||
'app_name' => 'oauth2_native_app',
|
||||
'app_description' => 'oauth2_native_app',
|
||||
@ -932,6 +946,7 @@ PPK;
|
||||
$client_confidential2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app2']);
|
||||
$client_confidential3 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app3']);
|
||||
$client_public = $client_repository->findOneBy(['app_name' => 'oauth2_test_app_public']);
|
||||
$client_public2 = $client_repository->findOneBy(['app_name' => 'oauth2_test_app_public_pkce']);
|
||||
$client_service = $client_repository->findOneBy(['app_name' => 'oauth2.service']);
|
||||
$client_native = $client_repository->findOneBy(['app_name' => 'oauth2_native_app']);
|
||||
$client_native2 = $client_repository->findOneBy(['app_name' => 'oauth2_native_app2']);
|
||||
@ -946,6 +961,7 @@ PPK;
|
||||
$client_confidential2->addScope($scope);
|
||||
$client_confidential3->addScope($scope);
|
||||
$client_public->addScope($scope);
|
||||
$client_public2->addScope($scope);
|
||||
$client_service->addScope($scope);
|
||||
$client_native->addScope($scope);
|
||||
$client_native2->addScope($scope);
|
||||
@ -1063,7 +1079,6 @@ PPK;
|
||||
TestKeys::$private_key_pem
|
||||
);
|
||||
|
||||
|
||||
EntityManager::persist($pkey_2);
|
||||
|
||||
EntityManager::flush();
|
||||
|
@ -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>
|
||||
|
@ -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> <span class="glyphicon glyphicon-info-sign accordion-toggle" aria-hidden="true" title="{!!$scope->description!!}"></span>
|
||||
|
@ -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?
|
||||
<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) <span class="glyphicon glyphicon-info-sign accordion-toggle"
|
||||
aria-hidden="true"
|
||||
|
@ -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>
|
||||
|
@ -223,7 +223,6 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
|
||||
'grant_type' => OAuth2Protocol::OAuth2Protocol_GrantType_AuthCode,
|
||||
);
|
||||
|
||||
|
||||
$response = $this->action
|
||||
(
|
||||
"POST",
|
||||
@ -251,7 +250,6 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
|
||||
$refresh_token = $response->refresh_token;
|
||||
$this->assertTrue(!empty($refresh_token));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function testTokenNTimes($n = 100){
|
||||
@ -1221,4 +1219,165 @@ final class OAuth2ProtocolTest extends OpenStackIDBaseTest
|
||||
$this->assertTrue(isset($comps["query"]));
|
||||
$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);
|
||||
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user