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 b644f167ae..79f8c87c60 100644 --- a/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java +++ b/common/src/main/java/org/keycloak/common/constants/KerberosConstants.java @@ -92,4 +92,9 @@ public class KerberosConstants { */ public static final String GSS_DELEGATION_CREDENTIAL_DISPLAY_NAME = "gss delegation credential"; + /** + * Attribute attached to the credential, which contains authenticated SPNEGO context. This is used in case that some LDAP/Kerberos provider was able to authenticate user via SPNEGO, but wasn't able + * 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"; } diff --git a/docs/documentation/release_notes/index.adoc b/docs/documentation/release_notes/index.adoc index e47b325d2f..d5cba420d5 100644 --- a/docs/documentation/release_notes/index.adoc +++ b/docs/documentation/release_notes/index.adoc @@ -16,6 +16,9 @@ include::topics/templates/release-header.adoc[] == {project_name_full} 23.0.0 include::topics/23_0_0.adoc[leveloffset=2] +== {project_name_full} 22.0.2 +include::topics/22_0_2.adoc[leveloffset=2] + == {project_name_full} 22.0.0 include::topics/22_0_0.adoc[leveloffset=2] diff --git a/docs/documentation/release_notes/topics/22_0_2.adoc b/docs/documentation/release_notes/topics/22_0_2.adoc new file mode 100644 index 0000000000..a15facf0f8 --- /dev/null +++ b/docs/documentation/release_notes/topics/22_0_2.adoc @@ -0,0 +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. 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 16cc8f6e16..e456560858 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 @@ -190,17 +190,27 @@ public class KerberosFederationProvider implements UserStorageProvider, if (!(input instanceof UserCredentialModel)) return null; UserCredentialModel credential = (UserCredentialModel)input; if (credential.getType().equals(UserCredentialModel.KERBEROS)) { - String spnegoToken = credential.getChallengeResponse(); - SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); + 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()); + } else { + String spnegoToken = credential.getChallengeResponse(); + spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); - spnegoAuthenticator.authenticate(); + spnegoAuthenticator.authenticate(); + } - Map state = new HashMap(); + Map state = new HashMap<>(); if (spnegoAuthenticator.isAuthenticated()) { String username = spnegoAuthenticator.getAuthenticatedUsername(); UserModel user = findOrCreateAuthenticatedUser(realm, username); if (user == null) { - return CredentialValidationOutput.failed(); + // 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) + // Note that SPNEGO authentication cannot be done again by the other provider due the Kerberos replay protection + credential.setNote(KerberosConstants.AUTHENTICATED_SPNEGO_CONTEXT, spnegoAuthenticator); + + return CredentialValidationOutput.fallback(); } else { String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential(); if (delegationCredential != null) { @@ -216,7 +226,7 @@ public class KerberosFederationProvider implements UserStorageProvider, return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); } else { logger.tracef("SPNEGO Handshake not successful"); - return CredentialValidationOutput.failed(); + return CredentialValidationOutput.fallback(); } } else { @@ -282,4 +292,9 @@ public class KerberosFederationProvider implements UserStorageProvider, return validate(realm, user); } + + @Override + public String toString() { + return "KerberosFederationProvider - " + model.getName(); + } } 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 74c745a20f..b077387b6f 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 @@ -725,14 +725,19 @@ public class LDAPStorageProvider implements UserStorageProvider, @Override public CredentialValidationOutput authenticate(RealmModel realm, CredentialInput cred) { - if (!(cred instanceof UserCredentialModel)) return CredentialValidationOutput.failed(); + if (!(cred instanceof UserCredentialModel)) return CredentialValidationOutput.fallback(); UserCredentialModel credential = (UserCredentialModel)cred; if (credential.getType().equals(UserCredentialModel.KERBEROS)) { if (kerberosConfig.isAllowKerberosAuthentication()) { - String spnegoToken = credential.getChallengeResponse(); - SPNEGOAuthenticator spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); + 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()); + } else { + String spnegoToken = credential.getChallengeResponse(); + spnegoAuthenticator = factory.createSPNEGOAuthenticator(spnegoToken, kerberosConfig); - spnegoAuthenticator.authenticate(); + spnegoAuthenticator.authenticate(); + } Map state = new HashMap<>(); if (spnegoAuthenticator.isAuthenticated()) { @@ -743,8 +748,13 @@ public class LDAPStorageProvider implements UserStorageProvider, UserModel user = findOrCreateAuthenticatedUser(realm, username); if (user == null) { - logger.warnf("Kerberos/SPNEGO authentication succeeded with username [%s], but couldn't find or create user with federation provider [%s]", username, model.getName()); - return CredentialValidationOutput.failed(); + logger.debugf("Kerberos/SPNEGO authentication succeeded with kerberos principal [%s], but couldn't find or create user with federation provider [%s]", username, 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) + // Note that SPNEGO authentication cannot be done again by the other provider due the Kerberos replay protection + credential.setNote(KerberosConstants.AUTHENTICATED_SPNEGO_CONTEXT, spnegoAuthenticator); + return CredentialValidationOutput.fallback(); } else { String delegationCredential = spnegoAuthenticator.getSerializedDelegationCredential(); if (delegationCredential != null) { @@ -760,12 +770,12 @@ public class LDAPStorageProvider implements UserStorageProvider, return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state); } else { logger.tracef("SPNEGO Handshake not successful"); - return CredentialValidationOutput.failed(); + return CredentialValidationOutput.fallback(); } } } - return CredentialValidationOutput.failed(); + return CredentialValidationOutput.fallback(); } @Override @@ -899,4 +909,9 @@ public class LDAPStorageProvider implements UserStorageProvider, return ldapQuery.getResultList().stream(); } + + @Override + public String toString() { + return "LDAPStorageProvider - " + getModel().getName(); + } } diff --git a/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java index cb3431fe79..2c82afb6d4 100755 --- a/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java +++ b/model/legacy-private/src/main/java/org/keycloak/storage/UserStorageManager.java @@ -146,11 +146,25 @@ public class UserStorageManager extends AbstractStorageManager credentialAuthentication.supportsCredentialAuthenticationFor(input.getType())) - .map(credentialAuthentication -> credentialAuthentication.authenticate(realm, input)) - .filter(Objects::nonNull) - .findFirst().orElse(null); + .collect(Collectors.toList())) { + CredentialValidationOutput validationOutput = credentialAuthentication.authenticate(realm, input); + if (Objects.nonNull(validationOutput)) { + CredentialValidationOutput.Status status = validationOutput.getAuthStatus(); + if (status == CredentialValidationOutput.Status.AUTHENTICATED || status == CredentialValidationOutput.Status.CONTINUE || status == CredentialValidationOutput.Status.FAILED) { + logger.tracef("Attempt to authenticate credential '%s' with provider '%s' finished with '%s'.", input.getType(), credentialAuthentication, status); + if (status == CredentialValidationOutput.Status.AUTHENTICATED) { + logger.tracef("Authenticated user is '%s'", validationOutput.getAuthenticatedUser().getUsername()); + } + result = validationOutput; + break; + } + } + logger.tracef("Did not authenticate user by provider '%s' with the credential type '%s'. Will try to fallback to other user storage providers", credentialAuthentication, input.getType()); + } + return result; } protected void deleteInvalidUser(final RealmModel realm, final UserModel user) { diff --git a/server-spi/src/main/java/org/keycloak/models/CredentialValidationOutput.java b/server-spi/src/main/java/org/keycloak/models/CredentialValidationOutput.java index be4d2f333e..3dc37c709d 100644 --- a/server-spi/src/main/java/org/keycloak/models/CredentialValidationOutput.java +++ b/server-spi/src/main/java/org/keycloak/models/CredentialValidationOutput.java @@ -38,7 +38,11 @@ public class CredentialValidationOutput { } public static CredentialValidationOutput failed() { - return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap()); + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FAILED, new HashMap<>()); + } + + public static CredentialValidationOutput fallback() { + return new CredentialValidationOutput(null, CredentialValidationOutput.Status.FALLBACK, new HashMap<>()); } public UserModel getAuthenticatedUser() { @@ -63,6 +67,27 @@ public class CredentialValidationOutput { } public enum Status { - AUTHENTICATED, FAILED, CONTINUE + + /** + * User was successfully authenticated. The {@link #getAuthenticatedUser()} must return authenticated user when this is used + */ + AUTHENTICATED, + + /** + * Federation provider failed to authenticate user. This is typically used when user storage provider recognizes the user, but credentials + * are incorrect, so federation provider can mark whole authentication as not successful without eventual fallback to other user storage provider + */ + FAILED, + + /** + * Federation provider was not able to recognize the user. It is possible that credential was valid, but fereration provider was not able to lookup the user in it's storage. + * Fallback to other user storage provider in the chain might be possible + */ + FALLBACK, + + /** + * Federation provider did not fully authenticate user. It may be needed to ask user for further challenge to then re-try authentication with same federation provider + */ + CONTINUE, } } diff --git a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java index b137d7116e..b1729df2c5 100755 --- a/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java +++ b/server-spi/src/main/java/org/keycloak/models/UserCredentialModel.java @@ -193,7 +193,7 @@ public class UserCredentialModel implements CredentialInput { this.algorithm = algorithm; } - public void setNote(String key, String value) { + public void setNote(String key, Object value) { this.notes.put(key, value); } 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 3be877d77d..ba5677febd 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 @@ -279,7 +279,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { } - protected void assertUser(String expectedUsername, String expectedEmail, String expectedFirstname, + protected UserRepresentation assertUser(String expectedUsername, String expectedEmail, String expectedFirstname, String expectedLastname, boolean updateProfileActionExpected) { try { UserRepresentation user = ApiUtil.findUserByUsername(testRealmResource(), expectedUsername); @@ -294,10 +294,17 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest { } else { Assert.assertTrue(user.getRequiredActions().isEmpty()); } + return user; } finally { } } + protected void assertUserStorageProvider(UserRepresentation user, String providerName) { + if (user.getFederationLink() == null) Assert.fail("Federation link on user " + user.getUsername() + " was null"); + ComponentRepresentation rep = testRealmResource().components().component(user.getFederationLink()).toRepresentation(); + Assert.assertEquals(providerName, rep.getName()); + } + protected OAuthClient.AccessTokenResponse assertAuthenticationSuccess(String codeUrl) throws Exception { List pairs = URLEncodedUtils.parse(new URI(codeUrl), "UTF-8"); 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 new file mode 100644 index 0000000000..1b8a229931 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapMultipleLDAPProvidersTest.java @@ -0,0 +1,113 @@ +/* + * 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.testsuite.federation.kerberos; + +import jakarta.ws.rs.core.Response; +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.component.PrioritizedComponentModel; +import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.models.LDAPConstants; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.storage.ldap.kerberos.LDAPProviderKerberosConfig; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.KerberosEmbeddedServer; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.util.KerberosRule; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class KerberosLdapMultipleLDAPProvidersTest extends AbstractKerberosTest { + + private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-ldap-crt-connection.properties"; + + @ClassRule + public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM); + + @ClassRule + public static KerberosRule kerberosRule2 = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM_2); + + + @Override + protected KerberosRule getKerberosRule() { + return kerberosRule; + } + + + @Override + protected CommonKerberosConfig getKerberosConfig() { + return new LDAPProviderKerberosConfig(getUserStorageConfiguration()); + } + + @Override + protected ComponentRepresentation getUserStorageConfiguration() { + ComponentRepresentation rep = getUserStorageConfiguration("kerberos-ldap", LDAPStorageProviderFactory.PROVIDER_NAME); + // This provider works. It would be executed as 2nd provider (individual tests are supposed to add other provider, which should have lower priority to be invoked first) + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "10"); + return rep; + } + + @Test + public void test01spnegoWith1stProviderBrokenKerberosConfiguration() throws Exception { + // Add LDAP, which is invoked first. The Kerberos configuration is broken, so SPNEGO workflow is failing entirely + ComponentRepresentation rep = getUserStorageConfiguration(); + rep.setName("kerberos-ldap-foo"); + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "1"); + rep.getConfig().putSingle(KerberosConstants.KERBEROS_REALM, "FOO.ORG"); + rep.getConfig().putSingle(KerberosConstants.SERVER_PRINCIPAL, "HTTP/localhost@FOO.ORG"); + Response resp = testRealmResource().components().add(rep); + getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); + resp.close(); + + 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); + assertUserStorageProvider(user, "kerberos-ldap"); + } + + @Test + public void test02spnegoWith1stProviderBrokenLookupOfKerberosUser() throws Exception { + // Add LDAP, which is invoked first. The Kerberos configuration is OK, so SPNEGO workflow should be fine. + // However lookup LDAP based on Kerberos principal is broken, so fallback to next provider would be needed + ComponentRepresentation rep = getUserStorageConfiguration(); + rep.setName("kerberos-ldap-broken-lookup"); + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "1"); + rep.getConfig().putSingle(LDAPConstants.CUSTOM_USER_SEARCH_FILTER, "(mail=nonexistent@email.org)"); + Response resp = testRealmResource().components().add(rep); + getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); + resp.close(); + + 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); + assertUserStorageProvider(user, "kerberos-ldap"); + } +} 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 new file mode 100644 index 0000000000..fe24470263 --- /dev/null +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosStandaloneMultipleProvidersTest.java @@ -0,0 +1,116 @@ +/* + * 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.testsuite.federation.kerberos; + +import jakarta.ws.rs.core.Response; +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.component.PrioritizedComponentModel; +import org.keycloak.federation.kerberos.CommonKerberosConfig; +import org.keycloak.federation.kerberos.KerberosConfig; +import org.keycloak.federation.kerberos.KerberosFederationProviderFactory; +import org.keycloak.models.LDAPConstants; +import org.keycloak.representations.AccessToken; +import org.keycloak.representations.idm.ComponentRepresentation; +import org.keycloak.representations.idm.UserRepresentation; +import org.keycloak.storage.ldap.LDAPStorageProviderFactory; +import org.keycloak.testsuite.Assert; +import org.keycloak.testsuite.KerberosEmbeddedServer; +import org.keycloak.testsuite.admin.ApiUtil; +import org.keycloak.testsuite.util.KerberosRule; +import org.keycloak.testsuite.util.OAuthClient; + +/** + * @author Marek Posolda + */ +@FixMethodOrder(MethodSorters.NAME_ASCENDING) +public class KerberosStandaloneMultipleProvidersTest extends AbstractKerberosTest { + + private static final String PROVIDER_CONFIG_LOCATION = "classpath:kerberos/kerberos-standalone-connection.properties"; + + + @ClassRule + public static KerberosRule kerberosRule = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM); + + + @ClassRule + public static KerberosRule kerberosRule2 = new KerberosRule(PROVIDER_CONFIG_LOCATION, KerberosEmbeddedServer.DEFAULT_KERBEROS_REALM_2); + + + @Override + protected KerberosRule getKerberosRule() { + return kerberosRule; + } + + @Override + protected CommonKerberosConfig getKerberosConfig() { + return new KerberosConfig(getUserStorageConfiguration()); + } + + @Override + protected ComponentRepresentation getUserStorageConfiguration() { + ComponentRepresentation rep = getUserStorageConfiguration("kerberos-standalone", KerberosFederationProviderFactory.PROVIDER_NAME); + // This provider works. It would be executed as 2nd provider (individual tests are supposed to add other provider, which should have lower priority to be invoked first) + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "10"); + return rep; + } + + @Test + public void test01spnegoWith1stProviderBrokenKerberosConfiguration() throws Exception { + // Add LDAP, which is invoked first. The Kerberos configuration is broken, so SPNEGO workflow is failing entirely + ComponentRepresentation rep = getUserStorageConfiguration(); + rep.setName("kerberos-foo"); + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "1"); + rep.getConfig().putSingle(KerberosConstants.KERBEROS_REALM, "FOO.ORG"); + rep.getConfig().putSingle(KerberosConstants.SERVER_PRINCIPAL, "HTTP/localhost@FOO.ORG"); + Response resp = testRealmResource().components().add(rep); + getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); + resp.close(); + + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + Assert.assertEquals(token.getEmail(), "hnelson2@keycloak.org"); + UserRepresentation user = assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false); + assertUserStorageProvider(user, "kerberos-standalone"); + } + + @Test + public void test02spnegoWith1stProviderBrokenLookupOfKerberosUser() throws Exception { + // Add LDAP, which is invoked first. The Kerberos configuration is OK, so SPNEGO workflow should be fine. + // However lookup LDAP based on Kerberos principal is broken, so fallback to next provider would be needed + ComponentRepresentation rep = getUserStorageConfiguration(); + rep.setName("kerberos-ldap-broken-lookup"); + rep.setProviderId(LDAPStorageProviderFactory.PROVIDER_NAME); + rep.getConfig().putSingle(PrioritizedComponentModel.PRIORITY, "1"); + rep.getConfig().putSingle(LDAPConstants.CUSTOM_USER_SEARCH_FILTER, "(mail=nonexistent@email.org)"); + Response resp = testRealmResource().components().add(rep); + getCleanup().addComponentId(ApiUtil.getCreatedId(resp)); + resp.close(); + + OAuthClient.AccessTokenResponse tokenResponse = assertSuccessfulSpnegoLogin("hnelson2@KC2.COM", "hnelson2", "secret"); + AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken()); + Assert.assertEquals(token.getEmail(), "hnelson2@keycloak.org"); + UserRepresentation user = assertUser("hnelson2", "hnelson2@keycloak.org", null, null, false); + assertUserStorageProvider(user, "kerberos-standalone"); + } +}