Skip to content

Commit 7b0d7d4

Browse files
authored
Merge pull request #63 from designmynight/devin/1765458670-fix-jwt-claims-deprecation
Fix JWT claims replication deprecation warning
2 parents ee1a568 + 698b118 commit 7b0d7d4

4 files changed

Lines changed: 291 additions & 2 deletions

File tree

src/MongodbPassportServiceProvider.php

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22

33
namespace DesignMyNight\Mongodb;
44

5-
use Illuminate\Support\ServiceProvider;
65
use DesignMyNight\Mongodb\Passport\AuthCode;
6+
use DesignMyNight\Mongodb\Passport\Bridge\AccessTokenRepository;
77
use DesignMyNight\Mongodb\Passport\Bridge\RefreshTokenRepository;
88
use DesignMyNight\Mongodb\Passport\Client;
99
use DesignMyNight\Mongodb\Passport\PersonalAccessClient;
10-
use DesignMyNight\Mongodb\Passport\RefreshToken;
1110
use DesignMyNight\Mongodb\Passport\Token;
11+
use Illuminate\Support\ServiceProvider;
12+
use Laravel\Passport\Bridge\AccessTokenRepository as PassportAccessTokenRepository;
1213
use Laravel\Passport\Bridge\RefreshTokenRepository as PassportRefreshTokenRepository;
1314
use Laravel\Passport\Passport;
1415

