Skip to content

Commit c973de1

Browse files
committed
Move to AddClaimsToIdTokenRule for ImplicitGrant
1 parent 440934f commit c973de1

4 files changed

Lines changed: 84 additions & 23 deletions

File tree

src/Server/Grants/ImplicitGrant.php

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use Psr\Http\Message\ServerRequestInterface;
1313
use RuntimeException;
1414
use SimpleSAML\Module\oidc\Entities\AccessTokenEntity;
15-
use SimpleSAML\Module\oidc\Entities\ClientEntity;
1615
use SimpleSAML\Module\oidc\Entities\Interfaces\EntityStringRepresentationInterface;
1716
use SimpleSAML\Module\oidc\Entities\UserEntity;
1817
use SimpleSAML\Module\oidc\Factories\Entities\AccessTokenEntityFactory;
@@ -276,19 +275,13 @@ private function completeOidcAuthorizationRequest(AuthorizationRequest $authoriz
276275
$responseParams['expires_in'] = $accessToken->getExpiryDateTime()->getTimestamp() - time();
277276
}
278277

279-
// Decide whether the user's (scope-derived) claims go into the ID Token. The response-type-driven decision
280-
// (AddClaimsToIdTokenRule) already requests them for response_type=id_token, where there is no access token
281-
// to call the UserInfo endpoint with. For id_token token the claims would otherwise be available only at
282-
// UserInfo, so we additionally honor the per-client, administrator-only `add_claims_to_id_token` option,
283-
// matching the authorization code flow (see TokenResponse::prepareIdTokenExtraParam()).
284-
$client = $authorizationRequest->getClient();
285-
$addClaimsToIdToken = $authorizationRequest->getAddClaimsToIdToken()
286-
|| ($client instanceof ClientEntity && $client->getAddClaimsToIdToken());
287-
278+
// Whether to release the user's claims in the ID Token is decided by AddClaimsToIdTokenRule (based on the
279+
// response type and the per-client `add_claims_to_id_token` option) and carried on the authorization
280+
// request; see validateAuthorizationRequestWithRequestRules().
288281
$idToken = $this->idTokenBuilder->buildFor(
289282
$user,
290283
$accessToken,
291-
$addClaimsToIdToken,
284+
$authorizationRequest->getAddClaimsToIdToken(),
292285
$addAccessTokenHashToIdToken,
293286
$authorizationRequest->getNonce(),
294287
$authorizationRequest->getAuthTime(),

src/Server/RequestRules/Rules/AddClaimsToIdTokenRule.php

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace SimpleSAML\Module\oidc\Server\RequestRules\Rules;
66

77
use Psr\Http\Message\ServerRequestInterface;
8+
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
89
use SimpleSAML\Module\oidc\Server\RequestRules\Interfaces\ResultBagInterface;
910
use SimpleSAML\Module\oidc\Server\RequestRules\Result;
1011
use SimpleSAML\Module\oidc\Server\ResponseModes\QueryResponseMode;
@@ -13,6 +14,14 @@
1314
use SimpleSAML\OpenID\Codebooks\HttpMethodsEnum;
1415

1516
/**
17+
* Decides whether the user's (scope-derived) claims should be released in the ID Token.
18+
*
19+
* This is true when either:
20+
* - the response type is exactly `id_token` (there is no access token, so the client cannot obtain the claims
21+
* from the UserInfo endpoint and they must go in the ID Token); or
22+
* - the client is configured with the administrator-only `add_claims_to_id_token` option (the client wants its
23+
* claims in the ID Token regardless, e.g. because it never calls the UserInfo endpoint).
24+
*
1625
* @extends AbstractRule<bool>
1726
*/
1827
class AddClaimsToIdTokenRule extends AbstractRule
@@ -34,6 +43,15 @@ public function checkRule(
3443
): ?Result {
3544
$responseType = $currentResultBag->getOrFail(ResponseTypeRule::class)->getValue();
3645

37-
return new Result($this->getKey(), $responseType === "id_token");
46+
$addClaimsToIdToken = $responseType === "id_token";
47+
48+
// Honor the per-client option. The client is resolved by ClientRule, which is predefined in the result bag
49+
// before this rule runs; if it is not available for some reason, fall back to the response-type decision.
50+
$client = $currentResultBag->get(ClientRule::class)?->getValue();
51+
if ($client instanceof ClientEntityInterface && $client->getAddClaimsToIdToken()) {
52+
$addClaimsToIdToken = true;
53+
}
54+
55+
return new Result($this->getKey(), $addClaimsToIdToken);
3856
}
3957
}

tests/unit/src/Server/Grants/ImplicitGrantTest.php

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -170,20 +170,18 @@ public function testCanCompleteAuthorizationRequest(): void
170170
}
171171

