Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -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.
*
Expand Down Expand Up @@ -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.
*
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