Skip to content

Commit 9b2219a

Browse files
committed
Introduce ID Token abstraction
1 parent 8d3a8e6 commit 9b2219a

File tree

6 files changed

+537
-2
lines changed

6 files changed

+537
-2
lines changed

src/Codebooks/ClaimsEnum.php

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ enum ClaimsEnum: string
1818
// @type
1919
case AtType = '@type';
2020

21+
// Authentication Context Class Reference
22+
case Acr = 'acr';
23+
2124
case AcrValuesSupported = 'acr_values_supported';
2225

2326
// Algorithm
@@ -29,11 +32,19 @@ enum ClaimsEnum: string
2932
// AlternativeText
3033
case AltText = 'alt_text';
3134

35+
// Authentication Methods References
36+
case Amr = 'amr';
37+
3238
case ApplicationType = 'application_type';
3339

40+
// Access Token hash
41+
case ATHash = 'at_hash';
42+
3443
// Audience
3544
case Aud = 'aud';
3645

46+
case AuthTime = 'auth_time';
47+
3748
case AuthorityHints = 'authority_hints';
3849

3950
case AuthorizationEndpoint = 'authorization_endpoint';
@@ -42,6 +53,9 @@ enum ClaimsEnum: string
4253

4354
case AuthorizationServers = 'authorization_servers';
4455

56+
// Authorized party
57+
case Azp = 'azp';
58+
4559
case BackChannelLogoutSessionSupported = 'backchannel_logout_session_supported';
4660

4761
case BackChannelLogoutSupported = 'backchannel_logout_supported';
@@ -56,6 +70,9 @@ enum ClaimsEnum: string
5670

5771
case BatchSize = 'batch_size';
5872

73+
// Code hash
74+
case CHash = 'c_hash';
75+
5976
case Claims = 'claims';
6077

6178
case ClaimsSupported = 'claims_supported';
@@ -310,6 +327,9 @@ enum ClaimsEnum: string
310327
// Subject
311328
case Sub = 'sub';
312329

330+
// Subject JWK
331+
case SubJwk = 'sub_jwk';
332+
313333
case SubjectTypesSupported = 'subject_types_supported';
314334

315335
case Terms_Of_Use = 'termsOfUse';

src/Codebooks/UriPattern.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\Codebooks;
6+
7+
enum UriPattern: string
8+
{
9+
case HttpNoQueryNoFragment = '/^http(s?):\/\/[^\s\/$.?#][^\s?#]*$/i';
10+
11+
case Uri = '/^[a-zA-Z][a-zA-Z0-9+.-]*:\S*$/i';
12+
}

