Added User-Session Note Idp mapper. (#19062)
Closes #17659 Co-authored-by: bal1imb <Artur.Baltabayev@bosch.com> Co-authored-by: Daniel Fesenmeyer <daniel.fesenmeyer@bosch.io> Co-authored-by: Sebastian Schuster <sebastian.schuster@bosch.io>
This commit is contained in:
parent
256bb84cc4
commit
33215ab6f4
5 changed files with 350 additions and 3 deletions
|
@ -82,7 +82,7 @@ public abstract class AbstractClaimMapper extends AbstractIdentityProviderMapper
|
|||
|
||||
}
|
||||
{ // search ID Token
|
||||
Object rawIdToken = context.getContextData().get(KeycloakOIDCIdentityProvider.VALIDATED_ID_TOKEN);
|
||||
Object rawIdToken = context.getContextData().get(OIDCIdentityProvider.VALIDATED_ID_TOKEN);
|
||||
JsonWebToken idToken;
|
||||
|
||||
if (rawIdToken instanceof String) {
|
||||
|
|
|
@ -0,0 +1,136 @@
|
|||
package org.keycloak.broker.oidc.mappers;
|
||||
|
||||
import static org.keycloak.utils.RegexUtils.valueMatchesRegex;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.oidc.OIDCIdentityProviderFactory;
|
||||
import org.keycloak.broker.provider.BrokeredIdentityContext;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.ProviderConfigProperty;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class ClaimToUserSessionNoteMapper extends AbstractClaimMapper {
|
||||
|
||||
private static final Logger LOG = Logger.getLogger(ClaimToUserSessionNoteMapper.class);
|
||||
|
||||
private static final String CLAIMS_PROPERTY_NAME = "claims";
|
||||
private static final String ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME = "are.claim.values.regex";
|
||||
private static final String[] COMPATIBLE_PROVIDERS = {KeycloakOIDCIdentityProviderFactory.PROVIDER_ID,
|
||||
OIDCIdentityProviderFactory.PROVIDER_ID};
|
||||
|
||||
private static final List<ProviderConfigProperty> CONFIG_PROPERTIES = new ArrayList<>();
|
||||
|
||||
private static final Set<IdentityProviderSyncMode> IDENTITY_PROVIDER_SYNC_MODES =
|
||||
new HashSet<>(Arrays.asList(IdentityProviderSyncMode.values()));
|
||||
|
||||
static {
|
||||
ProviderConfigProperty claimsProperty = new ProviderConfigProperty();
|
||||
claimsProperty.setName(CLAIMS_PROPERTY_NAME);
|
||||
claimsProperty.setLabel("Claims");
|
||||
claimsProperty.setHelpText(
|
||||
"Names and values of the claims to search for in the token. " +
|
||||
"You can reference nested claims using a '.', i.e. 'address.locality'. " +
|
||||
"To use dot (.) literally, escape it with backslash (\\.)");
|
||||
claimsProperty.setType(ProviderConfigProperty.MAP_TYPE);
|
||||
CONFIG_PROPERTIES.add(claimsProperty);
|
||||
ProviderConfigProperty isClaimValueRegexProperty = new ProviderConfigProperty();
|
||||
isClaimValueRegexProperty.setName(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME);
|
||||
isClaimValueRegexProperty.setLabel("Regex Claim Values");
|
||||
isClaimValueRegexProperty.setHelpText("If enabled, claim values are interpreted as regular expressions.");
|
||||
isClaimValueRegexProperty.setType(ProviderConfigProperty.BOOLEAN_TYPE);
|
||||
CONFIG_PROPERTIES.add(isClaimValueRegexProperty);
|
||||
}
|
||||
|
||||
public static final String PROVIDER_ID = "oidc-user-session-note-idp-mapper";
|
||||
|
||||
@Override
|
||||
public String[] getCompatibleProviders() {
|
||||
return COMPATIBLE_PROVIDERS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayCategory() {
|
||||
return "User Session";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDisplayType() {
|
||||
return "User Session Note Mapper";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHelpText() {
|
||||
return "Add every matching claim to the user session note. " +
|
||||
"This can be used together for instance with the 'User Session Note' protocol mapper configured for your " +
|
||||
"client scope or client, so that claims for 3rd party IDPs would be available in the access token sent to your client application.";
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ProviderConfigProperty> getConfigProperties() {
|
||||
return CONFIG_PROPERTIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return PROVIDER_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSyncMode(IdentityProviderSyncMode syncMode) {
|
||||
return IDENTITY_PROVIDER_SYNC_MODES.contains(syncMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void importNewUser(KeycloakSession session, RealmModel realm, UserModel user,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
addClaimsToSessionNote(mapperModel, context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateBrokeredUser(KeycloakSession session, RealmModel realm, UserModel user,
|
||||
IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
addClaimsToSessionNote(mapperModel, context);
|
||||
}
|
||||
|
||||
private void addClaimsToSessionNote(IdentityProviderMapperModel mapperModel, BrokeredIdentityContext context) {
|
||||
Map<String, String> claims = mapperModel.getConfigMap(CLAIMS_PROPERTY_NAME);
|
||||
boolean areClaimValuesRegex =
|
||||
Boolean.parseBoolean(mapperModel.getConfig().get(ARE_CLAIM_VALUES_REGEX_PROPERTY_NAME));
|
||||
|
||||
for (Map.Entry<String, String> claim : claims.entrySet()) {
|
||||
Object valueObj = getClaimValue(context, claim.getKey());
|
||||
|
||||
if (valueObj != null) {
|
||||
if (!(valueObj instanceof String)) {
|
||||
LOG.warnf(
|
||||
"Claim '%s' does not contain a string value for user with brokerUserId '%s'. " +
|
||||
"Actual value is of type '%s': %s",
|
||||
claim.getKey(),
|
||||
context.getBrokerUserId(), valueObj.getClass(), valueObj);
|
||||
continue;
|
||||
}
|
||||
|
||||
String value = (String) valueObj;
|
||||
|
||||
boolean claimValuesMatch = areClaimValuesRegex ? valueMatchesRegex(claim.getValue(), value)
|
||||
: valueEquals(claim.getValue(), value);
|
||||
|
||||
if (claimValuesMatch) {
|
||||
context.getAuthenticationSession().setUserSessionNote(claim.getKey(), value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -21,6 +21,7 @@ org.keycloak.broker.provider.HardcodedUserSessionAttributeMapper
|
|||
org.keycloak.broker.oidc.mappers.ClaimToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.AdvancedClaimToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.AdvancedClaimToGroupMapper
|
||||
org.keycloak.broker.oidc.mappers.ClaimToUserSessionNoteMapper
|
||||
org.keycloak.broker.oidc.mappers.ExternalKeycloakRoleToRoleMapper
|
||||
org.keycloak.broker.oidc.mappers.UserAttributeMapper
|
||||
org.keycloak.broker.oidc.mappers.UsernameTemplateMapper
|
||||
|
|
|
@ -568,12 +568,12 @@ public class IdentityProviderTest extends AbstractAdminTest {
|
|||
create(createRep("keycloak-oidc", "keycloak-oidc"));
|
||||
provider = realm.identityProviders().get("keycloak-oidc");
|
||||
mapperTypes = provider.getMapperTypes();
|
||||
assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper");
|
||||
assertMapperTypes(mapperTypes, "keycloak-oidc-role-to-role-idp-mapper", "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper", "oidc-user-session-note-idp-mapper");
|
||||
|
||||
create(createRep("oidc", "oidc"));
|
||||
provider = realm.identityProviders().get("oidc");
|
||||
mapperTypes = provider.getMapperTypes();
|
||||
assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper");
|
||||
assertMapperTypes(mapperTypes, "oidc-user-attribute-idp-mapper", "oidc-role-idp-mapper", "oidc-username-idp-mapper", "oidc-advanced-group-idp-mapper", "oidc-advanced-role-idp-mapper", "oidc-user-session-note-idp-mapper");
|
||||
|
||||
create(createRep("saml", "saml"));
|
||||
provider = realm.identityProviders().get("saml");
|
||||
|
|
|
@ -0,0 +1,210 @@
|
|||
package org.keycloak.testsuite.broker;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.nullValue;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.admin.client.CreatedResponseUtil;
|
||||
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||
import org.keycloak.admin.client.resource.ProtocolMappersResource;
|
||||
import org.keycloak.admin.client.resource.RealmResource;
|
||||
import org.keycloak.broker.oidc.mappers.ClaimToUserSessionNoteMapper;
|
||||
import org.keycloak.jose.jws.JWSInput;
|
||||
import org.keycloak.jose.jws.JWSInputException;
|
||||
import org.keycloak.models.IdentityProviderMapperModel;
|
||||
import org.keycloak.models.IdentityProviderMapperSyncMode;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
|
||||
import org.keycloak.protocol.oidc.mappers.UserSessionNoteMapper;
|
||||
import org.keycloak.representations.AccessToken;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.IdentityProviderMapperRepresentation;
|
||||
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class OidcClaimToUserSessionNoteMapperTest extends AbstractIdentityProviderMapperTest {
|
||||
|
||||
private static final String CLAIM_NAME = "sessionNoteTest";
|
||||
private static final String CLAIM_VALUE = "foo";
|
||||
private static final String CONFIG_PROPERTY_CLAIMS = "claims";
|
||||
|
||||
private static final String HARD_CODED_CLAIM_CONFIG_PROPERTY_CLAIM_VALUE = "claim.value";
|
||||
|
||||
private ClientRepresentation consumerClientRep;
|
||||
private String providerClientUuid;
|
||||
private String providerHardcodedClaimMapperId;
|
||||
|
||||
@Override
|
||||
protected BrokerConfiguration getBrokerConfiguration() {
|
||||
return new KcOidcBrokerConfiguration();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
RealmResource consumerRealm = adminClient.realm(bc.consumerRealmName());
|
||||
|
||||
consumerClientRep = consumerRealm.clients().findByClientId("broker-app").get(0);
|
||||
|
||||
setupIdentityProvider();
|
||||
// initialize the user with firstName and lastName, to avoid having to complete account data after login
|
||||
createUserInProviderRealm(Map.of(
|
||||
UserModel.FIRST_NAME, Collections.singletonList("FIRST NAME"),
|
||||
UserModel.LAST_NAME, Collections.singletonList("LAST NAME")));
|
||||
|
||||
ProtocolMapperRepresentation consumerSessionNoteToClaimMapper = new ProtocolMapperRepresentation();
|
||||
consumerSessionNoteToClaimMapper.setName("Session Note To Claim");
|
||||
consumerSessionNoteToClaimMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
consumerSessionNoteToClaimMapper.setProtocolMapper(UserSessionNoteMapper.PROVIDER_ID);
|
||||
consumerSessionNoteToClaimMapper.setConfig(Map.of("user.session.note", CLAIM_NAME, "claim.name", CLAIM_NAME,
|
||||
"access.token.claim", "true"));
|
||||
CreatedResponseUtil.getCreatedId(consumerRealm.clients().get(consumerClientRep.getId()).getProtocolMappers()
|
||||
.createMapper(consumerSessionNoteToClaimMapper));
|
||||
|
||||
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||
ClientRepresentation providerClientRep = providerRealm.clients().findByClientId("brokerapp").get(0);
|
||||
providerClientUuid = providerClientRep.getId();
|
||||
|
||||
ProtocolMapperRepresentation providerHardcodedClaimMapper = new ProtocolMapperRepresentation();
|
||||
providerHardcodedClaimMapper.setName("Hardcoded Claim");
|
||||
providerHardcodedClaimMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
|
||||
providerHardcodedClaimMapper.setProtocolMapper(HardcodedClaim.PROVIDER_ID);
|
||||
providerHardcodedClaimMapper.setConfig(Map.of("claim.name", CLAIM_NAME,
|
||||
HARD_CODED_CLAIM_CONFIG_PROPERTY_CLAIM_VALUE, CLAIM_VALUE, "access.token.claim", "true"));
|
||||
providerHardcodedClaimMapperId = CreatedResponseUtil
|
||||
.getCreatedId(providerRealm.clients().get(providerClientRep.getId()).getProtocolMappers()
|
||||
.createMapper(providerHardcodedClaimMapper));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void claimIsPropagatedOnFirstLoginOnlyWhenNameMatchesAndSyncModeIsImport() {
|
||||
createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode.IMPORT, CLAIM_VALUE);
|
||||
|
||||
AccessToken accessToken = login();
|
||||
|
||||
assertThat(accessToken.getOtherClaims().get(CLAIM_NAME), equalTo(CLAIM_VALUE));
|
||||
|
||||
logout();
|
||||
|
||||
AccessToken accessTokenSecondLogin = login();
|
||||
|
||||
// claim should still have a value, because mapping is only applied on import
|
||||
assertThat(accessTokenSecondLogin.getOtherClaims().get(CLAIM_NAME), nullValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void claimIsPropagatedOnAllLoginsWhenNameMatchesAndSyncModeIsForce() {
|
||||
IdentityProviderMapperRepresentation userSessionNoteIdpMapper =
|
||||
createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode.FORCE, CLAIM_VALUE);
|
||||
|
||||
AccessToken accessTokenFirstLogin = login();
|
||||
|
||||
assertThat(accessTokenFirstLogin.getOtherClaims().get(CLAIM_NAME), equalTo(CLAIM_VALUE));
|
||||
|
||||
logout();
|
||||
|
||||
String updatedClaimValue = "updated-claim-value";
|
||||
updateProviderHardcodedClaimMapper(updatedClaimValue);
|
||||
updateUserSessionNoteIdpMapper(userSessionNoteIdpMapper, updatedClaimValue);
|
||||
|
||||
AccessToken accessTokenSecondLogin = login();
|
||||
|
||||
assertThat(accessTokenSecondLogin.getOtherClaims().get(CLAIM_NAME), equalTo(updatedClaimValue));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void claimIsNotPropagatedWhenNameDoesNotMatch() {
|
||||
createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode.IMPORT, "something-unexpected");
|
||||
|
||||
AccessToken accessToken = login();
|
||||
|
||||
assertThat(accessToken.getOtherClaims().get(CLAIM_NAME), nullValue());
|
||||
}
|
||||
|
||||
private void logout() {
|
||||
logoutFromRealm(getConsumerRoot(), bc.consumerRealmName());
|
||||
}
|
||||
|
||||
private AccessToken login() {
|
||||
OAuthClient.AuthorizationEndpointResponse authzResponse = oauth.realm(bc.consumerRealmName())
|
||||
.clientId("broker-app")
|
||||
.redirectUri(getAuthServerRoot() + "realms/" + bc.consumerRealmName() + "/account")
|
||||
.doLoginSocial(bc.getIDPAlias(), bc.getUserLogin(), bc.getUserPassword());
|
||||
|
||||
String code = authzResponse.getCode();
|
||||
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, consumerClientRep.getSecret());
|
||||
return toAccessToken(response.getAccessToken());
|
||||
}
|
||||
|
||||
private AccessToken toAccessToken(String encoded) {
|
||||
AccessToken accessToken;
|
||||
|
||||
try {
|
||||
accessToken = new JWSInput(encoded).readJsonContent(AccessToken.class);
|
||||
} catch (JWSInputException cause) {
|
||||
throw new RuntimeException("Failed to deserialize token", cause);
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
private IdentityProviderMapperRepresentation createUserSessionNoteIdpMapper(IdentityProviderMapperSyncMode syncMode,
|
||||
String matchingValue) {
|
||||
IdentityProviderMapperRepresentation mapper = new IdentityProviderMapperRepresentation();
|
||||
mapper.setName("User Session Note Idp Mapper");
|
||||
mapper.setIdentityProviderMapper(ClaimToUserSessionNoteMapper.PROVIDER_ID);
|
||||
|
||||
mapper.setConfig(ImmutableMap.<String, String> builder()
|
||||
.put(IdentityProviderMapperModel.SYNC_MODE, syncMode.toString())
|
||||
.put(CONFIG_PROPERTY_CLAIMS, createClaimsConfig(matchingValue))
|
||||
.build());
|
||||
|
||||
return persistMapper(mapper);
|
||||
}
|
||||
|
||||
private String createClaimsConfig(String matchingValue) {
|
||||
return "[{\"key\":\"" + CLAIM_NAME + "\",\"value\":\"" + matchingValue + "\"}]";
|
||||
}
|
||||
|
||||
private void updateProviderHardcodedClaimMapper(String value) {
|
||||
RealmResource providerRealm = adminClient.realm(bc.providerRealmName());
|
||||
ProtocolMappersResource clientProtocolMappersResource =
|
||||
providerRealm.clients().get(providerClientUuid).getProtocolMappers();
|
||||
ProtocolMapperRepresentation mapper =
|
||||
clientProtocolMappersResource.getMapperById(providerHardcodedClaimMapperId);
|
||||
Map<String, String> existingConfig = mapper.getConfig();
|
||||
Map<String, String> newConfig = existingConfig == null ? new HashMap<>() : existingConfig;
|
||||
newConfig.put(HARD_CODED_CLAIM_CONFIG_PROPERTY_CLAIM_VALUE, value);
|
||||
mapper.setConfig(newConfig);
|
||||
|
||||
clientProtocolMappersResource.update(mapper.getId(), mapper);
|
||||
}
|
||||
|
||||
|
||||
private void updateUserSessionNoteIdpMapper(IdentityProviderMapperRepresentation mapper, String matchingValue) {
|
||||
Map<String, String> existingConfig = mapper.getConfig();
|
||||
Map<String, String> newConfig = existingConfig == null ? new HashMap<>() : existingConfig;
|
||||
newConfig.put(CONFIG_PROPERTY_CLAIMS, createClaimsConfig(matchingValue));
|
||||
mapper.setConfig(newConfig);
|
||||
|
||||
IdentityProviderResource idpResource = realm.identityProviders().get(bc.getIDPAlias());
|
||||
idpResource.update(mapper.getId(), mapper);
|
||||
}
|
||||
|
||||
private IdentityProviderMapperRepresentation persistMapper(IdentityProviderMapperRepresentation idpMapper) {
|
||||
String idpAlias = bc.getIDPAlias();
|
||||
IdentityProviderResource idpResource = realm.identityProviders().get(idpAlias);
|
||||
idpMapper.setIdentityProviderAlias(idpAlias);
|
||||
|
||||
String createdId = CreatedResponseUtil.getCreatedId(idpResource.addMapper(idpMapper));
|
||||
return idpResource.getMapperById(createdId);
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue