Skip to content

Commit

Permalink
[10.x] Add custom encryption key for JWT tokens (#1501)
Browse files Browse the repository at this point in the history
* Added Passport custom encryption key

* Refactored encryptUsing method

* StyleCI fixes

* formatting

Co-authored-by: Taylor Otwell <[email protected]>
  • Loading branch information
thijskok and taylorotwell authored Oct 29, 2021
1 parent fda2c2e commit 48b9534
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 2 deletions.
2 changes: 1 addition & 1 deletion src/ApiTokenCookieFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,6 @@ protected function createToken($userId, $csrfToken, Carbon $expiration)
'sub' => $userId,
'csrf' => $csrfToken,
'expiry' => $expiration->getTimestamp(),
], $this->encrypter->getKey());
], Passport::tokenEncryptionKey($this->encrypter));
}
}
2 changes: 1 addition & 1 deletion src/Guards/TokenGuard.php
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,7 @@ protected function decodeJwtTokenCookie($request)
{
return (array) JWT::decode(
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(Passport::cookie()), Passport::$unserializesCookies)),
$this->encrypter->getKey(),
Passport::tokenEncryptionKey($this->encrypter),
['HS256']
);
}
Expand Down
36 changes: 36 additions & 0 deletions src/Passport.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use Carbon\Carbon;
use DateInterval;
use DateTimeInterface;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Support\Facades\Route;
use League\OAuth2\Server\ResourceServer;
use Mockery;
Expand Down Expand Up @@ -161,10 +162,19 @@ class Passport
public static $unserializesCookies = false;

/**
* Indicates if client secrets will be hashed.
*
* @var bool
*/
public static $hashesClientSecrets = false;

/**
* The callback that should be used to generate JWT encryption keys.
*
* @var callable
*/
public static $tokenEncryptionKeyCallback;

/**
* Indicates the scope should inherit its parent scope.
*
Expand Down Expand Up @@ -640,6 +650,32 @@ public static function hashClientSecrets()
return new static;
}

/**
* Specify the callback that should be invoked to generate encryption keys for encrypting JWT tokens.
*
* @param callable $callback
* @return static
*/
public static function encryptTokensUsing($callback)
{
static::$tokenEncryptionKeyCallback = $callback;

return new static;
}

/**
* Generate an encryption key for encrypting JWT tokens.
*
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
* @return string
*/
public static function tokenEncryptionKey(Encrypter $encrypter)
{
return is_callable(static::$tokenEncryptionKeyCallback) ?
(static::$tokenEncryptionKeyCallback)($encrypter) :
$encrypter->getKey();
}

/**
* Configure Passport to not register its migrations.
*
Expand Down
27 changes: 27 additions & 0 deletions tests/Unit/ApiTokenCookieFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@
namespace Laravel\Passport\Tests\Unit;

use Illuminate\Contracts\Config\Repository;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
use Illuminate\Encryption\Encrypter;
use Laravel\Passport\ApiTokenCookieFactory;
use Laravel\Passport\Passport;
use Mockery as m;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpFoundation\Cookie;
Expand Down Expand Up @@ -33,4 +35,29 @@ public function test_cookie_can_be_successfully_created()

$this->assertInstanceOf(Cookie::class, $cookie);
}

public function test_cookie_can_be_successfully_created_when_using_a_custom_encryption_key()
{
Passport::encryptTokensUsing(function (EncrypterContract $encrypter) {
return $encrypter->getKey().'.mykey';
});

$config = m::mock(Repository::class);
$config->shouldReceive('get')->with('session')->andReturn([
'lifetime' => 120,
'path' => '/',
'domain' => null,
'secure' => true,
'same_site' => 'lax',
]);
$encrypter = new Encrypter(str_repeat('a', 16));
$factory = new ApiTokenCookieFactory($config, $encrypter);

$cookie = $factory->make(1, 'token');

$this->assertInstanceOf(Cookie::class, $cookie);

// Revert to the default encryption method
Passport::encryptTokensUsing(null);
}
}
41 changes: 41 additions & 0 deletions tests/Unit/TokenGuardTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Firebase\JWT\JWT;
use Illuminate\Container\Container;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
use Illuminate\Cookie\CookieValuePrefix;
use Illuminate\Encryption\Encrypter;
use Illuminate\Http\Request;
Expand Down Expand Up @@ -229,6 +230,46 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header()
$this->assertNull($guard->user($request));
}

public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_when_using_a_custom_encryption_key()
{
Passport::encryptTokensUsing(function (EncrypterContract $encrypter) {
return $encrypter->getKey().'.mykey';
});

$resourceServer = m::mock(ResourceServer::class);
$userProvider = m::mock(PassportUserProvider::class);
$tokens = m::mock(TokenRepository::class);
$clients = m::mock(ClientRepository::class);
$encrypter = new Encrypter(str_repeat('a', 16));

$clients->shouldReceive('findActive')
->with(1)
->andReturn(new TokenGuardTestClient);

$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);

$request = Request::create('/');
$request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt(CookieValuePrefix::create('X-XSRF-TOKEN', $encrypter->getKey()).'token', false));
$request->cookies->set('laravel_token',
$encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([
'sub' => 1,
'aud' => 1,
'csrf' => 'token',
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
], Passport::tokenEncryptionKey($encrypter)), false)
);

$userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser);
$userProvider->shouldReceive('getProviderName')->andReturn(null);

$user = $guard->user($request);

$this->assertEquals($expectedUser, $user);

// Revert to the default encryption method
Passport::encryptTokensUsing(null);
}

public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted()
{
$resourceServer = m::mock(ResourceServer::class);
Expand Down

0 comments on commit 48b9534

Please sign in to comment.