Repository navigation
[#243] Do not check the id_token nonce in getProfile for OAUTH providers - #244
Conversation
maximthomas
left a comment
There was a problem hiding this comment.
praise: The fix is in the right place, and a test that fails without it pins it.
getProfilenow applies the sameOPENID_CONNECTtest asgetAuthToken(OAuthHttpClient.java:164,:214-220), so registration, linking and login agree on when the nonce is required.testGetProfileIgnoresNonceForOAuthgoes red under the BASE behaviour (unconditionalcheckNonce). The in-processHandlerwith an HS256 id_token and a realJwtReconstructionexercises the real parsing path offline.
suggestion (non-blocking): No test covers an OAUTH provider without a userinfo_endpoint, though the description says that road is preserved.
openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/client/OAuthHttpClientTest.java:53-60, openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/client/OAuthHttpClient.java:160-181
The only OAUTH getProfile case sets USERINFO_ENDPOINT and an id_token with no nonce claim, so the profile always comes from userinfo. Only OPENID_CONNECT cases reach the id_token-claims-as-profile road at :177. On a HEAD export three mutants stay green: nulling jwtClaimSet for OAUTH (7/7), moving getClaims inside the OPENID_CONNECT guard (6/6), and checking the nonce for OAUTH whenever the claim is present (6/6). Under the first two, such a provider gets 500 "No means available for getting profile data". Under the third, a provider that returns some other nonce gets back the 400 this PR fixes.
@Test
public void testGetProfileReadsIdTokenClaimsForOAuthWithoutUserInfo() throws Exception {
final OAuthHttpClient client = newClient("OAUTH", null, idToken("other-nonce"));
final JsonValue profile = client.getProfile(new JwtReconstruction(), "code", NONCE, REDIRECT_URI);
assertThat(profile.get("sub").asString()).isEqualTo("id-token-subject");
}Pin: a mismatched nonce and no userinfo endpoint. The case fails at BASE and under all three mutants (traced, not run).
suggestion (non-blocking): No getAuthToken case covers a successful OpenID Connect login.
openidm-identity-provider/src/test/java/org/forgerock/openidm/idp/client/OAuthHttpClientTest.java:85-98, openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/client/OAuthHttpClient.java:217-220
The description says the getAuthToken cases "pin that login is unchanged", but the only OPENID_CONNECT case is a rejection. Two mutants stay green (6/6) on a HEAD export: passing null as the nonce at :219, and an unconditional BadRequestException on that branch. Both make every OIDC login fail with 400.
@Test
public void testGetAuthTokenAcceptsMatchingNonceForOpenIdConnect() throws Exception {
final String idToken = idToken(NONCE);
final OAuthHttpClient client = newClient("OPENID_CONNECT", USERINFO_ENDPOINT, idToken);
assertThat(client.getAuthToken(new JwtReconstruction(), "code", NONCE, REDIRECT_URI).getOrThrow())
.isEqualTo(idToken);
}Pin: with a matching nonce, getAuthToken returns the id_token itself. Both mutants turn this case red (traced, not run).
suggestion (non-blocking): Exempt OAUTH rather than require OPENID_CONNECT, so that an unknown type keeps the nonce check.
openidm-identity-provider/src/main/java/org/forgerock/openidm/idp/client/OAuthHttpClient.java:164
OPENID_CONNECT.equals(config.getType()) skips checkNonce for every type except that exact string, including a null type and a hand-written "openid_connect". BASE checked those, and getAuthToken's default branch (:221-223) rejects them with a 500. So on such a type login fails closed, while registration and linking now fail open. I did not show that such a provider can reach getProfile: AuthenticationService rejects non-enum types when SOCIAL_PROVIDERS is configured, and the Admin UI writes only the two known types. With the narrower guard all seven tests stay green.
// every provider type but OAUTH must echo the request nonce
if (!OAUTH.equals(config.getType())) {
checkNonce(jwtClaimSet, nonce);
}…y for OpenID Connect providers getProfile, used by social registration and account linking, checked the nonce of any id_token the token endpoint returned, while getAuthToken, used by login, checks it only for OPENID_CONNECT. An OAUTH provider that returns an id_token without the request nonce (LinkedIn with the openid scope) passed login but failed registration with 400 "Nonce provided does not match claim". Check the nonce in getProfile only for OPENID_CONNECT, and test both methods for both provider types. Fixes OpenIdentityPlatform#243
…e check Review of OpenIdentityPlatform#244: requiring OPENID_CONNECT skipped the nonce check for every other type string, including a null or hand-written type, which getAuthToken rejects at login. Skip the check for OAUTH only, so that registration and linking keep failing closed on an unknown type as before. Pin the behaviour the description promised but no test covered: an OAUTH provider without a userinfo endpoint gets its profile from the id_token claims even when the nonce differs, and getAuthToken returns the id_token of an OpenID Connect provider whose nonce matches.
f82e508 to
6d779c8
Compare
|
@maximthomas all three suggestions are taken in 6d779c8; the branch is also rebased onto the current
Successful OpenID Connect login. Added Exempt
|
maximthomas
left a comment
There was a problem hiding this comment.
praise: All three round-1 items are addressed, and the guard now fails closed for every type except the one the PR targets.
getProfilechecks the nonce underif (!OAUTH.equals(config.getType()))(OAuthHttpClient.java:165), so null and hand-written types still get the nonce check on registration and linking.testGetProfileReadsIdTokenClaimsForOAuthWithoutUserInfoandtestGetAuthTokenAcceptsMatchingNonceForOpenIdConnectcover the two roads that had no case in round 1.
Fixes #243
The problem
OAuthHttpClient.getProfile, which social registration and account linking use, checked the nonceof any id_token the token endpoint returned, whatever the provider type.
getAuthToken, which loginuses, checks it only for
OPENID_CONNECT(OAuthHttpClient.java:215-225).checkNoncealso failswhen the claim is absent (
:130).So an
OAUTHprovider whose token endpoint returns an id_token without the request nonce passeslogin but fails registration and account linking with 400 "Nonce provided does not match claim".
LinkedIn becomes such a provider with #231: its catalogue entry is
OAUTHwith theopenidscope,which
/v2/userinfoneeds, and Keycloak's LinkedIn OIDC provider turns the nonce check off becauseLinkedIn does not echo the request nonce (raised in the review of #231).
The change
getProfileskips the nonce check forOAUTHonly (OAuthHttpClient.java:165-167). Every othertype is still checked as before, so a null or hand-written type, which
getAuthTokenrejects witha 500 at login, keeps failing closed at registration and linking too. For an
OAUTHprovider theid_token claims are still read: they are the profile when the provider has no
userinfo_endpoint, as before (:180).OAuthHttpClientTest. The provider is an in-processHandleranswering the token and userinfoendpoints; the id_token is signed with
JwtBuilderFactory(HS256) and parsed by a realJwtReconstruction. It covers:getProfile,OAUTH, id_token without nonce → the userinfo profile;getProfile,OAUTHwithout a userinfo endpoint, id_token with another nonce → the profilefrom the id_token claims;
getProfile,OPENID_CONNECT, nonce missing or different →BadRequestException;getProfile,OPENID_CONNECT, matching nonce → the profile from the id_token claims;getProfile, a type that is neither (openid_connect), different nonce →BadRequestException;getAuthToken:OAUTH→ the access token;OPENID_CONNECTwith a missing nonce →BadRequestException, with a matching nonce → the id_token itself, to pin that login isunchanged.
Verification
testGetProfileIgnoresNonceForOAuthfailed withBadRequest Nonce provided does not match claim.mvn -o -pl openidm-identity-provider test: 13 tests, 0 failures(the nine in
OAuthHttpClientTestandIdentityProviderServiceTest).OAuthHttpClientTestcase red: ingetProfile, nulling theclaims for
OAUTH, reading the claims only inside the guard, checking the nonce forOAUTHwhenever the claim is present, and the earlier
OPENID_CONNECT.equals(...)guard; ingetAuthToken, checking against anullnonce and an unconditional 400 on the OpenID Connectbranch.
Not verified: a real registration against a LinkedIn application (needs a registered app).