Skip to content

Commit

Permalink
Merge pull request #643 from bshaffer/develop
Browse files Browse the repository at this point in the history
Tag 1.8.0
  • Loading branch information
bshaffer committed Sep 18, 2015
2 parents 7fde34c + 7fa4185 commit 058c98f
Show file tree
Hide file tree
Showing 30 changed files with 598 additions and 86 deletions.
6 changes: 4 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
language: php
sudo: false
php:
- 5.3
- 5.4
Expand All @@ -18,6 +19,7 @@ before_script:
- psql -c 'create database oauth2_server_php;' -U postgres
- composer require predis/predis:dev-master
- composer require thobbs/phpcassa:dev-master
- composer require aws/aws-sdk-php:dev-master
- composer require 'aws/aws-sdk-php:~2.8'
- composer require 'firebase/php-jwt:~2.2'
after_script:
- php test/cleanup.php
- php test/cleanup.php
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@ To see the files changed for a given bug, go to https://github.com/bshaffer/oaut
To get the diff between two versions, go to https://github.com/bshaffer/oauth2-server-php/compare/v1.0...v1.1
To get the diff for a specific change, go to https://github.com/bshaffer/oauth2-server-php/commit/XXX where XXX is the change hash

* 1.8.0 (2015-09-18)

PR: https://github.com/bshaffer/oauth2-server-php/pull/643

* bug #594 - adds jti
* bug #598 - fixes lifetime configurations for JWTs
* bug #634 - fixes travis builds, upgrade to containers
* bug #586 - support for revoking tokens
* bug #636 - Adds FirebaseJWT bridge
* bug #639 - Mongo HHVM compatibility

* 1.7.0 (2015-04-23)

PR: https://github.com/bshaffer/oauth2-server-php/pull/572
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"suggest": {
"predis/predis": "Required to use the Redis storage engine",
"thobbs/phpcassa": "Required to use the Cassandra storage engine",
"aws/aws-sdk-php": "Required to use the DynamoDB storage engine"
"aws/aws-sdk-php": "~2.8 is required to use the DynamoDB storage engine",
"firebase/php-jwt": "~2.2 is required to use JWT features"
}
}
54 changes: 54 additions & 0 deletions src/OAuth2/Controller/TokenController.php
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,58 @@ public function addGrantType(GrantTypeInterface $grantType, $identifier = null)

$this->grantTypes[$identifier] = $grantType;
}

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
* @return bool|null
*/
public function revokeToken(RequestInterface $request, ResponseInterface $response)
{
if (strtolower($request->server('REQUEST_METHOD')) != 'post') {
$response->setError(405, 'invalid_request', 'The request method must be POST when revoking an access token', '#section-3.2');
$response->addHttpHeaders(array('Allow' => 'POST'));

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);
throw new \RuntimeException("AccessToken {$class} does not implement required revokeToken method");
}

$this->accessToken->revokeToken($token, $token_type_hint);

return true;
}
}
47 changes: 47 additions & 0 deletions src/OAuth2/Encryption/FirebaseJwt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

namespace OAuth2\Encryption;

/**
* Bridge file to use the firebase/php-jwt package for JWT encoding and decoding.
* @author Francis Chuang <[email protected]>
*/
class FirebaseJwt implements EncryptionInterface
{
public function __construct()
{
if (!class_exists('\JWT')) {
throw new \ErrorException('firebase/php-jwt must be installed to use this feature. You can do this by running "composer require firebase/php-jwt"');
}
}

public function encode($payload, $key, $alg = 'HS256', $keyId = null)
{
return \JWT::encode($payload, $key, $alg, $keyId);
}

public function decode($jwt, $key = null, $allowedAlgorithms = null)
{
try {

//Maintain BC: Do not verify if no algorithms are passed in.
if (!$allowedAlgorithms) {
$key = null;
}

return (array)\JWT::decode($jwt, $key, $allowedAlgorithms);
} catch (\Exception $e) {
return false;
}
}

public function urlSafeB64Encode($data)
{
return \JWT::urlsafeB64Encode($data);
}

public function urlSafeB64Decode($b64)
{
return \JWT::urlsafeB64Decode($b64);
}
}
3 changes: 2 additions & 1 deletion src/OAuth2/Encryption/Jwt.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ protected function generateJwtHeader($payload, $algorithm)
'alg' => $algorithm,
);
}

