Implements: blueprint openid-oauth2-access-token-introspection-logic

[smarcet] - #5025 - Implements Access Token Introspection logic

Change-Id: I9c3f8f15a790db68b76dce947ee30bf7a94c5291
This commit is contained in:
smarcet 2014-01-08 18:56:17 -03:00
parent 5d60383556
commit 0425678643
17 changed files with 251 additions and 50 deletions

View File

@ -6,6 +6,7 @@ use oauth2\requests\OAuth2TokenRequest;
use oauth2\strategies\OAuth2ResponseStrategyFactoryMethod;
use oauth2\OAuth2Message;
use oauth2\requests\OAuth2TokenRevocationRequest;
use oauth2\requests\OAuth2AccessTokenValidationRequest;
/**
* Class OAuth2ProviderController
@ -66,4 +67,19 @@ class OAuth2ProviderController extends BaseController {
}
return $response;
}
/**
* http://tools.ietf.org/html/draft-richer-oauth-introspection-04
* Introspection Token HTTP Endpoint
* @return mixed
*/
public function introspection(){
$response = $this->oauth2_protocol->introspection(new OAuth2AccessTokenValidationRequest(new OAuth2Message(Input::all())));
$reflector = new ReflectionClass($response);
if ($reflector->isSubclassOf('oauth2\\responses\\OAuth2Response')) {
$strategy = OAuth2ResponseStrategyFactoryMethod::buildStrategy($response);
return $strategy->handle($response);
}
return $response;
}
}

View File

