Use an original domain name of Kerberos Principal in UserModel attribute instead of configured value of Kerberos realm in User federation
closes #20045
This commit is contained in:
parent
4cd34f8423
commit
57e51e9dd4
25 changed files with 393 additions and 113 deletions
|
@ -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";
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -47,6 +47,8 @@ import java.util.HashMap;
|
|||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.security.auth.login.LoginException;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -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<String, String> 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)) {
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -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,21 +153,13 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected CallbackHandler createJaasCallbackHandler(final String principal, final String password) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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<String, String> 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,
|
|||
}
|
||||
}
|
||||
|
||||
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", username);
|
||||
return getUserByUsername(realm, username);
|
||||
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) {
|
||||
|
|
|
@ -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<LD
|
|||
.property().name(KerberosConstants.KERBEROS_REALM)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE)
|
||||
.type(ProviderConfigProperty.STRING_TYPE)
|
||||
.add()
|
||||
.property().name(KerberosConstants.DEBUG)
|
||||
.type(ProviderConfigProperty.BOOLEAN_TYPE)
|
||||
.defaultValue("false")
|
||||
|
@ -429,12 +434,19 @@ public class LDAPStorageProviderFactory implements UserStorageProviderFactory<LD
|
|||
mapperModel = KeycloakModelUtils.createComponentModel("MSAD account controls", model.getId(), MSADUserAccountControlStorageMapperFactory.PROVIDER_ID,LDAPStorageMapper.class.getName());
|
||||
realm.addComponentModel(mapperModel);
|
||||
}
|
||||
String allowKerberosCfg = model.getConfig().getFirst(KerberosConstants.ALLOW_KERBEROS_AUTHENTICATION);
|
||||
if (Boolean.valueOf(allowKerberosCfg)) {
|
||||
|
||||
LDAPProviderKerberosConfig kerberosConfig = new LDAPProviderKerberosConfig(model);
|
||||
if (kerberosConfig.isAllowKerberosAuthentication()) {
|
||||
CredentialHelper.setOrReplaceAuthenticationRequirement(session, realm, CredentialRepresentation.KERBEROS,
|
||||
AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED);
|
||||
}
|
||||
|
||||
if (kerberosConfig.getKerberosPrincipalAttribute() == null) {
|
||||
String defaultKerberosUserPrincipalAttr = LDAPUtils.getDefaultKerberosUserPrincipalAttribute(ldapConfig.getVendor());
|
||||
model.getConfig().putSingle(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE, defaultKerberosUserPrincipalAttr);
|
||||
realm.updateComponent(model);
|
||||
}
|
||||
|
||||
// In case that "Sync Registration" is ON and the LDAP v3 Password-modify extension is ON, we will create hardcoded mapper to create
|
||||
// random "userPassword" every time when creating user. Otherwise users won't be able to register and login
|
||||
if (!activeDirectory && syncRegistrations && ldapConfig.useExtendedPasswordModifyOp()) {
|
||||
|
|
|
@ -32,6 +32,7 @@ import java.util.stream.Collectors;
|
|||
import javax.naming.directory.SearchControls;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
import org.keycloak.common.constants.KerberosConstants;
|
||||
import org.keycloak.component.ComponentModel;
|
||||
import org.keycloak.component.ComponentValidationException;
|
||||
import org.keycloak.models.LDAPConstants;
|
||||
|
@ -137,6 +138,12 @@ public class LDAPUtils {
|
|||
.collect(Collectors.toList());
|
||||
ldapQuery.addMappers(mapperModels);
|
||||
|
||||
String kerberosPrincipalAttr = ldapProvider.getKerberosConfig().getKerberosPrincipalAttribute();
|
||||
if (kerberosPrincipalAttr != null) {
|
||||
ldapQuery.addReturningLdapAttribute(kerberosPrincipalAttr);
|
||||
ldapQuery.addReturningReadOnlyLdapAttribute(kerberosPrincipalAttr);
|
||||
}
|
||||
|
||||
return ldapQuery;
|
||||
}
|
||||
|
||||
|
@ -377,4 +384,17 @@ public class LDAPUtils {
|
|||
|
||||
return userModelProperties;
|
||||
}
|
||||
|
||||
public static String getDefaultKerberosUserPrincipalAttribute(String vendor) {
|
||||
if (vendor != null) {
|
||||
switch (vendor) {
|
||||
case LDAPConstants.VENDOR_RHDS:
|
||||
return KerberosConstants.KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_KRB_PRINCIPAL_NAME;
|
||||
case LDAPConstants.VENDOR_ACTIVE_DIRECTORY:
|
||||
return KerberosConstants.KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_USER_PRINCIPAL_NAME;
|
||||
}
|
||||
}
|
||||
|
||||
return KerberosConstants.KERBEROS_PRINCIPAL_LDAP_ATTRIBUTE_KRB5_PRINCIPAL_NAME;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,4 +41,8 @@ public class LDAPProviderKerberosConfig extends CommonKerberosConfig {
|
|||
public boolean isUseKerberosForPasswordAuthentication() {
|
||||
return Boolean.valueOf(getConfig().getFirst(KerberosConstants.USE_KERBEROS_FOR_PASSWORD_AUTHENTICATION));
|
||||
}
|
||||
|
||||
public String getKerberosPrincipalAttribute() {
|
||||
return getConfig().getFirst(KerberosConstants.KERBEROS_PRINCIPAL_ATTRIBUTE);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@
|
|||
"kerberosRealmHelp": "Name of kerberos realm. For example, FOO.ORG",
|
||||
"serverPrincipalHelp": "Full name of server principal for HTTP service including server and domain name. For example, HTTP/host.foo.org@FOO.ORG",
|
||||
"keyTabHelp": "Location of Kerberos KeyTab file containing the credentials of server principal. For example, /etc/krb5.keytab",
|
||||
"krbPrincipalAttributeHelp": "Name of the LDAP attribute, which refers to Kerberos principal. This is used to lookup appropriate LDAP user after successful Kerberos/SPNEGO authentication in Keycloak. When this is empty, the LDAP user will be looked based on LDAP username corresponding to the first part of his Kerberos principal. For instance, for principal 'john@KEYCLOAK.ORG', it will assume that LDAP username is 'john'.",
|
||||
"debugHelp": "Enable/disable debug logging to standard output for Krb5LoginModule.",
|
||||
"allowPasswordAuthenticationHelp": "Enable/disable possibility of username/password authentication against Kerberos database",
|
||||
"editModeKerberosHelp": "READ_ONLY means that password updates are not allowed and user always authenticates with Kerberos password. UNSYNCED means that the user can change the password in the Keycloak database and this one will be used instead of the Kerberos password.",
|
||||
|
|
|
@ -64,6 +64,7 @@
|
|||
"kerberosRealm": "Kerberos realm",
|
||||
"serverPrincipal": "Server principal",
|
||||
"keyTab": "Key tab",
|
||||
"krbPrincipalAttribute": "Kerberos principal attribute",
|
||||
"debug": "Debug",
|
||||
"allowPasswordAuthentication": "Allow password authentication",
|
||||
"updateFirstLogin": "Update first login",
|
||||
|
|
|
@ -47,6 +47,7 @@ export const LdapSettingsGeneral = ({
|
|||
form.setValue("config.usernameLDAPAttribute[0]", "cn");
|
||||
form.setValue("config.rdnLDAPAttribute[0]", "cn");
|
||||
form.setValue("config.uuidLDAPAttribute[0]", "objectGUID");
|
||||
form.setValue("config.krbPrincipalAttribute[0]", "userPrincipalName");
|
||||
form.setValue(
|
||||
"config.userObjectClasses[0]",
|
||||
"person, organizationalPerson, user",
|
||||
|
@ -56,6 +57,7 @@ export const LdapSettingsGeneral = ({
|
|||
form.setValue("config.usernameLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.rdnLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.uuidLDAPAttribute[0]", "nsuniqueid");
|
||||
form.setValue("config.krbPrincipalAttribute[0]", "krbPrincipalName");
|
||||
form.setValue(
|
||||
"config.userObjectClasses[0]",
|
||||
"inetOrgPerson, organizationalPerson",
|
||||
|
@ -65,6 +67,7 @@ export const LdapSettingsGeneral = ({
|
|||
form.setValue("config.usernameLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.rdnLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.uuidLDAPAttribute[0]", "uniqueidentifier");
|
||||
form.setValue("config.krbPrincipalAttribute[0]", "krb5PrincipalName");
|
||||
form.setValue(
|
||||
"config.userObjectClasses[0]",
|
||||
"inetOrgPerson, organizationalPerson",
|
||||
|
@ -74,6 +77,7 @@ export const LdapSettingsGeneral = ({
|
|||
form.setValue("config.usernameLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.rdnLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.uuidLDAPAttribute[0]", "guid");
|
||||
form.setValue("config.krbPrincipalAttribute[0]", "krb5PrincipalName");
|
||||
form.setValue(
|
||||
"config.userObjectClasses[0]",
|
||||
"inetOrgPerson, organizationalPerson",
|
||||
|
@ -83,6 +87,7 @@ export const LdapSettingsGeneral = ({
|
|||
form.setValue("config.usernameLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.rdnLDAPAttribute[0]", "uid");
|
||||
form.setValue("config.uuidLDAPAttribute[0]", "entryUUID");
|
||||
form.setValue("config.krbPrincipalAttribute[0]", "krb5PrincipalName");
|
||||
form.setValue(
|
||||
"config.userObjectClasses[0]",
|
||||
"inetOrgPerson, organizationalPerson",
|
||||
|
|
|
@ -185,6 +185,40 @@ export const LdapSettingsKerberosIntegration = ({
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={t("krbPrincipalAttribute")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText={t("user-federation-help:krbPrincipalAttributeHelp")}
|
||||
fieldLabelId="user-federation:krbPrincipalAttribute"
|
||||
/>
|
||||
}
|
||||
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
|
||||
}
|
||||
>
|
||||
<KeycloakTextInput
|
||||
defaultValue="userPrincipalName"
|
||||
id="kc-krb-principal-attribute"
|
||||
data-testid="krb-principal-attribute"
|
||||
validated={
|
||||
(form.formState.errors.config as any)
|
||||
?.krbPrincipalAttribute?.[0]
|
||||
? "error"
|
||||
: "default"
|
||||
}
|
||||
{...form.register("config.krbPrincipalAttribute.0")}
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
label={t("debug")}
|
||||
labelIcon={
|
||||
|
|
|
@ -32,8 +32,6 @@ import java.net.URL;
|
|||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.keycloak.testsuite.utils.io.IOUtil.PROJECT_BUILD_DIRECTORY;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
*/
|
||||
|
@ -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");
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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");
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue