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,