diff --git a/docs/documentation/server_admin/topics/identity-broker/configuration.adoc b/docs/documentation/server_admin/topics/identity-broker/configuration.adoc index d86e9a433ee8..155d828ed2c5 100644 --- a/docs/documentation/server_admin/topics/identity-broker/configuration.adoc +++ b/docs/documentation/server_admin/topics/identity-broker/configuration.adoc @@ -76,4 +76,7 @@ Although each type of identity provider has its configuration options, all share |Sync Mode |Strategy to update user information from the identity provider through mappers. When choosing *legacy*, {project_name} used the current behavior. *Import* does not update user data and *force* updates user data when possible. See <<_mappers, Identity Provider Mappers>> for more information. + +|Case-sensitive username +|If enabled, the original username from the identity provider is kept as is when federating users. Otherwise, the username from the identity provider is lower-cased and might not match the original value if it is case-sensitive. This setting only affects the username associated with the federated identity as usernames in the server are always in lower-case. |=== diff --git a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties index 869dc0eb640e..a06d863e9c28 100644 --- a/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties +++ b/js/apps/admin-ui/maven-resources/theme/keycloak.v2/admin/messages/messages_en.properties @@ -3134,3 +3134,5 @@ logo=Logo avatarImage=Avatar image organizationsEnabled=Organizations organizationsEnabledHelp=If enabled, allows managing organizations. Otherwise, existing organizations are still kept but you will not be able to manage them anymore or authenticate their members. +caseSensitiveOriginalUsername=Case-sensitive username +caseSensitiveOriginalUsernameHelp=If enabled, the original username from the identity provider is kept as is when federating users. Otherwise, the username from the identity provider is lower-cased and might not match the original value if it is case-sensitive. This setting only affects the username associated with the federated identity as usernames in the server are always in lower-case. \ No newline at end of file diff --git a/js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx b/js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx index 5c7248cc3e88..81023fb4543a 100644 --- a/js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx +++ b/js/apps/admin-ui/src/identity-providers/add/AdvancedSettings.tsx @@ -282,6 +282,10 @@ export const AdvancedSettings = ({ isOIDC, isSAML }: AdvancedSettingsProps) => { }} /> )} + ); }; diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index af318589835c..0722234001c4 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -168,7 +168,7 @@ public void addFederatedIdentity(RealmModel realm, UserModel user, FederatedIden entity.setRealmId(realm.getId()); entity.setIdentityProvider(identity.getIdentityProvider()); entity.setUserId(identity.getUserId()); - entity.setUserName(identity.getUserName().toLowerCase()); + entity.setUserName(identity.getUserName()); entity.setToken(identity.getToken()); UserEntity userEntity = em.getReference(UserEntity.class, user.getId()); entity.setUser(userEntity); diff --git a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java index dc18b6dee3e2..61e02ee10a87 100755 --- a/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java +++ b/server-spi-private/src/main/java/org/keycloak/broker/provider/BrokeredIdentityContext.java @@ -16,6 +16,8 @@ */ package org.keycloak.broker.provider; +import static java.util.Optional.ofNullable; + import org.keycloak.models.Constants; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.UserSessionModel; @@ -51,12 +53,13 @@ public class BrokeredIdentityContext { private Map contextData = new HashMap<>(); private AuthenticationSessionModel authenticationSession; - public BrokeredIdentityContext(String id) { + public BrokeredIdentityContext(String id, IdentityProviderModel idpConfig) { if (id == null) { throw new RuntimeException("No identifier provider for identity."); } this.id = id; + this.idpConfig = idpConfig; } public String getId() { @@ -86,7 +89,11 @@ public void setLegacyId(String legacyId) { * @return */ public String getUsername() { - return username; + if (getIdpConfig().isCaseSensitiveOriginalUsername()) { + return username; + } + + return username == null ? null : username.toLowerCase(); } public void setUsername(String username) { @@ -142,10 +149,6 @@ public IdentityProviderModel getIdpConfig() { return idpConfig; } - public void setIdpConfig(IdentityProviderModel idpConfig) { - this.idpConfig = idpConfig; - } - public IdentityProvider getIdp() { return idp; } diff --git a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java index a7692d6a9f8a..56ff725fa28f 100755 --- a/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java +++ b/server-spi/src/main/java/org/keycloak/models/IdentityProviderModel.java @@ -44,6 +44,7 @@ public class IdentityProviderModel implements Serializable { public static final String CLAIM_FILTER_VALUE = "claimFilterValue"; public static final String DO_NOT_STORE_USERS = "doNotStoreUsers"; public static final String METADATA_DESCRIPTOR_URL = "metadataDescriptorUrl"; + public static final String CASE_SENSITIVE_ORIGINAL_USERNAME = "caseSensitiveOriginalUsername"; private String internalId; @@ -321,6 +322,14 @@ public void setMetadataDescriptorUrl(String metadataDescriptorUrl) { getConfig().put(METADATA_DESCRIPTOR_URL, metadataDescriptorUrl); } + public boolean isCaseSensitiveOriginalUsername() { + return Boolean.parseBoolean(getConfig().getOrDefault(CASE_SENSITIVE_ORIGINAL_USERNAME, Boolean.FALSE.toString())); + } + + public void setCaseSensitiveOriginalUsername(boolean caseSensitive) { + getConfig().put(CASE_SENSITIVE_ORIGINAL_USERNAME, Boolean.valueOf(caseSensitive).toString()); + } + @Override public int hashCode() { int hash = 5; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java index e8c77941bec3..aaf5d0e93600 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/broker/util/SerializedBrokeredIdentityContext.java @@ -264,7 +264,14 @@ public String getFirstAttribute(String name) { } public BrokeredIdentityContext deserialize(KeycloakSession session, AuthenticationSessionModel authSession) { - BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId()); + RealmModel realm = authSession.getRealm(); + IdentityProviderModel idpConfig = realm.getIdentityProviderByAlias(getIdentityProviderId()); + + if (idpConfig == null) { + throw new ModelException("Can't find identity provider with ID " + getIdentityProviderId() + " in realm " + realm.getName()); + } + + BrokeredIdentityContext ctx = new BrokeredIdentityContext(getId(), idpConfig); ctx.setUsername(getBrokerUsername()); ctx.setModelUsername(getModelUsername()); @@ -275,13 +282,7 @@ public BrokeredIdentityContext deserialize(KeycloakSession session, Authenticati ctx.setBrokerUserId(getBrokerUserId()); ctx.setToken(getToken()); - RealmModel realm = authSession.getRealm(); - IdentityProviderModel idpConfig = realm.getIdentityProviderByAlias(getIdentityProviderId()); - if (idpConfig == null) { - throw new ModelException("Can't find identity provider with ID " + getIdentityProviderId() + " in realm " + realm.getName()); - } IdentityProvider idp = IdentityBrokerService.getIdentityProvider(session, realm, idpConfig.getAlias()); - ctx.setIdpConfig(idpConfig); ctx.setIdp(idp); IdentityProviderDataMarshaller serializer = idp.getMarshaller(); diff --git a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java index 1ac07b445e88..d4307717e9fe 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/AbstractOAuth2IdentityProvider.java @@ -562,7 +562,6 @@ public Response authResponse(@QueryParam(AbstractOAuth2IdentityProvider.OAUTH2_P if (federatedIdentity.getToken() == null)federatedIdentity.setToken(response); } - federatedIdentity.setIdpConfig(providerConfig); federatedIdentity.setIdp(provider); federatedIdentity.setAuthenticationSession(authSession); @@ -712,7 +711,6 @@ final public BrokeredIdentityContext exchangeExternal(EventBuilder event, Multiv BrokeredIdentityContext context = exchangeExternalImpl(event, params); if (context != null) { context.setIdp(this); - context.setIdpConfig(getConfig()); } return context; } diff --git a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java index 215763ef9c18..87b4685584d4 100755 --- a/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java +++ b/services/src/main/java/org/keycloak/broker/oidc/OIDCIdentityProvider.java @@ -469,7 +469,7 @@ protected boolean isAuthTimeExpired(JsonWebToken idToken, AuthenticationSessionM protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException { String id = idToken.getSubject(); - BrokeredIdentityContext identity = new BrokeredIdentityContext(id); + BrokeredIdentityContext identity = new BrokeredIdentityContext(id, getConfig()); String name = (String) idToken.getOtherClaims().get(IDToken.NAME); String givenName = (String)idToken.getOtherClaims().get(IDToken.GIVEN_NAME); String familyName = (String)idToken.getOtherClaims().get(IDToken.FAMILY_NAME); @@ -791,7 +791,7 @@ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, event.error(Errors.INVALID_TOKEN); throw new ErrorResponseException(OAuthErrorException.INVALID_TOKEN, "invalid token", Response.Status.BAD_REQUEST); } - BrokeredIdentityContext identity = new BrokeredIdentityContext(id); + BrokeredIdentityContext identity = new BrokeredIdentityContext(id, getConfig()); String name = getJsonProperty(userInfo, "name"); String preferredUsername = getUsernameFromUserInfo(userInfo); @@ -881,7 +881,6 @@ final protected BrokeredIdentityContext validateJwt(EventBuilder event, String s } context.getContextData().put(EXCHANGE_PROVIDER, getConfig().getAlias()); context.setIdp(this); - context.setIdpConfig(getConfig()); return context; } catch (IOException e) { logger.debug("Unable to extract identity from identity token", e); diff --git a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java index 49d747e6f7df..955229c4ce95 100755 --- a/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java +++ b/services/src/main/java/org/keycloak/broker/saml/SAMLEndpoint.java @@ -550,7 +550,7 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h } //Map notes = new HashMap<>(); - BrokeredIdentityContext identity = new BrokeredIdentityContext(principal); + BrokeredIdentityContext identity = new BrokeredIdentityContext(principal, config); identity.getContextData().put(SAML_LOGIN_RESPONSE, responseType); identity.getContextData().put(SAML_ASSERTION, assertion); identity.setAuthenticationSession(authSession); @@ -601,7 +601,6 @@ protected Response handleLoginResponse(String samlResponse, SAMLDocumentHolder h String brokerUserId = config.getAlias() + "." + principal; identity.setBrokerUserId(brokerUserId); - identity.setIdpConfig(config); identity.setIdp(provider); if (authn != null && authn.getSessionIndex() != null) { identity.setBrokerSessionId(config.getAlias() + "." + authn.getSessionIndex()); diff --git a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java index 9da0dc31d607..964363b78406 100755 --- a/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/bitbucket/BitbucketIdentityProvider.java @@ -132,13 +132,10 @@ protected BrokeredIdentityContext validateExternalTokenThroughUserInfo(EventBuil } private BrokeredIdentityContext extractUserInfo(String subjectToken, JsonNode profile) { - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id")); - - + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "account_id"), getConfig()); String username = getJsonProperty(profile, "username"); user.setUsername(username); user.setName(getJsonProperty(profile, "display_name")); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java index 762b615f2b71..fec20aa69c62 100755 --- a/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/facebook/FacebookIdentityProvider.java @@ -74,7 +74,7 @@ protected String getProfileEndpointForValidation(EventBuilder event) { protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) { String id = getJsonProperty(profile, "id"); - BrokeredIdentityContext user = new BrokeredIdentityContext(id); + BrokeredIdentityContext user = new BrokeredIdentityContext(id, getConfig()); String email = getJsonProperty(profile, "email"); @@ -101,7 +101,6 @@ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, user.setFirstName(firstName); user.setLastName(lastName); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java index 4d80d9b15d9e..85d9fd5e1f54 100755 --- a/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/github/GitHubIdentityProvider.java @@ -121,13 +121,12 @@ protected String getProfileEndpointForValidation(EventBuilder event) { @Override protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) { - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id")); + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "id"), getConfig()); String username = getJsonProperty(profile, "login"); user.setUsername(username); user.setName(getJsonProperty(profile, "name")); user.setEmail(getJsonProperty(profile, "email")); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java index 28ed96f17f68..d91f73b7930c 100755 --- a/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/gitlab/GitLabIdentityProvider.java @@ -107,7 +107,7 @@ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, private BrokeredIdentityContext gitlabExtractFromProfile(JsonNode profile) { String id = getJsonProperty(profile, "id"); - BrokeredIdentityContext identity = new BrokeredIdentityContext(id); + BrokeredIdentityContext identity = new BrokeredIdentityContext(id, getConfig()); String name = getJsonProperty(profile, "name"); String preferredUsername = getJsonProperty(profile, "username"); diff --git a/services/src/main/java/org/keycloak/social/instagram/InstagramIdentityProvider.java b/services/src/main/java/org/keycloak/social/instagram/InstagramIdentityProvider.java index f46067826398..88602d4aba01 100755 --- a/services/src/main/java/org/keycloak/social/instagram/InstagramIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/instagram/InstagramIdentityProvider.java @@ -66,9 +66,8 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { String username = getJsonProperty(profile, "username"); String legacyId = getJsonProperty(profile, LEGACY_ID_FIELD); - BrokeredIdentityContext user = new BrokeredIdentityContext(id); + BrokeredIdentityContext user = new BrokeredIdentityContext(id, getConfig()); user.setUsername(username); - user.setIdpConfig(getConfig()); user.setIdp(this); if (legacyId != null && !legacyId.isEmpty()) { user.setLegacyId(legacyId); diff --git a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java index 6595fb19b126..a9466dd9f7e5 100755 --- a/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/microsoft/MicrosoftIdentityProvider.java @@ -86,7 +86,7 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { @Override protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) { String id = getJsonProperty(profile, "id"); - BrokeredIdentityContext user = new BrokeredIdentityContext(id); + BrokeredIdentityContext user = new BrokeredIdentityContext(id, getConfig()); String email = getJsonProperty(profile, "mail"); if (email == null && profile.has("userPrincipalName")) { @@ -100,7 +100,6 @@ protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, user.setLastName(getJsonProperty(profile, "surname")); if (email != null) user.setEmail(email); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java index ff7ddb79c222..b633d1a0f906 100644 --- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java @@ -58,10 +58,9 @@ protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { private BrokeredIdentityContext extractUserContext(JsonNode profile) { JsonNode metadata = profile.get("metadata"); - final BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(metadata, "uid")); + final BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(metadata, "uid"), getConfig()); user.setUsername(getJsonProperty(metadata, "name")); user.setName(getJsonProperty(profile, "fullName")); - user.setIdpConfig(getConfig()); user.setIdp(this); return user; } diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProvider.java index c4dbabe43e5b..6ca0eef9d33c 100644 --- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV4IdentityProvider.java @@ -94,10 +94,9 @@ private BrokeredIdentityContext extractUserContext(JsonNode profile) { getJsonProperty(metadata, "uid") != null ? getJsonProperty(metadata, "uid") : tryGetKubeAdmin(metadata) - ); + , getConfig()); user.setUsername(getJsonProperty(metadata, "name")); user.setName(getJsonProperty(profile, "fullName")); - user.setIdpConfig(getConfig()); user.setIdp(this); return user; } diff --git a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java index 17a8353bf995..264e2925ced3 100644 --- a/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/paypal/PayPalIdentityProvider.java @@ -58,12 +58,11 @@ protected String getProfileEndpointForValidation(EventBuilder event) { @Override protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode profile) { - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id")); + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"), getConfig()); user.setUsername(getJsonProperty(profile, "email")); user.setName(getJsonProperty(profile, "name")); user.setEmail(getJsonProperty(profile, "email")); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java index b44de94d2685..4a90e621ae54 100755 --- a/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/stackoverflow/StackoverflowIdentityProvider.java @@ -76,14 +76,13 @@ protected SimpleHttp buildUserInfoRequest(String subjectToken, String userInfoUr protected BrokeredIdentityContext extractIdentityFromProfile(EventBuilder event, JsonNode node) { JsonNode profile = node.get("items").get(0); - BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id")); + BrokeredIdentityContext user = new BrokeredIdentityContext(getJsonProperty(profile, "user_id"), getConfig()); String username = extractUsernameFromProfileURL(getJsonProperty(profile, "link")); user.setUsername(username); user.setName(unescapeHtml3(getJsonProperty(profile, "display_name"))); // email is not provided // user.setEmail(getJsonProperty(profile, "email")); - user.setIdpConfig(getConfig()); user.setIdp(this); AbstractJsonUserAttributeMapper.storeUserProfileForMapper(user, profile, getConfig().getAlias()); diff --git a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 58464ed6daa4..7f42c4a035b4 100755 --- a/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/services/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -222,7 +222,7 @@ public Response authResponse(@QueryParam("state") String state, .build(); User twitterUser = twitter.v1().users().verifyCredentials(); - BrokeredIdentityContext identity = new BrokeredIdentityContext(Long.toString(twitterUser.getId())); + BrokeredIdentityContext identity = new BrokeredIdentityContext(Long.toString(twitterUser.getId()), providerConfig); identity.setIdp(provider); identity.setUsername(twitterUser.getScreenName()); @@ -244,7 +244,6 @@ public Response authResponse(@QueryParam("state") String state, } identity.getContextData().put(IdentityProvider.FEDERATED_ACCESS_TOKEN, token); - identity.setIdpConfig(providerConfig); identity.setAuthenticationSession(authSession); return callback.authenticated(identity); diff --git a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java index b7643aeb3320..dc19a94de673 100755 --- a/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java +++ b/services/src/test/java/org/keycloak/test/broker/oidc/AbstractOAuth2IdentityProviderTest.java @@ -147,7 +147,7 @@ protected String getDefaultScopes() { } protected BrokeredIdentityContext doGetFederatedIdentity(String accessToken) { - return new BrokeredIdentityContext(accessToken); + return new BrokeredIdentityContext(accessToken, getConfig()); }; }; diff --git a/services/src/test/java/org/keycloak/test/broker/saml/XPathAttributeMapperTest.java b/services/src/test/java/org/keycloak/test/broker/saml/XPathAttributeMapperTest.java index fee1f730c6a2..adf5cd105e0d 100644 --- a/services/src/test/java/org/keycloak/test/broker/saml/XPathAttributeMapperTest.java +++ b/services/src/test/java/org/keycloak/test/broker/saml/XPathAttributeMapperTest.java @@ -20,6 +20,7 @@ import org.keycloak.dom.saml.v2.assertion.AttributeType; import org.keycloak.dom.saml.v2.assertion.NameIDType; import org.keycloak.models.IdentityProviderMapperModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.saml.common.exceptions.ParsingException; import org.keycloak.saml.processing.core.saml.v2.util.AssertionUtil; @@ -110,7 +111,7 @@ private String testMapping(String attributeValue, String xpath, String attribute config.put(XPathAttributeMapper.ATTRIBUTE_NAME, attributeNameToSearch); config.put(XPathAttributeMapper.USER_ATTRIBUTE, attribute); config.put(XPathAttributeMapper.ATTRIBUTE_XPATH, xpath); - BrokeredIdentityContext context = new BrokeredIdentityContext("brokeredIdentityContext"); + BrokeredIdentityContext context = new BrokeredIdentityContext("brokeredIdentityContext", new IdentityProviderModel()); AssertionType assertion = AssertionUtil.createAssertion("assertionId", NameIDType.deserializeFromString("nameIDType")); AttributeStatementType statement = new AttributeStatementType(); assertion.addStatement(statement); diff --git a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/TestKeycloakOidcIdentityProviderFactory.java b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/TestKeycloakOidcIdentityProviderFactory.java index dc864adb05c7..344856615320 100644 --- a/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/TestKeycloakOidcIdentityProviderFactory.java +++ b/testsuite/integration-arquillian/servers/auth-server/services/testsuite-providers/src/main/java/org/keycloak/testsuite/broker/oidc/TestKeycloakOidcIdentityProviderFactory.java @@ -40,6 +40,7 @@ public class TestKeycloakOidcIdentityProviderFactory extends KeycloakOIDCIdentit public static final String ID = "test-keycloak-oidc"; public static final String IGNORE_MAX_AGE_PARAM = "ignore-max-age-param"; public static final String USE_SINGLE_REFRESH_TOKEN = "use-single-refresh-token"; + public static final String PREFERRED_USERNAME = "preferred-username"; public static void setIgnoreMaxAgeParam(IdentityProviderRepresentation rep) { rep.getConfig().put(IGNORE_MAX_AGE_PARAM, Boolean.TRUE.toString()); @@ -59,6 +60,11 @@ public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProv @Override public BrokeredIdentityContext getFederatedIdentity(String response) { BrokeredIdentityContext context = super.getFederatedIdentity(response); + String preferredUsername = getPreferredUsername(); + + if (preferredUsername != null) { + context.setUsername(preferredUsername); + } if (Boolean.valueOf(model.getConfig().get(USE_SINGLE_REFRESH_TOKEN))) { // refresh token will be available only in the first login. if (!usernames.add(context.getUsername())) { @@ -92,6 +98,10 @@ protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) { private boolean isIgnoreMaxAgeParam() { return Boolean.parseBoolean(model.getConfig().getOrDefault(IGNORE_MAX_AGE_PARAM, Boolean.FALSE.toString())); } + + private String getPreferredUsername() { + return model.getConfig().get(PREFERRED_USERNAME); + } }; } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java index b4038bc7b03b..3aac2006cf27 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/broker/KcOidcFirstBrokerLoginTest.java @@ -6,11 +6,13 @@ import org.keycloak.admin.client.resource.IdentityProviderResource; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.models.FederatedIdentityModel; +import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.IdentityProviderSyncMode; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.idm.ClientRepresentation; +import org.keycloak.representations.idm.FederatedIdentityRepresentation; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.testsuite.Assert; @@ -42,6 +44,8 @@ import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ADMIN_EDITABLE; import static org.keycloak.testsuite.forms.VerifyProfileTest.PERMISSIONS_ALL; +import java.util.List; + /** * @author Marek Posolda */ @@ -805,6 +809,59 @@ public void testDynamicUserProfileReview_attributeRequiredButNotSelectedByScopeI assertEquals(null, user.firstAttribute(ATTRIBUTE_DEPARTMENT)); } + @Test + public void testFederatedIdentityCaseSensitiveOriginalUsername() { + String expectedBrokeredUserName = "camelCase"; + IdentityProviderResource idp = realmsResouce().realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias()); + IdentityProviderRepresentation representation = idp.toRepresentation(); + representation.getConfig().put(TestKeycloakOidcIdentityProviderFactory.PREFERRED_USERNAME, expectedBrokeredUserName); + representation.getConfig().put(IdentityProviderModel.CASE_SENSITIVE_ORIGINAL_USERNAME, Boolean.TRUE.toString()); + idp.update(representation); + createUser(bc.providerRealmName(), expectedBrokeredUserName, BrokerTestConstants.USER_PASSWORD, "f", "l", "fl@example.org"); + + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + // the username is stored as lower-case in the provider realm local database + logInWithIdp(bc.getIDPAlias(), expectedBrokeredUserName.toLowerCase(), BrokerTestConstants.USER_PASSWORD); + + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + UserRepresentation userRepresentation = AccountHelper.getUserRepresentation(realm, expectedBrokeredUserName.toLowerCase()); + // the username is in lower case in the local database + assertEquals(userRepresentation.getUsername(), expectedBrokeredUserName.toLowerCase()); + + // the original username is preserved + List federatedIdentities = realm.users().get(userRepresentation.getId()).getFederatedIdentity(); + assertFalse(federatedIdentities.isEmpty()); + FederatedIdentityRepresentation federatedIdentity = federatedIdentities.get(0); + assertEquals(expectedBrokeredUserName, federatedIdentity.getUserName()); + } + + @Test + public void testFederatedIdentityCaseInsensitiveOriginalUsername() { + String expectedBrokeredUserName = "camelCase"; + IdentityProviderResource idp = realmsResouce().realm(bc.consumerRealmName()).identityProviders().get(bc.getIDPAlias()); + IdentityProviderRepresentation representation = idp.toRepresentation(); + representation.getConfig().put(TestKeycloakOidcIdentityProviderFactory.PREFERRED_USERNAME, expectedBrokeredUserName); + idp.update(representation); + createUser(bc.providerRealmName(), expectedBrokeredUserName, BrokerTestConstants.USER_PASSWORD, "f", "l", "fl@example.org"); + + oauth.clientId("broker-app"); + loginPage.open(bc.consumerRealmName()); + // the username is stored as lower-case in the provider realm local database + logInWithIdp(bc.getIDPAlias(), expectedBrokeredUserName.toLowerCase(), BrokerTestConstants.USER_PASSWORD); + + RealmResource realm = adminClient.realm(bc.consumerRealmName()); + UserRepresentation userRepresentation = AccountHelper.getUserRepresentation(realm, expectedBrokeredUserName.toLowerCase()); + // the username is in lower case in the local database + assertEquals(userRepresentation.getUsername(), expectedBrokeredUserName.toLowerCase()); + + // the original username is preserved + List federatedIdentities = realm.users().get(userRepresentation.getId()).getFederatedIdentity(); + assertFalse(federatedIdentities.isEmpty()); + FederatedIdentityRepresentation federatedIdentity = federatedIdentities.get(0); + assertEquals(expectedBrokeredUserName.toLowerCase(), federatedIdentity.getUserName()); + } + public void addDepartmentScopeIntoRealm() { testRealm().clientScopes().create(ClientScopeBuilder.create().name("department").protocol("openid-connect").build()); } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java index 380f29d9db01..8ce1c7109633 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/exportimport/ExportImportUtil.java @@ -251,7 +251,7 @@ public static void assertDataImportedInRealm(Keycloak adminClient, KeycloakTesti } else if ("google1".equals(federatedIdentityRep.getIdentityProvider())) { googleFound = true; Assert.assertEquals("google1", federatedIdentityRep.getUserId()); - Assert.assertEquals("mysocialuser@gmail.com", federatedIdentityRep.getUserName()); + Assert.assertEquals("mySocialUser@gmail.com", federatedIdentityRep.getUserName()); } else if ("twitter1".equals(federatedIdentityRep.getIdentityProvider())) { twitterFound = true; Assert.assertEquals("twitter1", federatedIdentityRep.getUserId()); @@ -260,6 +260,12 @@ public static void assertDataImportedInRealm(Keycloak adminClient, KeycloakTesti } Assert.assertTrue(facebookFound && twitterFound && googleFound); + // make sure the username format is the same when importing + UserResource socialUserLowercase = realmRsc.users().get(findByUsername(realmRsc, "lowercasesocialuser").getId()); + List socialLowercaseLinks = socialUserLowercase.getFederatedIdentity(); + Assert.assertEquals(1, socialLowercaseLinks.size()); + Assert.assertEquals("lowercasesocialuser@gmail.com", socialLowercaseLinks.get(0).getUserName()); + UserRepresentation foundSocialUser = testingClient.testing().getUserByFederatedIdentity(realm.getRealm(), "facebook1", "facebook1", "fbuser1"); Assert.assertEquals(foundSocialUser.getUsername(), socialUser.toRepresentation().getUsername()); Assert.assertNull(testingClient.testing().getUserByFederatedIdentity(realm.getRealm(), "facebook", "not-existing", "not-existing")); @@ -283,7 +289,7 @@ public static void assertDataImportedInRealm(Keycloak adminClient, KeycloakTesti // Test identity providers List identityProviders = realm.getIdentityProviders(); - Assert.assertEquals(3, identityProviders.size()); + Assert.assertEquals(4, identityProviders.size()); IdentityProviderRepresentation google = null; for (IdentityProviderRepresentation idpRep : identityProviders) { if (idpRep.getAlias().equals("google1")) google = idpRep; diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json index 4f7e24a476a4..beb01c42f2f8 100755 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/model/testrealm.json @@ -35,6 +35,16 @@ "clientSecret": "googleSecret" } }, + { + "providerId" : "github", + "alias" : "github1", + "enabled": true, + "config": { + "syncMode": "IMPORT", + "clientId": "googleId", + "clientSecret": "googleSecret" + } + }, { "providerId" : "facebook", "alias" : "facebook1", @@ -239,6 +249,17 @@ } ] }, + { + "username": "lowercasesocialuser", + "enabled": true, + "federatedIdentities": [ + { + "identityProvider": "github1", + "userId": "github1", + "userName": "lowercasesocialuser@gmail.com" + } + ] + }, { "username": "service-account-otherapp", "enabled": true,