22
33namespace BookStack \Access \Oidc ;
44
5- class OidcIdToken implements ProvidesClaims
5+ class OidcIdToken extends OidcJwtWithClaims implements ProvidesClaims
66{
7- protected array $ header ;
8- protected array $ payload ;
9- protected string $ signature ;
10- protected string $ issuer ;
11- protected array $ tokenParts = [];
12-
13- /**
14- * @var array[]|string[]
15- */
16- protected array $ keys ;
17-
18- public function __construct (string $ token , string $ issuer , array $ keys )
19- {
20- $ this ->keys = $ keys ;
21- $ this ->issuer = $ issuer ;
22- $ this ->parse ($ token );
23- }
24-
25- /**
26- * Parse the token content into its components.
27- */
28- protected function parse (string $ token ): void
29- {
30- $ this ->tokenParts = explode ('. ' , $ token );
31- $ this ->header = $ this ->parseEncodedTokenPart ($ this ->tokenParts [0 ]);
32- $ this ->payload = $ this ->parseEncodedTokenPart ($ this ->tokenParts [1 ] ?? '' );
33- $ this ->signature = $ this ->base64UrlDecode ($ this ->tokenParts [2 ] ?? '' ) ?: '' ;
34- }
35-
36- /**
37- * Parse a Base64-JSON encoded token part.
38- * Returns the data as a key-value array or empty array upon error.
39- */
40- protected function parseEncodedTokenPart (string $ part ): array
41- {
42- $ json = $ this ->base64UrlDecode ($ part ) ?: '{} ' ;
43- $ decoded = json_decode ($ json , true );
44-
45- return is_array ($ decoded ) ? $ decoded : [];
46- }
47-
48- /**
49- * Base64URL decode. Needs some character conversions to be compatible
50- * with PHP's default base64 handling.
51- */
52- protected function base64UrlDecode (string $ encoded ): string
53- {
54- return base64_decode (strtr ($ encoded , '-_ ' , '+/ ' ));
55- }
56-
577 /**
588 * Validate all possible parts of the id token.
599 *
6010 * @throws OidcInvalidTokenException
6111 */
6212 public function validate (string $ clientId ): bool
6313 {
64- $ this ->validateTokenStructure ();
65- $ this ->validateTokenSignature ();
14+ parent ::validateCommonClaims ();
6615 $ this ->validateTokenClaims ($ clientId );
6716
6817 return true ;
6918 }
7019
71- /**
72- * Fetch a specific claim from this token.
73- * Returns null if it is null or does not exist.
74- */
75- public function getClaim (string $ claim ): mixed
76- {
77- return $ this ->payload [$ claim ] ?? null ;
78- }
79-
80- /**
81- * Get all returned claims within the token.
82- */
83- public function getAllClaims (): array
84- {
85- return $ this ->payload ;
86- }
87-
88- /**
89- * Replace the existing claim data of this token with that provided.
90- */
91- public function replaceClaims (array $ claims ): void
92- {
93- $ this ->payload = $ claims ;
94- }
95-
96- /**
97- * Validate the structure of the given token and ensure we have the required pieces.
98- * As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
99- *
100- * @throws OidcInvalidTokenException
101- */
102- protected function validateTokenStructure (): void
103- {
104- foreach (['header ' , 'payload ' ] as $ prop ) {
105- if (empty ($ this ->$ prop ) || !is_array ($ this ->$ prop )) {
106- throw new OidcInvalidTokenException ("Could not parse out a valid {$ prop } within the provided token " );
107- }
108- }
109-
110- if (empty ($ this ->signature ) || !is_string ($ this ->signature )) {
111- throw new OidcInvalidTokenException ('Could not parse out a valid signature within the provided token ' );
112- }
113- }
114-
115- /**
116- * Validate the signature of the given token and ensure it validates against the provided key.
117- *
118- * @throws OidcInvalidTokenException
119- */
120- protected function validateTokenSignature (): void
121- {
122- if ($ this ->header ['alg ' ] !== 'RS256 ' ) {
123- throw new OidcInvalidTokenException ("Only RS256 signature validation is supported. Token reports using {$ this ->header ['alg ' ]}" );
124- }
125-
126- $ parsedKeys = array_map (function ($ key ) {
127- try {
128- return new OidcJwtSigningKey ($ key );
129- } catch (OidcInvalidKeyException $ e ) {
130- throw new OidcInvalidTokenException ('Failed to read signing key with error: ' . $ e ->getMessage ());
131- }
132- }, $ this ->keys );
133-
134- $ parsedKeys = array_filter ($ parsedKeys );
135-
136- $ contentToSign = $ this ->tokenParts [0 ] . '. ' . $ this ->tokenParts [1 ];
137- /** @var OidcJwtSigningKey $parsedKey */
138- foreach ($ parsedKeys as $ parsedKey ) {
139- if ($ parsedKey ->verify ($ contentToSign , $ this ->signature )) {
140- return ;
141- }
142- }
143-
144- throw new OidcInvalidTokenException ('Token signature could not be validated using the provided keys ' );
145- }
146-
14720 /**
14821 * Validate the claims of the token.
14922 * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.
@@ -154,27 +27,18 @@ protected function validateTokenClaims(string $clientId): void
15427 {
15528 // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
15629 // MUST exactly match the value of the iss (issuer) Claim.
157- if (empty ($ this ->payload ['iss ' ]) || $ this ->issuer !== $ this ->payload ['iss ' ]) {
158- throw new OidcInvalidTokenException ('Missing or non-matching token issuer value ' );
159- }
30+ // Already done in parent.
16031
16132 // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
16233 // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
16334 // if the ID Token does not list the Client as a valid audience, or if it contains additional
16435 // audiences not trusted by the Client.
165- if (empty ($ this ->payload ['aud ' ])) {
166- throw new OidcInvalidTokenException ('Missing token audience value ' );
167- }
168-
36+ // Partially done in parent.
16937 $ aud = is_string ($ this ->payload ['aud ' ]) ? [$ this ->payload ['aud ' ]] : $ this ->payload ['aud ' ];
17038 if (count ($ aud ) !== 1 ) {
17139 throw new OidcInvalidTokenException ('Token audience value has ' . count ($ aud ) . ' values, Expected 1 ' );
17240 }
17341
174- if ($ aud [0 ] !== $ clientId ) {
175- throw new OidcInvalidTokenException ('Token audience value did not match the expected client_id ' );
176- }
177-
17842 // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
17943 // NOTE: Addressed by enforcing a count of 1 above.
18044
0 commit comments