172172
/**
173-
* For id_token token the AddClaimsToIdTokenRule does not request the claims (the rule only matches the exact
174-
* "id_token" response type), but a client configured with the administrator-only `add_claims_to_id_token`
175-
* option still gets the user's claims released in the ID Token.
173+
* The grant forwards the "add claims to ID Token" decision (made by AddClaimsToIdTokenRule and carried on the
174+
* authorization request) to the ID Token builder. When it is true, the user's claims are released in the ID
175+
* Token.
176176
*/
177-
public function testReleasesUserClaimsInIdTokenWhenClientConfiguredTo(): void
177+
public function testReleasesUserClaimsInIdTokenWhenRequested(): void
178178
{
179179
$this->authorizationRequestMock->method('getUser')->willReturn($this->userEntityMock);
180180
$this->authorizationRequestMock->method('getRedirectUri')->willReturn('redirectUri');
181181
$this->authorizationRequestMock->method('isAuthorizationApproved')->willReturn(true);
182182
$this->authorizationRequestMock->method('getScopes')->willReturn([$this->scopeEntityMock]);
183183
$this->authorizationRequestMock->method('getClient')->willReturn($this->clientEntityMock);
184-
// Response-type rule did NOT request the claims (e.g. id_token token), but the client opts in.
185-
$this->authorizationRequestMock->method('getAddClaimsToIdToken')->willReturn(false);
186-
$this->clientEntityMock->method('getAddClaimsToIdToken')->willReturn(true);
184+
$this->authorizationRequestMock->method('getAddClaimsToIdToken')->willReturn(true);
187185
$this->scopeRepositoryMock->method('finalizeScopes')->willReturn([$this->scopeEntityMock]);
188186

189187
$idTokenMock = $this->createMock(IdToken::class);
@@ -206,18 +204,17 @@ public function testReleasesUserClaimsInIdTokenWhenClientConfiguredTo(): void
206204
}
207205

208206
/**
209-
* When neither the response type nor the client requests it, the user's claims are not released in the ID
210-
* Token (they remain available at the UserInfo endpoint via the issued access token).
207+
* When the decision is false, the user's claims are not released in the ID Token (they remain available at
208+
* the UserInfo endpoint via the issued access token).
211209
*/
212-
public function testDoesNotReleaseUserClaimsInIdTokenByDefault(): void
210+
public function testDoesNotReleaseUserClaimsInIdTokenWhenNotRequested(): void
213211
{
214212
$this->authorizationRequestMock->method('getUser')->willReturn($this->userEntityMock);
215213
$this->authorizationRequestMock->method('getRedirectUri')->willReturn('redirectUri');
216214
$this->authorizationRequestMock->method('isAuthorizationApproved')->willReturn(true);
217215
$this->authorizationRequestMock->method('getScopes')->willReturn([$this->scopeEntityMock]);
218216
$this->authorizationRequestMock->method('getClient')->willReturn($this->clientEntityMock);
219217
$this->authorizationRequestMock->method('getAddClaimsToIdToken')->willReturn(false);
220-
$this->clientEntityMock->method('getAddClaimsToIdToken')->willReturn(false);
221218
$this->scopeRepositoryMock->method('finalizeScopes')->willReturn([$this->scopeEntityMock]);
222219

223220
$idTokenMock = $this->createMock(IdToken::class);

tests/unit/src/Server/RequestRules/Rules/AddClaimsToIdTokenRuleTest.php

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@
88
use PHPUnit\Framework\MockObject\Stub;
99
use PHPUnit\Framework\TestCase;
1010
use Psr\Http\Message\ServerRequestInterface;
11+
use SimpleSAML\Module\oidc\Entities\Interfaces\ClientEntityInterface;
1112
use SimpleSAML\Module\oidc\Helpers;
1213
use SimpleSAML\Module\oidc\Server\RequestRules\Result;
1314
use SimpleSAML\Module\oidc\Server\RequestRules\ResultBag;
1415
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\AddClaimsToIdTokenRule;
16+
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ClientRule;
1517
use SimpleSAML\Module\oidc\Server\RequestRules\Rules\ResponseTypeRule;
1618
use SimpleSAML\Module\oidc\Server\ResponseModes\ResponseModeInterface;
1719
use SimpleSAML\Module\oidc\Services\LoggerService;
@@ -136,6 +138,57 @@ public static function invalidResponseTypeProvider(): array
136138
];
137139
}
138140

141+
/**
142+
* A client configured with the administrator-only `add_claims_to_id_token` option gets the claims released
143+
* in the ID Token even for a response type that would not otherwise trigger it (e.g. `id_token token`).
144+
*
145+
* @throws \Throwable
146+
*/
147+
public function testAddsClaimsWhenClientConfiguredEvenForNonIdTokenResponseType(): void
148+
{
149+
$this->resultBag->add(new Result(ResponseTypeRule::class, 'id_token token'));
150+
151+
$clientStub = $this->createStub(ClientEntityInterface::class);
152+
$clientStub->method('getAddClaimsToIdToken')->willReturn(true);
153+
$this->resultBag->add(new Result(ClientRule::class, $clientStub));
154+
155+
$result = $this->sut()->checkRule(
156+
$this->requestStub,
157+
$this->resultBag,
158+
$this->loggerServiceStub,
159+
[],
160+
$this->responseModeStub,
161+
) ??
162+
new Result(AddClaimsToIdTokenRule::class, null);
163+
164+
$this->assertTrue($result->getValue());
165+
}
166+
167+
/**
168+
* When neither the response type nor the client requests it, claims are not released in the ID Token.
169+
*
170+
* @throws \Throwable
171+
*/
172+
public function testDoesNotAddClaimsWhenNeitherResponseTypeNorClientRequestIt(): void
173+
{
174+
$this->resultBag->add(new Result(ResponseTypeRule::class, 'id_token token'));
175+
176+
$clientStub = $this->createStub(ClientEntityInterface::class);
177+
$clientStub->method('getAddClaimsToIdToken')->willReturn(false);
178+
$this->resultBag->add(new Result(ClientRule::class, $clientStub));
179+
180+
$result = $this->sut()->checkRule(
181+
$this->requestStub,
182+
$this->resultBag,
183+
$this->loggerServiceStub,
184+
[],
185+
$this->responseModeStub,
186+
) ??
187+
new Result(AddClaimsToIdTokenRule::class, null);
188+
189+
$this->assertFalse($result->getValue());
190+
}
191+
139192
/**
140193
* @throws \Throwable
141194
* @throws \SimpleSAML\Module\oidc\Server\Exceptions\OidcServerException

0 commit comments

Comments
 (0)