diff --git a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java index 89e7885938..895a7856a7 100644 --- a/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java +++ b/connections/jpa-liquibase/src/main/java/org/keycloak/connections/jpa/updater/liquibase/custom/JpaUpdate1_2_0_Beta1.java @@ -293,7 +293,7 @@ public class JpaUpdate1_2_0_Beta1 extends CustomKeycloakTask { } Object acmObj = resultSet.getObject("ALLOWED_CLAIMS_MASK"); - long mask = (acmObj != null) ? (Long) acmObj : ClaimMask.ALL; + long mask = (acmObj != null) ? ((Number) acmObj).longValue() : ClaimMask.ALL; MigrationProvider migrationProvider = this.kcSession.getProvider(MigrationProvider.class); List protocolMappers = migrationProvider.getMappersForClaimMask(mask); diff --git a/distribution/demo-dist/src/main/xslt/standalone.xsl b/distribution/demo-dist/src/main/xslt/standalone.xsl index bc0233a99e..fed1ee8f35 100755 --- a/distribution/demo-dist/src/main/xslt/standalone.xsl +++ b/distribution/demo-dist/src/main/xslt/standalone.xsl @@ -8,6 +8,7 @@ exclude-result-prefixes="xalan j ds k sec"> + @@ -61,6 +62,18 @@ + + + + + + + + + + + + diff --git a/docbook/reference/en/en-US/modules/clustering.xml b/docbook/reference/en/en-US/modules/clustering.xml index 2b352bb661..b1ce9b2178 100755 --- a/docbook/reference/en/en-US/modules/clustering.xml +++ b/docbook/reference/en/en-US/modules/clustering.xml @@ -49,15 +49,20 @@ For realm and users Keycloak uses a invalidation cache. An invalidation cache doesn't share any data, but simply - removes stale data from remote caches. This reduces network traffic, as well as preventing sensitive data (such as + removes stale data from remote caches and makes sure all nodes re-load data from the database when it is changed. This reduces network traffic, as well as preventing sensitive data (such as realm keys and password hashes) from being sent between the nodes. User sessions and login failures supports either distributed caches or fully replicated caches. We recommend using a distributed - cache. + cache. A distributed + cache splits user sessions into segments where each node holds one or more segment. It is possible + to replicate each segment to multiple nodes, but this is not strictly necessary since the failure of a node + will only result in users having to log in again. If you need to prevent node failures from requiring users to + log in again, set the owners attribute to 2 or more for the sessions cache + of infinispan/Keycloak container as described below. - To enable clustering in Keycloak open standalone/configuration/keycloak-server.json and add: + The infinispan container is set by default in standalone/configuration/keycloak-server.json: "connectionsInfinispan": { "default" : { @@ -66,49 +71,13 @@ } - - -
- Enable realm and user cache invalidation + As you can see in this file, the realmCache, userCache and userSession providers are configured to use infinispan by default, which applies for both cluster and non-cluster environment. - To reduce number of requests to the database Keycloak caches realm and user data. In cluster mode - Keycloak uses an Infinispan invalidation cache to make sure all nodes re-load data from the database - when it is changed. Using an invalidation cache instead of a replicated cache reduces the network traffic - generated by the cluster, but more importantly prevents sensitive data from being sent. + For non-cluster configuration (server executed with standalone.xml ) is the infinispan container infinispan/Keycloak just uses local infinispan caches for realms, users and userSessions. - To enable realm and user cache invalidation open keycloak-server.json and change - the realmCache and userCache providers to infinispan: - -"realmCache": { - "provider": "infinispan" -}, - -"userCache": { - "provider": "infinispan" -} - - -
- -
- Enable distributed user sessions - - To help distribute the load of user sessions Keycloak uses an Infinispan distributed cache. A distributed - cache splits user sessions into segments where each node holds one or more segment. It is possible - to replicate each segment to multiple nodes, but this is not strictly necessary since the failure of a node - will only result in users having to log in again. If you need to prevent node failures from requiring users to - log in again, set the owners attribute to 2 or more for the sessions cache - (see Configure Infinispan). - - - To enable the Infinispan user sessions provider open keycloak-server.json and change the - userSessions provider to infinispan: - -"userSessions": { - "provider": "infinispan" -} - + For cluster configuration, you can edit the configuration of infinispan/Keycloak container in standalone/configuration/standalone-ha.xml (or standalone-keycloak-ha.xml + if you are using overlay or demo distribution) .
@@ -117,6 +86,8 @@ To start the server in HA mode, start it with: # bin/standalone --server-config=standalone-ha.xml + or if you are using overlay or demo distribution with: + # bin/standalone --server-config=standalone-keycloak-ha.xml Alternatively you can copy standalone/config/standalone-ha.xml to standalone/config/standalone.xml diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java index 4edbc833ee..6fdb33c30c 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProvider.java @@ -147,11 +147,12 @@ public class LDAPFederationProvider implements UserFederationProvider { if (editMode == EditMode.READ_ONLY || editMode == EditMode.UNSYNCED) throw new IllegalStateException("Registration is not supported by this ldap server"); if (!synchronizeRegistrations()) throw new IllegalStateException("Registration is not supported by this ldap server"); - LDAPObject ldapObject = LDAPUtils.addUserToLDAP(this, realm, user); - user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapObject.getUuid()); - user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapObject.getDn().toString()); + LDAPObject ldapUser = LDAPUtils.addUserToLDAP(this, realm, user); + LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig()); + user.setSingleAttribute(LDAPConstants.LDAP_ID, ldapUser.getUuid()); + user.setSingleAttribute(LDAPConstants.LDAP_ENTRY_DN, ldapUser.getDn().toString()); - return proxy(realm, user, ldapObject); + return proxy(realm, user, ldapUser); } @Override @@ -232,6 +233,8 @@ public class LDAPFederationProvider implements UserFederationProvider { if (ldapUser == null) { return null; } + LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig()); + if (ldapUser.getUuid().equals(local.getFirstAttribute(LDAPConstants.LDAP_ID))) { return ldapUser; } else { @@ -257,17 +260,16 @@ public class LDAPFederationProvider implements UserFederationProvider { protected UserModel importUserFromLDAP(KeycloakSession session, RealmModel realm, LDAPObject ldapUser) { String ldapUsername = LDAPUtils.getUsername(ldapUser, ldapIdentityStore.getConfig()); - - if (ldapUsername == null) { - throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. Mapped username LDAP attribute: " + - ldapIdentityStore.getConfig().getUsernameLdapAttribute() + ", attributes from LDAP: " + ldapUser.getAttributes()); - } + LDAPUtils.checkUuid(ldapUser, ldapIdentityStore.getConfig()); UserModel imported = session.userStorage().addUser(realm, ldapUsername); imported.setEnabled(true); Set federationMappers = realm.getUserFederationMappersByFederationProvider(getModel().getId()); for (UserFederationMapperModel mapperModel : federationMappers) { + if (logger.isTraceEnabled()) { + logger.tracef("Using mapper %s during import user from LDAP", mapperModel); + } LDAPFederationMapper ldapMapper = getMapper(mapperModel); ldapMapper.onImportUserFromLDAP(mapperModel, this, ldapUser, imported, realm, true); } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java index 319825577e..915e5bbf73 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPFederationProviderFactory.java @@ -291,6 +291,8 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi RealmModel currentRealm = session.realms().getRealm(realmId); String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig()); + exists.value = true; + LDAPUtils.checkUuid(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig()); UserModel currentUser = session.userStorage().getUserByUsername(username, currentRealm); if (currentUser == null) { @@ -332,10 +334,17 @@ public class LDAPFederationProviderFactory extends UserFederationEventAwareProvi public void run(KeycloakSession session) { LDAPFederationProvider ldapFedProvider = getInstance(session, fedModel); RealmModel currentRealm = session.realms().getRealm(realmId); - String username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig()); - UserModel existing = session.userStorage().getUserByUsername(username, currentRealm); - if (existing != null) { - session.userStorage().removeUser(currentRealm, existing); + String username = null; + try { + username = LDAPUtils.getUsername(ldapUser, ldapFedProvider.getLdapIdentityStore().getConfig()); + } catch (ModelException ignore) { + } + + if (username != null) { + UserModel existing = session.userStorage().getUserByUsername(username, currentRealm); + if (existing != null) { + session.userStorage().removeUser(currentRealm, existing); + } } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java index c731330576..91f0000c6a 100755 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/LDAPUtils.java @@ -72,6 +72,21 @@ public class LDAPUtils { public static String getUsername(LDAPObject ldapUser, LDAPConfig config) { String usernameAttr = config.getUsernameLdapAttribute(); - return ldapUser.getAttributeAsString(usernameAttr); + String ldapUsername = ldapUser.getAttributeAsString(usernameAttr); + + if (ldapUsername == null) { + throw new ModelException("User returned from LDAP has null username! Check configuration of your LDAP mappings. Mapped username LDAP attribute: " + + config.getUsernameLdapAttribute() + ", user DN: " + ldapUser.getDn() + ", attributes from LDAP: " + ldapUser.getAttributes()); + } + + return ldapUsername; + } + + public static void checkUuid(LDAPObject ldapUser, LDAPConfig config) { + if (ldapUser.getUuid() == null) { + throw new ModelException("User returned from LDAP has null uuid! Check configuration of your LDAP settings. UUID Attribute must be unique among your LDAP records and available on all the LDAP user records. " + + "If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN' . " + + "Mapped UUID LDAP attribute: " + config.getUuidLDAPAttributeName() + ", user DN: " + ldapUser.getDn()); + } } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java index 81f058d24c..cbb28f96ed 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/model/LDAPObject.java @@ -129,4 +129,9 @@ public class LDAPObject { result = 31 * result + (getUuid() != null ? getUuid().hashCode() : 0); return result; } + + @Override + public String toString() { + return "LDAP Object [ dn: " + dn + " , uuid: " + uuid + ", attributes: " + attributes + ", readOnly attribute names: " + readOnlyAttributeNames + " ]"; + } } diff --git a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java index 43057d1d5c..dc937421e8 100644 --- a/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java +++ b/federation/ldap/src/main/java/org/keycloak/federation/ldap/idm/store/ldap/LDAPIdentityStore.java @@ -81,8 +81,8 @@ public class LDAPIdentityStore implements IdentityStore { this.operationManager.createSubContext(entryDN, ldapAttributes); ldapObject.setUuid(getEntryIdentifier(ldapObject)); - if (logger.isTraceEnabled()) { - logger.tracef("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), entryDN); + if (logger.isDebugEnabled()) { + logger.debugf("Type with identifier [%s] and dn [%s] successfully added to LDAP store.", ldapObject.getUuid(), entryDN); } } @@ -94,8 +94,8 @@ public class LDAPIdentityStore implements IdentityStore { String entryDn = ldapObject.getDn().toString(); this.operationManager.modifyAttributes(entryDn, attributes); - if (logger.isTraceEnabled()) { - logger.tracef("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), entryDn); + if (logger.isDebugEnabled()) { + logger.debugf("Type with identifier [%s] and DN [%s] successfully updated to LDAP store.", ldapObject.getUuid(), entryDn); } } @@ -103,8 +103,8 @@ public class LDAPIdentityStore implements IdentityStore { public void remove(LDAPObject ldapObject) { this.operationManager.removeEntry(ldapObject.getDn().toString()); - if (logger.isTraceEnabled()) { - logger.tracef("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getUuid(), ldapObject.getDn().toString()); + if (logger.isDebugEnabled()) { + logger.debugf("Type with identifier [%s] and DN [%s] successfully removed from LDAP store.", ldapObject.getUuid(), ldapObject.getDn().toString()); } } @@ -429,7 +429,7 @@ public class LDAPIdentityStore implements IdentityStore { } if (logger.isTraceEnabled()) { - logger.tracef("Found ldap object [%s] and populated with the attributes [%s]. Read-only attributes are [%s]", ldapObject.getDn().toString(), ldapObject.getAttributes(), ldapObject.getReadOnlyAttributeNames()); + logger.tracef("Found ldap object and populated with the attributes. LDAP Object: %s", ldapObject.toString()); } return ldapObject; diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html index eb75db9445..e405f7218b 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/federated-ldap.html @@ -70,21 +70,27 @@
- Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server vendors it's 'uid'. For Active directory it's usually 'sAMAccountName' or 'cn' + Name of LDAP attribute, which is mapped as Keycloak username. For many LDAP server vendors it can be 'uid'. For Active directory it can be 'sAMAccountName' or 'cn' . + The attribute should be filled for all LDAP user records you want to import from LDAP to Keycloak. +
- Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN. Usually it's the same as Username LDAP attribute, however for Active directory it could be 'cn' when username attribute might be 'sAMAccountName' + Name of LDAP attribute, which is used as RDN (top attribute) of typical user DN. Usually it's the same as Username LDAP attribute, however it's not required. + For example for Active directory it's common to use 'cn' as RDN attribute when username attribute might be 'sAMAccountName' . +
- Name of LDAP attribute, which is used as unique object identifier (UUID) for objects in LDAP. For many LDAP server vendors it's 'entryUUID' however some are different. For example for Active directory it should be 'objectGUID' + Name of LDAP attribute, which is used as unique object identifier (UUID) for objects in LDAP. For many LDAP server vendors it's 'entryUUID' however some are different. For example for Active directory it should be 'objectGUID' . + If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN' . +
diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationMapperModel.java b/model/api/src/main/java/org/keycloak/models/UserFederationMapperModel.java index 80f22be9e5..ea08a58bb7 100644 --- a/model/api/src/main/java/org/keycloak/models/UserFederationMapperModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationMapperModel.java @@ -76,4 +76,12 @@ public class UserFederationMapperModel implements Serializable { public int hashCode() { return id.hashCode(); } + + @Override + public String toString() { + return new StringBuilder(" { name=" + name) + .append(", federationMapperType=" + federationMapperType) + .append(", config=" + config) + .append(" } ").toString(); + } } diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java index 57405637c3..daed728c80 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java @@ -33,7 +33,7 @@ public class ConstraintAuthorizationHandler implements HttpHandler { this.httpHeaderNames = new HashMap<>(); this.httpHeaderNames.put(KEYCLOAK_SUBJECT, new HttpString(getOrDefault(headerNames, "keycloak-subject", KEYCLOAK_SUBJECT))); - this.httpHeaderNames.put(KEYCLOAK_SUBJECT, new HttpString(getOrDefault(headerNames, "keycloak-username", KEYCLOAK_USERNAME))); + this.httpHeaderNames.put(KEYCLOAK_USERNAME, new HttpString(getOrDefault(headerNames, "keycloak-username", KEYCLOAK_USERNAME))); this.httpHeaderNames.put(KEYCLOAK_EMAIL, new HttpString(getOrDefault(headerNames, "keycloak-email", KEYCLOAK_EMAIL))); this.httpHeaderNames.put(KEYCLOAK_NAME, new HttpString(getOrDefault(headerNames, "keycloak-name", KEYCLOAK_NAME))); this.httpHeaderNames.put(KEYCLOAK_ACCESS_TOKEN, new HttpString(getOrDefault(headerNames, "keycloak-access-token", KEYCLOAK_ACCESS_TOKEN))); diff --git a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java index 0626f1c015..5a1f8ee509 100644 --- a/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/utils/AuthorizeClientUtil.java @@ -22,16 +22,23 @@ import java.util.Map; public class AuthorizeClientUtil { public static ClientModel authorizeClient(String authorizationHeader, MultivaluedMap formData, EventBuilder event, RealmModel realm) { - String client_id; - String clientSecret; + String client_id = null; + String clientSecret = null; if (authorizationHeader != null) { String[] usernameSecret = BasicAuthHelper.parseHeader(authorizationHeader); - if (usernameSecret == null) { - throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build()); + if (usernameSecret != null) { + client_id = usernameSecret[0]; + clientSecret = usernameSecret[1]; + } else { + + // Don't send 401 if client_id parameter was sent in request. For example IE may automatically send "Authorization: Negotiate" in XHR requests even for public clients + if (!formData.containsKey(OAuth2Constants.CLIENT_ID)) { + throw new UnauthorizedException("Bad Authorization header", Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, "Basic realm=\"" + realm.getName() + "\"").build()); + } } - client_id = usernameSecret[0]; - clientSecret = usernameSecret[1]; - } else { + } + + if (client_id == null) { client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); clientSecret = formData.getFirst("client_secret"); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java index abfe3c30be..b06e4331b7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/OAuthClient.java @@ -306,7 +306,7 @@ public class OAuthClient { } } - private void closeClient(CloseableHttpClient client) { + public void closeClient(CloseableHttpClient client) { try { client.close(); } catch (IOException ioe) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java index 939fe24351..63a27e6ce7 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/FederationTestUtils.java @@ -102,14 +102,14 @@ class FederationTestUtils { addUserAttributeMapper(realm, providerModel, "zipCodeMapper", "postal_code", LDAPConstants.POSTAL_CODE); } - public static void addUserAttributeMapper(RealmModel realm, UserFederationProviderModel providerModel, String mapperName, String userModelAttributeName, String ldapAttributeName) { + public static UserFederationMapperModel addUserAttributeMapper(RealmModel realm, UserFederationProviderModel providerModel, String mapperName, String userModelAttributeName, String ldapAttributeName) { UserFederationMapperModel mapperModel = KeycloakModelUtils.createUserFederationMapperModel(mapperName, providerModel.getId(), UserAttributeLDAPFederationMapperFactory.PROVIDER_ID, UserAttributeLDAPFederationMapper.USER_MODEL_ATTRIBUTE, userModelAttributeName, UserAttributeLDAPFederationMapper.LDAP_ATTRIBUTE, ldapAttributeName, UserAttributeLDAPFederationMapper.READ_ONLY, "false", UserAttributeLDAPFederationMapper.ALWAYS_READ_VALUE_FROM_LDAP, "false", UserAttributeLDAPFederationMapper.IS_MANDATORY_IN_LDAP, "false"); - realm.addUserFederationMapper(mapperModel); + return realm.addUserFederationMapper(mapperModel); } public static void addOrUpdateRoleLDAPMappers(RealmModel realm, UserFederationProviderModel providerModel, RoleLDAPFederationMapper.Mode mode) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java index e5e893ab8e..f946825437 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/SyncProvidersTest.java @@ -11,10 +11,12 @@ import org.keycloak.federation.ldap.LDAPConfig; import org.keycloak.federation.ldap.LDAPFederationProvider; import org.keycloak.federation.ldap.LDAPFederationProviderFactory; import org.keycloak.federation.ldap.idm.model.LDAPObject; +import org.keycloak.federation.ldap.mappers.UserAttributeLDAPFederationMapper; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.LDAPConstants; import org.keycloak.models.RealmModel; +import org.keycloak.models.UserFederationMapperModel; import org.keycloak.models.UserFederationProvider; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserFederationSyncResult; @@ -287,6 +289,68 @@ public class SyncProvidersTest { } } + // KEYCLOAK-1728 + @Test + public void test04MissingLDAPUsernameSync() { + KeycloakSession session = keycloakRule.startSession(); + String origUsernameAttrName; + + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Remove all users from model + for (UserModel user : session.userStorage().getUsers(testRealm, true)) { + session.userStorage().removeUser(testRealm, user); + } + + UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm); + + // Add street mapper and add some user including street + UserFederationMapperModel streetMapper = FederationTestUtils.addUserAttributeMapper(testRealm, ldapModel, "streetMapper", "street", LDAPConstants.STREET); + LDAPFederationProvider ldapFedProvider = FederationTestUtils.getLdapProvider(session, ldapModel); + LDAPObject streetUser = FederationTestUtils.addLDAPUser(ldapFedProvider, testRealm, "user8", "User8FN", "User8LN", "user8@email.org", "user8street", "126"); + + // Change name of username attribute name to street + origUsernameAttrName = providerModel.getConfig().get(LDAPConstants.USERNAME_LDAP_ATTRIBUTE); + providerModel.getConfig().put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, "street"); + + // Need to change this due to ApacheDS pagination bug (For other LDAP servers, pagination works fine) TODO: Remove once ApacheDS upgraded and pagination is fixed + providerModel.getConfig().put(LDAPConstants.BATCH_SIZE_FOR_SYNC, "10"); + testRealm.updateUserFederationProvider(providerModel); + + } finally { + keycloakRule.stopSession(session, true); + } + + // Just user8 synced. All others failed to sync + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm); + + KeycloakSessionFactory sessionFactory = session.getKeycloakSessionFactory(); + UserFederationSyncResult syncResult = new UsersSyncManager().syncAllUsers(sessionFactory, "test", providerModel); + Assert.assertEquals(1, syncResult.getAdded()); + Assert.assertTrue(syncResult.getFailed() > 0); + } finally { + keycloakRule.stopSession(session, false); + } + + session = keycloakRule.startSession(); + try { + RealmModel testRealm = session.realms().getRealm("test"); + + // Revert config changes + UserFederationProviderModel providerModel = KeycloakModelUtils.findUserFederationProviderByDisplayName(ldapModel.getDisplayName(), testRealm); + providerModel.getConfig().put(LDAPConstants.USERNAME_LDAP_ATTRIBUTE, origUsernameAttrName); + testRealm.updateUserFederationProvider(providerModel); + UserFederationMapperModel streetMapper = testRealm.getUserFederationMapperByName(providerModel.getId(), "streetMapper"); + testRealm.removeUserFederationMapper(streetMapper); + } finally { + keycloakRule.stopSession(session, true); + } + } + @Test public void testPeriodicSync() { KeycloakSession session = keycloakRule.startSession(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java index 4c62dd866d..cbfc76f7a0 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/oauth/AccessTokenTest.java @@ -21,12 +21,19 @@ */ package org.keycloak.testsuite.oauth; +import org.apache.http.NameValuePair; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicNameValuePair; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.keycloak.OAuth2Constants; import org.keycloak.VerificationException; +import org.keycloak.constants.AdapterConstants; import org.keycloak.enums.SslRequired; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -46,6 +53,7 @@ import org.keycloak.protocol.oidc.mappers.RoleNameMapper; import org.keycloak.protocol.oidc.mappers.UserAttributeMapper; import org.keycloak.representations.AccessToken; import org.keycloak.representations.IDToken; +import org.keycloak.services.managers.ClientManager; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; @@ -68,9 +76,11 @@ import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriBuilder; import java.io.IOException; +import java.io.UnsupportedEncodingException; import java.net.URI; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -719,6 +729,52 @@ public class AccessTokenTest { } + // KEYCLOAK-1595 Assert that public client is able to retrieve token even if header "Authorization: Negotiate something" was used (parameter client_id has preference in this case) + @Test + public void testAuthorizationNegotiateHeaderIgnored() throws Exception { + keycloakRule.configure(new KeycloakRule.KeycloakSetup() { + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + ClientModel client = new ClientManager(manager).createClient(appRealm, "sample-public-client"); + client.addRedirectUri("http://localhost:8081/app/auth"); + client.setEnabled(true); + client.setPublicClient(true); + } + }); + + oauth.clientId("sample-public-client"); + oauth.doLogin("test-user@localhost", "password"); + Event loginEvent = events.expectLogin().client("sample-public-client").assertEvent(); + + String sessionId = loginEvent.getSessionId(); + String codeId = loginEvent.getDetails().get(Details.CODE_ID); + + String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE); + + CloseableHttpClient client = new DefaultHttpClient(); + try { + HttpPost post = new HttpPost(oauth.getAccessTokenUrl()); + + List parameters = new LinkedList(); + parameters.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE)); + parameters.add(new BasicNameValuePair(OAuth2Constants.CODE, code)); + parameters.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, oauth.getRedirectUri())); + parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, oauth.getClientId())); + post.setHeader("Authorization", "Negotiate something-which-will-be-ignored"); + + UrlEncodedFormEntity formEntity = null; + formEntity = new UrlEncodedFormEntity(parameters, "UTF-8"); + post.setEntity(formEntity); + + AccessTokenResponse response = new AccessTokenResponse(client.execute(post)); + Assert.assertEquals(200, response.getStatusCode()); + AccessToken token = oauth.verifyToken(response.getAccessToken()); + events.expectCodeToToken(codeId, sessionId).client("sample-public-client").assertEvent(); + } finally { + oauth.closeClient(client); + } + } + private IDToken getIdToken(org.keycloak.representations.AccessTokenResponse tokenResponse) throws VerificationException { JWSInput input = new JWSInput(tokenResponse.getIdToken()); IDToken idToken = null;