From 57e51e9dd425d5a9da0064d6380b31ac497f9537 Mon Sep 17 00:00:00 2001 From: mposolda Date: Tue, 15 Aug 2023 12:03:22 +0200 Subject: [PATCH] Use an original domain name of Kerberos Principal in UserModel attribute instead of configured value of Kerberos realm in User federation closes #20045 --- .../common/constants/KerberosConstants.java | 10 +++ .../release_notes/topics/22_0_2.adoc | 2 +- .../topics/authentication/kerberos.adoc | 13 +-- .../kerberos/KerberosFederationProvider.java | 65 +++++++-------- .../kerberos/KerberosPrincipal.java | 57 +++++++++++++ ...KerberosUsernamePasswordAuthenticator.java | 19 ++--- .../kerberos/impl/SPNEGOAuthenticator.java | 9 +- .../storage/ldap/LDAPStorageProvider.java | 83 +++++++++++++++---- .../ldap/LDAPStorageProviderFactory.java | 16 +++- .../org/keycloak/storage/ldap/LDAPUtils.java | 20 +++++ .../kerberos/LDAPProviderKerberosConfig.java | 4 + .../locales/en/user-federation-help.json | 1 + .../public/locales/en/user-federation.json | 1 + .../ldap/LdapSettingsGeneral.tsx | 5 ++ .../ldap/LdapSettingsKerberosIntegration.tsx | 34 ++++++++ .../testsuite/util/LDAPTestConfiguration.java | 4 +- .../kerberos/AbstractKerberosTest.java | 14 +++- .../KerberosLdapCrossRealmTrustTest.java | 62 +++++++++++++- ...KerberosLdapMultipleLDAPProvidersTest.java | 4 +- .../federation/kerberos/KerberosLdapTest.java | 2 +- ...KerberosStandaloneCrossRealmTrustTest.java | 30 ++++++- ...rberosStandaloneMultipleProvidersTest.java | 12 +-- .../kerberos/KerberosStandaloneTest.java | 6 +- .../kerberos/KeycloakSPNegoSchemeFactory.java | 13 +-- .../kerberos/users-kerberos-kc2.ldif | 20 ++++- 25 files changed, 393 insertions(+), 113 deletions(-) create mode 100644 federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosPrincipal.java diff --git a/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java b/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java index 79f8c87c60..11281c18b6 100644 --- a/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java +++ b/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java @@ -71,6 +71,11 @@ public class KerberosConstants { public static final String SERVER_PRINCIPAL = "serverPrincipal"; public static final String KEYTAB = "keyTab"; public static final String DEBUG = "debug"; + public static final String KERBEROS_PRINCIPAL_ATTRIBUTE = "krbPrincipalAttribute"; + public static final String KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_KRB5_PRINCIPAL_NAME = "krb5PrincipalName"; // Used for instance in ApacheDS + public static final String KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_KRB_PRINCIPAL_NAME = "krbPrincipalName"; // Used for instance in FreeIPA + public static final String KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_USER_PRINCIPAL_NAME = "userPrincipalName"; // Used for instance in MSAD + public static final String ALLOW_PASSWORD_AUTHENTICATION = "allowPasswordAuthentication"; public static final String UPDATE_PROFILE_FIRST_LOGIN = "updateProfileFirstLogin"; public static final String USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION = "useKerberosForPasswordAuthentication"; @@ -97,4 +102,9 @@ public class KerberosConstants { * to lookup it in his LDAP tree. In this case, LDAP lookup might be performed by other providers in the chain. */ public static final String AUTHENTICATED_SPNEGO_CONTEXT = "authenticatedSpnegoContext"; + + /* + * User attribute for kerberos principal used for users from Kerberos/LDAP providers + */ + public static final String KERBEROS_PRINCIPAL = "KERBEROS_PRINCIPAL"; } diff --git a/docs/documentation/release_notes/topics/22_0_2.adoc b/docs/documentation/release_notes/topics/22_0_2.adoc index a15facf0f8..df878f999a 100644 --- a/docs/documentation/release_notes/topics/22_0_2.adoc +++ b/docs/documentation/release_notes/topics/22_0_2.adoc @@ -1,4 +1,4 @@ = Improvements in LDAP and Kerberos integration Keycloak now supports multiple LDAP providers in a realm, which support Kerberos integration with the same Kerberos realm. When an LDAP provider is not able to find the user which was authenticated through -Kerberos/SPNEGO, Keycloak ties to fallback to the next LDAP provider. +Kerberos/SPNEGO, Keycloak ties to fallback to the next LDAP provider. Keycloak has also better support for the case when single LDAP provider supports multiple Kerberos realms, which are in trust with each other. diff --git a/docs/documentation/server_admin/topics/authentication/kerberos.adoc b/docs/documentation/server_admin/topics/authentication/kerberos.adoc index 8627306e43..0f01e71a50 100644 --- a/docs/documentation/server_admin/topics/authentication/kerberos.adoc +++ b/docs/documentation/server_admin/topics/authentication/kerberos.adoc @@ -197,14 +197,17 @@ The cross-realm trust is unidirectional by default. You must add the principal ` ** When using an LDAP storage provider with Kerberos support, configure the server principal for realm B, as in this example: `HTTP/mydomain.com@B`. The LDAP server must find the users from realm A if users from realm A are to successfully authenticate to {project_name}, because {project_name} must perform the SPNEGO flow and then find the users. -For example, Kerberos principal user `john@A` must be available in the LDAP under an LDAP DN such as `uid=john,ou=People,dc=example,dc=com`. If you want users from realm A and B to authenticate, ensure that LDAP can find users from both realms A and B. +Finding users is based on the LDAP storage provider option `Kerberos principal attribute`. When this is configured for instance with value like `userPrincipalName`, then +after SPNEGO authentication of user `john@A`, {project_name} will try to lookup LDAP user with attribute `userPrincipalName` equivalent to `john@A`. If `Kerberos principal attribute` is left +empty, then {project_name} will lookup the LDAP user based on the prefix of his kerberos principal with the realm omitted. +For example, Kerberos principal user `john@A` must be available in the LDAP under username `john`, so typically under an LDAP DN such as `uid=john,ou=People,dc=example,dc=com`. If you want users from realm A and B to authenticate, ensure that LDAP can find users from both realms A and B. ** When using a Kerberos user storage provider (typically, Kerberos without LDAP integration), configure the server principal as `HTTP/mydomain.com@B`, and users from Kerberos realms A and B must be able to authenticate. -[WARNING] -==== -When using the Kerberos user storage provider, there cannot be conflicting users among Kerberos realms. If conflicting users exist, {project_name} maps them to the same user. -==== +Users from multiple Kerberos realms are allowed to authenticate as every user would have attribute `KERBEROS_PRINCIPAL` referring to the kerberos principal used for authentication and this is used +for further lookups of this user. To avoid conflicts when there is user `john` in both kerberos realms `A` and `B`, the username of the {project_name} user might contain the kerberos realm +lowercased. For instance username would be `john@a`. Just in case when realm matches with the configured `Kerberos realm`, the realm suffix might be omitted from the generated username. For +instance username would be `john` for the Kerberos principal `john@A` as long as the `Kerberos realm` is configured on the Kerberos provider is `A`. ==== Troubleshooting diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java index e456560858..cfdc58342e 100755 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosFederationProvider.java @@ -47,6 +47,8 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Stream; +import javax.security.auth.login.LoginException; + /** * @author Marek Posolda */ @@ -58,7 +60,7 @@ public class KerberosFederationProvider implements UserStorageProvider, ImportedUserValidation { private static final Logger logger = Logger.getLogger(KerberosFederationProvider.class); - public static final String KERBEROS_PRINCIPAL = "KERBEROS_PRINCIPAL"; + public static final String KERBEROS_PRINCIPAL = KerberosConstants.KERBEROS_PRINCIPAL; protected KeycloakSession session; protected UserStorageProviderModel model; @@ -74,10 +76,6 @@ public class KerberosFederationProvider implements UserStorageProvider, @Override public UserModel validate(RealmModel realm, UserModel user) { - if (!isValid(realm, user)) { - return null; - } - if (kerberosConfig.getEditMode() == EditMode.READ_ONLY) { return new ReadOnlyKerberosUserModelDelegate(user, this); } else { @@ -89,12 +87,12 @@ public class KerberosFederationProvider implements UserStorageProvider, public UserModel getUserByUsername(RealmModel realm, String username) { KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); if (authenticator.isUserAvailable(username)) { - // Case when method was called with username including kerberos realm like john@REALM.ORG . Authenticator already checked that kerberos realm was correct - if (username.contains("@")) { - username = username.split("@")[0]; + try { + String kerberosPrincipal = authenticator.getKerberosPrincipal(username); + return findOrCreateAuthenticatedUser(realm, new KerberosPrincipal(kerberosPrincipal)); + } catch (LoginException le) { + throw new IllegalStateException("Should not happen", le); } - - return findOrCreateAuthenticatedUser(realm, username); } else { return null; } @@ -125,13 +123,6 @@ public class KerberosFederationProvider implements UserStorageProvider, } - public boolean isValid(RealmModel realm, UserModel local) { - // KerberosUsernamePasswordAuthenticator.isUserAvailable is an overhead, so avoid it for now - - String kerberosPrincipal = local.getUsername() + "@" + kerberosConfig.getKerberosRealm(); - return kerberosPrincipal.equalsIgnoreCase(local.getFirstAttribute(KERBEROS_PRINCIPAL)); - } - @Override public boolean updateCredential(RealmModel realm, UserModel user, CredentialInput input) { if (!(input instanceof UserCredentialModel) || !PasswordCredentialModel.TYPE.equals(input.getType())) return false; @@ -170,16 +161,16 @@ public class KerberosFederationProvider implements UserStorageProvider, public boolean isValid(RealmModel realm, UserModel user, CredentialInput input) { if (!(input instanceof UserCredentialModel)) return false; if (input.getType().equals(PasswordCredentialModel.TYPE) && !((LegacyUserCredentialManager) user.credentialManager()).isConfiguredLocally(PasswordCredentialModel.TYPE)) { - return validPassword(user.getUsername(), input.getChallengeResponse()); + return validPassword(user.getFirstAttribute(KERBEROS_PRINCIPAL), input.getChallengeResponse()); } else { return false; // invalid cred type } } - protected boolean validPassword(String username, String password) { + protected boolean validPassword(String kerberosPrincipal, String password) { if (kerberosConfig.isAllowPasswordAuthentication()) { KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); - return authenticator.validUser(username, password); + return authenticator.validUser(kerberosPrincipal, password); } else { return false; } @@ -192,7 +183,7 @@ public class KerberosFederationProvider implements UserStorageProvider, if (credential.getType().equals(UserCredentialModel.KERBEROS)) { SPNEGOAuthenticator spnegoAuthenticator = (SPNEGOAuthenticator) credential.getNote(KerberosConstants.AUTHENTICATED_SPNEGO_CONTEXT); if (spnegoAuthenticator != null) { - logger.debugf("SPNEGO authentication already performed by previous provider. Provider '%s' will try to lookup user with principal kerberos principal '%s'", this, spnegoAuthenticator.getAuthenticatedUsername()); + logger.debugf("SPNEGO authentication already performed by previous provider. Provider '%s' will try to lookup user with kerberos principal '%s'", this, spnegoAuthenticator.getAuthenticatedKerberosPrincipal()); } else { String spnegoToken = credential.getChallengeResponse(); spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); @@ -202,8 +193,8 @@ public class KerberosFederationProvider implements UserStorageProvider, Map state = new HashMap<>(); if (spnegoAuthenticator.isAuthenticated()) { - String username = spnegoAuthenticator.getAuthenticatedUsername(); - UserModel user = findOrCreateAuthenticatedUser(realm, username); + KerberosPrincipal kerberosPrincipal = spnegoAuthenticator.getAuthenticatedKerberosPrincipal(); + UserModel user = findOrCreateAuthenticatedUser(realm, kerberosPrincipal); if (user == null) { // Adding the authenticated SPNEGO, in case that other LDAP/Kerberos providers in the chain are able to lookup user from their LDAP // This can be the case with more complex setup (like MSAD Forest Trust environment) @@ -243,24 +234,26 @@ public class KerberosFederationProvider implements UserStorageProvider, * Called after successful authentication * * @param realm realm - * @param username username without realm prefix + * @param kerberosPrincipal * @return user if found or successfully created. Null if user with same username already exists, but is not linked to this provider */ - protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) { - UserModel user = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username); + protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, KerberosPrincipal kerberosPrincipal) { + UserModel user = UserStoragePrivateUtil.userLocalStorage(session).searchForUserByUserAttributeStream(realm, KerberosConstants.KERBEROS_PRINCIPAL, kerberosPrincipal.toString()) + .findFirst().orElse(null); + if (user != null) { user = session.users().getUserById(realm, user.getId()); // make sure we get a cached instance - logger.debug("Kerberos authenticated user " + username + " found in Keycloak storage"); + logger.debug("Kerberos authenticated user " + kerberosPrincipal + " found in Keycloak storage"); if (!model.getId().equals(user.getFederationLink())) { - logger.warn("User with username " + username + " already exists, but is not linked to provider [" + model.getName() + "]"); + logger.warn("User with username " + kerberosPrincipal + " already exists, but is not linked to provider [" + model.getName() + "]"); return null; } else { UserModel proxied = validate(realm, user); if (proxied != null) { return proxied; } else { - logger.warn("User with username " + username + " already exists and is linked to provider [" + model.getName() + + logger.warn("User with username " + kerberosPrincipal.getPrefix() + " already exists and is linked to provider [" + model.getName() + "] but kerberos principal is not correct. Kerberos principal on user is: " + user.getFirstAttribute(KERBEROS_PRINCIPAL)); logger.warn("Will re-create user"); new UserManager(session).removeUser(realm, user, UserStoragePrivateUtil.userLocalStorage(session)); @@ -268,20 +261,22 @@ public class KerberosFederationProvider implements UserStorageProvider, } } - logger.debug("Kerberos authenticated user " + username + " not in Keycloak storage. Creating him"); - return importUserToKeycloak(realm, username); + logger.debug("Kerberos authenticated user " + kerberosPrincipal + " not in Keycloak storage. Creating him"); + return importUserToKeycloak(realm, kerberosPrincipal); } - protected UserModel importUserToKeycloak(RealmModel realm, String username) { + protected UserModel importUserToKeycloak(RealmModel realm, KerberosPrincipal kerberosPrincipal) { // Just guessing email from kerberos realm - String email = username + "@" + kerberosConfig.getKerberosRealm().toLowerCase(); + String email = kerberosPrincipal.getPrefix() + "@" + kerberosPrincipal.getRealm().toLowerCase(); + // In case that kerberos realm is same like configured realm, create just username as prefix (EG. "john"). Otherwise for trusted realms, use the full kerberos principal (EG. "john@TRUSTED_REALM.ORG") + String username = (kerberosPrincipal.getRealm().equalsIgnoreCase(kerberosConfig.getKerberosRealm())) ? kerberosPrincipal.getPrefix() : email; - logger.debugf("Creating kerberos user: %s, email: %s to local Keycloak storage", username, email); + logger.debugf("Creating kerberos user %s with username: %s, email: %s to local Keycloak storage", kerberosPrincipal, username, email); UserModel user = UserStoragePrivateUtil.userLocalStorage(session).addUser(realm, username); user.setEnabled(true); user.setEmail(email); user.setFederationLink(model.getId()); - user.setSingleAttribute(KERBEROS_PRINCIPAL, username + "@" + kerberosConfig.getKerberosRealm()); + user.setSingleAttribute(KERBEROS_PRINCIPAL, kerberosPrincipal.toString()); if (kerberosConfig.isUpdateProfileFirstLogin()) { if (Profile.isFeatureEnabled(Profile.Feature.UPDATE_EMAIL)) { diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosPrincipal.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosPrincipal.java new file mode 100644 index 0000000000..623617de91 --- /dev/null +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/KerberosPrincipal.java @@ -0,0 +1,57 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates + * and other contributors as indicated by the @author tags. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package org.keycloak.federation.kerberos; + +/** + * @author Marek Posolda + */ +public class KerberosPrincipal { + + // Full principal name like "john@KEYCLOAK.ORG" + private final String kerberosPrincipal; + private final String prefix; // Something like "john" + private final String realm; // Something like "KEYCLOAK.ORG" + public KerberosPrincipal(String kerberosPrincipal) { + String[] parts = kerberosPrincipal.split("@"); + if (parts.length != 2) { + throw new IllegalArgumentException("Kerberos principal '" + kerberosPrincipal + "' not valid"); + } + this.prefix = parts[0]; + this.realm = parts[1].toUpperCase(); + this.kerberosPrincipal = prefix + "@" + realm; + } + + public String getKerberosPrincipal() { + return kerberosPrincipal; + } + + public String getPrefix() { + return prefix; + } + + public String getRealm() { + return realm; + } + + @Override + public String toString() { + return this.kerberosPrincipal; + } +} diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java index 38d28e8e83..0965924bb3 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/KerberosUsernamePasswordAuthenticator.java @@ -20,6 +20,7 @@ package org.keycloak.federation.kerberos.impl; import org.jboss.logging.Logger; import org.keycloak.common.util.KerberosJdkProvider; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.federation.kerberos.KerberosPrincipal; import org.keycloak.models.ModelException; import javax.security.auth.Subject; @@ -136,7 +137,7 @@ public class KerberosUsernamePasswordAuthenticator { createJaasConfiguration()); loginContext.login(); - logger.debug("Principal " + principal + " authenticated succesfully"); + logger.debug("Principal " + principal + " authenticated successfully"); return loginContext.getSubject(); } @@ -152,20 +153,12 @@ public class KerberosUsernamePasswordAuthenticator { } - protected String getKerberosPrincipal(String username) throws LoginException { + public String getKerberosPrincipal(String username) throws LoginException { if (username.contains("@")) { - String[] tokens = username.split("@"); - - String kerberosRealm = tokens[1]; - if (!kerberosRealm.toUpperCase().equals(config.getKerberosRealm())) { - logger.warn("Invalid kerberos realm. Expected realm: " + config.getKerberosRealm() + ", username: " + username); - throw new LoginException("Client not found"); - } - - username = tokens[0]; + return new KerberosPrincipal(username).toString(); + } else { + return username + "@" + config.getKerberosRealm(); } - - return username + "@" + config.getKerberosRealm(); } diff --git a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java index 8fe53209f5..1afb5cb202 100644 --- a/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java +++ b/federation/kerberos/src/main/java/org/keycloak/federation/kerberos/impl/SPNEGOAuthenticator.java @@ -27,6 +27,7 @@ import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.util.Base64; import org.keycloak.common.util.KerberosSerializationUtils; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.federation.kerberos.KerberosPrincipal; import javax.security.auth.Subject; import javax.security.auth.kerberos.KerberosTicket; @@ -110,12 +111,10 @@ public class SPNEGOAuthenticator { } /** - * @return username to be used in Keycloak. Username is authenticated kerberos principal without realm name + * @return kerberos principal to be used in Keycloak */ - public String getAuthenticatedUsername() { - String[] tokens = authenticatedKerberosPrincipal.split("@"); - String username = tokens[0]; - return username; + public KerberosPrincipal getAuthenticatedKerberosPrincipal() { + return new KerberosPrincipal(authenticatedKerberosPrincipal); } diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java index b077387b6f..1d7d8c5d53 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProvider.java @@ -39,6 +39,7 @@ import org.keycloak.credential.CredentialInput; import org.keycloak.credential.CredentialInputUpdater; import org.keycloak.credential.CredentialInputValidator; import org.keycloak.credential.LegacyUserCredentialManager; +import org.keycloak.federation.kerberos.KerberosPrincipal; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; import org.keycloak.models.CredentialValidationOutput; @@ -159,6 +160,10 @@ public class LDAPStorageProvider implements UserStorageProvider, return model; } + public LDAPProviderKerberosConfig getKerberosConfig() { + return kerberosConfig; + } + public LDAPStorageMapperManager getMapperManager() { return mapperManager; } @@ -564,6 +569,15 @@ public class LDAPStorageProvider implements UserStorageProvider, if(getLdapIdentityStore().getConfig().isTrustEmail()){ imported.setEmailVerified(true); } + if (kerberosConfig.getKerberosPrincipalAttribute() != null) { + String kerberosPrincipal = ldapUser.getAttributeAsString(kerberosConfig.getKerberosPrincipalAttribute()); + if (kerberosPrincipal == null) { + logger.warnf("Kerberos principal attribute not found on LDAP user [%s]. Configured kerberos principal attribute name is [%s]", ldapUser.getDn(), kerberosConfig.getKerberosPrincipalAttribute()); + } else { + KerberosPrincipal kerberosPrinc = new KerberosPrincipal(kerberosPrincipal); + imported.setSingleAttribute(KerberosConstants.KERBEROS_PRINCIPAL, kerberosPrinc.toString()); + } + } logger.debugf("Imported new user from LDAP to Keycloak DB. Username: [%s], Email: [%s], LDAP_ID: [%s], LDAP Entry DN: [%s]", imported.getUsername(), imported.getEmail(), ldapUser.getUuid(), userDN); UserModel proxy = proxy(realm, imported, ldapUser, false); @@ -625,7 +639,11 @@ public class LDAPStorageProvider implements UserStorageProvider, if (kerberosConfig.isAllowKerberosAuthentication() && kerberosConfig.isUseKerberosForPasswordAuthentication()) { // Use Kerberos JAAS (Krb5LoginModule) KerberosUsernamePasswordAuthenticator authenticator = factory.createKerberosUsernamePasswordAuthenticator(kerberosConfig); - return authenticator.validUser(user.getUsername(), password); + String kerberosUsername = user.getFirstAttribute(KerberosConstants.KERBEROS_PRINCIPAL); + // Fallback to username (backwards compatibility) + if (kerberosUsername == null) kerberosUsername = user.getUsername(); + + return authenticator.validUser(kerberosUsername, password); } else { // Use Naming LDAP API LDAPObject ldapUser = loadAndValidateUser(realm, user); @@ -731,7 +749,7 @@ public class LDAPStorageProvider implements UserStorageProvider, if (kerberosConfig.isAllowKerberosAuthentication()) { SPNEGOAuthenticator spnegoAuthenticator = (SPNEGOAuthenticator) credential.getNote(KerberosConstants.AUTHENTICATED_SPNEGO_CONTEXT); if (spnegoAuthenticator != null) { - logger.debugf("SPNEGO authentication already performed by previous provider. Provider '%s' will try to lookup user with principal kerberos principal '%s'", this, spnegoAuthenticator.getAuthenticatedUsername()); + logger.debugf("SPNEGO authentication already performed by previous provider. Provider '%s' will try to lookup user with principal kerberos principal '%s'", this, spnegoAuthenticator.getAuthenticatedKerberosPrincipal()); } else { String spnegoToken = credential.getChallengeResponse(); spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); @@ -741,14 +759,11 @@ public class LDAPStorageProvider implements UserStorageProvider, Map state = new HashMap<>(); if (spnegoAuthenticator.isAuthenticated()) { - - // TODO: This assumes that LDAP "uid" is equal to kerberos principal name. Like uid "hnelson" and kerberos principal "hnelson@KEYCLOAK.ORG". - // Check if it's correct or if LDAP attribute for mapping kerberos principal should be available (For ApacheDS it seems to be attribute "krb5PrincipalName" but on MSAD it's likely different) - String username = spnegoAuthenticator.getAuthenticatedUsername(); - UserModel user = findOrCreateAuthenticatedUser(realm, username); + KerberosPrincipal kerberosPrincipal = spnegoAuthenticator.getAuthenticatedKerberosPrincipal(); + UserModel user = findOrCreateAuthenticatedUser(realm, kerberosPrincipal); if (user == null) { - logger.debugf("Kerberos/SPNEGO authentication succeeded with kerberos principal [%s], but couldn't find or create user with federation provider [%s]", username, model.getName()); + logger.debugf("Kerberos/SPNEGO authentication succeeded with kerberos principal [%s], but couldn't find or create user with federation provider [%s]", kerberosPrincipal.toString(), model.getName()); // Adding the authenticated SPNEGO, in case that other LDAP/Kerberos providers in the chain are able to lookup user from their LDAP // This can be the case with more complex setup (like MSAD Forest Trust environment) @@ -786,23 +801,40 @@ public class LDAPStorageProvider implements UserStorageProvider, * Called after successful kerberos authentication * * @param realm realm - * @param username username without realm prefix + * @param kerberosPrincipal kerberos principal of the authenticated user * @return finded or newly created user */ - protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, String username) { - UserModel user = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, username); + protected UserModel findOrCreateAuthenticatedUser(RealmModel realm, KerberosPrincipal kerberosPrincipal) { + String kerberosPrincipalAttrName = kerberosConfig.getKerberosPrincipalAttribute(); + UserModel user; + if (kerberosPrincipalAttrName != null) { + logger.tracef("Trying to find user with kerberos principal [%s] in local storage.", kerberosPrincipal.toString()); + user = UserStoragePrivateUtil.userLocalStorage(session).searchForUserByUserAttributeStream(realm, KerberosConstants.KERBEROS_PRINCIPAL, kerberosPrincipal.toString()) + .findFirst().orElse(null); + } else { + // For this case, assuming that for kerberos principal "john@KEYCLOAK.ORG", the username would be "john" (backwards compatibility) + logger.tracef("Trying to find user in local storage based on username [%s]. Full kerberos principal [%s]", kerberosPrincipal.getPrefix(), kerberosPrincipal); + user = UserStoragePrivateUtil.userLocalStorage(session).getUserByUsername(realm, kerberosPrincipal.getPrefix()); + } + if (user != null) { - logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", username); + logger.debugf("Kerberos authenticated user [%s] found in Keycloak storage", user.getUsername()); if (!model.getId().equals(user.getFederationLink())) { - logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]", username, model.getName()); + logger.warnf("User with username [%s] already exists, but is not linked to provider [%s]. Kerberos principal is [%s]", user.getUsername(), model.getName(), kerberosPrincipal); return null; } else { LDAPObject ldapObject = loadAndValidateUser(realm, user); + if (kerberosPrincipalAttrName != null && !kerberosPrincipal.toString().equalsIgnoreCase(ldapObject.getAttributeAsString(kerberosPrincipalAttrName))) { + logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Authenticated kerberos principal is [%s], but LDAP user has different kerberos principal [%s]", + user.getUsername(), model.getName(), kerberosPrincipal, ldapObject.getAttributeAsString(kerberosPrincipalAttrName)); + ldapObject = null; + } + if (ldapObject != null) { return proxy(realm, user, ldapObject, false); } else { logger.warnf("User with username [%s] aready exists and is linked to provider [%s] but is not valid. Stale LDAP_ID on local user is: %s", - username, model.getName(), user.getFirstAttribute(LDAPConstants.LDAP_ID)); + user.getUsername(), model.getName(), user.getFirstAttribute(LDAPConstants.LDAP_ID)); logger.warn("Will re-create user"); UserCache userCache = UserStorageUtil.userCache(session); if (userCache != null) { @@ -813,9 +845,26 @@ public class LDAPStorageProvider implements UserStorageProvider, } } - // Creating user to local storage - logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", username); - return getUserByUsername(realm, username); + if (kerberosPrincipalAttrName != null) { + logger.debugf("Trying to find kerberos authenticated user [%s] in LDAP. Kerberos principal attribute is [%s]", kerberosPrincipal.toString(), kerberosPrincipalAttrName); + try (LDAPQuery ldapQuery = LDAPUtils.createQueryForUserSearch(this, realm)) { + LDAPQueryConditionsBuilder conditionsBuilder = new LDAPQueryConditionsBuilder(); + Condition krbPrincipalCondition = conditionsBuilder.equal(kerberosPrincipalAttrName, kerberosPrincipal.toString(), EscapeStrategy.DEFAULT); + ldapQuery.addWhereCondition(krbPrincipalCondition); + LDAPObject ldapUser = ldapQuery.getFirstResult(); + + if (ldapUser == null) { + logger.warnf("Not found LDAP user with kerberos principal [%s]. Kerberos principal attribute is [%s].", kerberosPrincipal.toString(), kerberosPrincipalAttrName); + return null; + } + + return importUserFromLDAP(session, realm, ldapUser); + } + } else { + // Creating user to local storage + logger.debugf("Kerberos authenticated user [%s] not in Keycloak storage. Creating him", kerberosPrincipal.toString()); + return getUserByUsername(realm, kerberosPrincipal.getPrefix()); + } } public LDAPObject loadLDAPUserByUsername(RealmModel realm, String username) { diff --git a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java index c8d88c60f9..ea727305b3 100755 --- a/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java +++ b/federation/ldap/src/main/java/org/keycloak/storage/ldap/LDAPStorageProviderFactory.java @@ -23,6 +23,7 @@ import org.keycloak.common.constants.KerberosConstants; import org.keycloak.component.ComponentModel; import org.keycloak.component.ComponentValidationException; import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.impl.KerberosServerSubjectAuthenticator; import org.keycloak.federation.kerberos.impl.KerberosUsernamePasswordAuthenticator; import org.keycloak.federation.kerberos.impl.SPNEGOAuthenticator; @@ -49,6 +50,7 @@ import org.keycloak.storage.ldap.idm.query.Condition; import org.keycloak.storage.ldap.idm.query.internal.LDAPQuery; import org.keycloak.storage.ldap.idm.query.internal.LDAPQueryConditionsBuilder; import org.keycloak.storage.ldap.idm.store.ldap.LDAPIdentityStore; +import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig; import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapper; import org.keycloak.storage.ldap.mappers.FullNameLDAPStorageMapperFactory; import org.keycloak.storage.ldap.mappers.HardcodedLDAPAttributeMapper; @@ -214,6 +216,9 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory + + } + fieldId="kc-krb-principal-attribute" + validated={ + (form.formState.errors.config as any) + ?.krbPrincipalAttribute?.[0] + ? "error" + : "default" + } + helperTextInvalid={ + (form.formState.errors.config as any) + ?.krbPrincipalAttribute?.[0].message + } + > + + + Marek Posolda */ @@ -68,6 +66,7 @@ public class LDAPTestConfiguration { PROP_MAPPINGS.put(KerberosConstants.KERBEROS_REALM, "idm.test.kerberos.realm"); PROP_MAPPINGS.put(KerberosConstants.SERVER_PRINCIPAL, "idm.test.kerberos.server.principal"); PROP_MAPPINGS.put(KerberosConstants.KEYTAB, "idm.test.kerberos.keytab"); + PROP_MAPPINGS.put(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE, "idm.test.kerberos.principal.ldap.attribute"); PROP_MAPPINGS.put(KerberosConstants.DEBUG, "idm.test.kerberos.debug"); PROP_MAPPINGS.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "idm.test.kerberos.allow.password.authentication"); PROP_MAPPINGS.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "idm.test.kerberos.update.profile.first.login"); @@ -93,6 +92,7 @@ public class LDAPTestConfiguration { DEFAULT_VALUES.put(KerberosConstants.SERVER_PRINCIPAL, "HTTP/localhost@KEYCLOAK.ORG"); String keyTabPath = getResource("/kerberos/http.keytab"); DEFAULT_VALUES.put(KerberosConstants.KEYTAB, keyTabPath); + DEFAULT_VALUES.put(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE, KerberosConstants.KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_KRB5_PRINCIPAL_NAME); DEFAULT_VALUES.put(KerberosConstants.DEBUG, "true"); DEFAULT_VALUES.put(KerberosConstants.ALLOW_PASSWORD_AUTHENTICATION, "true"); DEFAULT_VALUES.put(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "true"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java index ba5677febd..63ab22b866 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/AbstractKerberosTest.java @@ -49,6 +49,7 @@ import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.admin.client.resource.RealmResource; import org.keycloak.authentication.authenticators.browser.SpnegoAuthenticatorFactory; import org.keycloak.common.Profile.Feature; +import org.keycloak.common.constants.KerberosConstants; import org.keycloak.common.util.MultivaluedHashMap; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; @@ -69,6 +70,7 @@ import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.ProfileAssume; import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.util.KerberosRule; import org.keycloak.testsuite.util.KerberosUtils; @@ -89,6 +91,9 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { @Page protected LoginPage loginPage; + @Page + protected AppPage appPage; + @Rule public AssertEvents events = new AssertEvents(this); @@ -279,8 +284,9 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { } + protected UserRepresentation assertUser(String expectedUsername, String expectedEmail, String expectedFirstname, - String expectedLastname, boolean updateProfileActionExpected) { + String expectedLastname, String expectedKerberosPrincipal, boolean updateProfileActionExpected) { try { UserRepresentation user = ApiUtil.findUserByUsername(testRealmResource(), expectedUsername); Assert.assertNotNull(user); @@ -288,6 +294,12 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { Assert.assertEquals(expectedFirstname, user.getFirstName()); Assert.assertEquals(expectedLastname, user.getLastName()); + if (expectedKerberosPrincipal == null) { + Assert.assertNull(user.getAttributes().get(KerberosConstants.KERBEROS_PRINCIPAL)); + } else { + Assert.assertEquals(expectedKerberosPrincipal, user.getAttributes().get(KerberosConstants.KERBEROS_PRINCIPAL).get(0)); + } + if (updateProfileActionExpected) { Assert.assertEquals(UserModel.RequiredAction.UPDATE_PROFILE.toString(), user.getRequiredActions().iterator().next()); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapCrossRealmTrustTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapCrossRealmTrustTest.java index 684ca55d3f..0a93741f41 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapCrossRealmTrustTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapCrossRealmTrustTest.java @@ -21,6 +21,7 @@ import org.junit.ClassRule; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runners.MethodSorters; +import org.keycloak.common.constants.KerberosConstants; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.ComponentRepresentation; @@ -32,6 +33,7 @@ import org.keycloak.testsuite.KerberosEmbeddedServer; import org.keycloak.testsuite.util.OAuthClient; import jakarta.ws.rs.core.Response; +import org.keycloak.testsuite.util.TestAppHelper; /** * @author Marek Posolda @@ -72,12 +74,68 @@ public class KerberosLdapCrossRealmTrustTest extends AbstractKerberosTest { AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com"); - assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", false); + assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", "hnelson2@KC2.COM", false); } + // Issue 20045 @Test - public void test02DisableTrust() throws Exception { + public void test02SpnegoLoginCorrectKerberosPrincipalUserFound() throws Exception { + // Login as kerberos user jduke@KC2.COM. Ensure I am logged as user "jduke2" from realm KC2.COM (not as user jduke@KEYCLOAK.ORG) + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("jduke@KC2.COM", "jduke2", "theduke2"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + + Assert.assertEquals(token.getEmail(), "jduke2@kc2.com"); + assertUser("jduke2", "jduke2@kc2.com", "Java", "Duke", "jduke@KC2.COM", false); + + // Logout + oauth.openLogout(); + events.poll(); + + // Another login to check the scenario when user is in local storage + tokenResponse = assertSuccessfulSpnegoLogin("jduke@KC2.COM", "jduke2", "theduke2"); + token = oauth.verifyToken(tokenResponse.getAccessToken()); + Assert.assertEquals(token.getEmail(), "jduke2@kc2.com"); + } + + // Issue 20045 - username/password form login + @Test + public void test03SpnegoLoginUsernamePassword() throws Exception { + // User jduke@KC2.COM + TestAppHelper testAppHelper = new TestAppHelper(oauth, loginPage, appPage); + Assert.assertFalse(testAppHelper.login("jduke2", "theduke")); + Assert.assertTrue(testAppHelper.login("jduke2", "theduke2")); + Assert.assertTrue(testAppHelper.logout()); + + // User jduke@KEYCLOAK.ORG + Assert.assertTrue(testAppHelper.login("jduke", "theduke")); + } + + // Test with "Kerberos Principal attribute name" set to empty value (backwards compatibility). + @Test + public void test04SpnegoLoginWithoutKerberosPrincipalAttrConfigured() throws Exception { + updateUserStorageProvider(kerberosProvider -> kerberosProvider.getConfig().putSingle(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE, null)); + + // Keycloak will lookup user just based on 1st part of kerberos principal. Hence for "jduke@KC2.COM", it will lookup user "jduke" + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("jduke@KC2.COM", "jduke", "theduke2"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + + Assert.assertEquals(token.getEmail(), "jduke@keycloak.org"); + assertUser("jduke", "jduke@keycloak.org", "Java", "Duke", null, false); + + // Logout + oauth.openLogout(); + events.poll(); + + // This refers to same user as above login + tokenResponse = assertSuccessfulSpnegoLogin("jduke@KEYCLOAK.ORG", "jduke", "theduke"); + token = oauth.verifyToken(tokenResponse.getAccessToken()); + + Assert.assertEquals(token.getEmail(), "jduke@keycloak.org"); + } + + @Test + public void test05DisableTrust() throws Exception { // Remove the LDAP entry corresponding to the Kerberos principal krbtgt/KEYCLOAK.ORG@KC2.COM // This will effectively disable kerberos cross-realm trust testingClient.testing().ldap("test").removeLDAPUser("krbtgt2"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapMultipleLDAPProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapMultipleLDAPProvidersTest.java index 1b8a229931..ebe9fd00dc 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapMultipleLDAPProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapMultipleLDAPProvidersTest.java @@ -88,7 +88,7 @@ public class KerberosLdapMultipleLDAPProvidersTest extends AbstractKerberosTest OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com"); - UserRepresentation user = assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", false); + UserRepresentation user = assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", "hnelson2@KC2.COM", false); assertUserStorageProvider(user, "kerberos-ldap"); } @@ -107,7 +107,7 @@ public class KerberosLdapMultipleLDAPProvidersTest extends AbstractKerberosTest OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com"); - UserRepresentation user = assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", false); + UserRepresentation user = assertUser("hnelson2", "hnelson2@kc2.com", "Horatio", "Nelson", "hnelson2@KC2.COM", false); assertUserStorageProvider(user, "kerberos-ldap"); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java index 049df3919e..e65fb821f6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java @@ -78,7 +78,7 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest { assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret"); // Assert user was imported and hasn't any required action on him. Profile info is synced from LDAP - assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false); + assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", "hnelson@KEYCLOAK.ORG", false); } @Test diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneCrossRealmTrustTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneCrossRealmTrustTest.java index 276f92082b..af89f900db 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneCrossRealmTrustTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneCrossRealmTrustTest.java @@ -25,8 +25,10 @@ import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.federation.kerberos.KerberosConfig; import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.testsuite.Assert; import org.keycloak.testsuite.util.KerberosRule; import org.keycloak.testsuite.KerberosEmbeddedServer; +import org.keycloak.testsuite.util.TestAppHelper; /** * @author Marek Posolda @@ -65,17 +67,37 @@ public class KerberosStandaloneCrossRealmTrustTest extends AbstractKerberosTest @Test public void test01spnegoLoginSameRealmTest() throws Exception { assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret"); - assertUser("hnelson", "hnelson@keycloak.org", null, null, false); + assertUser("hnelson", "hnelson@keycloak.org", null, null, "hnelson@KEYCLOAK.ORG", false); } @Test public void test02spnegoLoginDifferentRealmTest() throws Exception { // Cross-realm trust login. Realm KEYCLOAK.ORG trusts realm KC2.COM. - // TODO: email hnelson2@keycloak.org is not very good. Will be better to have more flexibility for mapping of kerberos principals to Keycloak UserModel in KerberosFederationProvider (if needed) - assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); - assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false); + assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2@kc2.com", "secret"); + assertUser("hnelson2@kc2.com", "hnelson2@kc2.com", null, null, "hnelson2@KC2.COM", false); + + // Logout + oauth.openLogout(); + events.poll(); + + // Another login to check the scenario when user is in local storage + assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2@kc2.com", "secret"); } + // Issue 20045 - username/password form login + @Test + public void test03SpnegoLoginWithCorrectKerberosPrincipalRealm() throws Exception { + // Login in username/password form as "jduke@KEYCLOAK.ORG" + TestAppHelper testAppHelper = new TestAppHelper(oauth, loginPage, appPage); + Assert.assertTrue(testAppHelper.login("jduke", "theduke")); + Assert.assertTrue(testAppHelper.logout()); + // Login in username/password form as "jduke@KC2.COM" + Assert.assertFalse(testAppHelper.login("jduke@kc2.com", "theduke")); + Assert.assertTrue(testAppHelper.login("jduke@kc2.com", "theduke2")); + + assertUser("jduke", "jduke@keycloak.org", null, null, "jduke@KEYCLOAK.ORG", false); + assertUser("jduke@kc2.com", "jduke@kc2.com", null, null, "jduke@KC2.COM", false); + } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneMultipleProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneMultipleProvidersTest.java index fe24470263..cbbcaa72e1 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneMultipleProvidersTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneMultipleProvidersTest.java @@ -87,10 +87,10 @@ public class KerberosStandaloneMultipleProvidersTest extends AbstractKerberosTes getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); resp.close(); - OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2@kc2.com", "secret"); AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); - Assert.assertEquals(token.getEmail(), "hnelson2@keycloak.org"); - UserRepresentation user = assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false); + Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com"); + UserRepresentation user = assertUser("hnelson2@kc2.com", "hnelson2@kc2.com", null, null, "hnelson2@KC2.COM", false); assertUserStorageProvider(user, "kerberos-standalone"); } @@ -107,10 +107,10 @@ public class KerberosStandaloneMultipleProvidersTest extends AbstractKerberosTes getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); resp.close(); - OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2@kc2.com", "secret"); AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); - Assert.assertEquals(token.getEmail(), "hnelson2@keycloak.org"); - UserRepresentation user = assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false); + Assert.assertEquals(token.getEmail(), "hnelson2@kc2.com"); + UserRepresentation user = assertUser("hnelson2@kc2.com", "hnelson2@kc2.com", null, null, "hnelson2@KC2.COM",false); assertUserStorageProvider(user, "kerberos-standalone"); } } diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java index 094d16c2ff..72b815efd7 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneTest.java @@ -80,7 +80,8 @@ public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest { assertSuccessfulSpnegoLogin("hnelson", "hnelson", "secret"); // Assert user was imported and hasn't any required action on him. Profile info is NOT synced from LDAP. Just username is filled and email is "guessed" - assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, false); + assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, + "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM), false); } @@ -103,7 +104,8 @@ public class KerberosStandaloneTest extends AbstractKerberosSingleRealmTest { spnegoResponse.close(); // Assert user was imported and has required action on him - assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, true); + assertUser("hnelson", "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM).toLowerCase(), null, null, + "hnelson@" + kerberosRule.getConfig().get(KerberosConstants.KERBEROS_REALM), true); // Switch updateProfileOnFirstLogin to off kerberosProvider.getConfig().putSingle(KerberosConstants.UPDATE_PROFILE_FIRST_LOGIN, "false"); diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java index b0c512cc56..db6c9aac3e 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KeycloakSPNegoSchemeFactory.java @@ -81,18 +81,7 @@ public class KeycloakSPNegoSchemeFactory extends SPNegoSchemeFactory { @Override protected byte[] generateGSSToken(byte[] input, Oid oid, String authServer, Credentials credentials) throws GSSException { - KerberosUsernamePasswordAuthenticator authenticator = new KerberosUsernamePasswordAuthenticator(kerberosConfig) { - - // Disable strict check for the configured kerberos realm, which is on super-method - @Override - protected String getKerberosPrincipal(String username) throws LoginException { - if (username.contains("@")) { - return username; - } else { - return username + "@" + config.getKerberosRealm(); - } - } - }; + KerberosUsernamePasswordAuthenticator authenticator = new KerberosUsernamePasswordAuthenticator(kerberosConfig); try { Subject clientSubject = authenticator.authenticateSubject(username, password); diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif index 8758643320..a07108c8d6 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif +++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/users-kerberos-kc2.ldif @@ -76,6 +76,20 @@ userPassword: secret krb5PrincipalName: hnelson2@KC2.COM krb5KeyVersionNumber: 0 +dn: uid=jduke,ou=People,dc=kc2,dc=com +objectClass: top +objectClass: person +objectClass: inetOrgPerson +objectClass: krb5principal +objectClass: krb5kdcentry +cn: Java +sn: Duke +mail: jduke@keycloak.org +uid: jduke +userPassword: theduke +krb5PrincipalName: jduke@KEYCLOAK.ORG +krb5KeyVersionNumber: 0 + dn: uid=jduke2,ou=People,dc=kc2,dc=com objectClass: top objectClass: person @@ -84,10 +98,10 @@ objectClass: krb5principal objectClass: krb5kdcentry cn: Java sn: Duke -mail: jduke2@keycloak.org +mail: jduke2@kc2.com uid: jduke2 -userPassword: theduke -krb5PrincipalName: jduke2@KC2.COM +userPassword: theduke2 +krb5PrincipalName: jduke@KC2.COM krb5KeyVersionNumber: 0 dn: uid=gsstestserver,ou=People,dc=kc2,dc=com