22
33namespace Laravel \Passport \Bridge ;
44
5+ use DateTimeImmutable ;
6+ use Laravel \Passport \Passport ;
7+ use Lcobucci \JWT \Token ;
58use League \OAuth2 \Server \Entities \AccessTokenEntityInterface ;
69use League \OAuth2 \Server \Entities \ClientEntityInterface ;
710use League \OAuth2 \Server \Entities \Traits \AccessTokenTrait ;
811use League \OAuth2 \Server \Entities \Traits \EntityTrait ;
912use League \OAuth2 \Server \Entities \Traits \TokenEntityTrait ;
13+ use RuntimeException ;
1014
1115class 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