diff --git a/src/ApiTokenCookieFactory.php b/src/ApiTokenCookieFactory.php index 23a6cd989..3d7d83d02 100644 --- a/src/ApiTokenCookieFactory.php +++ b/src/ApiTokenCookieFactory.php @@ -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)); } } diff --git a/src/Guards/TokenGuard.php b/src/Guards/TokenGuard.php index 7d81ed068..00cbe4863 100644 --- a/src/Guards/TokenGuard.php +++ b/src/Guards/TokenGuard.php @@ -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'] ); } diff --git a/src/Passport.php b/src/Passport.php index 675e43dff..adf4c53ca 100644 --- a/src/Passport.php +++ b/src/Passport.php @@ -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; @@ -134,10 +135,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. * @@ -616,6 +626,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. * diff --git a/tests/Unit/ApiTokenCookieFactoryTest.php b/tests/Unit/ApiTokenCookieFactoryTest.php index 5451bcb1d..1a82bbfbf 100644 --- a/tests/Unit/ApiTokenCookieFactoryTest.php +++ b/tests/Unit/ApiTokenCookieFactoryTest.php @@ -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; @@ -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); + } } diff --git a/tests/Unit/TokenGuardTest.php b/tests/Unit/TokenGuardTest.php index a7f4b8556..b586eff99 100644 --- a/tests/Unit/TokenGuardTest.php +++ b/tests/Unit/TokenGuardTest.php @@ -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; @@ -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);