[KEYCLOAK-6285] - HTTP Challenge Authentication Flow

This commit is contained in:
Pedro Igor 2018-08-24 13:38:46 -03:00 committed by Stian Thorgersen
parent bf758809ba
commit 0561d73ae2
15 changed files with 823 additions and 6 deletions

View file

@ -43,6 +43,7 @@ public class DefaultAuthenticationFlows {
public static final String LOGIN_FORMS_FLOW = "forms"; public static final String LOGIN_FORMS_FLOW = "forms";
public static final String SAML_ECP_FLOW = "saml ecp"; public static final String SAML_ECP_FLOW = "saml ecp";
public static final String DOCKER_AUTH = "docker auth"; 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 CLIENT_AUTHENTICATION_FLOW = "clients";
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login"; 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(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm); if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm); if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
} }
public static void migrateFlows(RealmModel realm) { public static void migrateFlows(RealmModel realm) {
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true); 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(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm); if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm); if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
} }
public static void registrationFlow(RealmModel realm) { public static void registrationFlow(RealmModel realm) {
@ -570,4 +573,46 @@ public class DefaultAuthenticationFlows {
realm.addAuthenticatorExecution(execution); 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);
}
} }

View file

@ -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() + "\"";
}
}

View file

@ -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;
}
}

View file

@ -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());
}
}

View file

@ -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;
}
}

View file

@ -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() {
}
}

View file

@ -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;
}
}

View file

@ -83,9 +83,15 @@ public class ClientIdAndSecretAuthenticator extends AbstractClientAuthenticator
} }
} }
if (formData != null && client_id == null) { if (formData != null) {
client_id = formData.getFirst(OAuth2Constants.CLIENT_ID); // even if basic challenge response exist, we check if client id was explicitly set in the request as a form param,
clientSecret = formData.getFirst(OAuth2Constants.CLIENT_SECRET); // 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) { if (client_id == null) {

View file

@ -39,3 +39,6 @@ org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticat
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
org.keycloak.protocol.docker.DockerAuthenticatorFactory org.keycloak.protocol.docker.DockerAuthenticatorFactory
org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory org.keycloak.authentication.authenticators.console.ConsoleUsernamePasswordAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.BasicAuthAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory

View file

@ -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}); addExecInfo(execs, "OTP Form", "auth-otp-form", false, 2, 1, OPTIONAL, null, new String[]{REQUIRED, OPTIONAL, DISABLED});
expected.add(new FlowExecutions(flow, execs)); 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); flow = newFlow("registration", "registration flow", "basic-flow", true, true);
addExecExport(flow, "registration form", false, "registration-page-form", true, null, REQUIRED, 10); addExecExport(flow, "registration form", false, "registration-page-form", true, null, REQUIRED, 10);

View file

@ -150,6 +150,8 @@ public class ProvidersTest extends AbstractAuthenticationTest {
"Validates a username and password from login form."); "Validates a username and password from login form.");
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username 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."); "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", addProviderInfo(result, "console-username-password", "Username Password Challenge",
"Proprietary challenge protocol for CLI clients that queries for username password"); "Proprietary challenge protocol for CLI clients that queries for username password");
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username", 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"); "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", 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"); "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", addProviderInfo(result, "push-button-authenticator", "TEST: Button Login",
"Just press the button to login."); "Just press the button to login.");
addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response."); addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response.");

View file

@ -178,13 +178,18 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
protected AccessToken assertSuccessfulSpnegoLogin(String loginUsername, String expectedUsername, String password) throws Exception { 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); Response spnegoResponse = spnegoLogin(loginUsername, password);
Assert.assertEquals(302, spnegoResponse.getStatus()); Assert.assertEquals(302, spnegoResponse.getStatus());
List<UserRepresentation> users = testRealmResource().users().search(expectedUsername, 0, 1); List<UserRepresentation> users = testRealmResource().users().search(expectedUsername, 0, 1);
String userId = users.get(0).getId(); String userId = users.get(0).getId();
events.expectLogin() events.expectLogin()
.client("kerberos-app") .client(clientId)
.user(userId) .user(userId)
.detail(Details.USERNAME, expectedUsername) .detail(Details.USERNAME, expectedUsername)
.assertEvent(); .assertEvent();
@ -233,7 +238,7 @@ public abstract class AbstractKerberosTest extends AbstractAuthTest {
if (response.getLocation() == null) if (response.getLocation() == null)
return response; return response;
String uri = response.getLocation().toString(); 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(); response = client.target(uri).request().get();
} }
} }

View file

@ -17,7 +17,9 @@
package org.keycloak.testsuite.federation.kerberos; package org.keycloak.testsuite.federation.kerberos;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
@ -26,6 +28,10 @@ import org.junit.ClassRule;
import org.junit.Test; import org.junit.Test;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.federation.kerberos.CommonKerberosConfig; 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.ComponentRepresentation;
import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.storage.UserStorageProvider; import org.keycloak.storage.UserStorageProvider;
@ -73,6 +79,36 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false); assertUser("hnelson", "hnelson@keycloak.org", "Horatio", "Nelson", false);
} }
@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 @Test
public void validatePasswordPolicyTest() throws Exception{ public void validatePasswordPolicyTest() throws Exception{
updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE); updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.forms; package org.keycloak.testsuite.forms;
import org.apache.http.client.utils.URLEncodedUtils;
import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page; import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive; 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.HttpHeaders;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.nio.charset.Charset;
import java.util.List; import java.util.List;
import static org.junit.Assert.assertEquals; 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_DIRECT_OVERRIDE = "test-app-direct-override";
public static final String TEST_APP_FLOW = "test-app-flow"; public static final String TEST_APP_FLOW = "test-app-flow";
public static final String TEST_APP_HTTP_CHALLENGE = "http-challenge-client";
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@ -189,7 +193,16 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.DIRECT_GRANT_BINDING, directGrant.getId()); 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());
}); });
} }
@ -323,6 +336,76 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
events.clear(); events.clear();
} }
@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 @Test
public void testRestInterface() throws Exception { public void testRestInterface() throws Exception {
ClientsResource clients = adminClient.realm("test").clients(); ClientsResource clients = adminClient.realm("test").clients();

View file

@ -42,6 +42,17 @@
"/auth/realms/master/app/*" "/auth/realms/master/app/*"
], ],
"secret": "password" "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" : { "roles" : {