KEYCLOAK-14380 Support Requesting Claims using the claims Request Parameter

This commit is contained in:
Takashi Norimatsu 2020-07-04 06:24:05 +09:00 committed by Marek Posolda
parent 47f5b56a9a
commit 0191f91850
5 changed files with 274 additions and 2 deletions

View file

@ -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<ClientScopeModel> scopes = realm.getClientScopes();
List<String> scopeNames = new LinkedList<>();

View file

@ -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<ProviderConfigProperty> 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<ProviderConfigProperty> 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<String, String> config = new HashMap<String, String>();
if (idToken) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
if (userInfo) config.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
mapper.setConfig(config);
return mapper;
}
}

View file

@ -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

View file

@ -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<String, Object> 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<String, Object> 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);
}
}
}

View file

@ -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,