Skip to content

Commit 48b9534

Browse files
[10.x] Add custom encryption key for JWT tokens (#1501)
* Added Passport custom encryption key * Refactored encryptUsing method * StyleCI fixes * formatting Co-authored-by: Taylor Otwell <[email protected]>
1 parent fda2c2e commit 48b9534

File tree

5 files changed

+106
-2
lines changed

5 files changed

+106
-2
lines changed

src/ApiTokenCookieFactory.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,6 @@ protected function createToken($userId, $csrfToken, Carbon $expiration)
7777
'sub' => $userId,
7878
'csrf' => $csrfToken,
7979
'expiry' => $expiration->getTimestamp(),
80-
], $this->encrypter->getKey());
80+
], Passport::tokenEncryptionKey($this->encrypter));
8181
}
8282
}

src/Guards/TokenGuard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ protected function decodeJwtTokenCookie($request)
269269
{
270270
return (array) JWT::decode(
271271
CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(Passport::cookie()), Passport::$unserializesCookies)),
272-
$this->encrypter->getKey(),
272+
Passport::tokenEncryptionKey($this->encrypter),
273273
['HS256']
274274
);
275275
}

src/Passport.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use Carbon\Carbon;
66
use DateInterval;
77
use DateTimeInterface;
8+
use Illuminate\Contracts\Encryption\Encrypter;
89
use Illuminate\Support\Facades\Route;
910
use League\OAuth2\Server\ResourceServer;
1011
use Mockery;
@@ -161,10 +162,19 @@ class Passport
161162
public static $unserializesCookies = false;
162163

163164
/**
165+
* Indicates if client secrets will be hashed.
166+
*
164167
* @var bool
165168
*/
166169
public static $hashesClientSecrets = false;
167170

171+
/**
172+
* The callback that should be used to generate JWT encryption keys.
173+
*
174+
* @var callable
175+
*/
176+
public static $tokenEncryptionKeyCallback;
177+
168178
/**
169179
* Indicates the scope should inherit its parent scope.
170180
*
@@ -640,6 +650,32 @@ public static function hashClientSecrets()
640650
return new static;
641651
}
642652

653+
/**
654+
* Specify the callback that should be invoked to generate encryption keys for encrypting JWT tokens.
655+
*
656+
* @param callable $callback
657+
* @return static
658+
*/
659+
public static function encryptTokensUsing($callback)
660+
{
661+
static::$tokenEncryptionKeyCallback = $callback;
662+
663+
return new static;
664+
}
665+
666+
/**
667+
* Generate an encryption key for encrypting JWT tokens.
668+
*
669+
* @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
670+
* @return string
671+
*/
672+
public static function tokenEncryptionKey(Encrypter $encrypter)
673+
{
674+
return is_callable(static::$tokenEncryptionKeyCallback) ?
675+
(static::$tokenEncryptionKeyCallback)($encrypter) :
676+
$encrypter->getKey();
677+
}
678+
643679
/**
644680
* Configure Passport to not register its migrations.
645681
*

tests/Unit/ApiTokenCookieFactoryTest.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
namespace Laravel\Passport\Tests\Unit;
44

55
use Illuminate\Contracts\Config\Repository;
6+
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
67
use Illuminate\Encryption\Encrypter;
78
use Laravel\Passport\ApiTokenCookieFactory;
9+
use Laravel\Passport\Passport;
810
use Mockery as m;
911
use PHPUnit\Framework\TestCase;
1012
use Symfony\Component\HttpFoundation\Cookie;
@@ -33,4 +35,29 @@ public function test_cookie_can_be_successfully_created()
3335

3436
$this->assertInstanceOf(Cookie::class, $cookie);
3537
}
38+
39+
public function test_cookie_can_be_successfully_created_when_using_a_custom_encryption_key()
40+
{
41+
Passport::encryptTokensUsing(function (EncrypterContract $encrypter) {
42+
return $encrypter->getKey().'.mykey';
43+
});
44+
45+
$config = m::mock(Repository::class);
46+
$config->shouldReceive('get')->with('session')->andReturn([
47+
'lifetime' => 120,
48+
'path' => '/',
49+
'domain' => null,
50+
'secure' => true,
51+
'same_site' => 'lax',
52+
]);
53+
$encrypter = new Encrypter(str_repeat('a', 16));
54+
$factory = new ApiTokenCookieFactory($config, $encrypter);
55+
56+
$cookie = $factory->make(1, 'token');
57+
58+
$this->assertInstanceOf(Cookie::class, $cookie);
59+
60+
// Revert to the default encryption method
61+
Passport::encryptTokensUsing(null);
62+
}
3663
}

tests/Unit/TokenGuardTest.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Firebase\JWT\JWT;
77
use Illuminate\Container\Container;
88
use Illuminate\Contracts\Debug\ExceptionHandler;
9+
use Illuminate\Contracts\Encryption\Encrypter as EncrypterContract;
910
use Illuminate\Cookie\CookieValuePrefix;
1011
use Illuminate\Encryption\Encrypter;
1112
use Illuminate\Http\Request;
@@ -229,6 +230,46 @@ public function test_cookie_xsrf_is_verified_against_xsrf_token_header()
229230
$this->assertNull($guard->user($request));
230231
}
231232

233+
public function test_users_may_be_retrieved_from_cookies_with_xsrf_token_header_when_using_a_custom_encryption_key()
234+
{
235+
Passport::encryptTokensUsing(function (EncrypterContract $encrypter) {
236+
return $encrypter->getKey().'.mykey';
237+
});
238+
239+
$resourceServer = m::mock(ResourceServer::class);
240+
$userProvider = m::mock(PassportUserProvider::class);
241+
$tokens = m::mock(TokenRepository::class);
242+
$clients = m::mock(ClientRepository::class);
243+
$encrypter = new Encrypter(str_repeat('a', 16));
244+
245+
$clients->shouldReceive('findActive')
246+
->with(1)
247+
->andReturn(new TokenGuardTestClient);
248+
249+
$guard = new TokenGuard($resourceServer, $userProvider, $tokens, $clients, $encrypter);
250+
251+
$request = Request::create('/');
252+
$request->headers->set('X-XSRF-TOKEN', $encrypter->encrypt(CookieValuePrefix::create('X-XSRF-TOKEN', $encrypter->getKey()).'token', false));
253+
$request->cookies->set('laravel_token',
254+
$encrypter->encrypt(CookieValuePrefix::create('laravel_token', $encrypter->getKey()).JWT::encode([
255+
'sub' => 1,
256+
'aud' => 1,
257+
'csrf' => 'token',
258+
'expiry' => Carbon::now()->addMinutes(10)->getTimestamp(),
259+
], Passport::tokenEncryptionKey($encrypter)), false)
260+
);
261+
262+
$userProvider->shouldReceive('retrieveById')->with(1)->andReturn($expectedUser = new TokenGuardTestUser);
263+
$userProvider->shouldReceive('getProviderName')->andReturn(null);
264+
265+
$user = $guard->user($request);
266+
267+
$this->assertEquals($expectedUser, $user);
268+
269+
// Revert to the default encryption method
270+
Passport::encryptTokensUsing(null);
271+
}
272+
232273
public function test_xsrf_token_cookie_without_a_token_header_is_not_accepted()
233274
{
234275
$resourceServer = m::mock(ResourceServer::class);

0 commit comments

Comments
 (0)