protected function hash_equals($a, $b)
{
if (function_exists('hash_equals')) {
Expand All @@ -167,6 +167,7 @@ protected function hash_equals($a, $b)
for ($i = 0; $i < strlen($a) && $i < strlen($b); $i++) {
$diff |= ord($a[$i]) ^ ord($b[$i]);
}

return $diff === 0;
}
}
40 changes: 39 additions & 1 deletion src/OAuth2/ResponseType/AccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ protected function generateAccessToken()
if ($randomData !== false && strlen($randomData) === 20) {
return bin2hex($randomData);
}
}
}
if (@file_exists('/dev/urandom')) { // Get 100 bytes of random data
$randomData = file_get_contents('/dev/urandom', false, null, 0, 20);
if ($randomData !== false && strlen($randomData) === 20) {
Expand All @@ -134,6 +134,7 @@ protected function generateAccessToken()
}
// Last resort which you probably should just get rid of:
$randomData = mt_rand() . mt_rand() . mt_rand() . mt_rand() . microtime(true) . uniqid(mt_rand(), true);

return substr(hash('sha512', $randomData), 0, 40);
}

Expand All @@ -153,4 +154,41 @@ protected function generateRefreshToken()
{
return $this->generateAccessToken(); // let's reuse the same scheme for token generation
}

/**
* Handle the revoking of refresh tokens, and access tokens if supported / desirable
* RFC7009 specifies that "If the server is unable to locate the token using
* the given hint, it MUST extend its search across all of its supported token types"
*
* @param $token
* @param null $tokenTypeHint
* @return boolean
*/
public function revokeToken($token, $tokenTypeHint = null)
{
if ($tokenTypeHint == 'refresh_token') {
if ($this->refreshStorage && $revoked = $this->refreshStorage->unsetRefreshToken($token)) {
return true;
}
}

/** @TODO remove in v2 */
if (!method_exists($this->tokenStorage, 'unsetAccessToken')) {
throw new \RuntimeException(
sprintf('Token storage %s must implement unsetAccessToken method', get_class($this->tokenStorage)
));
}

$revoked = $this->tokenStorage->unsetAccessToken($token);

// if a typehint is supplied and fails, try other storages
// @see https://tools.ietf.org/html/rfc7009#section-2.1
if (!$revoked && $tokenTypeHint != 'refresh_token') {
if ($this->refreshStorage) {
$revoked = $this->refreshStorage->unsetRefreshToken($token);
}
}

return $revoked;
}
}
11 changes: 11 additions & 0 deletions src/OAuth2/ResponseType/AccessTokenInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,15 @@ interface AccessTokenInterface extends ResponseTypeInterface
* @ingroup oauth2_section_5
*/
public function createAccessToken($client_id, $user_id, $scope = null, $includeRefreshToken = true);

/**
* Handle the revoking of refresh tokens, and access tokens if supported / desirable
*
* @param $token
* @param $tokenTypeHint
* @return mixed
*
* @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x
*/
//public function revokeToken($token, $tokenTypeHint);
}
4 changes: 3 additions & 1 deletion src/OAuth2/ResponseType/JwtAccessToken.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,10 @@ public function createAccessToken($client_id, $user_id, $scope = null, $includeR
{
// token to encrypt
$expires = time() + $this->config['access_lifetime'];
$id = $this->generateAccessToken();
$jwtAccessToken = array(
'id' => $this->generateAccessToken(),
'id' => $id, // for BC (see #591)
'jti' => $id,
'iss' => $this->config['issuer'],
'aud' => $client_id,
'sub' => $user_id,
Expand Down
32 changes: 25 additions & 7 deletions src/OAuth2/Server.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,24 @@ public function grantAccessToken(RequestInterface $request, ResponseInterface $r
return $value;
}

/**
* Handle a revoke token request
* This would be called from the "/revoke" endpoint as defined in the draft Token Revocation spec
*
* @see https://tools.ietf.org/html/rfc7009#section-2
*
* @param RequestInterface $request
* @param ResponseInterface $response
* @return Response|ResponseInterface
*/
public function handleRevokeRequest(RequestInterface $request, ResponseInterface $response = null)
{
$this->response = is_null($response) ? new Response() : $response;
$this->getTokenController()->handleRevokeRequest($request, $this->response);

return $this->response;
}

/**
* Redirect the user appropriately after approval.
*
Expand Down Expand Up @@ -348,17 +366,17 @@ public function getAccessTokenData(RequestInterface $request, ResponseInterface
return $value;
}

public function addGrantType(GrantTypeInterface $grantType, $key = null)
public function addGrantType(GrantTypeInterface $grantType, $identifier = null)
{
if (is_string($key)) {
$this->grantTypes[$key] = $grantType;
} else {
$this->grantTypes[$grantType->getQuerystringIdentifier()] = $grantType;
if (!is_string($identifier)) {
$identifier = $grantType->getQuerystringIdentifier();
}

$this->grantTypes[$identifier] = $grantType;

// persist added grant type down to TokenController
if (!is_null($this->tokenController)) {
$this->getTokenController()->addGrantType($grantType);
$this->getTokenController()->addGrantType($grantType, $identifier);
}
}

Expand Down Expand Up @@ -693,7 +711,7 @@ protected function createDefaultJwtAccessTokenResponseType()
$refreshStorage = $this->storages['refresh_token'];
}

$config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer')));
$config = array_intersect_key($this->config, array_flip(explode(' ', 'store_encrypted_token_string issuer access_lifetime refresh_token_lifetime')));

return new JwtAccessToken($this->storages['public_key'], $tokenStorage, $refreshStorage, $config);
}
Expand Down
15 changes: 15 additions & 0 deletions src/OAuth2/Storage/AccessTokenInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,4 +45,19 @@ public function getAccessToken($oauth_token);
* @ingroup oauth2_section_4
*/
public function setAccessToken($oauth_token, $client_id, $user_id, $expires, $scope = null);

/**
* Expire an access token.
*
* This is not explicitly required in the spec, but if defined in a draft RFC for token
* revoking (RFC 7009) https://tools.ietf.org/html/rfc7009
*
* @param $access_token
* Access token to be expired.
*
* @ingroup oauth2_section_6
*
* @todo v2.0 include this method in interface. Omitted to maintain BC in v1.x
*/
//public function unsetAccessToken($access_token);
}
10 changes: 8 additions & 2 deletions src/OAuth2/Storage/Cassandra.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ public function getUser($username)
public function setUser($username, $password, $first_name = null, $last_name = null)
{
$password = sha1($password);

return $this->setValue(
$this->config['user_key'] . $username,
compact('username', 'password', 'first_name', 'last_name')
Expand Down Expand Up @@ -258,7 +259,6 @@ public function checkRestrictedGrantType($client_id, $grant_type)
return true;
}


/* RefreshTokenInterface */
public function getRefreshToken($refresh_token)
{
Expand Down Expand Up @@ -294,6 +294,11 @@ public function setAccessToken($access_token, $client_id, $user_id, $expires, $s
);
}

public function unsetAccessToken($access_token)
{
return $this->expireValue($this->config['access_token_key'] . $access_token);
}

/* ScopeInterface */
public function scopeExists($scope)
{
Expand Down Expand Up @@ -378,7 +383,7 @@ public function setJti($client_id, $subject, $audience, $expiration, $jti)
throw new \Exception('setJti() for the Cassandra driver is currently unimplemented.');
}

/* PublicKeyInterface */
/* PublicKeyInterface */
public function getPublicKey($client_id = '')
{
$public_key = $this->getValue($this->config['public_key_key'] . $client_id);
Expand Down Expand Up @@ -413,6 +418,7 @@ public function getEncryptionAlgorithm($client_id = null)
if (is_array($public_key)) {
return $public_key['encryption_algorithm'];
}

return 'RS256';
}

Expand Down
Loading

0 comments on commit 058c98f

Please sign in to comment.