diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java index f3f54116b8..9ab18a79d5 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCWellKnownProvider.java @@ -112,7 +112,7 @@ public class OIDCWellKnownProvider implements WellKnownProvider { config.setClaimsSupported(DEFAULT_CLAIMS_SUPPORTED); config.setClaimTypesSupported(DEFAULT_CLAIM_TYPES_SUPPORTED); - config.setClaimsParameterSupported(false); + config.setClaimsParameterSupported(true); List scopes = realm.getClientScopes(); List scopeNames = new LinkedList<>(); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/mappers/ClaimsParameterTokenMapper.java b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ClaimsParameterTokenMapper.java new file mode 100644 index 0000000000..4ce1140945 --- /dev/null +++ b/services/src/main/java/org/keycloak/protocol/oidc/mappers/ClaimsParameterTokenMapper.java @@ -0,0 +1,139 @@ +/* + * Copyright 2020 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.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.keycloak.models.ClientSessionContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.ProtocolMapperModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCWellKnownProvider; +import org.keycloak.provider.ProviderConfigProperty; +import org.keycloak.representations.IDToken; +import org.keycloak.util.JsonSerialization; +import org.keycloak.util.TokenUtil; + +import com.fasterxml.jackson.databind.JsonNode; + +public class ClaimsParameterTokenMapper extends AbstractOIDCProtocolMapper implements OIDCIDTokenMapper, UserInfoTokenMapper { + + public static final String PROVIDER_ID = "oidc-claims-param-token-mapper"; + + private static final List configProperties = new ArrayList<>(); + + static { + OIDCAttributeMapperHelper.addIncludeInTokensConfig(configProperties, ClaimsParameterTokenMapper.class); + } + + @Override + public String getDisplayCategory() { + return TOKEN_MAPPER_CATEGORY; + } + + @Override + public String getDisplayType() { + return "Claims parameter Token"; + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getHelpText() { + return "Claims specified by Claims parameter are put into tokens."; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + protected void setClaim(IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession, KeycloakSession keycloakSession, ClientSessionContext clientSessionCtx) { + String claims = clientSessionCtx.getClientSession().getNote(OIDCLoginProtocol.CLAIMS_PARAM); + if (claims == null) return; + + if (TokenUtil.TOKEN_TYPE_ID.equals(token.getType())) { + // ID Token + putClaims("id_token", claims, token, mappingModel, userSession); + } else { + // UserInfo + putClaims("userinfo", claims, token, mappingModel, userSession); + } + } + + private void putClaims(String tokenType, String claims, IDToken token, ProtocolMapperModel mappingModel, UserSessionModel userSession) { + JsonNode requestParams = null; + + try { + requestParams = JsonSerialization.readValue(claims, JsonNode.class); + } catch (IOException e) { + return; + } + if (!requestParams.has(tokenType)) return; + + JsonNode tokenNode = requestParams.findValue(tokenType); + + OIDCWellKnownProvider.DEFAULT_CLAIMS_SUPPORTED.stream() + .filter(i->tokenNode.has(i)) + .filter(i->tokenNode.findValue(i).has("essential")) + .filter(i->tokenNode.findValue(i).findValue("essential").isBoolean()) + .filter(i->tokenNode.findValue(i).findValue("essential").asBoolean()) + .forEach(i -> { + // insert claim to Token + // "aud", "sub", "iss", "auth_time", "acr" are set as default. + // "name", "given_name", "family_name", "preferred_username", "email" need to be set explicitly using existing mapper. + if (i.equals(IDToken.NAME)) { + FullNameMapper fullNameMapper = new FullNameMapper(); + fullNameMapper.setClaim(token, mappingModel, userSession); + } else if (i.equals(IDToken.GIVEN_NAME)) { + UserPropertyMapper userPropertyMapper = new UserPropertyMapper(); + userPropertyMapper.setClaim(token, UserPropertyMapper.createClaimMapper("requested firstName", "firstName", IDToken.GIVEN_NAME, "String", false, true), userSession); + } else if (i.equals(IDToken.FAMILY_NAME)) { + UserPropertyMapper userPropertyMapper = new UserPropertyMapper(); + userPropertyMapper.setClaim(token, UserPropertyMapper.createClaimMapper("requested lastName", "lastName", IDToken.FAMILY_NAME, "String", false, true), userSession); + } else if (i.equals(IDToken.PREFERRED_USERNAME)) { + UserPropertyMapper userPropertyMapper = new UserPropertyMapper(); + userPropertyMapper.setClaim(token, UserPropertyMapper.createClaimMapper("requested username", "username", IDToken.PREFERRED_USERNAME, "String", false, true), userSession); + } else if (i.equals(IDToken.EMAIL)) { + UserPropertyMapper userPropertyMapper = new UserPropertyMapper(); + userPropertyMapper.setClaim(token, UserPropertyMapper.createClaimMapper("requested email", "email", IDToken.EMAIL, "String", false, true), userSession); + } + }); + } + + public static ProtocolMapperModel createMapper(String name, boolean idToken, boolean userInfo) { + ProtocolMapperModel mapper = new ProtocolMapperModel(); + mapper.setName(name); + mapper.setProtocolMapper(PROVIDER_ID); + mapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL); + Map config = new HashMap(); + if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true"); + if (userInfo) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true"); + mapper.setConfig(config); + return mapper; + } + +} diff --git a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper index fe0daa2296..c9b37f933f 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper +++ b/services/src/main/resources/META-INF/services/org.keycloak.protocol.ProtocolMapper @@ -43,3 +43,4 @@ org.keycloak.protocol.docker.mapper.AllowAllDockerProtocolMapper org.keycloak.protocol.oidc.mappers.ScriptBasedOIDCProtocolMapper org.keycloak.protocol.saml.mappers.SAMLAudienceProtocolMapper org.keycloak.protocol.saml.mappers.SAMLAudienceResolveProtocolMapper +org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper \ No newline at end of file diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java index 0f259dd788..83b425fef8 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCAdvancedRequestParamsTest.java @@ -25,24 +25,33 @@ import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.OAuthErrorException; import org.keycloak.admin.client.resource.ClientResource; +import org.keycloak.admin.client.resource.ClientScopeResource; +import org.keycloak.admin.client.resource.ProtocolMappersResource; import org.keycloak.admin.client.resource.UserResource; import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator; import org.keycloak.common.util.Time; import org.keycloak.events.Details; +import org.keycloak.events.EventType; import org.keycloak.jose.jws.Algorithm; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.models.AuthenticatedClientSessionModel; import org.keycloak.models.Constants; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper; import org.keycloak.protocol.oidc.OIDCConfigAttributes; import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.mappers.ClaimsParameterTokenMapper; import org.keycloak.representations.IDToken; +import org.keycloak.representations.UserInfo; import org.keycloak.representations.idm.CertificateRepresentation; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.ClientScopeRepresentation; import org.keycloak.representations.idm.EventRepresentation; +import org.keycloak.representations.idm.ProtocolMapperRepresentation; import org.keycloak.representations.idm.RealmRepresentation; +import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.util.CertificateInfoHelper; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.Assert; @@ -60,17 +69,28 @@ import org.keycloak.testsuite.pages.OAuthGrantPage; import org.keycloak.testsuite.rest.resource.TestingOIDCEndpointsApplicationResource; import org.keycloak.testsuite.util.ClientManager; import org.keycloak.testsuite.util.OAuthClient; +import org.keycloak.testsuite.util.ProtocolMapperUtil; +import org.keycloak.testsuite.util.UserInfoClientUtil; import org.keycloak.util.JsonSerialization; +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; +import static org.keycloak.testsuite.admin.ApiUtil.findClientResourceByClientId; +import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId; +import static org.keycloak.testsuite.util.ProtocolMapperUtil.createHardcodedClaim; + import org.keycloak.testsuite.arquillian.annotation.AuthServerContainerExclude.AuthServer; /** @@ -1037,4 +1057,116 @@ public class OIDCAdvancedRequestParamsTest extends AbstractTestRealmKeycloakTest }); } + @Test + public void processClaimsRequestParamSupported() throws Exception { + String clientScopeId = null; + try { + for (ClientScopeRepresentation rep : adminClient.realm("test").clientScopes().findAll()) { + if (rep.getName().equals("profile")) { + clientScopeId = rep.getId(); + break; + } + } + findClientResourceByClientId(adminClient.realm("test"), "test-app").removeDefaultClientScope(clientScopeId); + + ClientResource app = findClientResourceByClientId(adminClient.realm("test"), "test-app"); + ProtocolMappersResource res = app.getProtocolMappers(); + res.createMapper(ModelToRepresentation.toRepresentation(ClaimsParameterTokenMapper.createMapper("claimsParameterTokenMapper", true, false))).close(); + + Map claims = ImmutableMap.of( + "id_token", ImmutableMap.of( + "email", ImmutableMap.of("essential", true), + "preferred_username", ImmutableMap.of("essential", true), + "family_name", ImmutableMap.of("essential", false), + "given_name", ImmutableMap.of("wesentlich", true), + "name", ImmutableMap.of("essential", true)), + "userinfo", ImmutableMap.of( + "preferred_username", ImmutableMap.of("essential", "Ja"), + "family_name", ImmutableMap.of("essential", true), + "given_name", ImmutableMap.of("essential", true))); + Map oidcRequest = new HashMap<>(); + + oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, "test-app"); + oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); + oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri()); + oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claims); + String request = new JWSBuilder().jsonContent(oidcRequest).none(); + + oauth = oauth.request(request); + oauth.doLogin("test-user@localhost", "password"); + EventRepresentation loginEvent = events.expectLogin().assertEvent(); + + OAuthClient.AccessTokenResponse accessTokenResponse = sendTokenRequestAndGetResponse(loginEvent); + IDToken idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken()); + assertEquals("test-user@localhost", idToken.getEmail()); + assertEquals("test-user@localhost", idToken.getPreferredUsername()); + assertNull(idToken.getFamilyName()); + assertNull(idToken.getGivenName()); + assertEquals("Tom Brady", idToken.getName()); + + Client client = ClientBuilder.newClient(); + try { + Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken()); + UserInfo userInfo = response.readEntity(UserInfo.class); + assertEquals("test-user@localhost", userInfo.getEmail()); + assertNull(userInfo.getPreferredUsername()); + assertEquals("Brady", userInfo.getFamilyName()); + assertEquals("Tom", userInfo.getGivenName()); + assertNull(userInfo.getName()); + } finally { + events.expect(EventType.USER_INFO_REQUEST).session(accessTokenResponse.getSessionState()).client("test-app").assertEvent(); + client.close(); + } + + oauth.doLogout(accessTokenResponse.getRefreshToken(), "password"); + events.expectLogout(accessTokenResponse.getSessionState()).client("test-app").clearDetails().assertEvent(); + + + claims = ImmutableMap.of( + "id_token", ImmutableMap.of( + "test_claim", ImmutableMap.of( + "essential", true)), + "access_token", ImmutableMap.of( + "email", ImmutableMap.of("essential", true), + "preferred_username", ImmutableMap.of("essential", true), + "family_name", ImmutableMap.of("essential", true), + "given_name", ImmutableMap.of("essential", true), + "name", ImmutableMap.of("essential", true))); + oidcRequest = new HashMap<>(); + oidcRequest.put(OIDCLoginProtocol.CLIENT_ID_PARAM, "test-app"); + oidcRequest.put(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, OAuth2Constants.CODE); + oidcRequest.put(OIDCLoginProtocol.REDIRECT_URI_PARAM, oauth.getRedirectUri()); + oidcRequest.put(OIDCLoginProtocol.CLAIMS_PARAM, claims); + request = new JWSBuilder().jsonContent(oidcRequest).none(); + + oauth = oauth.request(request); + oauth.doLogin("test-user@localhost", "password"); + loginEvent = events.expectLogin().assertEvent(); + + accessTokenResponse = sendTokenRequestAndGetResponse(loginEvent); + idToken = oauth.verifyIDToken(accessTokenResponse.getIdToken()); + assertEquals("test-user@localhost", idToken.getEmail()); // "email" default scope still remains + assertNull(idToken.getPreferredUsername()); + assertNull(idToken.getFamilyName()); + assertNull(idToken.getGivenName()); + assertNull(idToken.getName()); + + client = ClientBuilder.newClient(); + try { + Response response = UserInfoClientUtil.executeUserInfoRequest_getMethod(client, accessTokenResponse.getAccessToken()); + UserInfo userInfo = response.readEntity(UserInfo.class); + assertEquals("test-user@localhost", userInfo.getEmail()); + assertNull(userInfo.getPreferredUsername()); + assertNull(userInfo.getFamilyName()); + assertNull(userInfo.getGivenName()); + assertNull(userInfo.getName()); + } finally { + client.close(); + } + + } finally { + // revert "profile" default client scope + findClientResourceByClientId(adminClient.realm("test"), "test-app").addDefaultClientScope(clientScopeId); + } + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java index 775c4a7208..2c763d80b7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oidc/OIDCWellKnownProviderTest.java @@ -142,7 +142,7 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest { // Claims assertContains(oidcConfig.getClaimsSupported(), IDToken.NAME, IDToken.EMAIL, IDToken.PREFERRED_USERNAME, IDToken.FAMILY_NAME, IDToken.ACR); Assert.assertNames(oidcConfig.getClaimTypesSupported(), "normal"); - Assert.assertFalse(oidcConfig.getClaimsParameterSupported()); + Assert.assertTrue(oidcConfig.getClaimsParameterSupported()); // Scopes supported Assert.assertNames(oidcConfig.getScopesSupported(), OAuth2Constants.SCOPE_OPENID, OAuth2Constants.OFFLINE_ACCESS,