keycloak-uncached

Changes

Details

diff --git a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
index 43b40e4..fca1a71 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java
@@ -43,6 +43,7 @@ public class DefaultAuthenticationFlows {
     public static final String LOGIN_FORMS_FLOW = "forms";
     public static final String SAML_ECP_FLOW = "saml ecp";
     public static final String DOCKER_AUTH = "docker auth";
+    public static final String HTTP_CHALLENGE_FLOW = "http challenge";
 
     public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
     public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
@@ -60,6 +61,7 @@ public class DefaultAuthenticationFlows {
         if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
         if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
         if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
+        if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
     }
     public static void migrateFlows(RealmModel realm) {
         if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
@@ -70,6 +72,7 @@ public class DefaultAuthenticationFlows {
         if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
         if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
         if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
+        if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
     }
 
     public static void registrationFlow(RealmModel realm) {
@@ -570,4 +573,46 @@ public class DefaultAuthenticationFlows {
 
         realm.addAuthenticatorExecution(execution);
     }
+
+    public static void httpChallengeFlow(RealmModel realm) {
+        AuthenticationFlowModel challengeFlow = new AuthenticationFlowModel();
+        challengeFlow.setAlias(HTTP_CHALLENGE_FLOW);
+        challengeFlow.setDescription("An authentication flow based on challenge-response HTTP Authentication Schemes");
+        challengeFlow.setProviderId("basic-flow");
+        challengeFlow.setTopLevel(true);
+        challengeFlow.setBuiltIn(true);
+        challengeFlow = realm.addAuthenticationFlow(challengeFlow);
+
+        AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(challengeFlow.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+        execution.setAuthenticator("no-cookie-redirect");
+        execution.setPriority(10);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(challengeFlow.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
+        execution.setAuthenticator("basic-auth");
+        execution.setPriority(20);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(challengeFlow.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
+        execution.setAuthenticator("basic-auth-otp");
+        execution.setPriority(30);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+
+        execution = new AuthenticationExecutionModel();
+        execution.setParentFlow(challengeFlow.getId());
+        execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
+        execution.setAuthenticator("auth-spnego");
+        execution.setPriority(40);
+        execution.setAuthenticatorFlow(false);
+        realm.addAuthenticatorExecution(execution);
+    }
 }
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticator.java
new file mode 100644
index 0000000..74e8760
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticator.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.AuthenticationFlowError;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.representations.idm.CredentialRepresentation;
+import org.keycloak.services.managers.AuthenticationManager;
+import org.keycloak.util.BasicAuthHelper;
+
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MultivaluedHashMap;
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class BasicAuthAuthenticator extends AbstractUsernameFormAuthenticator implements Authenticator {
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        String authorizationHeader = getAuthorizationHeader(context);
+
+        if (authorizationHeader == null) {
+            context.challenge(challengeResponse(context));
+            return;
+        }
+
+        String[] challenge = getChallenge(authorizationHeader);
+
+        if (challenge == null) {
+            context.challenge(challengeResponse(context));
+            return;
+        }
+
+        if (onAuthenticate(context, challenge)) {
+            context.success();
+            return;
+        }
+
+        context.setUser(null);
+        context.challenge(challengeResponse(context));
+    }
+
+    protected boolean onAuthenticate(AuthenticationFlowContext context, String[] challenge) {
+        if (checkUsernameAndPassword(context, challenge[0], challenge[1])) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected String getAuthorizationHeader(AuthenticationFlowContext context) {
+        return context.getHttpRequest().getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
+    }
+
+    protected boolean checkUsernameAndPassword(AuthenticationFlowContext context, String username, String password) {
+        MultivaluedMap<String, String> map = new MultivaluedHashMap<>();
+
+        map.putSingle(AuthenticationManager.FORM_USERNAME, username);
+        map.putSingle(CredentialRepresentation.PASSWORD, password);
+
+        if (validateUserAndPassword(context, map)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    protected String[] getChallenge(String authorizationHeader) {
+        String[] challenge = BasicAuthHelper.parseHeader(authorizationHeader);
+
+        if (challenge.length < 2) {
+            return null;
+        }
+
+        return challenge;
+    }
+
+    @Override
+    protected Response invalidUser(AuthenticationFlowContext context) {
+        return challengeResponse(context);
+    }
+
+    @Override
+    protected Response disabledUser(AuthenticationFlowContext context) {
+        return challengeResponse(context);
+    }
+
+    @Override
+    protected Response temporarilyDisabledUser(AuthenticationFlowContext context) {
+        return challengeResponse(context);
+    }
+
+    @Override
+    protected Response invalidCredentials(AuthenticationFlowContext context) {
+        return challengeResponse(context);
+    }
+
+    @Override
+    protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
+        return challengeResponse(context);
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return true;
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    private Response challengeResponse(AuthenticationFlowContext context) {
+        return Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, getHeader(context)).build();
+    }
+
+    private String getHeader(AuthenticationFlowContext context) {
+        return "Basic realm=\"" + context.getRealm().getName() + "\"";
+    }
+}
\ No newline at end of file
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticatorFactory.java
new file mode 100644
index 0000000..b4540b7
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthAuthenticatorFactory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class BasicAuthAuthenticatorFactory implements AuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "basic-auth";
+    public static final BasicAuthAuthenticator SINGLETON = new BasicAuthAuthenticator();
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return UserCredentialModel.PASSWORD;
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.OPTIONAL, AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Basic Auth Challenge";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Challenge-response authentication using HTTP BASIC scheme.";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticator.java
new file mode 100644
index 0000000..99f8eb5
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticator.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.OTPPolicy;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.models.UserModel;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class BasicAuthOTPAuthenticator extends BasicAuthAuthenticator implements Authenticator {
+
+    @Override
+    protected boolean onAuthenticate(AuthenticationFlowContext context, String[] challenge) {
+        String username = challenge[0];
+        String password = challenge[1];
+        OTPPolicy otpPolicy = context.getRealm().getOTPPolicy();
+        int otpLength = otpPolicy.getDigits();
+
+        if (password.length() < otpLength) {
+            return false;
+        }
+
+        password = password.substring(0, password.length() - otpLength);
+
+        if (checkUsernameAndPassword(context, username, password)) {
+            String otp = password.substring(password.length() - otpLength);
+
+            if (checkOtp(context, otp)) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private boolean checkOtp(AuthenticationFlowContext context, String otp) {
+        return context.getSession().userCredentialManager().isValid(context.getRealm(), context.getUser(),
+                UserCredentialModel.otp(context.getRealm().getOTPPolicy().getType(), otp));
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return session.userCredentialManager().isConfiguredFor(realm, user, realm.getOTPPolicy().getType());
+    }
+}
+
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticatorFactory.java
new file mode 100644
index 0000000..580e1e2
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/BasicAuthOTPAuthenticatorFactory.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class BasicAuthOTPAuthenticatorFactory implements AuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "basic-auth-otp";
+    public static final BasicAuthOTPAuthenticator SINGLETON = new BasicAuthOTPAuthenticator();
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return UserCredentialModel.PASSWORD;
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.OPTIONAL, AuthenticationExecutionModel.Requirement.DISABLED
+    };
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Basic Auth Password+OTP";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Challenge-response authentication using HTTP BASIC scheme.  Password param should contain a combination of password + otp. Realm's OTP policy is used to determine how to parse this. This SHOULD NOT BE USED in conjection with regular basic auth provider.";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+}
+
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticator.java
new file mode 100644
index 0000000..3ee6eed
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticator.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import javax.ws.rs.HttpMethod;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+
+import org.jboss.resteasy.spi.HttpRequest;
+import org.keycloak.authentication.AuthenticationFlowContext;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.RealmModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.services.resources.LoginActionsService;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class NoCookieFlowRedirectAuthenticator implements Authenticator {
+
+    @Override
+    public boolean requiresUser() {
+        return false;
+    }
+
+    @Override
+    public void authenticate(AuthenticationFlowContext context) {
+        HttpRequest httpRequest = context.getHttpRequest();
+
+        // only do redirects for GET requests
+        if (HttpMethod.GET.equalsIgnoreCase(httpRequest.getHttpMethod())) {
+            if (!httpRequest.getUri().getQueryParameters().containsKey(LoginActionsService.AUTH_SESSION_ID)) {
+                Response response = Response.status(302).header(HttpHeaders.LOCATION, context.getRefreshUrl(true)).build();
+                context.challenge(response);
+                return;
+            }
+        }
+
+        context.success();
+    }
+
+    @Override
+    public void action(AuthenticationFlowContext context) {
+
+    }
+
+    @Override
+    public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
+        return true;
+    }
+
+    @Override
+    public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
+    }
+
+    @Override
+    public void close() {
+
+    }
+}
+
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticatorFactory.java
new file mode 100644
index 0000000..452df58
--- /dev/null
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/challenge/NoCookieFlowRedirectAuthenticatorFactory.java
@@ -0,0 +1,103 @@
+/*
+ * Copyright 2018 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.authentication.authenticators.challenge;
+
+import java.util.Collections;
+import java.util.List;
+
+import org.keycloak.Config;
+import org.keycloak.authentication.Authenticator;
+import org.keycloak.authentication.AuthenticatorFactory;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.models.KeycloakSession;
+import org.keycloak.models.KeycloakSessionFactory;
+import org.keycloak.models.UserCredentialModel;
+import org.keycloak.provider.ProviderConfigProperty;
+
+/**
+ * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
+ * @version $Revision: 1 $
+ */
+public class NoCookieFlowRedirectAuthenticatorFactory implements AuthenticatorFactory {
+
+    public static final String PROVIDER_ID = "no-cookie-redirect";
+    public static final NoCookieFlowRedirectAuthenticator SINGLETON = new NoCookieFlowRedirectAuthenticator();
+
+    @Override
+    public Authenticator create(KeycloakSession session) {
+        return SINGLETON;
+    }
+
+    @Override
+    public void init(Config.Scope config) {
+
+    }
+
+    @Override
+    public void postInit(KeycloakSessionFactory factory) {
+
+    }
+
+    @Override
+    public void close() {
+
+    }
+
+    @Override
+    public String getId() {
+        return PROVIDER_ID;
+    }
+
+    @Override
+    public String getReferenceCategory() {
+        return UserCredentialModel.PASSWORD;
+    }
+
+    @Override
+    public boolean isConfigurable() {
+        return false;
+    }
+    public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
+            AuthenticationExecutionModel.Requirement.REQUIRED
+    };
+
+    @Override
+    public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
+        return REQUIREMENT_CHOICES;
+    }
+
+    @Override
+    public String getDisplayType() {
+        return "Browser Redirect/Refresh";
+    }
+
+    @Override
+    public String getHelpText() {
+        return "Perform a 302 redirect to get user agent's current URI on authenticate path with an auth_session_id query parameter.  This is for client's that do not support cookies.";
+    }
+
+    @Override
+    public List<ProviderConfigProperty> getConfigProperties() {
+        return Collections.EMPTY_LIST;
+    }
+
+    @Override
+    public boolean isUserSetupAllowed() {
+        return false;
+    }
+
+}
diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
index 6e051df..f268f36 100644
--- a/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
+++ b/services/src/main/java/org/keycloak/authentication/authenticators/client/ClientIdAndSecretAuthenticator.java
@@ -83,9 +83,15 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator 
             }
         }
 
-        if (formData != null && client_id == null) {
-            client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
-            clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET);
+        if (formData != null) {
+            // even if basic challenge response exist, we check if client id was explicitly set in the request as a form param,
+            // so we can also support clients overriding flows and using challenges (e.g: basic) to authenticate their users
+            if (formData.containsKey(OAuth2Constants.CLIENT_ID)) {
+                client_id = formData.getFirst(OAuth2Constants.CLIENT_ID);
+            }
+            if (formData.containsKey(OAuth2Constants.CLIENT_SECRET)) {
+                clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET);
+            }
         }
 
         if (client_id == null) {
diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
index ee29448..4ecc8ff 100755
--- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
+++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory
@@ -39,3 +39,6 @@ org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticat
 org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
 org.keycloak.protocol.docker.DockerAuthenticatorFactory
 org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
+org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
+org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
+org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
\ No newline at end of file
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
index d6742cf..6ac78f8 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/InitialFlowsTest.java
@@ -185,6 +185,19 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
         addExecInfo(execs, "OTP Form", "auth-otp-form", false, 2, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
         expected.add(new FlowExecutions(flow, execs));
 
+        flow = newFlow("http challenge", "An authentication flow based on challenge-response HTTP Authentication Schemes","basic-flow", true, true);
+        addExecExport(flow, null, false, "no-cookie-redirect", false, null, REQUIRED, 10);
+        addExecExport(flow, null, false, "basic-auth", false, null, REQUIRED, 20);
+        addExecExport(flow, null, false, "basic-auth-otp", false, null, DISABLED, 30);
+        addExecExport(flow, null, false, "auth-spnego", false, null, DISABLED, 40);
+
+        execs = new LinkedList<>();
+        addExecInfo(execs, "Browser Redirect/Refresh", "no-cookie-redirect", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
+        addExecInfo(execs, "Basic Auth Challenge", "basic-auth", false, 0, 1, REQUIRED, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
+        addExecInfo(execs, "Basic Auth Password+OTP", "basic-auth-otp", false, 0, 2, DISABLED, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
+        addExecInfo(execs, "Kerberos", "auth-spnego", false, 0, 3, DISABLED, null, new String[]{ALTERNATIVE, REQUIRED, DISABLED});
+        expected.add(new FlowExecutions(flow, execs));
+
         flow = newFlow("registration", "registration flow", "basic-flow", true, true);
         addExecExport(flow, "registration form", false, "registration-page-form", true, null, REQUIRED, 10);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
index 794199b..b078e15 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/admin/authentication/ProvidersTest.java
@@ -150,6 +150,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "Validates a username and password from login form.");
         addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
                 "Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
+        addProviderInfo(result, "basic-auth", "Basic Auth Challenge", "Challenge-response authentication using HTTP BASIC scheme.");
+        addProviderInfo(result, "basic-auth-otp", "Basic Auth Password+OTP", "Challenge-response authentication using HTTP BASIC scheme.  Password param should contain a combination of password + otp. Realm's OTP policy is used to determine how to parse this. This SHOULD NOT BE USED in conjection with regular basic auth provider.");
         addProviderInfo(result, "console-username-password", "Username Password Challenge",
                 "Proprietary challenge protocol for CLI clients that queries for username password");
         addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
@@ -174,6 +176,7 @@ public class ProvidersTest extends AbstractAuthenticationTest {
                 "User reviews and updates profile data retrieved from Identity Provider in the displayed form");
         addProviderInfo(result, "idp-username-password-form", "Username Password Form for identity provider reauthentication",
                 "Validates a password from login form. Username is already known from identity provider authentication");
+        addProviderInfo(result, "no-cookie-redirect", "Browser Redirect/Refresh", "Perform a 302 redirect to get user agent's current URI on authenticate path with an auth_session_id query parameter.  This is for client's that do not support cookies.");
         addProviderInfo(result, "push-button-authenticator", "TEST: Button Login",
                 "Just press the button to login.");
         addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response.");
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 58094b5..64f70dd 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
@@ -178,13 +178,18 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
 
 
     protected AccessToken assertSuccessfulSpnegoLogin(String loginUsername, String expectedUsername, String password) throws Exception {
+        return assertSuccessfulSpnegoLogin("kerberos-app", loginUsername, expectedUsername, password);
+    }
+
+    protected AccessToken assertSuccessfulSpnegoLogin(String clientId, String loginUsername, String expectedUsername, String password) throws Exception {
+        oauth.clientId(clientId);
         Response spnegoResponse = spnegoLogin(loginUsername, password);
         Assert.assertEquals(302, spnegoResponse.getStatus());
 
         List<UserRepresentation> users = testRealmResource().users().search(expectedUsername, 0, 1);
         String userId = users.get(0).getId();
         events.expectLogin()
-                .client("kerberos-app")
+                .client(clientId)
                 .user(userId)
                 .detail(Details.USERNAME, expectedUsername)
                 .assertEvent();
@@ -233,7 +238,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
             if (response.getLocation() == null)
                 return response;
             String uri = response.getLocation().toString();
-            if (uri.contains("login-actions/required-action")) {
+            if (uri.contains("login-actions/required-action") || uri.contains("auth_session_id")) {
                 response = client.target(uri).request().get();
             }
         }
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java
index 1488a4c..7d032e7 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/federation/kerberos/KerberosLdapTest.java
@@ -17,7 +17,9 @@
 
 package org.keycloak.testsuite.federation.kerberos;
 
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 import javax.ws.rs.core.Response;
 
@@ -26,6 +28,10 @@ import org.junit.ClassRule;
 import org.junit.Test;
 import org.keycloak.events.Details;
 import org.keycloak.federation.kerberos.CommonKerberosConfig;
+import org.keycloak.models.AuthenticationFlowBindings;
+import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.ClientRepresentation;
 import org.keycloak.representations.idm.ComponentRepresentation;
 import org.keycloak.representations.idm.UserRepresentation;
 import org.keycloak.storage.UserStorageProvider;
@@ -74,6 +80,36 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
     }
 
     @Test
+    public void testClientOverrideFlowUsingBrowserHttpChallenge() throws Exception {
+        List<AuthenticationExecutionInfoRepresentation> executions = testRealmResource().flows().getExecutions("http challenge");
+
+        for (AuthenticationExecutionInfoRepresentation execution : executions) {
+            if ("basic-auth".equals(execution.getProviderId())) {
+                execution.setRequirement("OPTIONAL");
+                testRealmResource().flows().updateExecutions("http challenge", execution);
+            }
+            if ("auth-spnego".equals(execution.getProviderId())) {
+                execution.setRequirement("ALTERNATIVE");
+                testRealmResource().flows().updateExecutions("http challenge", execution);
+            }
+        }
+
+
+        Map<String, String> flows = new HashMap<>();
+        AuthenticationFlowRepresentation flow = testRealmResource().flows().getFlows().stream().filter(flowRep -> flowRep.getAlias().equalsIgnoreCase("http challenge")).findAny().get();
+
+        flows.put(AuthenticationFlowBindings.BROWSER_BINDING, flow.getId());
+
+        ClientRepresentation client = testRealmResource().clients().findByClientId("kerberos-app-challenge").get(0);
+
+        client.setAuthenticationFlowBindingOverrides(flows);
+
+        testRealmResource().clients().get(client.getId()).update(client);
+
+        assertSuccessfulSpnegoLogin(client.getClientId(),"hnelson", "hnelson", "secret");
+    }
+
+    @Test
     public void validatePasswordPolicyTest() throws Exception{
          updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);
 
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java
index 80bff7b..036bdb3 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/forms/FlowOverrideTest.java
@@ -17,6 +17,7 @@
 
 package org.keycloak.testsuite.forms;
 
+import org.apache.http.client.utils.URLEncodedUtils;
 import org.jboss.arquillian.container.test.api.Deployment;
 import org.jboss.arquillian.graphene.page.Page;
 import org.jboss.shrinkwrap.api.spec.WebArchive;
@@ -53,6 +54,7 @@ import javax.ws.rs.core.Form;
 import javax.ws.rs.core.HttpHeaders;
 import javax.ws.rs.core.Response;
 
+import java.nio.charset.Charset;
 import java.util.List;
 
 import static org.junit.Assert.assertEquals;
@@ -66,6 +68,8 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
 
     public static final String TEST_APP_DIRECT_OVERRIDE = "test-app-direct-override";
     public static final String TEST_APP_FLOW = "test-app-flow";
+    public static final String TEST_APP_HTTP_CHALLENGE = "http-challenge-client";
+
     @Rule
     public AssertEvents events = new AssertEvents(this);
 
@@ -189,7 +193,16 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
             client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrant.getId());
 
 
-
+            client = realm.addClient(TEST_APP_HTTP_CHALLENGE);
+            client.setSecret("password");
+            client.setBaseUrl("http://localhost:8180/auth/realms/master/app/auth");
+            client.setManagementUrl("http://localhost:8180/auth/realms/master/app/admin");
+            client.setEnabled(true);
+            client.addRedirectUri("http://localhost:8180/auth/realms/master/app/auth/*");
+            client.setPublicClient(true);
+            client.setDirectAccessGrantsEnabled(true);
+            client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, realm.getFlowByAlias("http challenge").getId());
+            client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, realm.getFlowByAlias("http challenge").getId());
         });
     }
 
@@ -324,6 +337,76 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
     }
 
     @Test
+    public void testClientOverrideFlowUsingDirectGrantHttpChallenge() {
+        Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
+        String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
+        WebTarget grantTarget = httpClient.target(grantUri);
+
+        // no username/password
+        Form form = new Form();
+        form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
+        form.param(OAuth2Constants.CLIENT_ID, TEST_APP_HTTP_CHALLENGE);
+        Response response = grantTarget.request()
+                .post(Entity.form(form));
+        assertEquals("Basic realm=\"test\"", response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE));
+        assertEquals(401, response.getStatus());
+        response.close();
+
+        // now, username password using basic challenge response
+        response = grantTarget.request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password"))
+                .post(Entity.form(form));
+        assertEquals(200, response.getStatus());
+        response.close();
+
+        httpClient.close();
+        events.clear();
+    }
+
+    @Test
+    public void testClientOverrideFlowUsingBrowserHttpChallenge() {
+        Client httpClient = javax.ws.rs.client.ClientBuilder.newClient();
+        oauth.clientId(TEST_APP_HTTP_CHALLENGE);
+        String grantUri = oauth.getLoginFormUrl();
+        WebTarget grantTarget = httpClient.target(grantUri);
+
+        Response response = grantTarget.request().get();
+        assertEquals(302, response.getStatus());
+        String location = response.getHeaderString(HttpHeaders.LOCATION);
+        response.close();
+
+        // first challenge
+        response = httpClient.target(location).request().get();
+        assertEquals("Basic realm=\"test\"", response.getHeaderString(HttpHeaders.WWW_AUTHENTICATE));
+        assertEquals(401, response.getStatus());
+        response.close();
+
+        // now, username password using basic challenge response
+        response = httpClient.target(location).request()
+                .header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password"))
+                .post(Entity.form(new Form()));
+        assertEquals(302, response.getStatus());
+        location = response.getHeaderString(HttpHeaders.LOCATION);
+        response.close();
+
+        Form form = new Form();
+
+        form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.AUTHORIZATION_CODE);
+        form.param(OAuth2Constants.CLIENT_ID, TEST_APP_HTTP_CHALLENGE);
+        form.param(OAuth2Constants.REDIRECT_URI, "http://localhost:8180/auth/realms/master/app/auth");
+        form.param(OAuth2Constants.CODE, location.substring(location.indexOf(OAuth2Constants.CODE) + OAuth2Constants.CODE.length() + 1));
+
+        // exchange code to token
+        response = httpClient.target(oauth.getAccessTokenUrl()).request()
+                .post(Entity.form(form));
+        assertEquals(200, response.getStatus());
+        response.close();
+
+        httpClient.close();
+        events.clear();
+    }
+
+    @Test
     public void testRestInterface() throws Exception {
         ClientsResource clients = adminClient.realm("test").clients();
         List<ClientRepresentation> query = clients.findByClientId(TEST_APP_DIRECT_OVERRIDE);
diff --git a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json
index 2588e4a..ec41b8d 100644
--- a/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json
+++ b/testsuite/integration-arquillian/tests/base/src/test/resources/kerberos/kerberosrealm.json
@@ -42,6 +42,17 @@
                 "/auth/realms/master/app/*"
             ],
             "secret": "password"
+        },
+        {
+            "clientId": "kerberos-app-challenge",
+            "enabled": true,
+            "adminUrl": "/kerberos-portal/logout",
+            "baseUrl": "/kerberos-portal",
+            "redirectUris": [
+                "/kerberos-portal/*",
+                "/auth/realms/master/app/*"
+            ],
+            "secret": "password"
         }
     ],
     "roles" : {