@ -22,7 +22,8 @@ class CreateOauth2ClientsTable extends Migration {
$table->smallInteger('client_type');
$table->boolean('active')->default(true);
$table->boolean('locked')->default(false);
$table->bigInteger("user_id")->unsigned();
$table->bigInteger("user_id")->unsigned()->nullable();
$table->index('user_id');
$table->foreign('user_id')->references('id')->on('openid_users');

View File

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
class AlterTableOauth2Client extends Migration {
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('oauth2_client', function($table)
{
$table->bigInteger("resource_server_id")->unsigned()->nullable();
$table->index('resource_server_id');
$table->foreign('resource_server_id')->references('id')->on('oauth2_resource_server');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('oauth2_client', function($table)
{
$table->dropForeign('resource_server_id');
$table->dropColumn('resource_server_id');
});
}
}

View File

@ -19,13 +19,13 @@ class TestSeeder extends Seeder {
DB::table('oauth2_client_api_scope')->delete();
DB::table('oauth2_api_scope')->delete();
DB::table('oauth2_api')->delete();
DB::table('oauth2_resource_server')->delete();
DB::table('oauth2_client_authorized_uri')->delete();
DB::table('oauth2_client_api_scope')->delete();
DB::table('oauth2_access_token')->delete();
DB::table('oauth2_refresh_token')->delete();
DB::table('oauth2_client')->delete();
DB::table('openid_users')->delete();
DB::table('oauth2_resource_server')->delete();
ResourceServer::create(
array(
@ -338,6 +338,20 @@ class TestSeeder extends Seeder {
)
);
Client::create(
array(
'app_name' => 'resource_server_client',
'app_description' => 'resource_server_client',
'app_logo' => null,
'client_id' => 'resource.server.1.openstack.client',
'client_secret' => '123456789',
'client_type' => IClient::ClientType_Confidential,
'resource_server_id' => $resource_server->id,
'rotate_refresh_token' => false,
'use_refresh_token' => false
)
);
$client_confidential = Client::where('app_name','=','oauth2_test_app')->first();
$client_public = Client::where('app_name','=','oauth2_test_app_public')->first();
//attach scopes

View File

@ -29,6 +29,15 @@ interface IOAuth2Protocol {
*/
public function revoke(OAuth2Request $request = null);
/**
* Introspection Token Endpoint
* http://tools.ietf.org/html/draft-richer-oauth-introspection-04
* @param OAuth2Request $request
* @return mixed
*/
public function introspection(OAuth2Request $request = null);
/**
* Get all available grant types set on the protocol

View File

@ -2,11 +2,15 @@
namespace oauth2;
use Exception;
//endpoints
use oauth2\endpoints\AuthorizationEndpoint;
use oauth2\endpoints\RevocationEndpoint;
use oauth2\endpoints\TokenEndpoint;
use oauth2\endpoints\TokenIntrospectionEndpoint;
use oauth2\endpoints\TokenRevocationEndpoint;
//exceptions
use Exception;
use oauth2\exceptions\AccessDeniedException;
use oauth2\exceptions\BearerTokenDisclosureAttemptException;
use oauth2\exceptions\ExpiredAuthorizationCodeException;
@ -20,29 +24,26 @@ use oauth2\exceptions\ReplayAttackException;
use oauth2\exceptions\ScopeNotAllowedException;
use oauth2\exceptions\UnAuthorizedClientException;
use oauth2\exceptions\UnsupportedResponseTypeException;
use oauth2\exceptions\UriNotAllowedException;
//grant types
use oauth2\exceptions\UriNotAllowedException;
use oauth2\grant_types\AuthorizationCodeGrantType;
use oauth2\grant_types\ImplicitGrantType;
use oauth2\grant_types\RefreshBearerTokenGrantType;
use oauth2\grant_types\ValidateBearerTokenGrantType;
use oauth2\requests\OAuth2Request;
use oauth2\responses\OAuth2DirectErrorResponse;
use oauth2\responses\OAuth2IndirectErrorResponse;
use oauth2\responses\OAuth2TokenRevocationResponse;
use oauth2\services\IApiScopeService;
use oauth2\services\IClientService;
use oauth2\services\IMementoOAuth2AuthenticationRequestService;
use oauth2\services\ITokenService;
use oauth2\strategies\IOAuth2AuthenticationStrategy;
use oauth2\strategies\OAuth2IndirectErrorResponseFactoryMethod;
use utils\services\IAuthService;
use utils\services\ICheckPointService;
use utils\services\ILogService;
@ -113,6 +114,7 @@ class OAuth2Protocol implements IOAuth2Protocol
private $authorize_endpoint;
private $token_endpoint;
private $revoke_endpoint;
private $introspection_endpoint;
//grant types
private $grant_types = array();
@ -128,26 +130,22 @@ class OAuth2Protocol implements IOAuth2Protocol
IApiScopeService $scope_service)
{
//todo: add dynamic creation logic (configure grants types from db)
$authorization_code_grant_type = new AuthorizationCodeGrantType($scope_service, $client_service, $token_service, $auth_service, $memento_service, $auth_strategy, $log_service);
$implicit_grant_type = new ImplicitGrantType($scope_service, $client_service, $token_service, $auth_service, $memento_service, $auth_strategy, $log_service);
$validate_bearer_token_grant_type = new ValidateBearerTokenGrantType($client_service, $token_service, $log_service);
$refresh_bearer_token_grant_type = new RefreshBearerTokenGrantType($client_service, $token_service, $log_service);
$this->grant_types[$authorization_code_grant_type->getType()] = $authorization_code_grant_type;
$this->grant_types[$implicit_grant_type->getType()] = $implicit_grant_type;
$this->grant_types[$validate_bearer_token_grant_type->getType()] = $validate_bearer_token_grant_type;
$this->grant_types[$refresh_bearer_token_grant_type->getType()] = $refresh_bearer_token_grant_type;
$this->log_service = $log_service;
$this->checkpoint_service = $checkpoint_service;
$this->client_service = $client_service;
$this->log_service = $log_service;
$this->checkpoint_service = $checkpoint_service;
$this->client_service = $client_service;
$this->authorize_endpoint = new AuthorizationEndpoint($this);
$this->token_endpoint = new TokenEndpoint($this);
$this->revoke_endpoint = new TokenRevocationEndpoint($this,$client_service, $token_service, $log_service);
$this->authorize_endpoint = new AuthorizationEndpoint($this);
$this->token_endpoint = new TokenEndpoint($this);
$this->revoke_endpoint = new TokenRevocationEndpoint($this,$client_service, $token_service, $log_service);
$this->introspection_endpoint = new TokenIntrospectionEndpoint($this,$client_service, $token_service, $log_service);
}
/**
@ -337,6 +335,36 @@ class OAuth2Protocol implements IOAuth2Protocol
}
}
/**
* Introspection Token Endpoint
* http://tools.ietf.org/html/draft-richer-oauth-introspection-04
* @param OAuth2Request $request
* @return mixed
*/
public function introspection(OAuth2Request $request = null){
try {
if (is_null($request) || !$request->isValid())
throw new InvalidOAuth2Request;
return $this->introspection_endpoint->handle($request);
}
catch(UnAuthorizedClientException $ex1){
$this->log_service->error($ex1);
$this->checkpoint_service->trackException($ex1);
return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_UnauthorizedClient);
}
catch(BearerTokenDisclosureAttemptException $ex2){
$this->log_service->error($ex2);
$this->checkpoint_service->trackException($ex2);
return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidGrant);
}
catch (Exception $ex) {
$this->log_service->error($ex);
$this->checkpoint_service->trackException($ex);
return new OAuth2DirectErrorResponse(OAuth2Protocol::OAuth2Protocol_Error_InvalidRequest);
}
}
public function getAvailableGrants()
{
return $this->grant_types;

View File

@ -3,12 +3,33 @@
namespace oauth2\endpoints;
use oauth2\exceptions\InvalidGrantTypeException;
use oauth2\requests\OAuth2Request;
use oauth2\IOAuth2Protocol;
use oauth2\services\IClientService;
use oauth2\services\ITokenService;
use utils\services\ILogService;
use oauth2\grant_types\ValidateBearerTokenGrantType;
class TokenIntrospectionEndpoint implements IOAuth2Endpoint {
private $protocol;
private $grant_type;
public function __construct(IOAuth2Protocol $protocol, IClientService $client_service, ITokenService $token_service, ILogService $log_service)
{
$this->protocol = $protocol;
$this->grant_type = new ValidateBearerTokenGrantType($client_service, $token_service, $log_service);
}
public function handle(OAuth2Request $request)
{
// TODO: Implement handle() method.
if($this->grant_type->canHandle($request))
{
return $this->grant_type->completeFlow($request);
}
throw new InvalidOAuth2Request;
}
}

View File

@ -4,6 +4,7 @@ namespace oauth2\endpoints;
use oauth2\exceptions\InvalidGrantTypeException;
use oauth2\exceptions\InvalidOAuth2Request;
use oauth2\requests\OAuth2Request;
use oauth2\IOAuth2Protocol;
use oauth2\services\IClientService;
@ -29,6 +30,6 @@ class TokenRevocationEndpoint implements IOAuth2Endpoint {
{
return $this->grant_type->completeFlow($request);
}
throw new InvalidGrantTypeException;
throw new InvalidOAuth2Request;
}
}

View File

@ -5,26 +5,53 @@ namespace oauth2\grant_types;
use oauth2\exceptions\InvalidOAuth2Request;
use oauth2\exceptions\InvalidAccessTokenException;
use oauth2\exceptions\BearerTokenDisclosureAttemptException;
use oauth2\exceptions\UnAuthorizedClientException;
use oauth2\exceptions\InvalidGrantTypeException;
use oauth2\requests\OAuth2AccessTokenValidationRequest;
use oauth2\requests\OAuth2Request;
use oauth2\responses\OAuth2AccessTokenValidationResponse;
use oauth2\services\IClientService;
use oauth2\services\ITokenService;
use services\IPHelper;
use utils\services\ILogService;
use oauth2\models\IClient;
use ReflectionClass;
/**
* Class ValidateBearerTokenGrantType
* In OAuth2, the contents of tokens are opaque to clients. This means
* that the client does not need to know anything about the content or
* structure of the token itself, if there is any. However, there is
* still a large amount of metadata that may be attached to a token,
* such as its current validity, approved scopes, and extra information
* about the authentication context in which the token was issued.
* These pieces of information are often vital to Protected Resources
* making authorization decisions based on the tokens being presented.
* Since OAuth2 defines no direct relationship between the Authorization
* Server and the Protected Resource, only that they must have an
* agreement on the tokens themselves, there have been many different
* approaches to bridging this gap.
* This specification defines an Introspection Endpoint that allows the
* holder of a token to query the Authorization Server to discover the
* set of metadata for a token. A Protected Resource may use the
* mechanism described in this draft to query the Introspection Endpoint
* in a particular authorization decision context and ascertain the
* relevant metadata about the token in order to make this authorization
* decision appropriately.
* The endpoint SHOULD also require some form of authentication to
* access this endpoint, such as the Client Authentication as described
* in OAuth 2 Core Specification [RFC6749] or a separate OAuth 2.0
* Access Token. The methods of managing and validating these
* authentication credentials are out of scope of this specification.
* http://tools.ietf.org/html/draft-richer-oauth-introspection-04
* @package oauth2\grant_types
*/
class ValidateBearerTokenGrantType extends AbstractGrantType
{
const OAuth2Protocol_GrantType_Extension_ValidateBearerToken = 'urn:pingidentity.com:oauth2:grant_type:validate_bearer';
const OAuth2Protocol_GrantType_Extension_ValidateBearerToken = 'urn:tools.ietf.org:oauth2:grant_type:validate_bearer';
public function __construct(IClientService $client_service, ITokenService $token_service, ILogService $log_service)
{
@ -35,7 +62,7 @@ class ValidateBearerTokenGrantType extends AbstractGrantType
{
$reflector = new ReflectionClass($request);
$class_name = $reflector->getName();
return $class_name == 'oauth2\requests\OAuth2TokenRequest' && $request->isValid() && $request->getGrantType() === $this->getType();
return $class_name == 'oauth2\requests\OAuth2AccessTokenValidationRequest' && $request->isValid();
}
public function getType()
@ -43,37 +70,69 @@ class ValidateBearerTokenGrantType extends AbstractGrantType
return self::OAuth2Protocol_GrantType_Extension_ValidateBearerToken;
}
/** Not implemented , there is no first process phase on this grant type
/**
* @param OAuth2Request $request
* @return mixed|void
* @throws Exception
* @throws \oauth2\exceptions\InvalidOAuth2Request
*/
public function handle(OAuth2Request $request)
{
throw new InvalidOAuth2Request('Not Implemented!');
}
/**
* @param OAuth2Request $request
* @return mixed|OAuth2AccessTokenValidationResponse|void
* @throws \oauth2\exceptions\UnAuthorizedClientException
* @throws \oauth2\exceptions\InvalidOAuth2Request
* @throws \oauth2\exceptions\BearerTokenDisclosureAttemptException
*/
public function completeFlow(OAuth2Request $request)
{
$reflector = new ReflectionClass($request);
$class_name = $reflector->getName();
if ($class_name == 'oauth2\requests\OAuth2AccessTokenValidationRequest') {
parent::completeFlow($request);
$token_value = $request->getToken();
try{
$access_token = $this->token_service->getAccessToken($token_value);
//checks is current ip belongs to any registered resource server
$current_ip = IPHelper::getUserIp();
if(!$this->token_service->checkAccessTokenAudience($access_token,$current_ip))
throw new BearerTokenDisclosureAttemptException(sprintf("Access Token %s was not emitted for ip %s",$token_value,$current_ip));
if(!$this->current_client->isResourceServerClient()){
// if current client is not a resource server, then we could only access to our own tokens
if($access_token->getClientId()!== $this->current_client_id)
throw new BearerTokenDisclosureAttemptException(sprintf('access token %s does not belongs to client id %s',$token_value, $this->current_client_id));
}
else{
// current client is a resource server, validate client type (must be confidential)
if($this->current_client->getClientType()!== IClient::ClientType_Confidential)
throw new UnAuthorizedClientException('resource server client is not of confidential type!');
//validate resource server IP address
$current_ip = IPHelper::getUserIp();
$resource_server = $this->current_client->getResourceServer();
//check if resource server is active
if(!$resource_server->active)
throw new UnAuthorizedClientException('resource server is disabled!');
//check resource server ip address
if($current_ip !== $resource_server->ip)
throw new BearerTokenDisclosureAttemptException(sprintf('resource server ip (%s) differs from current request ip %s',$resource_server->ip,$current_ip));
// check if current ip belongs to a registered resource server audience
if(!$this->token_service->checkAccessTokenAudience($access_token,$current_ip))
throw new BearerTokenDisclosureAttemptException(sprintf('access token current audience does not match with current request ip %s', $current_ip));
}
return new OAuth2AccessTokenValidationResponse($token_value, $access_token->getScope(), $access_token->getAudience(),$access_token->getClientId(),$access_token->getRemainingLifetime());
}
catch(InvalidAccessTokenException $ex1){
$this->log_service->error($ex1);
throw new BearerTokenDisclosureAttemptException();
throw new BearerTokenDisclosureAttemptException($ex1->getMessage());
}
catch(InvalidGrantTypeException $ex2){
$this->log_service->error($ex2);
throw new BearerTokenDisclosureAttemptException($ex2->getMessage());
}
}
throw new InvalidOAuth2Request;
@ -86,14 +145,7 @@ class ValidateBearerTokenGrantType extends AbstractGrantType
public function buildTokenRequest(OAuth2Request $request)
{
$reflector = new ReflectionClass($request);
$class_name = $reflector->getName();
if ($class_name == 'oauth2\requests\OAuth2TokenRequest') {
if($request->getGrantType() !== $this->getType())
return null;
return new OAuth2AccessTokenValidationRequest($request->getMessage());
}
return null;
throw new InvalidOAuth2Request('Not Implemented!');
}
}

View File

@ -26,5 +26,7 @@ interface IClient {
public function getUserId();
public function isLocked();
public function isActive();
public function isResourceServerClient();
public function getResourceServer();
}

View File

@ -9,7 +9,7 @@ use oauth2\OAuth2Message;
* @package oauth2\requests
*/
class OAuth2AccessTokenValidationRequest extends OAuth2TokenRequest {
class OAuth2AccessTokenValidationRequest extends OAuth2Request {
public function __construct(OAuth2Message $msg)
{
@ -18,9 +18,6 @@ class OAuth2AccessTokenValidationRequest extends OAuth2TokenRequest {
public function isValid()
{
if(!parent::isValid())
return false;
$token = $this->getToken();
if(is_null($token))

View File

@ -8,6 +8,8 @@ use oauth2\models\RefreshToken;
/**
* Interface ITokenService
* Defines the interface for an OAuth2 Token Service
* Provides all Tokens related operations (create, get and revoke)
* @package oauth2\services
*/
interface ITokenService {
@ -23,6 +25,7 @@ interface ITokenService {
/**
* Retrieves a given Authorization Code
* @param $value
* @return AuthorizationCode
* @throws \oauth2\exceptions\ReplayAttackException
@ -39,6 +42,7 @@ interface ITokenService {
/**
* Create a brand new Access Token by params
* @param $scope
* @param $client_id
* @param $audience
@ -56,6 +60,7 @@ interface ITokenService {
public function createAccessTokenFromRefreshToken(RefreshToken $refresh_token, $scope=null);
/**
* Retrieves a given Access Token
* @param $value
* @return AccessToken
* @throws \oauth2\exceptions\InvalidAccessTokenException
@ -105,7 +110,7 @@ interface ITokenService {
/**
* Revokes a given access token and optionally , its associated refresh token
* Revokes a given access token
* @param $value
* @param bool $is_hashed
* @return bool
@ -136,6 +141,4 @@ interface ITokenService {
*/
public function revokeRefreshToken($value, $is_hashed = false);
}

View File

@ -32,6 +32,11 @@ class Client extends Eloquent implements IClient {
return $this->belongsTo('auth\OpenIdUser');
}
public function resource_server()
{
return $this->belongsTo('ResourceServer');
}
public function scopes()
{
return $this->belongsToMany('ApiScope','oauth2_client_api_scope','client_id','scope_id');
@ -162,4 +167,15 @@ class Client extends Eloquent implements IClient {
{
return $this->active;
}
public function isResourceServerClient()
{
$id = $this->resource_server_id;
return !is_null($id);
}
public function getResourceServer()
{
return $this->resource_server()->first();
}
}

View File

@ -8,4 +8,9 @@ class ResourceServer extends Eloquent {
{
return $this->hasMany('Api','resource_server_id');
}
public function client(){
return $this->hasOne('Client');
}
}

View File

@ -35,10 +35,10 @@ Route::group(array("before" => "ssl"), function () {
Route::get('/accounts/user/login/cancel', "UserController@cancelLogin");
//oauth2 endpoint
Route::any('/oauth2/auth',"OAuth2ProviderController@authorize");
Route::post('/oauth2/token',"OAuth2ProviderController@token");
Route::post('/oauth2/revoke',"OAuth2ProviderController@revoke");
Route::post('/oauth2/token/revoke',"OAuth2ProviderController@revoke");
Route::post('/oauth2/token/introspection',"OAuth2ProviderController@introspection");
});
Route::group(array("before" => array("ssl", "auth")), function () {

View File

@ -32,6 +32,7 @@ use DateTime;
/**
* Class TokenService
* Provides all Tokens related operations (create, get and revoke)
* @package services\oauth2
*/

View File

@ -188,10 +188,9 @@ class OAuth2TokenEndpointTest extends TestCase
//do token validation ....
$params = array(
'token' => $access_token,
'grant_type' => oauth2\grant_types\ValidateBearerTokenGrantType::OAuth2Protocol_GrantType_Extension_ValidateBearerToken,
);
$response = $this->action("POST", "OAuth2ProviderController@token",
$response = $this->action("POST", "OAuth2ProviderController@introspection",
$params,
array(),
array(),