Skip to content

Commit 9a88fc0

Browse files
committed
feat: Add 'kid' determination methods to AccessToken class
1 parent 36af05a commit 9a88fc0

File tree

1 file changed

+85
-0
lines changed

1 file changed

+85
-0
lines changed

src/Bridge/AccessToken.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,15 @@
22

33
namespace Laravel\Passport\Bridge;
44

5+
use DateTimeImmutable;
6+
use Laravel\Passport\Passport;
7+
use Lcobucci\JWT\Token;
58
use League\OAuth2\Server\Entities\AccessTokenEntityInterface;
69
use League\OAuth2\Server\Entities\ClientEntityInterface;
710
use League\OAuth2\Server\Entities\Traits\AccessTokenTrait;
811
use League\OAuth2\Server\Entities\Traits\EntityTrait;
912
use League\OAuth2\Server\Entities\Traits\TokenEntityTrait;
13+
use RuntimeException;
1014

1115
class AccessToken implements AccessTokenEntityInterface
1216
{
@@ -30,4 +34,85 @@ public function __construct(?string $userIdentifier, array $scopes, ClientEntity
3034

3135
$this->setClient($client);
3236
}
37+
38+
/**
39+
* {@inheritdoc}
40+
*/
41+
private function convertToJWT(): Token
42+
{
43+
$this->initJwtConfiguration();
44+
45+
return $this->jwtConfiguration->builder()
46+
->withHeader('kid', $this->determineKid())
47+
->permittedFor($this->getClient()->getIdentifier())
48+
->identifiedBy($this->getIdentifier())
49+
->issuedAt(new DateTimeImmutable())
50+
->canOnlyBeUsedAfter(new DateTimeImmutable())
51+
->expiresAt($this->getExpiryDateTime())
52+
->relatedTo($this->getSubjectIdentifier())
53+
->withClaim('scopes', $this->getScopes())
54+
->getToken($this->jwtConfiguration->signer(), $this->jwtConfiguration->signingKey());
55+
}
56+
57+
/**
58+
* {@inheritdoc}
59+
*/
60+
public function toString(): string
61+
{
62+
return $this->convertToJWT()->toString();
63+
}
64+
65+
/**
66+
* Determine the 'kid' (key ID) for the JWT header, based on the public key's JWK thumbprint.
67+
* See RFC 7638: https://tools.ietf.org/html/rfc7638.
68+
*/
69+
protected function determineKid(): string
70+
{
71+
$pem = $this->getPublicKeyMaterial();
72+
73+
$res = openssl_pkey_get_public($pem);
74+
if ($res === false) {
75+
throw new RuntimeException('Invalid public key material');
76+
}
77+
78+
$details = openssl_pkey_get_details($res);
79+
if ($details === false || $details['type'] !== OPENSSL_KEYTYPE_RSA) {
80+
throw new RuntimeException('Only RSA public keys are supported');
81+
}
82+
83+
// Base64url helpers
84+
$b64url = fn (string $bin) => rtrim(strtr(base64_encode($bin), '+/', '-_'), '=');
85+
86+
// Build minimal JWK (kty, n, e)
87+
$jwk = [
88+
'e' => $b64url($details['rsa']['e']),
89+
'kty' => 'RSA',
90+
'n' => $b64url($details['rsa']['n']),
91+
];
92+
93+
// Canonical JSON: keys sorted, no spaces or escapes that change semantics
94+
ksort($jwk);
95+
$json = json_encode($jwk, JSON_UNESCAPED_SLASHES);
96+
97+
// RFC 7638 thumbprint = SHA-256 over canonical JSON, base64url-encoded
98+
$thumb = hash('sha256', $json, true);
99+
100+
return $b64url($thumb);
101+
}
102+
103+
/**
104+
* Find the public key material from config or file.
105+
*/
106+
private function getPublicKeyMaterial(): string
107+
{
108+
if ($keyMaterial = config('passport.public_key')) {
109+
return $keyMaterial;
110+
}
111+
112+
if (file_exists($publicKey = Passport::keyPath('oauth-public.key'))) {
113+
return file_get_contents($publicKey);
114+
}
115+
116+
throw new RuntimeException('No public key available');
117+
}
33118
}

0 commit comments

Comments
 (0)