Only add the nonce claim to the ID Token (mapper for backwards compatibility)
Closes #26893 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
e7ff12e010
commit
dea15e25da
11 changed files with 351 additions and 28 deletions
|
@ -0,0 +1,5 @@
|
|||
= Nonce claim is only added to the ID token
|
||||
|
||||
The nonce claim is now only added to the ID token strictly following the OpenID Connect Core 1.0 specification. As indicated in the specification, the claim is compulsory inside the https://openid.net/specs/openid-connect-core-1_0.html#IDToken[ID token] when the same parameter was sent in the authorization request. The specification also recommends to not add the `nonce` after a https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokenResponse[refresh request]. Previously, the claim was set to all the tokens (Access, Refresh and ID) in all the responses (refresh included).
|
||||
|
||||
A new `Nonce backwards compatible` mapper is also included in the software that can be assigned to client scopes to revert to the old behavior. For example, the JS adapter checked the returned `nonce` claim in all the tokens before fixing issue https://github.com/keycloak/keycloak/issues/26651[#26651] in version 24.0.0. Therefore, if an old version of the JS adapter is used, the mapper should be added to the required clients by using client scopes.
|
|
@ -1,5 +1,9 @@
|
|||
== Migration Changes
|
||||
|
||||
=== Migrating to 25.0.0
|
||||
|
||||
include::changes-25_0_0.adoc[leveloffset=3]
|
||||
|
||||
=== Migrating to 24.0.0
|
||||
|
||||
include::changes-24_0_0.adoc[leveloffset=3]
|
||||
|
|
|
@ -229,7 +229,9 @@ public class TokenManager {
|
|||
throw new OAuthErrorException(OAuthErrorException.INVALID_SCOPE, "Client no longer has requested consent from user");
|
||||
}
|
||||
|
||||
if (oldToken.getNonce() != null) {
|
||||
clientSessionCtx.setAttribute(OIDCLoginProtocol.NONCE_PARAM, oldToken.getNonce());
|
||||
}
|
||||
|
||||
// recreate token.
|
||||
AccessToken newToken = createClientAccessToken(session, realm, client, user, userSession, clientSessionCtx);
|
||||
|
@ -984,7 +986,6 @@ public class TokenManager {
|
|||
|
||||
AuthenticatedClientSessionModel clientSession = clientSessionCtx.getClientSession();
|
||||
token.issuer(clientSession.getNote(OIDCLoginProtocol.ISSUER));
|
||||
token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
|
||||
token.setScope(clientSessionCtx.getScopeString());
|
||||
|
||||
// Backwards compatibility behaviour prior step-up authentication was introduced
|
||||
|
@ -1192,7 +1193,7 @@ public class TokenManager {
|
|||
idToken.issuedNow();
|
||||
idToken.issuedFor(accessToken.getIssuedFor());
|
||||
idToken.issuer(accessToken.getIssuer());
|
||||
idToken.setNonce(accessToken.getNonce());
|
||||
idToken.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
|
||||
idToken.setAuthTime(accessToken.getAuthTime());
|
||||
idToken.setSessionState(accessToken.getSessionState());
|
||||
idToken.expiration(accessToken.getExpiration());
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.protocol.oidc.mappers;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.models.ClientSessionContext;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.ProtocolMapper;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
import org.keycloak.provider.ProviderConfigurationBuilder;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
|
||||
/**
|
||||
* <p>Simple mapper that adds the nonce claim into the access token as before.
|
||||
* Just adding the mapper reverts back to full old behavior in all the
|
||||
* tokens (access, refresh and ID).</p>
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class NonceBackwardsCompatibleMapper implements OIDCAccessTokenMapper, ProtocolMapper {
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-nonce-backwards-compatible-mapper";
|
||||
|
||||
@Override
|
||||
public String getProtocol() {
|
||||
return OIDCLoginProtocol.LOGIN_PROTOCOL;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public final ProtocolMapper create(KeycloakSession session) {
|
||||
return new NonceBackwardsCompatibleMapper();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "Nonce backwards compatible";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return AbstractOIDCProtocolMapper.TOKEN_MAPPER_CATEGORY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Adds the nonce claim to Access, Refresh and ID token";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return ProviderConfigurationBuilder.create().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessToken transformAccessToken(AccessToken token, ProtocolMapperModel mappingModel, KeycloakSession session, UserSessionModel userSession, ClientSessionContext clientSessionCtx) {
|
||||
token.setNonce(clientSessionCtx.getAttribute(OIDCLoginProtocol.NONCE_PARAM, String.class));
|
||||
return token;
|
||||
}
|
||||
|
||||
public static ProtocolMapperModel create(String name) {
|
||||
ProtocolMapperModel mapper = new ProtocolMapperModel();
|
||||
mapper.setName(name);
|
||||
mapper.setProtocolMapper(PROVIDER_ID);
|
||||
mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
Map<String, String> config = new HashMap<>();
|
||||
mapper.setConfig(config);
|
||||
return mapper;
|
||||
}
|
||||
}
|
|
@ -46,3 +46,4 @@ org.keycloak.protocol.saml.mappers.SAMLAudienceResolveProtocolMapper
|
|||
org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper
|
||||
org.keycloak.protocol.saml.mappers.UserAttributeNameIdMapper
|
||||
org.keycloak.protocol.oidc.mappers.ClaimsParameterWithValueIdTokenMapper
|
||||
org.keycloak.protocol.oidc.mappers.NonceBackwardsCompatibleMapper
|
||||
|
|
|
@ -106,6 +106,11 @@ public class ClientAttributeUpdater extends ServerResourceUpdater<ClientAttribut
|
|||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater setImplicitFlowEnabled(Boolean implicitFlowEnabled) {
|
||||
rep.setImplicitFlowEnabled(implicitFlowEnabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ClientAttributeUpdater setDefaultClientScopes(List<String> defaultClientScopes) {
|
||||
rep.setDefaultClientScopes(defaultClientScopes);
|
||||
return this;
|
||||
|
|
|
@ -405,6 +405,16 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
OAuthClient.AccessTokenResponse accessRes = oauth1.doAccessTokenRequest(code, "password");
|
||||
Assert.assertEquals("AccessTokenResponse: client: " + oauth1.getClientId() + ", error: '" + accessRes.getError() + "' desc: '" + accessRes.getErrorDescription() + "'",
|
||||
200, accessRes.getStatusCode());
|
||||
|
||||
AccessToken token = JsonSerialization.readValue(new JWSInput(accessRes.getAccessToken()).getContent(), AccessToken.class);
|
||||
Assert.assertNull(token.getNonce());
|
||||
|
||||
AccessToken refreshedToken = JsonSerialization.readValue(new JWSInput(accessRes.getRefreshToken()).getContent(), AccessToken.class);
|
||||
Assert.assertNull(refreshedToken.getNonce());
|
||||
|
||||
AccessToken idToken = JsonSerialization.readValue(new JWSInput(accessRes.getIdToken()).getContent(), AccessToken.class);
|
||||
Assert.assertEquals(oauth1.getNonce(), idToken.getNonce());
|
||||
|
||||
accessResRef.set(accessRes);
|
||||
|
||||
// Refresh access + refresh token using refresh token
|
||||
|
@ -420,11 +430,14 @@ public class ConcurrentLoginTest extends AbstractConcurrencyTest {
|
|||
|
||||
retryHistogram[invocationIndex].incrementAndGet();
|
||||
|
||||
AccessToken token = JsonSerialization.readValue(new JWSInput(accessResRef.get().getAccessToken()).getContent(), AccessToken.class);
|
||||
Assert.assertEquals("Invalid nonce.", token.getNonce(), oauth1.getNonce());
|
||||
token = JsonSerialization.readValue(new JWSInput(accessResRef.get().getAccessToken()).getContent(), AccessToken.class);
|
||||
Assert.assertNull(token.getNonce());
|
||||
|
||||
AccessToken refreshedToken = JsonSerialization.readValue(new JWSInput(refreshResRef.get().getAccessToken()).getContent(), AccessToken.class);
|
||||
Assert.assertEquals("Invalid nonce.", refreshedToken.getNonce(), oauth1.getNonce());
|
||||
refreshedToken = JsonSerialization.readValue(new JWSInput(refreshResRef.get().getRefreshToken()).getContent(), AccessToken.class);
|
||||
Assert.assertNull(refreshedToken.getNonce());
|
||||
|
||||
idToken = JsonSerialization.readValue(new JWSInput(refreshResRef.get().getIdToken()).getContent(), AccessToken.class);
|
||||
Assert.assertNull(idToken.getNonce());
|
||||
|
||||
if (userSessionId.get() == null) {
|
||||
userSessionId.set(token.getSessionState());
|
||||
|
|
|
@ -204,7 +204,10 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
assertEquals("123456", token.getNonce());
|
||||
assertNull(token.getNonce());
|
||||
|
||||
IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken());
|
||||
assertEquals("123456", idToken.getNonce());
|
||||
|
||||
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
|
||||
|
@ -213,7 +216,7 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
assertNotNull(refreshTokenString);
|
||||
|
||||
assertEquals("123456", refreshToken.getNonce());
|
||||
assertNull(refreshToken.getNonce());
|
||||
assertNull("RealmAccess should be null for RefreshTokens", refreshToken.getRealmAccess());
|
||||
assertTrue("ResourceAccess should be null for RefreshTokens", refreshToken.getResourceAccess().isEmpty());
|
||||
}
|
||||
|
@ -232,15 +235,16 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
|
||||
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||
assertEquals("123456", token.getNonce());
|
||||
assertNull(token.getNonce());
|
||||
|
||||
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(refreshTokenString);
|
||||
IDToken idToken = oauth.verifyToken(tokenResponse.getIdToken(), IDToken.class);
|
||||
assertEquals("123456", idToken.getNonce());
|
||||
|
||||
assertNotNull(tokenResponse.getRefreshToken());
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(tokenResponse.getRefreshToken());
|
||||
|
||||
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||
|
||||
assertNotNull(refreshTokenString);
|
||||
|
||||
assertEquals("Bearer", tokenResponse.getTokenType());
|
||||
|
||||
assertThat(token.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350)));
|
||||
|
@ -248,8 +252,9 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
assertThat(actual, allOf(greaterThanOrEqualTo(1799 - ALLOWED_CLOCK_SKEW), lessThanOrEqualTo(1800 + ALLOWED_CLOCK_SKEW)));
|
||||
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
assertNull(refreshToken.getNonce());
|
||||
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(refreshTokenString, "password");
|
||||
OAuthClient.AccessTokenResponse response = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(), "password");
|
||||
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||
RefreshToken refreshedRefreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
|
||||
|
@ -286,7 +291,15 @@ public class RefreshTokenTest extends AbstractKeycloakTest {
|
|||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
|
||||
|
||||
assertEquals("123456", refreshedToken.getNonce());
|
||||
assertNull(refreshedToken.getNonce());
|
||||
|
||||
idToken = oauth.verifyToken(response.getIdToken(), IDToken.class);
|
||||
assertNull(idToken.getNonce()); // null after refresh as recommended by spec
|
||||
|
||||
assertNotNull(response.getRefreshToken());
|
||||
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
assertEquals(sessionId, refreshToken.getSessionState());
|
||||
assertNull(refreshToken.getNonce());
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -42,6 +42,7 @@ import static org.junit.Assert.assertEquals;
|
|||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertNotEquals;
|
||||
import static org.junit.Assert.assertNotNull;
|
||||
import static org.junit.Assert.assertNull;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
public class AuthorizationTokenResponseModeTest extends AbstractTestRealmKeycloakTest {
|
||||
|
@ -193,7 +194,7 @@ public class AuthorizationTokenResponseModeTest extends AbstractTestRealmKeycloa
|
|||
Assert.assertNotNull(responseToken.getOtherClaims().get("access_token"));
|
||||
String accessTokenEncoded = (String) responseToken.getOtherClaims().get("access_token");
|
||||
AccessToken accessToken = oauth.verifyToken(accessTokenEncoded);
|
||||
assertEquals("123456", accessToken.getNonce());
|
||||
assertNull(accessToken.getNonce());
|
||||
|
||||
URI currentUri = new URI(driver.getCurrentUrl());
|
||||
Assert.assertNull(currentUri.getRawQuery());
|
||||
|
|
|
@ -458,16 +458,8 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
|
|||
Assert.assertNotNull(token.getOtherClaims().get("token_type"));
|
||||
}
|
||||
|
||||
private void assertNonce(AccessToken token, boolean isAuthCodeFlow, boolean exchangeToken) {
|
||||
if (isAuthCodeFlow && !exchangeToken) {
|
||||
Assert.assertNotNull(token.getNonce());
|
||||
} else {
|
||||
Assert.assertNull(token.getNonce());
|
||||
}
|
||||
}
|
||||
|
||||
private void assertAccessToken(AccessToken token, boolean isAuthCodeFlow, boolean isAddToAccessToken) {
|
||||
assertNonce(token, isAuthCodeFlow, false);
|
||||
Assert.assertNull(token.getNonce());
|
||||
assertMapperClaims(token, isAddToAccessToken, isAuthCodeFlow);
|
||||
assertInitClaims(token, isAuthCodeFlow);
|
||||
}
|
||||
|
@ -477,7 +469,7 @@ public class LightWeightAccessTokenTest extends AbstractClientPoliciesTest {
|
|||
}
|
||||
|
||||
private void assertTokenIntrospectionResponse(AccessToken token, boolean isAuthCodeFlow, boolean isAddToIntrospect, boolean exchangeToken) {
|
||||
assertNonce(token, isAuthCodeFlow, exchangeToken);
|
||||
Assert.assertNull(token.getNonce());
|
||||
assertMapperClaims(token, isAddToIntrospect, isAuthCodeFlow);
|
||||
assertInitClaims(token, isAuthCodeFlow);
|
||||
assertIntrospectClaims(token);
|
||||
|
|
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
* and other contributors as indicated by the @author tags.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.keycloak.testsuite.oidc;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientResource;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.models.ProtocolMapperModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.NonceBackwardsCompatibleMapper;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseMode;
|
||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.AuthorizationResponseToken;
|
||||
import org.keycloak.representations.IDToken;
|
||||
import org.keycloak.representations.RefreshToken;
|
||||
import org.keycloak.representations.idm.EventRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
import org.keycloak.testsuite.admin.ApiUtil;
|
||||
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
/**
|
||||
*
|
||||
* @author rmartinc
|
||||
*/
|
||||
public class NonceBackwardsCompatibleMapperTest extends AbstractTestRealmKeycloakTest {
|
||||
|
||||
@Rule
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
||||
@Override
|
||||
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceWithoutMapper() throws JsonProcessingException {
|
||||
testNonce(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNonceWithMapper() throws JsonProcessingException {
|
||||
ClientResource testApp = ApiUtil.findClientByClientId(testRealm(), "test-app");
|
||||
String mapperId = createNonceMapper(testApp);
|
||||
try {
|
||||
testNonce(true);
|
||||
} finally {
|
||||
testApp.getProtocolMappers().delete(mapperId);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitFlowWithoutMapper() throws Exception {
|
||||
try (ClientAttributeUpdater client = ClientAttributeUpdater.forClient(adminClient, TEST_REALM_NAME, "test-app")
|
||||
.setImplicitFlowEnabled(true)
|
||||
.update()) {
|
||||
testNonceImplicit(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testImplicitFlowWithMapper() throws Exception {
|
||||
ClientResource testApp = ApiUtil.findClientByClientId(testRealm(), "test-app");
|
||||
String mapperId = createNonceMapper(testApp);
|
||||
try (ClientAttributeUpdater client = ClientAttributeUpdater.forClient(adminClient, TEST_REALM_NAME, "test-app")
|
||||
.setImplicitFlowEnabled(true)
|
||||
.update()) {
|
||||
testNonceImplicit(true);
|
||||
} finally {
|
||||
testApp.getProtocolMappers().delete(mapperId);
|
||||
}
|
||||
}
|
||||
|
||||
private String createNonceMapper(ClientResource testApp) {
|
||||
ProtocolMapperModel nonceMapper = NonceBackwardsCompatibleMapper.create("nonce");
|
||||
ProtocolMapperRepresentation nonceMapperRep = ModelToRepresentation.toRepresentation(nonceMapper);
|
||||
try (Response res = testApp.getProtocolMappers().createMapper(nonceMapperRep)) {
|
||||
Assert.assertEquals(Response.Status.CREATED.getStatusCode(), res.getStatus());
|
||||
return ApiUtil.getCreatedId(res);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkNonce(String expectedNonce, String nonce, boolean expected) {
|
||||
if (expected) {
|
||||
Assert.assertEquals(expectedNonce, nonce);
|
||||
} else {
|
||||
Assert.assertNull(nonce);
|
||||
}
|
||||
}
|
||||
|
||||
private void testIntrospection(String accessToken, String expectedNonce, boolean expected) throws JsonProcessingException {
|
||||
String tokenResponse = oauth.introspectAccessTokenWithClientCredential("test-app", "password", accessToken);
|
||||
JsonNode nonce = new ObjectMapper().readTree(tokenResponse).get(OIDCLoginProtocol.NONCE_PARAM);
|
||||
checkNonce(expectedNonce, nonce != null? nonce.asText() : null, expected);
|
||||
}
|
||||
|
||||
private void testNonceImplicit(boolean mapper) throws JsonProcessingException {
|
||||
String nonce = KeycloakModelUtils.generateId();
|
||||
oauth.nonce(nonce);
|
||||
oauth.responseMode(OIDCResponseMode.JWT.value());
|
||||
oauth.responseType(OIDCResponseType.TOKEN + " " + OIDCResponseType.ID_TOKEN);
|
||||
OAuthClient.AuthorizationEndpointResponse response = oauth.doLogin("test-user@localhost", "password");
|
||||
|
||||
Assert.assertTrue(response.isRedirected());
|
||||
AuthorizationResponseToken responseToken = oauth.verifyAuthorizationResponseToken(response.getResponse());
|
||||
|
||||
String accessTokenString = (String) responseToken.getOtherClaims().get("access_token");
|
||||
AccessToken token = oauth.verifyToken(accessTokenString);
|
||||
checkNonce(nonce, token.getNonce(), mapper);
|
||||
String idTokenString = (String) responseToken.getOtherClaims().get("id_token");
|
||||
IDToken idToken = oauth.verifyToken(idTokenString, IDToken.class);
|
||||
checkNonce(nonce, idToken.getNonce(), true);
|
||||
|
||||
testIntrospection(accessTokenString, nonce, mapper);
|
||||
testIntrospection(idTokenString, nonce, true);
|
||||
}
|
||||
|
||||
private void testNonce(boolean mapper) throws JsonProcessingException {
|
||||
String nonce = KeycloakModelUtils.generateId();
|
||||
oauth.nonce(nonce);
|
||||
oauth.doLogin("test-user@localhost", "password");
|
||||
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||
|
||||
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
|
||||
|
||||
AccessToken token = oauth.verifyToken(response.getAccessToken());
|
||||
checkNonce(nonce, token.getNonce(), mapper);
|
||||
IDToken idToken = oauth.verifyToken(response.getIdToken(), IDToken.class);
|
||||
checkNonce(nonce, idToken.getNonce(), true);
|
||||
RefreshToken refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
checkNonce(nonce, refreshToken.getNonce(), mapper);
|
||||
|
||||
EventRepresentation tokenEvent = events.expectCodeToToken(loginEvent.getDetails().get(Details.CODE_ID),
|
||||
loginEvent.getSessionId()).assertEvent();
|
||||
|
||||
response = oauth.doRefreshTokenRequest(response.getRefreshToken(), "password");
|
||||
events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID),
|
||||
loginEvent.getSessionId()).assertEvent();
|
||||
|
||||
token = oauth.verifyToken(response.getAccessToken());
|
||||
checkNonce(nonce, token.getNonce(), mapper);
|
||||
idToken = oauth.verifyToken(response.getIdToken(), IDToken.class);
|
||||
checkNonce(nonce, idToken.getNonce(), mapper);
|
||||
refreshToken = oauth.parseRefreshToken(response.getRefreshToken());
|
||||
checkNonce(nonce, refreshToken.getNonce(), mapper);
|
||||
|
||||
testIntrospection(response.getAccessToken(), nonce, mapper);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue