KEYCLOAK-12424 SPNEGO / Kerberos sends multiple 401 responses with WWW-Authenticate: Negotiate header when kerberos token is invalid

This commit is contained in:
mposolda 2020-01-02 17:59:24 +01:00 committed by Stian Thorgersen
parent 0f8d988d58
commit fea7b4e031
4 changed files with 68 additions and 8 deletions

View File

@ -208,9 +208,14 @@ public class KerberosFederationProvider implements UserStorageProvider,
return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state);
}
} else {
} else if (spnegoAuthenticator.getResponseToken() != null) {
// Case when SPNEGO handshake requires multiple steps
logger.tracef("SPNEGO Handshake will continue");
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
} else {
logger.tracef("SPNEGO Handshake not successful");
return CredentialValidationOutput.failed();
}
} else {

View File

@ -712,9 +712,14 @@ public class LDAPStorageProvider implements UserStorageProvider,
return new CredentialValidationOutput(user, CredentialValidationOutput.Status.AUTHENTICATED, state);
}
} else {
} else if (spnegoAuthenticator.getResponseToken() != null) {
// Case when SPNEGO handshake requires multiple steps
logger.tracef("SPNEGO Handshake will continue");
state.put(KerberosConstants.RESPONSE_TOKEN, spnegoAuthenticator.getResponseToken());
return new CredentialValidationOutput(null, CredentialValidationOutput.Status.CONTINUE, state);
} else {
logger.tracef("SPNEGO Handshake not successful");
return CredentialValidationOutput.failed();
}
}
}

View File

@ -17,9 +17,13 @@
package org.keycloak.testsuite.federation.kerberos;
import java.net.URI;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.ietf.jgss.GSSCredential;
@ -37,6 +41,7 @@ import org.keycloak.representations.AccessToken;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider;
import org.keycloak.testsuite.ActionURIUtils;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.admin.ApiUtil;
@ -62,6 +67,43 @@ public abstract class AbstractKerberosSingleRealmTest extends AbstractKerberosTe
response.close();
}
// KEYCLOAK-12424
@Test
public void spnegoWithInvalidTokenTest() throws Exception {
initHttpClient(true);
// Update kerberos configuration with some invalid location of keytab file
AtomicReference<String> origKeytab = new AtomicReference<>();
updateUserStorageProvider(kerberosProviderRep -> {
String keytab = kerberosProviderRep.getConfig().getFirst(KerberosConstants.KEYTAB);
origKeytab.set(keytab);
kerberosProviderRep.getConfig().putSingle(KerberosConstants.KEYTAB, keytab + "-invalid");
});
try {
/*
To do this we do a valid kerberos login on client side. The authenticator will obtain a valid token, but user
storage provider is incorrectly configured, so SPNEGO login will fail on server side. However the server should continue to
the login page (username/password) and return status 200. It should not return 401 with "Kerberos unsupported" page as that
would display some strange dialogs in the web browser on windows - see KEYCLOAK-12424
*/
Response spnegoResponse = spnegoLogin("hnelson", "secret");
Assert.assertEquals(200, spnegoResponse.getStatus());
String context = spnegoResponse.readEntity(String.class);
spnegoResponse.close();
org.junit.Assert.assertTrue(context.contains("Log in to test"));
events.clear();
} finally {
// Revert keytab configuration
updateUserStorageProvider(kerberosProviderRep -> kerberosProviderRep.getConfig().putSingle(KerberosConstants.KEYTAB, origKeytab.get()));
}
}
// KEYCLOAK-7823
@Test
public void spnegoLoginWithRequiredKerberosAuthExecutionTest() {

View File

@ -25,6 +25,7 @@ import java.util.Hashtable;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.function.Consumer;
import javax.naming.Context;
import javax.naming.NamingException;
@ -337,18 +338,25 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
protected void updateProviderEditMode(UserStorageProvider.EditMode editMode) {
List<ComponentRepresentation> reps = testRealmResource().components().query("test", UserStorageProvider.class.getName());
Assert.assertEquals(1, reps.size());
ComponentRepresentation kerberosProvider = reps.get(0);
kerberosProvider.getConfig().putSingle(LDAPConstants.EDIT_MODE, editMode.toString());
testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider);
updateUserStorageProvider(kerberosProvider -> kerberosProvider.getConfig().putSingle(LDAPConstants.EDIT_MODE, editMode.toString()));
}
protected void updateProviderValidatePasswordPolicy(Boolean validatePasswordPolicy) {
updateUserStorageProvider(kerberosProvider -> kerberosProvider.getConfig().putSingle(LDAPConstants.VALIDATE_PASSWORD_POLICY, validatePasswordPolicy.toString()));
}
/**
* Update UserStorage provider (Kerberos provider or LDAP provider with Kerberos enabled) with specified updater and save it
*
*/
protected void updateUserStorageProvider(Consumer<ComponentRepresentation> updater) {
List<ComponentRepresentation> reps = testRealmResource().components().query("test", UserStorageProvider.class.getName());
Assert.assertEquals(1, reps.size());
ComponentRepresentation kerberosProvider = reps.get(0);
kerberosProvider.getConfig().putSingle(LDAPConstants.VALIDATE_PASSWORD_POLICY, validatePasswordPolicy.toString());
updater.accept(kerberosProvider);
testRealmResource().components().component(kerberosProvider.getId()).update(kerberosProvider);
}