Conditional login through identity provider (#20188)

Closes #20191


Co-authored-by: Jon Koops <jonkoops@gmail.com>
Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
Co-authored-by: Marek Posolda <mposolda@gmail.com>
This commit is contained in:
Daniele Martinoli 2023-06-29 18:44:15 +02:00 committed by GitHub
parent e945a056bb
commit e2ac9487f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 357 additions and 7 deletions

View file

@ -129,6 +129,14 @@ q=<name>:<value> <name>:<value> ...
Where `<name>` and `<value>` represent the attribute name and value, respectively.
= Essential claim configuration in OpenID Connect identity providers
OpenID Connect identity providers support a new configuration to specify that the ID tokens issued by the identity provider must have a specific claim,
otherwise the user can not authenticate through this broker.
The option is disabled by default; when it is enabled, you can specify the name of the JWT token claim to filter and the value to match
(supports regular expression format).
= LDAPS-only Truststore option removed
LDAP option to use truststore SPI `Only for ldaps` has been removed. This parameter is used to

View file

@ -58,6 +58,16 @@ Although each type of identity provider has its configuration options, all share
|GUI Order
|The sort order of the available identity providers on the login page.
|Verify essential claim
|When *ON*, ID tokens issued by the identity provider must have a specific claim, otherwise, the user can not authenticate through this broker
|Essential claim
|When *Verify essential claim* is *ON*, the name of the JWT token claim to filter (match is case sensitive)
|Essential claim value
|When *Verify essential claim* is *ON*, the value of the JWT token claim to match (supports regular expression format)
|First Login Flow
|The authentication flow {project_name} triggers when users use this identity provider to log into {project_name} for the first time.

View file

@ -314,7 +314,7 @@ describe("Identity provider test", () => {
listingPage.goToItemDetails("github");
advancedSettings.typeScopesInput("openid");
//advancedSettings.assertScopesInputEqual("openid"); //this line doesn't work
advancedSettings.assertScopesInputEqual("openid");
advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
@ -331,7 +331,11 @@ describe("Identity provider test", () => {
advancedSettings.clickTrustEmailSwitch();
advancedSettings.clickAccountLinkingOnlySwitch();
advancedSettings.clickHideOnLoginPageSwitch();
advancedSettings.clickEssentialClaimSwitch();
advancedSettings.typeClaimNameInput("claim-name");
advancedSettings.typeClaimValueInput("claim-value");
advancedSettings.ensureAdvancedSettingsAreVisible();
advancedSettings.assertStoreTokensSwitchTurnedOn(true);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
true
@ -340,6 +344,9 @@ describe("Identity provider test", () => {
advancedSettings.assertTrustEmailSwitchTurnedOn(true);
advancedSettings.assertAccountLinkingOnlySwitchTurnedOn(true);
advancedSettings.assertHideOnLoginPageSwitchTurnedOn(true);
advancedSettings.assertEssentialClaimSwitchTurnedOn(true);
advancedSettings.assertClaimInputEqual("claim-name");
advancedSettings.assertClaimValueInputEqual("claim-value");
cy.findByTestId("idp-details-save").click();
});
@ -355,6 +362,7 @@ describe("Identity provider test", () => {
);
advancedSettings.clickStoreTokensSwitch();
advancedSettings.clickAcceptsPromptNoneForwardFromClientSwitch();
advancedSettings.ensureAdvancedSettingsAreVisible();
advancedSettings.assertStoreTokensSwitchTurnedOn(false);
advancedSettings.assertAcceptsPromptNoneForwardFromClientSwitchTurnedOn(
false

View file

@ -69,6 +69,9 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
private firstLoginFlowSelect = "#firstBrokerLoginFlowAlias";
private postLoginFlowSelect = "#postBrokerLoginFlowAlias";
private syncModeSelect = "#syncMode";
private essentialClaimSwitch = "#filteredByClaim";
private claimNameInput = "#kc-claim-filter-name";
private claimValueInput = "#kc-claim-filter-value";
private addBtn = "createProvider";
private saveBtn = "idp-details-save";
private revertBtn = "idp-details-revert";
@ -94,6 +97,11 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}
public ensureAdvancedSettingsAreVisible() {
cy.findByTestId("jump-link-general-settings").click();
cy.findByTestId("jump-link-advanced-settings").click();
}
public clickStoreTokensSwitch() {
cy.get(this.storeTokensSwitch).parent().click();
return this;
@ -129,6 +137,21 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}
public clickEssentialClaimSwitch() {
cy.get(this.essentialClaimSwitch).parent().click();
return this;
}
public typeClaimNameInput(text: string) {
cy.get(this.claimNameInput).type(text).blur();
return this;
}
public typeClaimValueInput(text: string) {
cy.get(this.claimValueInput).type(text).blur();
return this;
}
public selectFirstLoginFlowOption(loginFlowOption: LoginFlowOption) {
cy.get(this.firstLoginFlowSelect).click();
super.clickSelectMenuItem(
@ -182,7 +205,7 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
}
public assertScopesInputEqual(text: string) {
cy.get(this.scopesInput).should("have.text", text).parent();
cy.get(this.scopesInput).should("have.value", text).parent();
return this;
}
@ -230,6 +253,21 @@ export default class ProviderBaseGeneralSettingsPage extends PageObject {
return this;
}
public assertEssentialClaimSwitchTurnedOn(isOn: boolean) {
super.assertSwitchStateOn(cy.get(this.essentialClaimSwitch).parent(), isOn);
return this;
}
public assertClaimInputEqual(text: string) {
cy.get(this.claimNameInput).should("have.value", text).parent();
return this;
}
public assertClaimValueInputEqual(text: string) {
cy.get(this.claimValueInput).should("have.value", text).parent();
return this;
}
public assertFirstLoginFlowSelectOptionEqual(
loginFlowOption: LoginFlowOption
) {

View file

@ -38,6 +38,9 @@
"trustEmail": "If enabled, email provided by this provider is not verified even if verification is enabled for the realm.",
"accountLinkingOnly": "If true, users cannot log in through this provider. They can only link to this provider. This is useful if you don't want to allow login from the provider, but want to integrate with a provider",
"hideOnLoginPage": "If hidden, login with this provider is possible only if requested explicitly, for example using the 'kc_idp_hint' parameter.",
"filteredByClaim": "If true, ID tokens issued by the identity provider must have a specific claim. Otherwise, the user can not authenticate through this broker.",
"claimFilterName": "Name of the essential claim",
"claimFilterValue": "Value of the essential claim (with regex support)",
"firstBrokerLoginFlowAlias": "Alias of authentication flow, which is triggered after first login with this identity provider. Term 'First Login' means that no Keycloak account is currently linked to the authenticated identity provider account.",
"postBrokerLoginFlowAlias": "Alias of authentication flow, which is triggered after each login with this identity provider. Useful if you want additional verification of each user authenticated with this identity provider (for example OTP). Leave this to \"None\" if you need no any additional authenticators to be triggered after login with this identity provider. Also note that authenticator implementations must assume that user is already set in ClientSession as identity provider already set it.",
"syncMode": "Default sync mode for all mappers. The sync mode determines when user data will be synced using the mappers. Possible values are: 'legacy' to keep the behaviour before this option was introduced, 'import' to only import the user once during first login of the user with this identity provider, 'force' to always update the user during every login with this identity provider.",

View file

@ -149,6 +149,9 @@
"trustEmail": "Trust Email",
"accountLinkingOnly": "Account linking only",
"hideOnLoginPage": "Hide on login page",
"filteredByClaim": "Verify essential claim",
"claimFilterName": "Essential claim",
"claimFilterValue": "Essential claim value",
"firstBrokerLoginFlowAlias": "First login flow",
"postBrokerLoginFlowAlias": "Post login flow",
"syncMode": "Sync mode",

View file

@ -1,20 +1,25 @@
import type AuthenticationFlowRepresentation from "@keycloak/keycloak-admin-client/lib/defs/authenticationFlowRepresentation";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
Switch,
ValidatedOptions,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { Controller, useFormContext, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared";
import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch";
import type { FieldProps } from "../component/FormGroupField";
import { FormGroupField } from "../component/FormGroupField";
import { SwitchField } from "../component/SwitchField";
import { TextField } from "../component/TextField";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
const LoginFlow = ({
field,
@ -93,8 +98,18 @@ type AdvancedSettingsProps = { isOIDC: boolean; isSAML: boolean };
export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
const { t } = useTranslation("identity-providers");
const { control } = useFormContext();
const {
control,
register,
formState: { errors },
} = useFormContext<IdentityProviderRepresentation>();
const [syncModeOpen, setSyncModeOpen] = useState(false);
const filteredByClaim = useWatch({
control,
name: "config.filteredByClaim",
defaultValue: "false",
});
const claimFilterRequired = filteredByClaim === "true";
return (
<>
{!isOIDC && !isSAML && (
@ -125,6 +140,88 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => {
/>
<SwitchField field="config.hideOnLoginPage" label="hideOnLoginPage" />
{(!isSAML || isOIDC) && (
<FormGroupField label="filteredByClaim">
<Controller
name="config.filteredByClaim"
defaultValue="false"
control={control}
render={({ field }) => (
<Switch
id="filteredByClaim"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={field.value === "true"}
onChange={(value) => {
field.onChange(value.toString());
}}
/>
)}
/>
</FormGroupField>
)}
{(!isSAML || isOIDC) && claimFilterRequired && (
<>
<FormGroup
label={t("identity-providers:claimFilterName")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:claimFilterName")}
fieldLabelId="identity-providers:claimFilterName"
/>
}
fieldId="kc-claim-filter-name"
isRequired
validated={
errors.config?.claimFilterName
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
isRequired
id="kc-claim-filter-name"
data-testid="claimFilterName"
validated={
errors.config?.claimFilterName
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("config.claimFilterName", { required: true })}
/>
</FormGroup>
<FormGroup
label={t("identity-providers:claimFilterValue")}
labelIcon={
<HelpItem
helpText={t("identity-providers-help:claimFilterValue")}
fieldLabelId="identity-providers:claimFilterName"
/>
}
fieldId="kc-claim-filter-value"
isRequired
validated={
errors.config?.claimFilterValue
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<KeycloakTextInput
isRequired
id="kc-claim-filter-value"
data-testid="claimFilterValue"
validated={
errors.config?.claimFilterValue
? ValidatedOptions.error
: ValidatedOptions.default
}
{...register("config.claimFilterValue", { required: true })}
/>
</FormGroup>
</>
)}
<LoginFlow
field="firstBrokerLoginFlowAlias"
label="firstBrokerLoginFlowAlias"

View file

@ -36,6 +36,10 @@ public class IdentityProviderModel implements Serializable {
public static final String HIDE_ON_LOGIN = "hideOnLoginPage";
public static final String FILTERED_BY_CLAIMS = "filteredByClaim";
public static final String CLAIM_FILTER_NAME = "claimFilterName";
public static final String CLAIM_FILTER_VALUE = "claimFilterValue";
private String internalId;
/**
@ -254,4 +258,28 @@ public class IdentityProviderModel implements Serializable {
public void setHideOnLogin(boolean hideOnLogin) {
getConfig().put(HIDE_ON_LOGIN, String.valueOf(hideOnLogin));
}
public boolean isFilteredByClaims() {
return Boolean.valueOf(getConfig().getOrDefault(FILTERED_BY_CLAIMS, Boolean.toString(false)));
}
public void setFilteredByClaims(boolean filteredByClaims) {
getConfig().put(FILTERED_BY_CLAIMS, String.valueOf(filteredByClaims));
}
public String getClaimFilterName() {
return String.valueOf(getConfig().getOrDefault(CLAIM_FILTER_NAME, ""));
}
public void setClaimFilterName(String claimFilterName) {
getConfig().put(CLAIM_FILTER_NAME, claimFilterName);
}
public String getClaimFilterValue() {
return String.valueOf(getConfig().getOrDefault(CLAIM_FILTER_VALUE, ""));
}
public void setClaimFilterValue(String claimFilterValue) {
getConfig().put(CLAIM_FILTER_VALUE, claimFilterValue);
}
}

View file

@ -74,6 +74,9 @@ import jakarta.ws.rs.core.UriInfo;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import static org.keycloak.utils.LockObjectsForModification.lockUserSessionsForModification;
@ -389,6 +392,30 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
throw new IdentityBrokerException("Mismatch between the subject in the id_token and the subject from the user_info endpoint");
}
if (getConfig().isFilteredByClaims()) {
String filterName = getConfig().getClaimFilterName();
String filterValue = getConfig().getClaimFilterValue();
logger.tracef("Filtering user %s by %s=%s", idToken.getOtherClaims().get(getusernameClaimNameForIdToken()), filterName, filterValue);
if (idToken.getOtherClaims().containsKey(filterName)) {
Object claimObject = idToken.getOtherClaims().get(filterName);
List<String> claimValues = new ArrayList<>();
if (claimObject instanceof List) {
((List<?>)claimObject).forEach(v->claimValues.add(Objects.toString(v)));
} else {
claimValues.add(Objects.toString(claimObject));
}
logger.tracef("Found claim %s with values %s", filterName, claimValues);
if (!claimValues.stream().anyMatch(v->v.matches(filterValue))) {
logger.warnf("Claim %s has values \"%s\" that does not match the expected filter \"%s\"", filterName, claimValues, filterValue);
throw new IdentityBrokerException(String.format("Unmatched claim value for %s.", filterName));
}
} else {
logger.debugf("Claim %s was not found", filterName);
throw new IdentityBrokerException(String.format("Claim %s not found", filterName));
}
}
identity.getContextData().put(BROKER_NONCE_PARAM, idToken.getOtherClaims().get(OIDCLoginProtocol.NONCE_PARAM));
if (getConfig().isStoreToken()) {

View file

@ -36,6 +36,8 @@ public abstract class AbstractInitializedBaseBrokerTest extends AbstractBaseBrok
protected IdentityProviderResource identityProviderResource;
protected void postInitializeUser(UserRepresentation user) {}
@Override
@Before
public void beforeBrokerTest() {
@ -47,6 +49,7 @@ public abstract class AbstractInitializedBaseBrokerTest extends AbstractBaseBrok
user.setEmail(bc.getUserEmail());
user.setEmailVerified(true);
user.setEnabled(true);
postInitializeUser(user);
RealmResource realmResource = adminClient.realm(bc.providerRealmName());
userId = createUserWithAdminClient(realmResource, user);

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.broker;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Lists;
import org.apache.http.impl.client.CloseableHttpClient;
@ -23,7 +24,10 @@ import org.keycloak.models.IdentityProviderMapperSyncMode;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.protocol.ProtocolMapperUtils;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.mappers.OIDCAttributeMapperHelper;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.FederatedIdentityRepresentation;
@ -42,8 +46,10 @@ import org.keycloak.testsuite.util.WaitUtils;
import jakarta.ws.rs.core.Response;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
@ -66,10 +72,13 @@ import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot;
* Final class as it's not intended to be overriden. Feel free to remove "final" if you really know what you are doing.
*/
public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
private final static String USER_ATTRIBUTE_NAME = "user-attribute";
private final static String USER_ATTRIBUTE_VALUE = "attribute-value";
private final static String CLAIM_FILTER_REGEXP = ".*-value";
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return KcOidcBrokerConfiguration.INSTANCE;
return BROKER_CONFIG_INSTANCE;
}
@Before
@ -164,6 +173,10 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
@Test
public void loginFetchingUserFromUserEndpoint() {
loginFetchingUserFromUserEndpoint(false);
}
private void loginFetchingUserFromUserEndpoint(boolean loginIsDenied) {
RealmResource realm = realmsResouce().realm(bc.providerRealmName());
ClientsResource clients = realm.clients();
ClientRepresentation brokerApp = clients.findByClientId("brokerapp").get(0);
@ -184,7 +197,11 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
logInWithBroker(bc);
waitForPage(driver, "update account information", false);
waitForPage(driver, loginIsDenied? "We are sorry..." : "update account information", false);
if (loginIsDenied) {
return;
}
updateAccountInformationPage.assertCurrent();
Assert.assertTrue("We must be on correct realm right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/"));
@ -495,6 +512,84 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
checkUpdatedUserAttributesIdP(false);
}
@Test
public void loginWithClaimFilter() {
IdentityProviderResource identityProviderResource = getIdentityProviderResource();
IdentityProviderRepresentation identityProvider = identityProviderResource.toRepresentation();
updateIdPClaimFilter(identityProvider, identityProviderResource, true, USER_ATTRIBUTE_NAME, USER_ATTRIBUTE_VALUE);
WaitUtils.waitForPageToLoad();
loginFetchingUserFromUserEndpoint();
UserRepresentation user = getFederatedIdentity();
Assert.assertNotNull(user);
}
@Test
public void loginWithClaimRegexpFilter() {
IdentityProviderResource identityProviderResource = getIdentityProviderResource();
IdentityProviderRepresentation identityProvider = identityProviderResource.toRepresentation();
updateIdPClaimFilter(identityProvider, identityProviderResource, true, USER_ATTRIBUTE_NAME, CLAIM_FILTER_REGEXP);
WaitUtils.waitForPageToLoad();
loginFetchingUserFromUserEndpoint();
UserRepresentation user = getFederatedIdentity();
Assert.assertNotNull(user);
}
@Test
public void denyLoginWithClaimFilter() {
IdentityProviderResource identityProviderResource = getIdentityProviderResource();
IdentityProviderRepresentation identityProvider = identityProviderResource.toRepresentation();
updateIdPClaimFilter(identityProvider, identityProviderResource, true, "hardcoded-missing-claim", "hardcoded-missing-claim-value");
WaitUtils.waitForPageToLoad();
loginFetchingUserFromUserEndpoint(true);
List<UserRepresentation> users = realmsResouce().realm(bc.consumerRealmName()).users().search(bc.getUserLogin());
assertThat(users, Matchers.empty());
}
protected void postInitializeUser(UserRepresentation user) {
user.setAttributes(ImmutableMap.<String, List<String>> builder()
.put(USER_ATTRIBUTE_NAME, ImmutableList.<String> builder().add(USER_ATTRIBUTE_VALUE).build())
.build());
}
private void updateIdPClaimFilter(IdentityProviderRepresentation idProvider, IdentityProviderResource idProviderResource, boolean filteredByClaim, String claimFilterName, String claimFilterValue) {
assertThat(idProvider, Matchers.notNullValue());
assertThat(idProviderResource, Matchers.notNullValue());
assertThat(claimFilterName, Matchers.notNullValue());
assertThat(claimFilterValue, Matchers.notNullValue());
if (idProvider.getConfig().getOrDefault(IdentityProviderModel.FILTERED_BY_CLAIMS, "false").equals(Boolean.toString(filteredByClaim)) &&
idProvider.getConfig().getOrDefault(IdentityProviderModel.CLAIM_FILTER_NAME, "").equals(claimFilterName) &&
idProvider.getConfig().getOrDefault(IdentityProviderModel.CLAIM_FILTER_VALUE, "").equals(claimFilterValue)
) {
return;
}
idProvider.getConfig().put(IdentityProviderModel.FILTERED_BY_CLAIMS, Boolean.toString(filteredByClaim));
idProvider.getConfig().put(IdentityProviderModel.CLAIM_FILTER_NAME, claimFilterName);
idProvider.getConfig().put(IdentityProviderModel.CLAIM_FILTER_VALUE, claimFilterValue);
idProviderResource.update(idProvider);
idProvider = idProviderResource.toRepresentation();
assertThat("Cannot get Identity Provider", idProvider, Matchers.notNullValue());
assertThat("Filtered by claim didn't change", idProvider.getConfig().get(IdentityProviderModel.FILTERED_BY_CLAIMS), Matchers.equalTo(Boolean.toString(filteredByClaim)));
assertThat("Claim name didn't change", idProvider.getConfig().get(IdentityProviderModel.CLAIM_FILTER_NAME), Matchers.equalTo(claimFilterName));
assertThat("Claim value didn't change", idProvider.getConfig().get(IdentityProviderModel.CLAIM_FILTER_VALUE), Matchers.equalTo(claimFilterValue));
}
private void checkUpdatedUserAttributesIdP(boolean isForceSync) {
final String IDP_NAME = getBrokerConfiguration().getIDPAlias();
final String USERNAME = "demo-user";
@ -625,4 +720,34 @@ public final class KcOidcBrokerTest extends AbstractAdvancedBrokerTest {
private IdentityProviderResource getIdentityProviderResource() {
return realmsResouce().realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias());
}
private static final CustomKcOidcBrokerConfiguration BROKER_CONFIG_INSTANCE = new CustomKcOidcBrokerConfiguration();
static class CustomKcOidcBrokerConfiguration extends KcOidcBrokerConfiguration {
@Override
public List<ClientRepresentation> createProviderClients() {
List<ClientRepresentation> clients = super.createProviderClients();
ClientRepresentation client = clients.get(0);
ProtocolMapperRepresentation userAttrMapper = new ProtocolMapperRepresentation();
userAttrMapper.setName(USER_ATTRIBUTE_NAME);
userAttrMapper.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
userAttrMapper.setProtocolMapper(UserAttributeMapper.PROVIDER_ID);
Map<String, String> userAttrMapperConfig = userAttrMapper.getConfig();
userAttrMapperConfig.put(ProtocolMapperUtils.USER_ATTRIBUTE, USER_ATTRIBUTE_NAME);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.TOKEN_CLAIM_NAME, USER_ATTRIBUTE_NAME);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.JSON_TYPE, ProviderConfigProperty.STRING_TYPE);
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ACCESS_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_ID_TOKEN, "true");
userAttrMapperConfig.put(OIDCAttributeMapperHelper.INCLUDE_IN_USERINFO, "true");
userAttrMapperConfig.put(ProtocolMapperUtils.MULTIVALUED, "false");
userAttrMapperConfig.put(ProtocolMapperUtils.AGGREGATE_ATTRS, "false");
List<ProtocolMapperRepresentation> mappers = new ArrayList<>(client.getProtocolMappers());
mappers.add(userAttrMapper);
client.setProtocolMappers(mappers);
return clients;
}
}
}