2017-08-08 03:01:11 +08:00
< ? php
namespace OAuth2\Controller ;
use OAuth2\ResponseType\AccessTokenInterface ;
use OAuth2\ClientAssertionType\ClientAssertionTypeInterface ;
use OAuth2\GrantType\GrantTypeInterface ;
use OAuth2\ScopeInterface ;
use OAuth2\Scope ;
use OAuth2\Storage\ClientInterface ;
use OAuth2\RequestInterface ;
use OAuth2\ResponseInterface ;
2020-04-30 21:43:07 +08:00
use InvalidArgumentException ;
use LogicException ;
use RuntimeException ;
2017-08-08 03:01:11 +08:00
/**
2020-04-30 21:43:07 +08:00
* @ see TokenControllerInterface
2017-08-08 03:01:11 +08:00
*/
class TokenController implements TokenControllerInterface
{
/**
* @ var AccessTokenInterface
*/
protected $accessToken ;
/**
2020-04-30 21:43:07 +08:00
* @ var array < GrantTypeInterface >
2017-08-08 03:01:11 +08:00
*/
protected $grantTypes ;
/**
* @ var ClientAssertionTypeInterface
*/
protected $clientAssertionType ;
/**
2020-04-30 21:43:07 +08:00
* @ var ScopeInterface
2017-08-08 03:01:11 +08:00
*/
protected $scopeUtil ;
/**
* @ var ClientInterface
*/
protected $clientStorage ;
2020-04-30 21:43:07 +08:00
/**
* Constructor
*
* @ param AccessTokenInterface $accessToken
* @ param ClientInterface $clientStorage
* @ param array $grantTypes
* @ param ClientAssertionTypeInterface $clientAssertionType
* @ param ScopeInterface $scopeUtil
* @ throws InvalidArgumentException
*/
2017-08-08 03:01:11 +08:00
public function __construct ( AccessTokenInterface $accessToken , ClientInterface $clientStorage , array $grantTypes = array (), ClientAssertionTypeInterface $clientAssertionType = null , ScopeInterface $scopeUtil = null )
{
if ( is_null ( $clientAssertionType )) {
foreach ( $grantTypes as $grantType ) {
if ( ! $grantType instanceof ClientAssertionTypeInterface ) {
2020-04-30 21:43:07 +08:00
throw new InvalidArgumentException ( 'You must supply an instance of OAuth2\ClientAssertionType\ClientAssertionTypeInterface or only use grant types which implement OAuth2\ClientAssertionType\ClientAssertionTypeInterface' );
2017-08-08 03:01:11 +08:00
}
}
}
$this -> clientAssertionType = $clientAssertionType ;
$this -> accessToken = $accessToken ;
$this -> clientStorage = $clientStorage ;
foreach ( $grantTypes as $grantType ) {
$this -> addGrantType ( $grantType );
}
if ( is_null ( $scopeUtil )) {
$scopeUtil = new Scope ();
}
$this -> scopeUtil = $scopeUtil ;
}
2020-04-30 21:43:07 +08:00
/**
* Handle the token request .
*
* @ param RequestInterface $request - Request object to grant access token
* @ param ResponseInterface $response - Response object
*/
2017-08-08 03:01:11 +08:00
public function handleTokenRequest ( RequestInterface $request , ResponseInterface $response )
{
if ( $token = $this -> grantAccessToken ( $request , $response )) {
// @see http://tools.ietf.org/html/rfc6749#section-5.1
// server MUST disable caching in headers when tokens are involved
$response -> setStatusCode ( 200 );
$response -> addParameters ( $token );
$response -> addHttpHeaders ( array (
'Cache-Control' => 'no-store' ,
'Pragma' => 'no-cache' ,
'Content-Type' => 'application/json'
));
}
}
/**
* Grant or deny a requested access token .
* This would be called from the " /token " endpoint as defined in the spec .
* You can call your endpoint whatever you want .
*
2020-04-30 21:43:07 +08:00
* @ param RequestInterface $request - Request object to grant access token
* @ param ResponseInterface $response - Response object
*
* @ return bool | null | array
2017-08-08 03:01:11 +08:00
*
* @ throws \InvalidArgumentException
* @ throws \LogicException
*
* @ see http :// tools . ietf . org / html / rfc6749 #section-4
* @ see http :// tools . ietf . org / html / rfc6749 #section-10.6
* @ see http :// tools . ietf . org / html / rfc6749 #section-4.1.3
*
* @ ingroup oauth2_section_4
*/
public function grantAccessToken ( RequestInterface $request , ResponseInterface $response )
{
2020-04-30 21:43:07 +08:00
if ( strtolower ( $request -> server ( 'REQUEST_METHOD' )) === 'options' ) {
$response -> addHttpHeaders ( array ( 'Allow' => 'POST, OPTIONS' ));
return null ;
}
if ( strtolower ( $request -> server ( 'REQUEST_METHOD' )) !== 'post' ) {
2017-08-08 03:01:11 +08:00
$response -> setError ( 405 , 'invalid_request' , 'The request method must be POST when requesting an access token' , '#section-3.2' );
2020-04-30 21:43:07 +08:00
$response -> addHttpHeaders ( array ( 'Allow' => 'POST, OPTIONS' ));
2017-08-08 03:01:11 +08:00
return null ;
}
/**
* Determine grant type from request
* and validate the request for that grant type
*/
if ( ! $grantTypeIdentifier = $request -> request ( 'grant_type' )) {
$response -> setError ( 400 , 'invalid_request' , 'The grant type was not specified in the request' );
return null ;
}
if ( ! isset ( $this -> grantTypes [ $grantTypeIdentifier ])) {
/* TODO: If this is an OAuth2 supported grant type that we have chosen not to implement, throw a 501 Not Implemented instead */
$response -> setError ( 400 , 'unsupported_grant_type' , sprintf ( 'Grant type "%s" not supported' , $grantTypeIdentifier ));
return null ;
}
2020-04-30 21:43:07 +08:00
/** @var GrantTypeInterface $grantType */
2017-08-08 03:01:11 +08:00
$grantType = $this -> grantTypes [ $grantTypeIdentifier ];
/**
* Retrieve the client information from the request
* ClientAssertionTypes allow for grant types which also assert the client data
* in which case ClientAssertion is handled in the validateRequest method
*
2020-04-30 21:43:07 +08:00
* @ see \OAuth2\GrantType\JWTBearer
* @ see \OAuth2\GrantType\ClientCredentials
2017-08-08 03:01:11 +08:00
*/
if ( ! $grantType instanceof ClientAssertionTypeInterface ) {
if ( ! $this -> clientAssertionType -> validateRequest ( $request , $response )) {
return null ;
}
$clientId = $this -> clientAssertionType -> getClientId ();
}
/**
* Retrieve the grant type information from the request
* The GrantTypeInterface object handles all validation
* If the object is an instance of ClientAssertionTypeInterface ,
* That logic is handled here as well
*/
if ( ! $grantType -> validateRequest ( $request , $response )) {
return null ;
}
if ( $grantType instanceof ClientAssertionTypeInterface ) {
$clientId = $grantType -> getClientId ();
} else {
// validate the Client ID (if applicable)
if ( ! is_null ( $storedClientId = $grantType -> getClientId ()) && $storedClientId != $clientId ) {
$response -> setError ( 400 , 'invalid_grant' , sprintf ( '%s doesn\'t exist or is invalid for the client' , $grantTypeIdentifier ));
return null ;
}
}
/**
* Validate the client can use the requested grant type
*/
if ( ! $this -> clientStorage -> checkRestrictedGrantType ( $clientId , $grantTypeIdentifier )) {
$response -> setError ( 400 , 'unauthorized_client' , 'The grant type is unauthorized for this client_id' );
return false ;
}
/**
* Validate the scope of the token
*
* requestedScope - the scope specified in the token request
* availableScope - the scope associated with the grant type
* ex : in the case of the " Authorization Code " grant type ,
* the scope is specified in the authorize request
*
* @ see http :// tools . ietf . org / html / rfc6749 #section-3.3
*/
$requestedScope = $this -> scopeUtil -> getScopeFromRequest ( $request );
$availableScope = $grantType -> getScope ();
if ( $requestedScope ) {
// validate the requested scope
if ( $availableScope ) {
if ( ! $this -> scopeUtil -> checkScope ( $requestedScope , $availableScope )) {
$response -> setError ( 400 , 'invalid_scope' , 'The scope requested is invalid for this request' );
return null ;
}
} else {
// validate the client has access to this scope
if ( $clientScope = $this -> clientStorage -> getClientScope ( $clientId )) {
if ( ! $this -> scopeUtil -> checkScope ( $requestedScope , $clientScope )) {
$response -> setError ( 400 , 'invalid_scope' , 'The scope requested is invalid for this client' );
return false ;
}
} elseif ( ! $this -> scopeUtil -> scopeExists ( $requestedScope )) {
$response -> setError ( 400 , 'invalid_scope' , 'An unsupported scope was requested' );
return null ;
}
}
} elseif ( $availableScope ) {
// use the scope associated with this grant type
$requestedScope = $availableScope ;
} else {
// use a globally-defined default scope
$defaultScope = $this -> scopeUtil -> getDefaultScope ( $clientId );
// "false" means default scopes are not allowed
if ( false === $defaultScope ) {
$response -> setError ( 400 , 'invalid_scope' , 'This application requires you specify a scope parameter' );
return null ;
}
$requestedScope = $defaultScope ;
}
return $grantType -> createAccessToken ( $this -> accessToken , $clientId , $grantType -> getUserId (), $requestedScope );
}
/**
2020-04-30 21:43:07 +08:00
* Add grant type
2017-08-08 03:01:11 +08:00
*
2020-04-30 21:43:07 +08:00
* @ param GrantTypeInterface $grantType - the grant type to add for the specified identifier
* @ param string | null $identifier - a string passed in as " grant_type " in the response that will call this grantType
2017-08-08 03:01:11 +08:00
*/
public function addGrantType ( GrantTypeInterface $grantType , $identifier = null )
{
if ( is_null ( $identifier ) || is_numeric ( $identifier )) {
2020-04-30 21:43:07 +08:00
$identifier = $grantType -> getQueryStringIdentifier ();
2017-08-08 03:01:11 +08:00
}
$this -> grantTypes [ $identifier ] = $grantType ;
}
2020-04-30 21:43:07 +08:00
/**
* @ param RequestInterface $request
* @ param ResponseInterface $response
*/
2017-08-08 03:01:11 +08:00
public function handleRevokeRequest ( RequestInterface $request , ResponseInterface $response )
{
if ( $this -> revokeToken ( $request , $response )) {
$response -> setStatusCode ( 200 );
$response -> addParameters ( array ( 'revoked' => true ));
}
}
/**
* Revoke a refresh or access token . Returns true on success and when tokens are invalid
*
* Note : invalid tokens do not cause an error response since the client
* cannot handle such an error in a reasonable way . Moreover , the
* purpose of the revocation request , invalidating the particular token ,
* is already achieved .
*
* @ param RequestInterface $request
* @ param ResponseInterface $response
2020-04-30 21:43:07 +08:00
* @ throws RuntimeException
2017-08-08 03:01:11 +08:00
* @ return bool | null
*/
public function revokeToken ( RequestInterface $request , ResponseInterface $response )
{
2020-04-30 21:43:07 +08:00
if ( strtolower ( $request -> server ( 'REQUEST_METHOD' )) === 'options' ) {
$response -> addHttpHeaders ( array ( 'Allow' => 'POST, OPTIONS' ));
return null ;
}
if ( strtolower ( $request -> server ( 'REQUEST_METHOD' )) !== 'post' ) {
2017-08-08 03:01:11 +08:00
$response -> setError ( 405 , 'invalid_request' , 'The request method must be POST when revoking an access token' , '#section-3.2' );
2020-04-30 21:43:07 +08:00
$response -> addHttpHeaders ( array ( 'Allow' => 'POST, OPTIONS' ));
2017-08-08 03:01:11 +08:00
return null ;
}
$token_type_hint = $request -> request ( 'token_type_hint' );
if ( ! in_array ( $token_type_hint , array ( null , 'access_token' , 'refresh_token' ), true )) {
$response -> setError ( 400 , 'invalid_request' , 'Token type hint must be either \'access_token\' or \'refresh_token\'' );
return null ;
}
$token = $request -> request ( 'token' );
if ( $token === null ) {
$response -> setError ( 400 , 'invalid_request' , 'Missing token parameter to revoke' );
return null ;
}
// @todo remove this check for v2.0
if ( ! method_exists ( $this -> accessToken , 'revokeToken' )) {
$class = get_class ( $this -> accessToken );
2020-04-30 21:43:07 +08:00
throw new RuntimeException ( " AccessToken { $class } does not implement required revokeToken method " );
2017-08-08 03:01:11 +08:00
}
$this -> accessToken -> revokeToken ( $token , $token_type_hint );
return true ;
}
}