2
2
3
3
namespace BookStack \Access \Oidc ;
4
4
5
- class OidcIdToken
5
+ class OidcIdToken extends OidcJwtWithClaims implements ProvidesClaims
6
6
{
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
-
57
7
/**
58
8
* Validate all possible parts of the id token.
59
9
*
60
10
* @throws OidcInvalidTokenException
61
11
*/
62
12
public function validate (string $ clientId ): bool
63
13
{
64
- $ this ->validateTokenStructure ();
65
- $ this ->validateTokenSignature ();
14
+ parent ::validateCommonTokenDetails ($ clientId );
66
15
$ this ->validateTokenClaims ($ clientId );
67
16
68
17
return true ;
69
18
}
70
19
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
-
149
20
/**
150
21
* Validate the claims of the token.
151
22
* As per https://openid.net/specs/openid-connect-basic-1_0.html#IDTokenValidation.
@@ -156,27 +27,18 @@ protected function validateTokenClaims(string $clientId): void
156
27
{
157
28
// 1. The Issuer Identifier for the OpenID Provider (which is typically obtained during Discovery)
158
29
// 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.
162
31
163
32
// 2. The Client MUST validate that the aud (audience) Claim contains its client_id value registered
164
33
// at the Issuer identified by the iss (issuer) Claim as an audience. The ID Token MUST be rejected
165
34
// if the ID Token does not list the Client as a valid audience, or if it contains additional
166
35
// 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.
171
37
$ aud = is_string ($ this ->payload ['aud ' ]) ? [$ this ->payload ['aud ' ]] : $ this ->payload ['aud ' ];
172
38
if (count ($ aud ) !== 1 ) {
173
39
throw new OidcInvalidTokenException ('Token audience value has ' . count ($ aud ) . ' values, Expected 1 ' );
174
40
}
175
41
176
- if ($ aud [0 ] !== $ clientId ) {
177
- throw new OidcInvalidTokenException ('Token audience value did not match the expected client_id ' );
178
- }
179
-
180
42
// 3. If the ID Token contains multiple audiences, the Client SHOULD verify that an azp Claim is present.
181
43
// NOTE: Addressed by enforcing a count of 1 above.
182
44
0 commit comments