This commit is contained in:
Alex Bilbie 2016-03-17 14:37:21 +00:00
parent a350705a01
commit 251190d828
17 changed files with 415 additions and 271 deletions

View File

@ -6,9 +6,9 @@
"require": { "require": {
"php": ">=5.5.9", "php": ">=5.5.9",
"league/event": "^2.1", "league/event": "^2.1",
"zendframework/zend-diactoros": "^1.1",
"lcobucci/jwt": "^3.1", "lcobucci/jwt": "^3.1",
"paragonie/random_compat": "^1.1" "paragonie/random_compat": "^1.1",
"psr/http-message": "^1.0"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^4.8", "phpunit/phpunit": "^4.8",

View File

@ -0,0 +1,18 @@
<?php
namespace League\OAuth2\Server\AuthorizationValidators;
use Psr\Http\Message\ServerRequestInterface;
interface AuthorizationValidatorInterface
{
/**
* Determine the access token in the authorization header and append OAUth properties to the request
* as attributes.
*
* @param ServerRequestInterface $request
*
* @return ServerRequestInterface
*/
public function validateAuthorization(ServerRequestInterface $request);
}

View File

@ -0,0 +1,66 @@
<?php
namespace League\OAuth2\Server\AuthorizationValidators;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
use Psr\Http\Message\ServerRequestInterface;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use League\OAuth2\Server\Exception\OAuthServerException;
class BearerTokenValidator implements AuthorizationValidatorInterface
{
use CryptTrait;
/**
* @var \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface
*/
private $accessTokenRepository;
/**
* BearerTokenValidator constructor.
*
* @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository
*/
public function __construct(AccessTokenRepositoryInterface $accessTokenRepository)
{
$this->accessTokenRepository = $accessTokenRepository;
}
/**
* {@inheritdoc}
*/
public function validateAuthorization(ServerRequestInterface $request)
{
if ($request->hasHeader('authorization') === false) {
throw OAuthServerException::accessDenied('Missing "Authorization" header');
}
$header = $request->getHeader('authorization');
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
try {
// Attempt to parse and validate the JWT
$token = (new Parser())->parse($jwt);
if ($token->verify(new Sha256(), $this->publicKeyPath) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}
// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
->withAttribute('oauth_client_id', $token->getClaim('aud'))
->withAttribute('oauth_user_id', $token->getClaim('sub'))
->withAttribute('oauth_scopes', $token->getClaim('scopes'));
} catch (\InvalidArgumentException $exception) {
// JWT couldn't be parsed so return the request as is
throw OAuthServerException::accessDenied($exception->getMessage());
}
}
}

View File

@ -8,24 +8,61 @@
* *
* @link https://github.com/thephpleague/oauth2-server * @link https://github.com/thephpleague/oauth2-server
*/ */
namespace League\OAuth2\Server\Utils; namespace League\OAuth2\Server;
class KeyCrypt trait CryptTrait
{ {
/**
* @var string
*/
protected $privateKeyPath;
/**
* @var string
*/
protected $publicKeyPath;
/**
* Set path to private key.
*
* @param string $privateKeyPath
*/
public function setPrivateKeyPath($privateKeyPath)
{
if (strpos($privateKeyPath, 'file://') !== 0) {
$privateKeyPath = 'file://' . $privateKeyPath;
}
$this->privateKeyPath = $privateKeyPath;
}
/**
* Set path to public key.
*
* @param string $publicKeyPath
*/
public function setPublicKeyPath($publicKeyPath)
{
if (strpos($publicKeyPath, 'file://') !== 0) {
$publicKeyPath = 'file://' . $publicKeyPath;
}
$this->publicKeyPath = $publicKeyPath;
}
/** /**
* Encrypt data with a private key. * Encrypt data with a private key.
* *
* @param string $unencryptedData * @param string $unencryptedData
* @param string $pathToPrivateKey
* *
* @return string * @return string
*/ */
public static function encrypt($unencryptedData, $pathToPrivateKey) protected function encrypt($unencryptedData)
{ {
$privateKey = openssl_pkey_get_private($pathToPrivateKey); $privateKey = openssl_pkey_get_private($this->privateKeyPath);
$privateKeyDetails = @openssl_pkey_get_details($privateKey); $privateKeyDetails = @openssl_pkey_get_details($privateKey);
if ($privateKeyDetails === null) { if ($privateKeyDetails === null) {
throw new \LogicException(sprintf('Could not get details of private key: %s', $pathToPrivateKey)); throw new \LogicException(sprintf('Could not get details of private key: %s', $this->privateKeyPath));
} }
$chunkSize = ceil($privateKeyDetails['bits'] / 8) - 11; $chunkSize = ceil($privateKeyDetails['bits'] / 8) - 11;
@ -50,18 +87,17 @@ class KeyCrypt
* Decrypt data with a public key. * Decrypt data with a public key.
* *
* @param string $encryptedData * @param string $encryptedData
* @param string $pathToPublicKey
* *
* @throws \LogicException * @throws \LogicException
* *
* @return string * @return string
*/ */
public static function decrypt($encryptedData, $pathToPublicKey) protected function decrypt($encryptedData)
{ {
$publicKey = openssl_pkey_get_public($pathToPublicKey); $publicKey = openssl_pkey_get_public($this->publicKeyPath);
$publicKeyDetails = @openssl_pkey_get_details($publicKey); $publicKeyDetails = @openssl_pkey_get_details($publicKey);
if ($publicKeyDetails === null) { if ($publicKeyDetails === null) {
throw new \LogicException(sprintf('Could not get details of public key: %s', $pathToPublicKey)); throw new \LogicException(sprintf('Could not get details of public key: %s', $this->publicKeyPath));
} }
$chunkSize = ceil($publicKeyDetails['bits'] / 8); $chunkSize = ceil($publicKeyDetails['bits'] / 8);
@ -72,7 +108,7 @@ class KeyCrypt
while ($encryptedData) { while ($encryptedData) {
$chunk = substr($encryptedData, 0, $chunkSize); $chunk = substr($encryptedData, 0, $chunkSize);
$encryptedData = substr($encryptedData, $chunkSize); $encryptedData = substr($encryptedData, $chunkSize);
if (openssl_public_decrypt($chunk, $decrypted, $publicKey) === false) { if (openssl_public_decrypt($chunk, $decrypted, $publicKey, OPENSSL_PKCS1_OAEP_PADDING) === false) {
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
throw new \LogicException('Failed to decrypt data'); throw new \LogicException('Failed to decrypt data');
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd

View File

@ -3,9 +3,6 @@
namespace League\OAuth2\Server\Exception; namespace League\OAuth2\Server\Exception;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequest;
use Zend\Diactoros\Uri;
class OAuthServerException extends \Exception class OAuthServerException extends \Exception
{ {
@ -183,12 +180,8 @@ class OAuthServerException extends \Exception
* *
* @return \Psr\Http\Message\ResponseInterface * @return \Psr\Http\Message\ResponseInterface
*/ */
public function generateHttpResponse(ResponseInterface $response = null, $useFragment = false) public function generateHttpResponse(ResponseInterface $response, $useFragment = false)
{ {
if (!$response instanceof ResponseInterface) {
$response = new Response();
}
$headers = $this->getHttpHeaders(); $headers = $this->getHttpHeaders();
$payload = [ $payload = [
@ -201,18 +194,13 @@ class OAuthServerException extends \Exception
} }
if ($this->redirectUri !== null) { if ($this->redirectUri !== null) {
$redirectUri = new Uri($this->redirectUri);
parse_str($redirectUri->getQuery(), $redirectPayload);
if ($useFragment === true) { if ($useFragment === true) {
$headers['Location'] = (string) $redirectUri->withFragment(http_build_query( $this->redirectUri .= (strstr($this->redirectUri, '#') === false) ? '#' : '&';
array_merge($redirectPayload, $payload)
));
} else { } else {
$headers['Location'] = (string) $redirectUri->withQuery(http_build_query( $this->redirectUri .= (strstr($this->redirectUri, '?') === false) ? '?' : '&';
array_merge($redirectPayload, $payload)
));
} }
return $response->withStatus(302)->withHeader('Location', $this->redirectUri . http_build_query($payload));
} }
foreach ($headers as $header => $content) { foreach ($headers as $header => $content) {
@ -245,29 +233,14 @@ class OAuthServerException extends \Exception
// matching the authentication scheme used by the client. // matching the authentication scheme used by the client.
// @codeCoverageIgnoreStart // @codeCoverageIgnoreStart
if ($this->errorType === 'invalid_client') { if ($this->errorType === 'invalid_client') {
$authScheme = null; $authScheme = 'Basic';
$request = new ServerRequest(); if (array_key_exists('HTTP_AUTHORIZATION', $_SERVER) !== false
if (isset($request->getServerParams()['PHP_AUTH_USER']) && && strpos($_SERVER['HTTP_AUTHORIZATION'], 'Bearer') === 0
$request->getServerParams()['PHP_AUTH_USER'] !== null
) { ) {
$authScheme = 'Basic'; $authScheme = 'Bearer';
} else {
$authHeader = $request->getHeader('authorization');
if ($authHeader !== []) {
if (strpos($authHeader[0], 'Bearer') === 0) {
$authScheme = 'Bearer';
} elseif (strpos($authHeader[0], 'MAC') === 0) {
$authScheme = 'MAC';
} elseif (strpos($authHeader[0], 'Basic') === 0) {
$authScheme = 'Basic';
}
}
}
if ($authScheme !== null) {
$headers[] = 'WWW-Authenticate: ' . $authScheme . ' realm="OAuth"';
} }
$headers[] = 'WWW-Authenticate: ' . $authScheme . ' realm="OAuth"';
} }
// @codeCoverageIgnoreEnd // @codeCoverageIgnoreEnd
return $headers; return $headers;
} }

View File

@ -39,4 +39,17 @@ abstract class AbstractAuthorizeGrant extends AbstractGrant
return $this->templateRenderer; return $this->templateRenderer;
} }
/**
* @param string $uri
* @param array $params
* @param string $queryDelimiter
*
* @return string
*/
public function makeRedirectUri($uri, $params = [], $queryDelimiter = '?')
{
$uri .= (strstr($uri, $queryDelimiter) === false) ? $queryDelimiter : '&';
return $uri . http_build_query($params);
}
} }

View File

@ -12,6 +12,7 @@ namespace League\OAuth2\Server\Grant;
use League\Event\EmitterAwareTrait; use League\Event\EmitterAwareTrait;
use League\Event\Event; use League\Event\Event;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\AccessTokenEntity; use League\OAuth2\Server\Entities\AccessTokenEntity;
use League\OAuth2\Server\Entities\AuthCodeEntity; use League\OAuth2\Server\Entities\AuthCodeEntity;
use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface;
@ -32,7 +33,7 @@ use Psr\Http\Message\ServerRequestInterface;
*/ */
abstract class AbstractGrant implements GrantTypeInterface abstract class AbstractGrant implements GrantTypeInterface
{ {
use EmitterAwareTrait; use EmitterAwareTrait, CryptTrait;
const SCOPE_DELIMITER_STRING = ' '; const SCOPE_DELIMITER_STRING = ' ';
@ -71,16 +72,6 @@ abstract class AbstractGrant implements GrantTypeInterface
*/ */
protected $userRepository; protected $userRepository;
/**
* @var string
*/
protected $pathToPrivateKey;
/**
* @var string
*/
protected $pathToPublicKey;
/** /**
* @var \DateInterval * @var \DateInterval
*/ */
@ -134,22 +125,6 @@ abstract class AbstractGrant implements GrantTypeInterface
$this->userRepository = $userRepository; $this->userRepository = $userRepository;
} }
/**
* @param string $pathToPrivateKey
*/
public function setPathToPrivateKey($pathToPrivateKey)
{
$this->pathToPrivateKey = $pathToPrivateKey;
}
/**
* @param string $pathToPublicKey
*/
public function setPathToPublicKey($pathToPublicKey)
{
$this->pathToPublicKey = $pathToPublicKey;
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -10,12 +10,11 @@ use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface; use League\OAuth2\Server\Repositories\AuthCodeRepositoryInterface;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\ResponseTypes\HtmlResponse;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use League\OAuth2\Server\TemplateRenderer\AbstractRenderer; use League\OAuth2\Server\TemplateRenderer\AbstractRenderer;
use League\OAuth2\Server\Utils\KeyCrypt;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Uri;
class AuthCodeGrant extends AbstractAuthorizeGrant class AuthCodeGrant extends AbstractAuthorizeGrant
{ {
@ -88,13 +87,11 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$client, $client,
$client->getRedirectUri() $client->getRedirectUri()
); );
$queryString = http_build_query($request->getQueryParams());
$postbackUri = new Uri( $postbackUri = sprintf(
sprintf( '//%s%s',
'//%s%s', $request->getServerParams()['HTTP_HOST'],
$request->getServerParams()['HTTP_HOST'], $request->getServerParams()['REQUEST_URI']
$request->getServerParams()['REQUEST_URI']
)
); );
$userId = null; $userId = null;
@ -107,7 +104,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$oauthCookie = $this->getCookieParameter('oauth_authorize_request', $request, null); $oauthCookie = $this->getCookieParameter('oauth_authorize_request', $request, null);
if ($oauthCookie !== null) { if ($oauthCookie !== null) {
try { try {
$oauthCookiePayload = json_decode(KeyCrypt::decrypt($oauthCookie, $this->pathToPublicKey)); $oauthCookiePayload = json_decode($this->decrypt($oauthCookie));
if (is_object($oauthCookiePayload)) { if (is_object($oauthCookiePayload)) {
$userId = $oauthCookiePayload->user_id; $userId = $oauthCookiePayload->user_id;
} }
@ -140,10 +137,16 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
if ($userId === null) { if ($userId === null) {
$html = $this->getTemplateRenderer()->renderLogin([ $html = $this->getTemplateRenderer()->renderLogin([
'error' => $loginError, 'error' => $loginError,
'postback_uri' => (string) $postbackUri->withQuery($queryString), 'postback_uri' => $this->makeRedirectUri(
$postbackUri,
$request->getQueryParams()
),
]); ]);
return new Response\HtmlResponse($html); $htmlResponse = new HtmlResponse($this->accessTokenRepository);
$htmlResponse->setStatusCode(403);
$htmlResponse->setHtml($html);
return $htmlResponse;
} }
// The user hasn't approved the client yet so show an authorize form // The user hasn't approved the client yet so show an authorize form
@ -151,30 +154,31 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$html = $this->getTemplateRenderer()->renderAuthorize([ $html = $this->getTemplateRenderer()->renderAuthorize([
'client' => $client, 'client' => $client,
'scopes' => $scopes, 'scopes' => $scopes,
'postback_uri' => (string) $postbackUri->withQuery($queryString), 'postback_uri' => $this->makeRedirectUri(
$postbackUri,
$request->getQueryParams()
),
]); ]);
return new Response\HtmlResponse( $htmlResponse = new HtmlResponse($this->accessTokenRepository);
$html, $htmlResponse->setStatusCode(200);
200, $htmlResponse->setHtml($html);
[ $htmlResponse->setHeader('set-cookie', sprintf(
'Set-Cookie' => sprintf( 'oauth_authorize_request=%s; Expires=%s',
'oauth_authorize_request=%s; Expires=%s', urlencode($this->encrypt(
urlencode(KeyCrypt::encrypt( json_encode([
json_encode([ 'user_id' => $userId,
'user_id' => $userId, ])
]), )),
$this->pathToPrivateKey (new \DateTime())->add(new \DateInterval('PT5M'))->format('D, d M Y H:i:s e')
)), ));
(new \DateTime())->add(new \DateInterval('PT5M'))->format('D, d M Y H:i:s e')
), return $htmlResponse;
]
);
} }
// The user has either approved or denied the client, so redirect them back // The user has either approved or denied the client, so redirect them back
$redirectUri = new Uri($client->getRedirectUri()); $redirectUri = $client->getRedirectUri();
parse_str($redirectUri->getQuery(), $redirectPayload); $redirectPayload = [];
$stateParameter = $this->getQueryStringParameter('state', $request); $stateParameter = $this->getQueryStringParameter('state', $request);
if ($stateParameter !== null) { if ($stateParameter !== null) {
@ -191,7 +195,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
$scopes $scopes
); );
$redirectPayload['code'] = KeyCrypt::encrypt( $redirectPayload['code'] = $this->encrypt(
json_encode( json_encode(
[ [
'client_id' => $authCode->getClient()->getIdentifier(), 'client_id' => $authCode->getClient()->getIdentifier(),
@ -201,17 +205,22 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
'user_id' => $authCode->getUserIdentifier(), 'user_id' => $authCode->getUserIdentifier(),
'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'), 'expire_time' => (new \DateTime())->add($this->authCodeTTL)->format('U'),
] ]
), )
$this->pathToPrivateKey
); );
return new Response\RedirectResponse($redirectUri->withQuery(http_build_query($redirectPayload))); $response = new RedirectResponse($this->accessTokenRepository);
$response->setRedirectUri(
$this->makeRedirectUri(
$redirectUri,
$redirectPayload
)
);
return $response;
} }
// The user denied the client, redirect them back with an error // The user denied the client, redirect them back with an error
$exception = OAuthServerException::accessDenied('The user denied the request', (string) $redirectUri); throw OAuthServerException::accessDenied('The user denied the request', (string) $redirectUri);
return $exception->generateHttpResponse();
} }
/** /**
@ -246,7 +255,7 @@ class AuthCodeGrant extends AbstractAuthorizeGrant
// Validate the authorization code // Validate the authorization code
try { try {
$authCodePayload = json_decode(KeyCrypt::decrypt($encryptedAuthCode, $this->pathToPublicKey)); $authCodePayload = json_decode($this->decrypt($encryptedAuthCode));
if (time() > $authCodePayload->expire_time) { if (time() > $authCodePayload->expire_time) {
throw OAuthServerException::invalidRequest('code', 'Authorization code has expired'); throw OAuthServerException::invalidRequest('code', 'Authorization code has expired');
} }

View File

@ -91,12 +91,12 @@ interface GrantTypeInterface extends EmitterAwareInterface
* *
* @param string $pathToPrivateKey * @param string $pathToPrivateKey
*/ */
public function setPathToPrivateKey($pathToPrivateKey); public function setPrivateKeyPath($pathToPrivateKey);
/** /**
* Set the path to the public key. * Set the path to the public key.
* *
* @param string $pathToPublicKey * @param string $pathToPublicKey
*/ */
public function setPathToPublicKey($pathToPublicKey); public function setPublicKeyPath($pathToPublicKey);
} }

View File

@ -7,12 +7,11 @@ use League\OAuth2\Server\Entities\Interfaces\ClientEntityInterface;
use League\OAuth2\Server\Entities\Interfaces\UserEntityInterface; use League\OAuth2\Server\Entities\Interfaces\UserEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\UserRepositoryInterface; use League\OAuth2\Server\Repositories\UserRepositoryInterface;
use League\OAuth2\Server\ResponseTypes\HtmlResponse;
use League\OAuth2\Server\ResponseTypes\RedirectResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use League\OAuth2\Server\TemplateRenderer\AbstractRenderer; use League\OAuth2\Server\TemplateRenderer\AbstractRenderer;
use League\OAuth2\Server\Utils\KeyCrypt;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\Uri;
class ImplicitGrant extends AbstractAuthorizeGrant class ImplicitGrant extends AbstractAuthorizeGrant
{ {
@ -86,13 +85,11 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$client, $client,
$client->getRedirectUri() $client->getRedirectUri()
); );
$queryString = http_build_query($request->getQueryParams());
$postbackUri = new Uri( $postbackUri = sprintf(
sprintf( '//%s%s',
'//%s%s', $request->getServerParams()['HTTP_HOST'],
$request->getServerParams()['HTTP_HOST'], $request->getServerParams()['REQUEST_URI']
$request->getServerParams()['REQUEST_URI']
)
); );
$userId = null; $userId = null;
@ -105,7 +102,7 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$oauthCookie = $this->getCookieParameter('oauth_authorize_request', $request, null); $oauthCookie = $this->getCookieParameter('oauth_authorize_request', $request, null);
if ($oauthCookie !== null) { if ($oauthCookie !== null) {
try { try {
$oauthCookiePayload = json_decode(KeyCrypt::decrypt($oauthCookie, $this->pathToPublicKey)); $oauthCookiePayload = json_decode($this->decrypt($oauthCookie));
if (is_object($oauthCookiePayload)) { if (is_object($oauthCookiePayload)) {
$userId = $oauthCookiePayload->user_id; $userId = $oauthCookiePayload->user_id;
} }
@ -138,10 +135,16 @@ class ImplicitGrant extends AbstractAuthorizeGrant
if ($userId === null) { if ($userId === null) {
$html = $this->getTemplateRenderer()->renderLogin([ $html = $this->getTemplateRenderer()->renderLogin([
'error' => $loginError, 'error' => $loginError,
'postback_uri' => (string) $postbackUri->withQuery($queryString), 'postback_uri' => $this->makeRedirectUri(
$postbackUri,
$request->getQueryParams()
),
]); ]);
return new Response\HtmlResponse($html); $htmlResponse = new HtmlResponse($this->accessTokenRepository);
$htmlResponse->setStatusCode(403);
$htmlResponse->setHtml($html);
return $htmlResponse;
} }
// The user hasn't approved the client yet so show an authorize form // The user hasn't approved the client yet so show an authorize form
@ -149,25 +152,26 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$html = $this->getTemplateRenderer()->renderAuthorize([ $html = $this->getTemplateRenderer()->renderAuthorize([
'client' => $client, 'client' => $client,
'scopes' => $scopes, 'scopes' => $scopes,
'postback_uri' => (string) $postbackUri->withQuery($queryString), 'postback_uri' => $this->makeRedirectUri(
$postbackUri,
$request->getQueryParams()
)
]); ]);
return new Response\HtmlResponse( $htmlResponse = new HtmlResponse($this->accessTokenRepository);
$html, $htmlResponse->setStatusCode(200);
200, $htmlResponse->setHtml($html);
[ $htmlResponse->setHeader('set-cookie', sprintf(
'Set-Cookie' => sprintf( 'oauth_authorize_request=%s; Expires=%s',
'oauth_authorize_request=%s; Expires=%s', urlencode($this->encrypt(
urlencode(KeyCrypt::encrypt( json_encode([
json_encode([ 'user_id' => $userId,
'user_id' => $userId, ])
]), )),
$this->pathToPrivateKey (new \DateTime())->add(new \DateInterval('PT5M'))->format('D, d M Y H:i:s e')
)), ));
(new \DateTime())->add(new \DateInterval('PT5M'))->format('D, d M Y H:i:s e')
), return $htmlResponse;
]
);
} }
// The user has either approved or denied the client, so redirect them back // The user has either approved or denied the client, so redirect them back
@ -188,11 +192,18 @@ class ImplicitGrant extends AbstractAuthorizeGrant
$scopes $scopes
); );
$redirectPayload['access_token'] = (string) $accessToken->convertToJWT($this->pathToPrivateKey); $redirectPayload['access_token'] = (string) $accessToken->convertToJWT($this->privateKeyPath);
$redirectPayload['token_type'] = 'bearer'; $redirectPayload['token_type'] = 'bearer';
$redirectPayload['expires_in'] = time() - $accessToken->getExpiryDateTime()->getTimestamp(); $redirectPayload['expires_in'] = time() - $accessToken->getExpiryDateTime()->getTimestamp();
return new Response\RedirectResponse($redirectUri->withFragment(http_build_query($redirectPayload))); $response = new RedirectResponse($this->accessTokenRepository);
$response->setRedirectUri(
$this->makeRedirectUri(
$redirectUri,
$redirectPayload,
'#'
)
);
} }
// The user denied the client, redirect them back with an error // The user denied the client, redirect them back with an error

View File

@ -14,7 +14,6 @@ use League\Event\Event;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface; use League\OAuth2\Server\Repositories\RefreshTokenRepositoryInterface;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use League\OAuth2\Server\Utils\KeyCrypt;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
/** /**
@ -106,7 +105,7 @@ class RefreshTokenGrant extends AbstractGrant
// Validate refresh token // Validate refresh token
try { try {
$refreshToken = KeyCrypt::decrypt($encryptedRefreshToken, $this->pathToPublicKey); $refreshToken = $this->decrypt($encryptedRefreshToken);
} catch (\LogicException $e) { } catch (\LogicException $e) {
throw OAuthServerException::invalidRefreshToken('Cannot parse refresh token: ' . $e->getMessage()); throw OAuthServerException::invalidRefreshToken('Cannot parse refresh token: ' . $e->getMessage());
} }

View File

@ -10,21 +10,14 @@
*/ */
namespace League\OAuth2\Server\ResponseTypes; namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\CryptTrait;
use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
abstract class AbstractResponseType implements ResponseTypeInterface abstract class AbstractResponseType implements ResponseTypeInterface
{ {
/** use CryptTrait;
* @var string
*/
protected $pathToPrivateKey;
/**
* @var string
*/
protected $pathToPublicKey;
/** /**
* @var \League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface * @var \League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface
@ -42,17 +35,10 @@ abstract class AbstractResponseType implements ResponseTypeInterface
protected $accessTokenRepository; protected $accessTokenRepository;
/** /**
* @param string $pathToPrivateKey
* @param string $pathToPublicKey
* @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository * @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository
*/ */
public function __construct( public function __construct(AccessTokenRepositoryInterface $accessTokenRepository)
$pathToPrivateKey, {
$pathToPublicKey,
AccessTokenRepositoryInterface $accessTokenRepository
) {
$this->pathToPrivateKey = $pathToPrivateKey;
$this->pathToPublicKey = $pathToPublicKey;
$this->accessTokenRepository = $accessTokenRepository; $this->accessTokenRepository = $accessTokenRepository;
} }

View File

@ -10,13 +10,8 @@
*/ */
namespace League\OAuth2\Server\ResponseTypes; namespace League\OAuth2\Server\ResponseTypes;
use Lcobucci\JWT\Parser;
use Lcobucci\JWT\Signer\Rsa\Sha256;
use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface;
use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Utils\KeyCrypt;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class BearerTokenResponse extends AbstractResponseType class BearerTokenResponse extends AbstractResponseType
{ {
@ -27,7 +22,7 @@ class BearerTokenResponse extends AbstractResponseType
{ {
$expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp(); $expireDateTime = $this->accessToken->getExpiryDateTime()->getTimestamp();
$jwtAccessToken = $this->accessToken->convertToJWT($this->pathToPrivateKey); $jwtAccessToken = $this->accessToken->convertToJWT($this->privateKeyPath);
$responseParams = [ $responseParams = [
'token_type' => 'Bearer', 'token_type' => 'Bearer',
@ -36,7 +31,7 @@ class BearerTokenResponse extends AbstractResponseType
]; ];
if ($this->refreshToken instanceof RefreshTokenEntityInterface) { if ($this->refreshToken instanceof RefreshTokenEntityInterface) {
$refreshToken = KeyCrypt::encrypt( $refreshToken = $this->encrypt(
json_encode( json_encode(
[ [
'client_id' => $this->accessToken->getClient()->getIdentifier(), 'client_id' => $this->accessToken->getClient()->getIdentifier(),
@ -46,8 +41,7 @@ class BearerTokenResponse extends AbstractResponseType
'user_id' => $this->accessToken->getUserIdentifier(), 'user_id' => $this->accessToken->getUserIdentifier(),
'expire_time' => $expireDateTime, 'expire_time' => $expireDateTime,
] ]
), )
$this->pathToPrivateKey
); );
$responseParams['refresh_token'] = $refreshToken; $responseParams['refresh_token'] = $refreshToken;
@ -63,40 +57,4 @@ class BearerTokenResponse extends AbstractResponseType
return $response; return $response;
} }
/**
* {@inheritdoc}
*/
public function validateAccessToken(ServerRequestInterface $request)
{
if ($request->hasHeader('authorization') === false) {
throw OAuthServerException::accessDenied('Missing "Authorization" header');
}
$header = $request->getHeader('authorization');
$jwt = trim(preg_replace('/^(?:\s+)?Bearer\s/', '', $header[0]));
try {
// Attempt to parse and validate the JWT
$token = (new Parser())->parse($jwt);
if ($token->verify(new Sha256(), $this->pathToPublicKey) === false) {
throw OAuthServerException::accessDenied('Access token could not be verified');
}
// Check if token has been revoked
if ($this->accessTokenRepository->isAccessTokenRevoked($token->getClaim('jti'))) {
throw OAuthServerException::accessDenied('Access token has been revoked');
}
// Return the request with additional attributes
return $request
->withAttribute('oauth_access_token_id', $token->getClaim('jti'))
->withAttribute('oauth_client_id', $token->getClaim('aud'))
->withAttribute('oauth_user_id', $token->getClaim('sub'))
->withAttribute('oauth_scopes', $token->getClaim('scopes'));
} catch (\InvalidArgumentException $exception) {
// JWT couldn't be parsed so return the request as is
throw OAuthServerException::accessDenied($exception->getMessage());
}
}
} }

View File

@ -0,0 +1,66 @@
<?php
namespace League\OAuth2\Server\ResponseTypes;
use Psr\Http\Message\ResponseInterface;
class HtmlResponse extends AbstractResponseType
{
/**
* @var string
*/
private $html = '';
/**
* @var int
*/
private $statusCode = 200;
/**
* @var array
*/
private $headers = [];
/**
* @param string $html
*/
public function setHtml($html)
{
$this->html = $html;
}
/**
* @param int $statusCode
*/
public function setStatusCode($statusCode = 200)
{
$this->statusCode = $statusCode;
}
/**
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response)
{
$response->getBody()->write($this->html);
foreach ($this->headers as $key => $value) {
$response = $response->withHeader($key, $value);
}
return $response
->withStatus($this->statusCode)
->withHeader('content-type', 'text/html');
}
/**
* @param string $key
* @param string $value
*/
public function setHeader($key, $value)
{
$this->headers[$key] = $value;
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace League\OAuth2\Server\ResponseTypes;
use Psr\Http\Message\ResponseInterface;
class RedirectResponse extends AbstractResponseType
{
/**
* @var string
*/
private $redirectUri;
/**
* @param string $redirectUri
*/
public function setRedirectUri($redirectUri)
{
$this->redirectUri = $redirectUri;
}
/**
* @param ResponseInterface $response
*
* @return ResponseInterface
*/
public function generateHttpResponse(ResponseInterface $response)
{
return $response->withStatus(302)->withHeader('location', $this->redirectUri);
}
}

View File

@ -13,7 +13,6 @@ namespace League\OAuth2\Server\ResponseTypes;
use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\AccessTokenEntityInterface;
use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface; use League\OAuth2\Server\Entities\Interfaces\RefreshTokenEntityInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
interface ResponseTypeInterface interface ResponseTypeInterface
{ {
@ -27,16 +26,6 @@ interface ResponseTypeInterface
*/ */
public function setRefreshToken(RefreshTokenEntityInterface $refreshToken); public function setRefreshToken(RefreshTokenEntityInterface $refreshToken);
/**
* Determine the access token in the authorization header and append OAUth properties to the request
* as attributes.
*
* @param ServerRequestInterface $request
*
* @return ServerRequestInterface
*/
public function validateAccessToken(ServerRequestInterface $request);
/** /**
* @param ResponseInterface $response * @param ResponseInterface $response
* *

View File

@ -5,6 +5,8 @@ namespace League\OAuth2\Server;
use DateInterval; use DateInterval;
use League\Event\EmitterAwareInterface; use League\Event\EmitterAwareInterface;
use League\Event\EmitterAwareTrait; use League\Event\EmitterAwareTrait;
use League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface;
use League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator;
use League\OAuth2\Server\Exception\OAuthServerException; use League\OAuth2\Server\Exception\OAuthServerException;
use League\OAuth2\Server\Grant\GrantTypeInterface; use League\OAuth2\Server\Grant\GrantTypeInterface;
use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface; use League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface;
@ -14,8 +16,6 @@ use League\OAuth2\Server\ResponseTypes\BearerTokenResponse;
use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface; use League\OAuth2\Server\ResponseTypes\ResponseTypeInterface;
use Psr\Http\Message\ResponseInterface; use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Message\ServerRequestInterface;
use Zend\Diactoros\Response;
use Zend\Diactoros\ServerRequestFactory;
class Server implements EmitterAwareInterface class Server implements EmitterAwareInterface
{ {
@ -61,15 +61,21 @@ class Server implements EmitterAwareInterface
*/ */
private $scopeRepository; private $scopeRepository;
/**
* @var \League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface
*/
private $authorizationValidator;
/** /**
* New server instance. * New server instance.
* *
* @param \League\OAuth2\Server\Repositories\ClientRepositoryInterface $clientRepository * @param \League\OAuth2\Server\Repositories\ClientRepositoryInterface $clientRepository
* @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository * @param \League\OAuth2\Server\Repositories\AccessTokenRepositoryInterface $accessTokenRepository
* @param \League\OAuth2\Server\Repositories\ScopeRepositoryInterface $scopeRepository * @param \League\OAuth2\Server\Repositories\ScopeRepositoryInterface $scopeRepository
* @param string $privateKeyPath * @param string $privateKeyPath
* @param string $publicKeyPath * @param string $publicKeyPath
* @param null|\League\OAuth2\Server\ResponseTypes\ResponseTypeInterface $responseType * @param null|\League\OAuth2\Server\ResponseTypes\ResponseTypeInterface $responseType
* @param null|\League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface $authorizationValidator
*/ */
public function __construct( public function __construct(
ClientRepositoryInterface $clientRepository, ClientRepositoryInterface $clientRepository,
@ -77,7 +83,8 @@ class Server implements EmitterAwareInterface
ScopeRepositoryInterface $scopeRepository, ScopeRepositoryInterface $scopeRepository,
$privateKeyPath, $privateKeyPath,
$publicKeyPath, $publicKeyPath,
ResponseTypeInterface $responseType = null ResponseTypeInterface $responseType = null,
AuthorizationValidatorInterface $authorizationValidator = null
) { ) {
$this->clientRepository = $clientRepository; $this->clientRepository = $clientRepository;
$this->accessTokenRepository = $accessTokenRepository; $this->accessTokenRepository = $accessTokenRepository;
@ -85,6 +92,7 @@ class Server implements EmitterAwareInterface
$this->privateKeyPath = $privateKeyPath; $this->privateKeyPath = $privateKeyPath;
$this->publicKeyPath = $publicKeyPath; $this->publicKeyPath = $publicKeyPath;
$this->responseType = $responseType; $this->responseType = $responseType;
$this->authorizationValidator = $authorizationValidator;
} }
/** /**
@ -98,8 +106,8 @@ class Server implements EmitterAwareInterface
$grantType->setAccessTokenRepository($this->accessTokenRepository); $grantType->setAccessTokenRepository($this->accessTokenRepository);
$grantType->setClientRepository($this->clientRepository); $grantType->setClientRepository($this->clientRepository);
$grantType->setScopeRepository($this->scopeRepository); $grantType->setScopeRepository($this->scopeRepository);
$grantType->setPathToPrivateKey($this->privateKeyPath); $grantType->setPrivateKeyPath($this->privateKeyPath);
$grantType->setPathToPublicKey($this->publicKeyPath); $grantType->setPublicKeyPath($this->publicKeyPath);
$grantType->setEmitter($this->getEmitter()); $grantType->setEmitter($this->getEmitter());
$this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType; $this->enabledGrantTypes[$grantType->getIdentifier()] = $grantType;
@ -117,37 +125,29 @@ class Server implements EmitterAwareInterface
* *
* @return \Psr\Http\Message\ResponseInterface * @return \Psr\Http\Message\ResponseInterface
*/ */
public function respondToRequest(ServerRequestInterface $request = null, ResponseInterface $response = null) public function respondToRequest(ServerRequestInterface $request, ResponseInterface $response)
{ {
if (!$request instanceof ServerRequestInterface) { try {
$request = ServerRequestFactory::fromGlobals(); $tokenResponse = null;
} while ($tokenResponse === null && $grantType = array_shift($this->enabledGrantTypes)) {
/** @var \League\OAuth2\Server\Grant\GrantTypeInterface $grantType */
if (!$response instanceof ResponseInterface) { if ($grantType->canRespondToRequest($request)) {
$response = new Response(); $tokenResponse = $grantType->respondToRequest(
} $request,
$this->getResponseType(),
$tokenResponse = null; $this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
while ($tokenResponse === null && $grantType = array_shift($this->enabledGrantTypes)) { );
/** @var \League\OAuth2\Server\Grant\GrantTypeInterface $grantType */ }
if ($grantType->canRespondToRequest($request)) {
$tokenResponse = $grantType->respondToRequest(
$request,
$this->getResponseType(),
$this->grantTypeAccessTokenTTL[$grantType->getIdentifier()]
);
} }
}
if ($tokenResponse instanceof ResponseInterface) { if ($tokenResponse instanceof ResponseTypeInterface) {
return $tokenResponse; return $tokenResponse->generateHttpResponse($response);
} }
if ($tokenResponse instanceof ResponseTypeInterface) { throw OAuthServerException::unsupportedGrantType();
return $tokenResponse->generateHttpResponse($response); } catch (OAuthServerException $e) {
return $e->generateHttpResponse($response);
} }
throw OAuthServerException::unsupportedGrantType();
} }
/** /**
@ -161,7 +161,7 @@ class Server implements EmitterAwareInterface
*/ */
public function validateAuthenticatedRequest(ServerRequestInterface $request) public function validateAuthenticatedRequest(ServerRequestInterface $request)
{ {
return $this->getResponseType()->validateAccessToken($request); return $this->getAuthorizationValidator()->validateAuthorization($request);
} }
/** /**
@ -172,13 +172,27 @@ class Server implements EmitterAwareInterface
protected function getResponseType() protected function getResponseType()
{ {
if (!$this->responseType instanceof ResponseTypeInterface) { if (!$this->responseType instanceof ResponseTypeInterface) {
$this->responseType = new BearerTokenResponse( $this->responseType = new BearerTokenResponse($this->accessTokenRepository);
$this->privateKeyPath,
$this->publicKeyPath,
$this->accessTokenRepository
);
} }
$this->responseType->setPublicKeyPath($this->publicKeyPath);
$this->responseType->setPrivateKeyPath($this->privateKeyPath);
return $this->responseType; return $this->responseType;
} }
/**
* @return \League\OAuth2\Server\AuthorizationValidators\AuthorizationValidatorInterface
*/
protected function getAuthorizationValidator()
{
if (!$this->authorizationValidator instanceof AuthorizationValidatorInterface) {
$this->authorizationValidator = new BearerTokenValidator($this->accessTokenRepository);
}
$this->authorizationValidator->setPublicKeyPath($this->publicKeyPath);
$this->authorizationValidator->setPrivateKeyPath($this->privateKeyPath);
return $this->authorizationValidator;
}
} }