22
33namespace BookStack \Access \Oidc ;
44
5- class OidcIdToken
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 ::validateCommonTokenDetails ($ clientId );
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- * @return mixed|null
76- */
77- public function getClaim (string $ claim )
78- {
79- return $ this ->payload [$ claim ] ?? null ;
80- }
81-
82- /**
83- * Get all returned claims within the token.
84- */
85- public function getAllClaims (): array
86- {
87- return $ this ->payload ;
88- }
89-
90- /**
91- * Replace the existing claim data of this token with that provided.
92- */
93- public function replaceClaims (array $ claims ): void
94- {
95- $ this ->payload = $ claims ;
96- }
97-
98- /**
99- * Validate the structure of the given token and ensure we have the required pieces.
100- * As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
101- *
102- * @throws OidcInvalidTokenException
103- */
104- protected function validateTokenStructure (): void
105- {
106- foreach (['header ' , 'payload ' ] as $ prop ) {
107- if (empty ($ this ->$ prop ) || !is_array ($ this ->$ prop )) {
108- throw new OidcInvalidTokenException ("Could not parse out a valid {$ prop } within the provided token " );
109- }
110- }
111-
112- if (empty ($ this ->signature ) || !is_string ($ this ->signature )) {
113- throw new OidcInvalidTokenException ('Could not parse out a valid signature within the provided token ' );
114- }
115- }
116-
117- /**
118- * Validate the signature of the given token and ensure it validates against the provided key.
119- *
120- * @throws OidcInvalidTokenException
121- */
122- protected function validateTokenSignature (): void
123- {
124- if ($ this ->header ['alg ' ] !== 'RS256 ' ) {
125- throw new OidcInvalidTokenException ("Only RS256 signature validation is supported. Token reports using {$ this ->header ['alg ' ]}" );
126- }
127-
128- $ parsedKeys = array_map (function ($ key ) {
129- try {
130- return new OidcJwtSigningKey ($ key );
131- } catch (OidcInvalidKeyException $ e ) {
132- throw new OidcInvalidTokenException ('Failed to read signing key with error: ' . $ e ->getMessage ());
133- }
134- }, $ this ->keys );
135-
136- $ parsedKeys = array_filter ($ parsedKeys );
137-
138- $ contentToSign = $ this ->tokenParts [0 ] . '. ' . $ this ->tokenParts [1 ];
139- /** @var OidcJwtSigningKey $parsedKey */
140- foreach ($ parsedKeys as $ parsedKey ) {
141- if ($ parsedKey ->verify ($ contentToSign , $ this ->signature )) {
142- return ;
143- }
144- }
145-
146- throw new OidcInvalidTokenException ('Token signature could not be validated using the provided keys ' );
147- }
148-
14920 /**
15021 * Validate the claims of the token.
15122 * As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.
@@ -156,27 +27,18 @@ protected function validateTokenClaims(string $clientId): void
15627 {
15728 // 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
15829 // MUST exactly match the value of the iss (issuer) Claim.
159- if (empty ($ this ->payload ['iss ' ]) || $ this ->issuer !== $ this ->payload ['iss ' ]) {
160- throw new OidcInvalidTokenException ('Missing or non-matching token issuer value ' );
161- }
30+ // Already done in parent.
16231
16332 // 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
16433 // at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
16534 // if the ID Token does not list the Client as a valid audience, or if it contains additional
16635 // audiences not trusted by the Client.
167- if (empty ($ this ->payload ['aud ' ])) {
168- throw new OidcInvalidTokenException ('Missing token audience value ' );
169- }
170-
36+ // Partially done in parent.
17137 $ aud = is_string ($ this ->payload ['aud ' ]) ? [$ this ->payload ['aud ' ]] : $ this ->payload ['aud ' ];
17238 if (count ($ aud ) !== 1 ) {
17339 throw new OidcInvalidTokenException ('Token audience value has ' . count ($ aud ) . ' values, Expected 1 ' );
17440 }
17541
176- if ($ aud [0 ] !== $ clientId ) {
177- throw new OidcInvalidTokenException ('Token audience value did not match the expected client_id ' );
178- }
179-
18042 // 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
18143 // NOTE: Addressed by enforcing a count of 1 above.
18244
0 commit comments