@@ -27,5 +28,9 @@ public function register()
2728
$this->app->bind(PassportRefreshTokenRepository::class, function () {
2829
return $this->app->make(RefreshTokenRepository::class);
2930
});
31+
32+
$this->app->bind(PassportAccessTokenRepository::class, function () {
33+
return $this->app->make(AccessTokenRepository::class);
34+
});
3035
}
3136
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
<?php
2+
3+
namespace DesignMyNight\Mongodb\Passport\Bridge;
4+
5+
use DateTimeImmutable;
6+
use Lcobucci\JWT\Builder;
7+
use Lcobucci\JWT\Signer\Key;
8+
use Lcobucci\JWT\Signer\Rsa\Sha256;
9+
use Lcobucci\JWT\Token;
10+
use League\OAuth2\Server\CryptKey;
11+
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
12+
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
13+
use League\OAuth2\Server\Entities\Traits\EntityTrait;
14+
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
15+
16+
class AccessToken implements AccessTokenEntityInterface
17+
{
18+
use AccessTokenTrait, EntityTrait, TokenEntityTrait;
19+
20+
/**
21+
* Create a new token instance.
22+
*
23+
* @param string|int $userIdentifier
24+
* @param array $scopes
25+
*/
26+
public function __construct($userIdentifier, array $scopes = [])
27+
{
28+
$this->setUserIdentifier($userIdentifier);
29+
30+
foreach ($scopes as $scope) {
31+
$this->addScope($scope);
32+
}
33+
}
34+
35+
/**
36+
* Generate a JWT from the access token.
37+
*
38+
* This method overrides the default implementation from AccessTokenTrait
39+
* to avoid the deprecated replicateAsHeader functionality in lcobucci/jwt.
40+
*
41+
* @param CryptKey $privateKey
42+
* @return Token
43+
*/
44+
public function convertToJWT(CryptKey $privateKey)
45+
{
46+
$now = new DateTimeImmutable();
47+
$expiresAt = new DateTimeImmutable('@' . $this->getExpiryDateTime()->getTimestamp());
48+
49+
$builder = (new Builder())
50+
->setAudience($this->getClient()->getIdentifier())
51+
->setId($this->getIdentifier())
52+
->withHeader('jti', $this->getIdentifier())
53+
->issuedAt($now)
54+
->canOnlyBeUsedAfter($now)
55+
->expiresAt($expiresAt)
56+
->relatedTo($this->getUserIdentifier())
57+
->withClaim('scopes', $this->getScopes());
58+
59+
return $builder->getToken(
60+
new Sha256(),
61+
new Key($privateKey->getKeyPath(), $privateKey->getPassPhrase())
62+
);
63+
}
64+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?php
2+
3+
namespace DesignMyNight\Mongodb\Passport\Bridge;
4+
5+
use Laravel\Passport\Bridge\AccessTokenRepository as PassportAccessTokenRepository;
6+
use League\OAuth2\Server\Entities\ClientEntityInterface;
7+
8+
class AccessTokenRepository extends PassportAccessTokenRepository
9+
{
10+
/**
11+
* {@inheritdoc}
12+
*/
13+
public function getNewToken(ClientEntityInterface $clientEntity, array $scopes, $userIdentifier = null)
14+
{
15+
return new AccessToken($userIdentifier, $scopes);
16+
}
17+
}
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
<?php
2+
3+
namespace Tests\Passport\Bridge;
4+
5+
use DateTime;
6+
use Lcobucci\JWT\Parser;
7+
use League\OAuth2\Server\CryptKey;
8+
use League\OAuth2\Server\Entities\ClientEntityInterface;
9+
use League\OAuth2\Server\Entities\ScopeEntityInterface;
10+
use PHPUnit\Framework\TestCase;
11+
12+
class AccessTokenTest extends TestCase
13+
{
14+
/** @var string */
15+
private $privateKeyPath;
16+
17+
/** @var string */
18+
private $publicKeyPath;
19+
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->privateKeyPath = $this->createTemporaryKeyPair();
25+
}
26+
27+
protected function tearDown(): void
28+
{
29+
parent::tearDown();
30+
31+
if (file_exists($this->privateKeyPath)) {
32+
unlink($this->privateKeyPath);
33+
}
34+
35+
if (file_exists($this->publicKeyPath)) {
36+
unlink($this->publicKeyPath);
37+
}
38+
}
39+
40+
private function createTemporaryKeyPair(): string
41+
{
42+
$privateKeyPath = sys_get_temp_dir() . '/oauth-private-' . uniqid() . '.key';
43+
$publicKeyPath = sys_get_temp_dir() . '/oauth-public-' . uniqid() . '.key';
44+
45+
$config = [
46+
'private_key_bits' => 2048,
47+
'private_key_type' => OPENSSL_KEYTYPE_RSA,
48+
];
49+
50+
$privateKey = openssl_pkey_new($config);
51+
openssl_pkey_export($privateKey, $privateKeyPem);
52+
file_put_contents($privateKeyPath, $privateKeyPem);
53+
54+
$publicKeyDetails = openssl_pkey_get_details($privateKey);
55+
file_put_contents($publicKeyPath, $publicKeyDetails['key']);
56+
57+
$this->publicKeyPath = $publicKeyPath;
58+
59+
return $privateKeyPath;
60+
}
61+
62+
private function createMockClient(string $identifier = 'test-client'): ClientEntityInterface
63+
{
64+
$client = $this->createMock(ClientEntityInterface::class);
65+
$client->method('getIdentifier')->willReturn($identifier);
66+
67+
return $client;
68+
}
69+
70+
private function createMockScope(string $identifier): ScopeEntityInterface
71+
{
72+
$scope = $this->createMock(ScopeEntityInterface::class);
73+
$scope->method('getIdentifier')->willReturn($identifier);
74+
$scope->method('jsonSerialize')->willReturn($identifier);
75+
76+
return $scope;
77+
}
78+
79+
/**
80+
* @test
81+
*/
82+
public function it_converts_to_jwt_without_deprecation_warning(): void
83+
{
84+
$accessToken = new \DesignMyNight\Mongodb\Passport\Bridge\AccessToken('user-123', []);
85+
$accessToken->setIdentifier('token-id-123');
86+
$accessToken->setClient($this->createMockClient());
87+
$accessToken->setExpiryDateTime(new DateTime('+1 hour'));
88+
89+
$previousErrorHandler = set_error_handler(function ($errno, $errstr) {
90+
if ($errno === E_USER_DEPRECATED && strpos($errstr, 'Replicating claims as headers is deprecated') !== false) {
91+
$this->fail('Deprecation warning was triggered: ' . $errstr);
92+
}
93+
94+
return false;
95+
});
96+
97+
try {
98+
$cryptKey = new CryptKey($this->privateKeyPath, null, false);
99+
$jwt = $accessToken->convertToJWT($cryptKey);
100+
101+
$this->assertNotNull($jwt);
102+
$this->assertInstanceOf(\Lcobucci\JWT\Token::class, $jwt);
103+
} finally {
104+
restore_error_handler();
105+
}
106+
}
107+
108+
/**
109+
* @test
110+
*/
111+
public function it_includes_jti_claim_in_token(): void
112+
{
113+
$tokenId = 'token-id-456';
114+
$accessToken = new \DesignMyNight\Mongodb\Passport\Bridge\AccessToken('user-123', []);
115+
$accessToken->setIdentifier($tokenId);
116+
$accessToken->setClient($this->createMockClient());
117+
$accessToken->setExpiryDateTime(new DateTime('+1 hour'));
118+
119+
$cryptKey = new CryptKey($this->privateKeyPath, null, false);
120+
$jwt = $accessToken->convertToJWT($cryptKey);
121+
122+
$parser = new Parser();
123+
$parsedToken = $parser->parse((string) $jwt);
124+
125+
$this->assertEquals($tokenId, $parsedToken->getClaim('jti'));
126+
}
127+
128+
/**
129+
* @test
130+
*/
131+
public function it_includes_jti_in_header_for_backwards_compatibility(): void
132+
{
133+
$tokenId = 'token-id-789';
134+
$accessToken = new \DesignMyNight\Mongodb\Passport\Bridge\AccessToken('user-123', []);
135+
$accessToken->setIdentifier($tokenId);
136+
$accessToken->setClient($this->createMockClient());
137+
$accessToken->setExpiryDateTime(new DateTime('+1 hour'));
138+
139+
$cryptKey = new CryptKey($this->privateKeyPath, null, false);
140+
$jwt = $accessToken->convertToJWT($cryptKey);
141+
142+
$parser = new Parser();
143+
$parsedToken = $parser->parse((string) $jwt);
144+
145+
$this->assertEquals($tokenId, $parsedToken->getHeader('jti'));
146+
}
147+
148+
/**
149+
* @test
150+
*/
151+
public function it_includes_all_standard_claims(): void
152+
{
153+
$userId = 'user-123';
154+
$clientId = 'client-456';
155+
$tokenId = 'token-789';
156+
157+
$accessToken = new \DesignMyNight\Mongodb\Passport\Bridge\AccessToken($userId, [
158+
$this->createMockScope('read'),
159+
$this->createMockScope('write'),
160+
]);
161+
$accessToken->setIdentifier($tokenId);
162+
$accessToken->setClient($this->createMockClient($clientId));
163+
$accessToken->setExpiryDateTime(new DateTime('+1 hour'));
164+
165+
$cryptKey = new CryptKey($this->privateKeyPath, null, false);
166+
$jwt = $accessToken->convertToJWT($cryptKey);
167+
168+
$parser = new Parser();
169+
$parsedToken = $parser->parse((string) $jwt);
170+
171+
$this->assertEquals($clientId, $parsedToken->getClaim('aud'));
172+
$this->assertEquals($tokenId, $parsedToken->getClaim('jti'));
173+
$this->assertEquals($userId, $parsedToken->getClaim('sub'));
174+
$this->assertNotNull($parsedToken->getClaim('iat'));
175+
$this->assertNotNull($parsedToken->getClaim('nbf'));
176+
$this->assertNotNull($parsedToken->getClaim('exp'));
177+
178+
$scopes = $parsedToken->getClaim('scopes');
179+
$this->assertCount(2, $scopes);
180+
}
181+
182+
/**
183+
* @test
184+
*/
185+
public function it_can_be_parsed_after_creation(): void
186+
{
187+
$accessToken = new \DesignMyNight\Mongodb\Passport\Bridge\AccessToken('user-123', []);
188+
$accessToken->setIdentifier('token-id-123');
189+
$accessToken->setClient($this->createMockClient());
190+
$accessToken->setExpiryDateTime(new DateTime('+1 hour'));
191+
192+
$cryptKey = new CryptKey($this->privateKeyPath, null, false);
193+
$jwt = $accessToken->convertToJWT($cryptKey);
194+
195+
$tokenString = (string) $jwt;
196+
197+
$parser = new Parser();
198+
$parsedToken = $parser->parse($tokenString);
199+
200+
$this->assertNotNull($parsedToken);
201+
$this->assertEquals('token-id-123', $parsedToken->getClaim('jti'));
202+
}
203+
}

0 commit comments

Comments
 (0)