Skip to content

Commit 900b981

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

File tree

1 file changed

+84
-0
lines changed

1 file changed

+84
-0
lines changed

src/Bridge/AccessToken.php

Lines changed: 84 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,84 @@ 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+
return $b64url($thumb);
100+
}
101+
102+
/**
103+
* Find the public key material from config or file.
104+
*/
105+
private function getPublicKeyMaterial(): string
106+
{
107+
if ($keyMaterial = config('passport.public_key')) {
108+
return $keyMaterial;
109+
}
110+
111+
if (file_exists($publicKey = Passport::keyPath('oauth-public.key'))) {
112+
return file_get_contents($publicKey);
113+
}
114+
115+
throw new RuntimeException('No public key available');
116+
}
33117
}

0 commit comments

Comments
 (0)