2020use SimpleSAML \OpenID \Jwks ;
2121
2222/**
23- * @extends AbstractRule<\SimpleSAML\OpenID\Core\IdToken |null>
23+ * @extends AbstractRule<\SimpleSAML\OpenID\Core\IdTokenHint |null>
2424 */
2525class IdTokenHintRule extends AbstractRule
2626{
@@ -52,6 +52,13 @@ public function checkRule(
5252 ): ?Result {
5353 $ state = $ currentResultBag ->getOrFail (StateRule::class)->getValue ();
5454
55+ // When this rule runs in the authorization flow, the redirect URI has already been validated and is
56+ // available in the result bag, so validation errors can be redirected back to the client (as required at
57+ // the authorization endpoint). In the logout (end session) flow there is no ClientRedirectUriRule, so this
58+ // resolves to null and the error is returned directly, preserving the previous behavior.
59+ $ redirectUriValue = $ currentResultBag ->get (ClientRedirectUriRule::class)?->getValue();
60+ $ redirectUri = is_string ($ redirectUriValue ) ? $ redirectUriValue : null ;
61+
5562 $ idTokenHintParam = $ this ->requestParamsResolver ->getAsStringBasedOnAllowedMethods (
5663 ParamsEnum::IdTokenHint->value ,
5764 $ request ,
@@ -63,52 +70,96 @@ public function checkRule(
6370 }
6471
6572 if (empty ($ idTokenHintParam )) {
66- $ loggerService ->notice ('End session request rejected: `id_token_hint` was provided but empty. ' );
73+ $ loggerService ->notice ('Request rejected: `id_token_hint` was provided but empty. ' );
6774 throw OidcServerException::invalidRequest (
6875 ParamsEnum::IdTokenHint->value ,
6976 'Received empty id_token_hint ' ,
7077 null ,
71- null ,
78+ $ redirectUri ,
7279 $ state ,
80+ $ responseMode ,
7381 );
7482 }
7583
7684 $ jwks = $ this ->jwks ->jwksDecoratorFactory ()->fromJwkDecorators (
7785 ...$ this ->moduleConfig ->getProtocolSignatureKeyPairBag ()->getAllPublicKeys (),
7886 )->jsonSerialize ();
7987
80- $ idTokenHint = $ this ->core ->idTokenFactory ()->fromToken ($ idTokenHintParam );
88+ // Parsing constructs and validates the ID Token Hint (structure and required claims), throwing on any
89+ // problem. We translate those failures into a protocol-level invalid_request error (which is redirected back
90+ // to the client in the authorization flow) instead of letting a raw exception surface as an HTTP 500. The
91+ // dedicated IdTokenHint abstraction deliberately does not validate the `exp` claim, so an otherwise-valid but
92+ // expired hint is accepted (as recommended by OpenID Connect Core, since a hint is commonly sent after it has
93+ // expired); the `nbf` and `iat` timestamps are still validated.
94+ try {
95+ $ idTokenHint = $ this ->core ->idTokenHintFactory ()->fromToken ($ idTokenHintParam );
96+ } catch (\Throwable $ exception ) {
97+ $ loggerService ->notice (
98+ 'Request rejected: `id_token_hint` could not be parsed or validated. ' ,
99+ ['exception ' => $ exception ->getMessage ()],
100+ );
101+ throw OidcServerException::invalidRequest (
102+ ParamsEnum::IdTokenHint->value ,
103+ $ exception ->getMessage (),
104+ null ,
105+ $ redirectUri ,
106+ $ state ,
107+ $ responseMode ,
108+ );
109+ }
81110
82111 if ($ idTokenHint ->getIssuer () !== $ this ->moduleConfig ->getIssuer ()) {
83112 $ loggerService ->notice (
84- 'End session request rejected: `id_token_hint` was not issued by this OP. ' ,
113+ 'Request rejected: `id_token_hint` was not issued by this OP. ' ,
85114 ['issuer ' => $ idTokenHint ->getIssuer (), 'expected_issuer ' => $ this ->moduleConfig ->getIssuer ()],
86115 );
87116 throw OidcServerException::invalidRequest (
88117 ParamsEnum::IdTokenHint->value ,
89118 'Invalid ID Token Hint Issuer ' ,
90119 null ,
91- null ,
120+ $ redirectUri ,
92121 $ state ,
122+ $ responseMode ,
93123 );
94124 }
95125
96126 try {
97127 $ idTokenHint ->verifyWithKeySet ($ jwks );
98128 } catch (\Throwable $ exception ) {
99129 $ loggerService ->notice (
100- 'End session request rejected: `id_token_hint` signature verification failed. ' ,
130+ 'Request rejected: `id_token_hint` signature verification failed. ' ,
101131 ['exception ' => $ exception ->getMessage ()],
102132 );
103133 throw OidcServerException::invalidRequest (
104134 ParamsEnum::IdTokenHint->value ,
105135 $ exception ->getMessage (),
106136 null ,
107- null ,
137+ $ redirectUri ,
108138 $ state ,
139+ $ responseMode ,
109140 );
110141 }
111142
143+ // In the authorization flow the requesting client is known (ClientRule). An id_token_hint represents the
144+ // End-User's session with the requesting client, so require that client to be an audience of the hint. This
145+ // binds the hint to the requesting client and rejects a token that was issued to a different client. In the
146+ // logout (end session) flow there is no ClientRule in the result bag, so this is skipped, preserving that
147+ // flow's behavior.
148+ $ client = $ currentResultBag ->get (ClientRule::class)?->getValue();
149+ if ($ client !== null && !in_array ($ client ->getIdentifier (), $ idTokenHint ->getAudience (), true )) {
150+ $ loggerService ->notice (
151+ 'Request rejected: `id_token_hint` was not issued to the requesting client. ' ,
152+ ['client_id ' => $ client ->getIdentifier ()],
153+ );
154+ throw OidcServerException::invalidRequest (
155+ ParamsEnum::IdTokenHint->value ,
156+ 'ID Token Hint audience does not include the requesting client ' ,
157+ null ,
158+ $ redirectUri ,
159+ $ state ,
160+ $ responseMode ,
161+ );
162+ }
112163
113164 return new Result ($ this ->getKey (), $ idTokenHint );
114165 }
0 commit comments