src/Core/IdToken.php

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\Core;
6+
7+
use SimpleSAML\OpenID\Codebooks\ClaimsEnum;
8+
use SimpleSAML\OpenID\Codebooks\UriPattern;
9+
use SimpleSAML\OpenID\Exceptions\IdTokenException;
10+
use SimpleSAML\OpenID\Jws\ParsedJws;
11+
12+
/**
13+
* ID Token abstraction from
14+
* https://openid.net/specs/openid-connect-core-1_0.html#IDToken
15+
*/
16+
class IdToken extends ParsedJws
17+
{
18+
public function getIssuer(): string
19+
{
20+
// REQUIRED. Issuer Identifier for the Issuer of the response. The iss
21+
// value is a case-sensitive URL using the https scheme that contains
22+
// scheme, host, and optionally, port number and path components and
23+
// no query or fragment components.
24+
$iss = parent::getIssuer() ?? throw new IdTokenException('No Issuer claim found.');
25+
26+
// We will leave the possibility of http usage for local testing purposes.
27+
return $this->helpers->type()->enforceUri($iss, 'Issuer claim', UriPattern::HttpNoQueryNoFragment->value);
28+
}
29+
30+
31+
public function getSubject(): string
32+
{
33+
// REQUIRED. Subject Identifier. A locally unique and never reassigned
34+
// identifier within the Issuer for the End-User, which is intended to
35+
// be consumed by the Client, e.g., 24400320 or
36+
// AItOawmwtWwcT0k51BayewNvutrJUqsvl6qs7A4. It MUST NOT exceed 255
37+
// ASCII [RFC20] characters in length. The sub value is a
38+
// case-sensitive string.
39+
$sub = parent::getSubject() ?? throw new IdTokenException('No Subject claim found.');
40+
41+
if (!mb_check_encoding($sub, 'ASCII')) {
42+
throw new IdTokenException('Subject claim contains non-ASCII characters.');
43+
}
44+
45+
if (strlen($sub) > 255) {
46+
throw new IdTokenException('Subject claim exceeds 255 ASCII characters limit.');
47+
}
48+
49+
return $sub;
50+
}
51+
52+
53+
public function getAudience(): array
54+
{
55+
// REQUIRED. Audience(s) that this ID Token is intended for. It MUST
56+
// contain the OAuth 2.0 client_id of the Relying Party as an audience
57+
// value. It MAY also contain identifiers for other audiences. In the
58+
// general case, the aud value is an array of case-sensitive strings.
59+
// In the common special case when there is one audience, the aud value
60+
// MAY be a single case-sensitive string.
61+
return parent::getAudience() ?? throw new IdTokenException('No Audience claim found.');
62+
}
63+
64+
65+
public function getExpirationTime(): int
66+
{
67+
return parent::getExpirationTime() ?? throw new IdTokenException('No Expiration Time claim found.');
68+
}
69+
70+
71+
public function getIssuedAt(): int
72+
{
73+
return parent::getIssuedAt() ?? throw new IdTokenException('No Issued At claim found.');
74+
}
75+
76+
77+
/**
78+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
79+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
80+
*/
81+
public function getAuthTime(): ?int
82+
{
83+
// Time when the End-User authentication occurred. Its value is a JSON
84+
// number representing the number of seconds from 1970-01-01T00:00:00Z
85+
// as measured in UTC until the date/time. When a max_age request is
86+
// made or when auth_time is requested as an Essential Claim, then
87+
// this Claim is REQUIRED; otherwise, its inclusion is OPTIONAL.
88+
// (The auth_time Claim semantically corresponds to the OpenID 2.0 PAPE
89+
// [OpenID.PAPE] auth_time response parameter.)
90+
91+
$authTime = $this->getPayloadClaim(ClaimsEnum::AuthTime->value);
92+
93+
if (is_null($authTime)) {
94+
return null;
95+
}
96+
97+
return $this->helpers->type()->ensureInt($authTime, ClaimsEnum::AuthTime->value);
98+
}
99+
100+
101+
/**
102+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
103+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
104+
*/
105+
public function getNonce(): ?string
106+
{
107+
// String value used to associate a Client session with an ID Token,
108+
// and to mitigate replay attacks. The value is passed through
109+
// unmodified from the Authentication Request to the ID Token. If
110+
// present in the ID Token, Clients MUST verify that the nonce Claim
111+
// Value is equal to the value of the nonce parameter sent in the
112+
// Authentication Request. If present in the Authentication Request,
113+
// Authorization Servers MUST include a nonce Claim in the ID Token
114+
// with the Claim Value being the nonce value sent in the Authentication
115+
// Request. Authorization Servers SHOULD perform no other processing
116+
// on nonce values used. The nonce value is a case-sensitive string.
117+
$nonce = $this->getPayloadClaim(ClaimsEnum::Nonce->value);
118+
119+
if (is_null($nonce)) {
120+
return null;
121+
}
122+
123+
return $this->helpers->type()->ensureNonEmptyString($nonce, ClaimsEnum::Nonce->value);
124+
}
125+
126+
127+
/**
128+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
129+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
130+
*/
131+
public function getAuthenticationContextClassReference(): ?string
132+
{
133+
// OPTIONAL. Authentication Context Class Reference. String specifying
134+
// an Authentication Context Class Reference value that identifies the
135+
// Authentication Context Class that the authentication performed
136+
// satisfied. The value "0" indicates the End-User authentication did
137+
// not meet the requirements of ISO/IEC 29115 [ISO29115] level 1.
138+
// For historic reasons, the value "0" is used to indicate that there
139+
// is no confidence that the same person is actually there.
140+
// Authentications with level 0 SHOULD NOT be used to authorize access
141+
// to any resource of any monetary value. (This corresponds to the
142+
// OpenID 2.0 PAPE [OpenID.PAPE] nist_auth_level 0.) An absolute URI
143+
// or an RFC 6711 [RFC6711] registered name SHOULD be used as the acr
144+
// value; registered names MUST NOT be used with a different meaning
145+
// than that which is registered. Parties using this claim will need to
146+
// agree upon the meanings of the values used, which may be context -
147+
// specific. The acr value is a case-sensitive string.
148+
149+
$acr = $this->getPayloadClaim(ClaimsEnum::Acr->value);
150+
151+
if (is_null($acr)) {
152+
return null;
153+
}
154+
155+
return $this->helpers->type()->ensureNonEmptyString($acr, ClaimsEnum::Acr->value);
156+
}
157+
158+
159+
/**
160+
* @return ?string[]
161+
*
162+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
163+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
164+
*/
165+
public function getAuthenticationMethodsReferences(): ?array
166+
{
167+
// OPTIONAL. Authentication Methods References. JSON array of strings
168+
// that are identifiers for authentication methods used in the
169+
// authentication. For instance, values might indicate that both
170+
// password and OTP authentication methods were used. The amr value is
171+
// an array of case-sensitive strings. Values used in the amr Claim
172+
// SHOULD be from those registered in the IANA Authentication Method
173+
// Reference Values registry [IANA.AMR] established by [RFC8176];
174+
// parties using this claim will need to agree upon the meanings of any
175+
// unregistered values used, which may be context-specific.
176+
177+
$amr = $this->getPayloadClaim(ClaimsEnum::Amr->value);
178+
179+
if (is_null($amr)) {
180+
return null;
181+
}
182+
183+
return $this->helpers->type()->ensureArrayWithValuesAsNonEmptyStrings($amr, ClaimsEnum::Amr->value);
184+
}
185+
186+
187+
/**
188+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
189+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
190+
*/
191+
public function getAuthorizedParty(): ?string
192+
{
193+
// OPTIONAL. Authorized party - the party to which the ID Token was
194+
// issued. If present, it MUST contain the OAuth 2.0 Client ID of this
195+
// party. The azp value is a case-sensitive string containing a
196+
// StringOrURI value. Note that in practice, the azp Claim only occurs
197+
// when extensions beyond the scope of this specification are used;
198+
// therefore, implementations not using such extensions are encouraged
199+
// to not use azp and to ignore it when it does occur.
200+
201+
$azp = $this->getPayloadClaim(ClaimsEnum::Azp->value);
202+
203+
if (is_null($azp)) {
204+
return null;
205+
}
206+
207+
return $this->helpers->type()->ensureNonEmptyString($azp, ClaimsEnum::Azp->value);
208+
}
209+
210+
211+
/**
212+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
213+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
214+
*/
215+
public function getAccessTokenHash(): ?string
216+
{
217+
// OPTIONAL. Access Token hash value. Its value is the base64url
218+
// encoding of the left-most half of the hash of the octets of the ASCII
219+
// representation of the access_token value, where the hash algorithm
220+
// used is the hash algorithm used in the alg Header Parameter of the
221+
// ID Token's JOSE Header. For instance, if the alg is RS256, hash the
222+
// access_token value with SHA-256, then take the left-most 128 bits
223+
// and base64url-encode them. The at_hash value is a case-sensitive
224+
// string.
225+
$aTHash = $this->getPayloadClaim(ClaimsEnum::ATHash->value);
226+
227+
if (is_null($aTHash)) {
228+
return null;
229+
}
230+
231+
return $this->helpers->type()->ensureNonEmptyString($aTHash, ClaimsEnum::ATHash->value);
232+
}
233+
234+
235+
/**
236+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
237+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
238+
*/
239+
public function getCodeHash(): ?string
240+
{
241+
// Code hash value. Its value is the base64url encoding of the left-most
242+
// half of the hash of the octets of the ASCII representation of the
243+
// code value, where the hash algorithm used is the hash algorithm used
244+
// in the alg Header Parameter of the ID Token's JOSE Header. For
245+
// instance, if the alg is HS512, hash the code value with SHA-512,
246+
// then take the left-most 256 bits and base64url-encode them. The
247+
// c_hash value is a case-sensitive string.
248+
//If the ID Token is issued from the Authorization Endpoint with a code,
249+
// which is the case for the response_type values code id_token and
250+
// code id_token token, this is REQUIRED; otherwise, its inclusion is
251+
// OPTIONAL.
252+
253+
$cHash = $this->getPayloadClaim(ClaimsEnum::CHash->value);
254+
255+
if (is_null($cHash)) {
256+
return null;
257+
}
258+
259+
return $this->helpers->type()->ensureNonEmptyString($cHash, ClaimsEnum::CHash->value);
260+
}
261+
262+
263+
/**
264+
* @return ?array<string, string>
265+
*
266+
* @throws \SimpleSAML\OpenID\Exceptions\JwsException
267+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
268+
*/
269+
public function getSubJwk(): ?array
270+
{
271+
// Public key used to check the signature of an ID Token issued by a
272+
// Self-Issued OpenID Provider, as specified in Section 7. The key is a
273+
// bare key in JWK [JWK] format (not an X.509 certificate value).
274+
// The sub_jwk value is a JSON object. Use of the sub_jwk Claim is NOT
275+
// RECOMMENDED when the OP is not Self-Issued.
276+
$subJwk = $this->getPayloadClaim(ClaimsEnum::SubJwk->value);
277+
278+
if (is_null($subJwk)) {
279+
return null;
280+
}
281+
282+
return $this->helpers->type()
283+
->ensureArrayWithKeysAndValuesAsNonEmptyStrings($subJwk, ClaimsEnum::SubJwk->value);
284+
}
285+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace SimpleSAML\OpenID\Exceptions;
6+
7+
class IdTokenException extends JwsException
8+
{
9+
}

src/Helpers/Type.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SimpleSAML\OpenID\Helpers;
66

77
use JsonSerializable;
8+
use SimpleSAML\OpenID\Codebooks\UriPattern;
89
use SimpleSAML\OpenID\Exceptions\InvalidValueException;
910
use Stringable;
1011
use Traversable;
@@ -57,8 +58,8 @@ public function ensureNonEmptyString(mixed $value, ?string $context = null): str
5758

5859

5960
/**
60-
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
6161
* @return mixed[]
62+
* @throws \SimpleSAML\OpenID\Exceptions\InvalidValueException
6263
*/
6364
public function ensureArray(mixed $value, ?string $context = null): array
6465
{
@@ -258,7 +259,7 @@ public function enforceRegex(
258259
public function enforceUri(
259260
mixed $value,
260261
?string $context = null,
261-
string $pattern = '/^[a-zA-Z][a-zA-Z0-9+.-]*:[^\s]*$/',
262+
string $pattern = UriPattern::Uri->value,
262263
): string {
263264
try {
264265
$value = $this->enforceRegex($value, $pattern, $context);

0 commit comments

Comments
 (0)