KEYCLOAK-14380 Support Requesting Claims using the claims Request Parameter
This commit is contained in:
parent
47f5b56a9a
commit
0191f91850
5 changed files with 274 additions and 2 deletions
|
@ -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<>();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in a new issue