Removing 'http challenge' authentication flow and related authenticators (#20731)

closes #20497


Co-authored-by: andymunro <48995441+andymunro@users.noreply.github.com>
This commit is contained in:
Marek Posolda 2023-06-08 14:52:34 +02:00 committed by GitHub
parent 4d0fa6796f
commit 8080085cc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 136 additions and 1014 deletions

View file

@ -243,6 +243,19 @@ If you used this feature, you should not use the `openshift-integration` feature
the JAR file from custom extension. You can check the https://github.com/keycloak-extensions/keycloak-openshift-ext/[Openshift extension] and the instructions the JAR file from custom extension. You can check the https://github.com/keycloak-extensions/keycloak-openshift-ext/[Openshift extension] and the instructions
in it's README file for how to deploy the extension to your Keycloak server. in it's README file for how to deploy the extension to your Keycloak server.
NOTE: The Openshift extension is not officially supported and maintained by Keycloak team. You can use it only at your own risk.
== Http Challenge flow removed
The built-in authentication flow `http challenge` was removed along with the authenticator implementations `no-cookie-redirect`, `basic-auth`, and `basic-auth-otp`.
The `http challenge` authentication flow was also intended for Openshift integration and therefore it was removed along with other related capabilities as described above.
Authenticator implementations were moved to the Openshift extension described in the previous paragraph.
If you use the `http challenge` flow as a realm flow or as `First Broker Login` or `Post Broker Login` flow for any of your identity providers, the migration is not possible. Be sure to update
your realm configuration to eliminate the use of the `http challenge` flow before migration.
If you use the `http challenge` flow as `Authentication Flow Binding Override` for any client, the migration would complete, but you could no longer log in to that client.
After the migration, you would need to re-create the flow and update the configuration of your clients to use the new/differentJson flow.
= Removing thirdparty dependencies = Removing thirdparty dependencies
The removal of openshift-integration allows us to remove few thirdparty dependencies from Keycloak distribution. This includes The removal of openshift-integration allows us to remove few thirdparty dependencies from Keycloak distribution. This includes

View file

@ -0,0 +1,70 @@
/*
* 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.migration.migrators;
import org.jboss.logging.Logger;
import org.keycloak.migration.ModelVersion;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.ModelException;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.RealmRepresentation;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class MigrateTo22_0_0 implements Migration {
public static final ModelVersion VERSION = new ModelVersion("22.0.0");
public static final String HTTP_CHALLENGE_FLOW = "http challenge";
private static final Logger LOG = Logger.getLogger(MigrateTo22_0_0.class);
@Override
public void migrate(KeycloakSession session) {
session.realms().getRealmsStream().forEach(this::removeHttpChallengeFlow);
}
@Override
public void migrateImport(KeycloakSession session, RealmModel realm, RealmRepresentation rep, boolean skipUserDependent) {
removeHttpChallengeFlow(realm);
}
private void removeHttpChallengeFlow(RealmModel realm) {
AuthenticationFlowModel httpChallenge = realm.getFlowByAlias(HTTP_CHALLENGE_FLOW);
if (httpChallenge == null) return;
try {
KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, httpChallenge, () -> {}, () -> {});
LOG.debugf("Removed '%s' authentication flow in realm '%s'", HTTP_CHALLENGE_FLOW, realm.getName());
} catch (ModelException me) {
LOG.errorf("Authentication flow '%s' is in use in realm '%s' and cannot be removed. Please update your deployment to avoid using this flow before migration to latest Keycloak",
HTTP_CHALLENGE_FLOW, realm.getName());
throw me;
}
}
@Override
public ModelVersion getVersion() {
return VERSION;
}
}

View file

@ -35,6 +35,7 @@ import org.keycloak.migration.migrators.MigrateTo1_8_0;
import org.keycloak.migration.migrators.MigrateTo1_9_0; import org.keycloak.migration.migrators.MigrateTo1_9_0;
import org.keycloak.migration.migrators.MigrateTo1_9_2; import org.keycloak.migration.migrators.MigrateTo1_9_2;
import org.keycloak.migration.migrators.MigrateTo21_0_0; import org.keycloak.migration.migrators.MigrateTo21_0_0;
import org.keycloak.migration.migrators.MigrateTo22_0_0;
import org.keycloak.migration.migrators.MigrateTo2_0_0; import org.keycloak.migration.migrators.MigrateTo2_0_0;
import org.keycloak.migration.migrators.MigrateTo2_1_0; import org.keycloak.migration.migrators.MigrateTo2_1_0;
import org.keycloak.migration.migrators.MigrateTo2_2_0; import org.keycloak.migration.migrators.MigrateTo2_2_0;
@ -108,7 +109,8 @@ public class LegacyMigrationManager implements MigrationManager {
new MigrateTo14_0_0(), new MigrateTo14_0_0(),
new MigrateTo18_0_0(), new MigrateTo18_0_0(),
new MigrateTo20_0_0(), new MigrateTo20_0_0(),
new MigrateTo21_0_0() new MigrateTo21_0_0(),
new MigrateTo22_0_0()
}; };
private final KeycloakSession session; private final KeycloakSession session;

View file

@ -40,7 +40,6 @@ 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";
@ -58,7 +57,6 @@ 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);
@ -69,7 +67,6 @@ 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) {
@ -676,61 +673,4 @@ 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);
AuthenticationFlowModel authType = new AuthenticationFlowModel();
authType.setTopLevel(false);
authType.setBuiltIn(true);
authType.setAlias("Authentication Options");
authType.setDescription("Authentication options.");
authType.setProviderId("basic-flow");
authType = realm.addAuthenticationFlow(authType);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeFlow.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setFlowId(authType.getId());
execution.setPriority(20);
execution.setAuthenticatorFlow(true);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator("basic-auth");
execution.setPriority(10);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("basic-auth-otp");
execution.setPriority(20);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(authType.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator("auth-spnego");
execution.setPriority(30);
execution.setAuthenticatorFlow(false);
realm.addAuthenticatorExecution(execution);
}
} }

View file

@ -848,6 +848,32 @@ public final class KeycloakModelUtils {
Objects.equals(idp.getPostBrokerLoginFlowId(), model.getId())); Objects.equals(idp.getPostBrokerLoginFlowId(), model.getId()));
} }
/**
* Recursively remove authentication flow (including all subflows and executions) from the model storage
*
* @param realm
* @param authFlow flow to delete
* @param flowUnavailableHandler Will be executed when flow or some of it's subflow is null
* @param builtinFlowHandler will be executed when flow is built-in flow
*/
public static void deepDeleteAuthenticationFlow(RealmModel realm, AuthenticationFlowModel authFlow, Runnable flowUnavailableHandler, Runnable builtinFlowHandler) {
if (authFlow == null) {
flowUnavailableHandler.run();
return;
}
if (authFlow.isBuiltIn()) {
builtinFlowHandler.run();
}
realm.getAuthenticationExecutionsStream(authFlow.getId())
.map(AuthenticationExecutionModel::getFlowId)
.filter(Objects::nonNull)
.map(realm::getAuthenticationFlowById)
.forEachOrdered(subflow -> deepDeleteAuthenticationFlow(realm, subflow, flowUnavailableHandler, builtinFlowHandler));
realm.removeAuthenticationFlow(authFlow);
}
public static ClientScopeModel getClientScopeByName(RealmModel realm, String clientScopeName) { public static ClientScopeModel getClientScopeByName(RealmModel realm, String clientScopeName) {
return realm.getClientScopesStream() return realm.getClientScopesStream()
.filter(clientScope -> Objects.equals(clientScopeName, clientScope.getName())) .filter(clientScope -> Objects.equals(clientScopeName, clientScope.getName()))

View file

@ -1,149 +0,0 @@
/*
* 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 jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.MultivaluedHashMap;
import jakarta.ws.rs.core.MultivaluedMap;
import jakarta.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) {
if (context.getExecution().isRequired()) {
context.challenge(challenge(context, null));
} else {
context.attempted();
}
return;
}
String[] challenge = getChallenge(authorizationHeader);
if (challenge == null) {
if (context.getExecution().isRequired()) {
context.challenge(challenge(context, null));
} else {
context.attempted();
}
return;
}
if (onAuthenticate(context, challenge)) {
context.success();
return;
}
}
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.RFC6749.parseHeader(authorizationHeader);
if (challenge == null || challenge.length < 2) {
return null;
}
return challenge;
}
@Override
protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) {
return challenge(context, null);
}
@Override
protected Response challenge(AuthenticationFlowContext context, String error) {
return Response.status(401).header(HttpHeaders.WWW_AUTHENTICATE, getHeader(context)).build();
}
@Override
protected Response challenge(AuthenticationFlowContext context, String error, String field) {
return challenge(context, error);
}
@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 String getHeader(AuthenticationFlowContext context) {
return "Basic realm=\"" + context.getRealm().getName() + "\"";
}
}

View file

@ -1,100 +0,0 @@
/*
* 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.credential.PasswordCredentialModel;
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 PasswordCredentialModel.TYPE;
}
@Override
public boolean isConfigurable() {
return false;
}
@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.emptyList();
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -1,97 +0,0 @@
/*
* 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.CredentialValidator;
import org.keycloak.credential.CredentialProvider;
import org.keycloak.credential.OTPCredentialProvider;
import org.keycloak.events.Errors;
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;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.validation.Validation;
import jakarta.ws.rs.core.Response;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class BasicAuthOTPAuthenticator extends BasicAuthAuthenticator implements Authenticator, CredentialValidator<OTPCredentialProvider> {
@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 = challenge[1].substring(password.length(), challenge[1].length());
if (checkOtp(context, otp)) {
return true;
}
}
return false;
}
private boolean checkOtp(AuthenticationFlowContext context, String otp) {
OTPCredentialModel preferredCredential = getCredentialProvider(context.getSession())
.getDefaultCredential(context.getSession(), context.getRealm(), context.getUser());
boolean valid = getCredentialProvider(context.getSession()).isValid(context.getRealm(), context.getUser(),
new UserCredentialModel(preferredCredential.getId(), getCredentialProvider(context.getSession()).getType(), otp));
if (!valid) {
context.getEvent().user(context.getUser()).error(Errors.INVALID_USER_CREDENTIALS);
if (context.getExecution().isRequired()){
Response challengeResponse = challenge(context, Messages.INVALID_TOTP, Validation.FIELD_OTP_CODE);
context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challengeResponse);
} else {
context.attempted();
}
return false;
}
return true;
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return getCredentialProvider(session).isConfiguredFor(realm, user);
}
@Override
public OTPCredentialProvider getCredentialProvider(KeycloakSession session) {
return (OTPCredentialProvider)session.getProvider(CredentialProvider.class, "keycloak-otp");
}
}

View file

@ -1,101 +0,0 @@
/*
* 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.credential.PasswordCredentialModel;
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 PasswordCredentialModel.TYPE;
}
@Override
public boolean isConfigurable() {
return false;
}
@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.emptyList();
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -1,79 +0,0 @@
/*
* 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 jakarta.ws.rs.HttpMethod;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.core.Response;
import org.keycloak.http.HttpRequest;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.Authenticator;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
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())) {
KeycloakUriInfo uriInfo = context.getSession().getContext().getUri();
if (!uriInfo.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

@ -1,103 +0,0 @@
/*
* 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.credential.PasswordCredentialModel;
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 PasswordCredentialModel.TYPE;
}
@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 for Cookie free authentication";
}
@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.emptyList();
}
@Override
public boolean isUserSetupAllowed() {
return false;
}
}

View file

@ -39,6 +39,7 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.RequiredActionProviderModel; import org.keycloak.models.RequiredActionProviderModel;
import org.keycloak.models.utils.Base32; import org.keycloak.models.utils.Base32;
import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.models.utils.RepresentationToModel;
import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ConfiguredProvider;
@ -309,27 +310,17 @@ public class AuthenticationManagementResource {
public void deleteFlow(@PathParam("id") String id) { public void deleteFlow(@PathParam("id") String id) {
auth.realm().requireManageRealm(); auth.realm().requireManageRealm();
deleteFlow(id, true); KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, realm.getAuthenticationFlowById(id),
} () -> {
private void deleteFlow(String id, boolean isTopMostLevel) {
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id);
if (flow == null) {
throw new NotFoundException("Could not find flow with id"); throw new NotFoundException("Could not find flow with id");
} },
if (flow.isBuiltIn()) { () -> {
throw new BadRequestException("Can't delete built in flow"); throw new BadRequestException("Can't delete built in flow");
} }
);
realm.getAuthenticationExecutionsStream(id)
.map(AuthenticationExecutionModel::getFlowId)
.filter(Objects::nonNull)
.forEachOrdered(flowId -> deleteFlow(flowId, false));
realm.removeAuthenticationFlow(flow);
// Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger // Use just one event for top-level flow. Using separate events won't work properly for flows of depth 2 or bigger
if (isTopMostLevel) adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success(); adminEvent.operation(OperationType.DELETE).resourcePath(session.getContext().getUri()).success();
} }
/** /**

View file

@ -45,9 +45,6 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
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.challenge.BasicAuthAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory
org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory org.keycloak.authentication.authenticators.browser.WebAuthnPasswordlessAuthenticatorFactory
org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory

View file

@ -22,7 +22,6 @@ import org.junit.Test;
import org.keycloak.authentication.AuthenticationFlow; import org.keycloak.authentication.AuthenticationFlow;
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory; import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory; import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
import org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory;
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator; import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
import org.keycloak.events.admin.OperationType; import org.keycloak.events.admin.OperationType;
import org.keycloak.events.admin.ResourceType; import org.keycloak.events.admin.ResourceType;
@ -330,7 +329,6 @@ public class ExecutionTest extends AbstractAuthenticationTest {
addExecutionCheckReq(newBrowserFlow, UsernameFormFactory.PROVIDER_ID, params, REQUIRED); addExecutionCheckReq(newBrowserFlow, UsernameFormFactory.PROVIDER_ID, params, REQUIRED);
addExecutionCheckReq(newBrowserFlow, WebAuthnAuthenticatorFactory.PROVIDER_ID, params, DISABLED); addExecutionCheckReq(newBrowserFlow, WebAuthnAuthenticatorFactory.PROVIDER_ID, params, DISABLED);
addExecutionCheckReq(newBrowserFlow, NoCookieFlowRedirectAuthenticatorFactory.PROVIDER_ID, params, REQUIRED);
AuthenticationFlowRepresentation rep = findFlowByAlias(newBrowserFlow, authMgmtResource.getFlows()); AuthenticationFlowRepresentation rep = findFlowByAlias(newBrowserFlow, authMgmtResource.getFlows());
Assert.assertNotNull(rep); Assert.assertNotNull(rep);

View file

@ -190,18 +190,6 @@ public class InitialFlowsTest extends AbstractAuthenticationTest {
addExecInfo(execs, "First broker login - Conditional OTP", null, false, 4, 1, CONDITIONAL, true, new String[]{REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL}); addExecInfo(execs, "First broker login - Conditional OTP", null, false, 4, 1, CONDITIONAL, true, new String[]{REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL});
addExecInfo(execs, "Condition - user configured", "conditional-user-configured", false, 5, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED}); addExecInfo(execs, "Condition - user configured", "conditional-user-configured", false, 5, 0, REQUIRED, null, new String[]{REQUIRED, DISABLED});
addExecInfo(execs, "OTP Form", "auth-otp-form", false, 5, 1, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED}); addExecInfo(execs, "OTP Form", "auth-otp-form", false, 5, 1, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, 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, "Authentication Options", false, null, true, null, REQUIRED, 20);
execs = new LinkedList<>();
addExecInfo(execs, "Browser Redirect for Cookie free authentication", "no-cookie-redirect", false, 0, 0, REQUIRED, null, new String[]{REQUIRED});
addExecInfo(execs, "Authentication Options", null, false, 0, 1, REQUIRED, true, new String[]{REQUIRED, ALTERNATIVE, DISABLED, CONDITIONAL});
addExecInfo(execs, "Basic Auth Challenge", "basic-auth", false, 1, 0, REQUIRED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED});
addExecInfo(execs, "Basic Auth Password+OTP", "basic-auth-otp", false, 1, 1, DISABLED, null, new String[]{REQUIRED, ALTERNATIVE, DISABLED});
addExecInfo(execs, "Kerberos", "auth-spnego", false, 1, 2, DISABLED, null, kerberosAuthExpectedChoices);
expected.add(new FlowExecutions(flow, execs)); expected.add(new FlowExecutions(flow, execs));
flow = newFlow("registration", "registration flow", "basic-flow", true, true); flow = newFlow("registration", "registration flow", "basic-flow", true, true);

View file

@ -154,8 +154,6 @@ 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, "direct-grant-auth-x509-username", "X509/Validate Username", addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
"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, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request"); addProviderInfo(result, "direct-grant-validate-otp", "OTP", "Validates the one time password supplied as a 'totp' form parameter in direct grant request");
@ -179,7 +177,6 @@ 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 may be already known from identity provider authentication"); "Validates a password from login form. Username may be already known from identity provider authentication");
addProviderInfo(result, "no-cookie-redirect", "Browser Redirect for Cookie free authentication", "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

@ -17,9 +17,7 @@
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 jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.Response;
@ -30,10 +28,6 @@ import org.junit.Test;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
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;
@ -87,36 +81,6 @@ 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("ALTERNATIVE");
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

@ -25,7 +25,6 @@ import org.junit.Test;
import org.keycloak.OAuth2Constants; import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientsResource; import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory; import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
import org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory;
import org.keycloak.common.Profile; import org.keycloak.common.Profile;
import org.keycloak.events.Details; import org.keycloak.events.Details;
import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationExecutionModel;
@ -33,12 +32,9 @@ import org.keycloak.models.AuthenticationFlowBindings;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel; import org.keycloak.models.ClientModel;
import org.keycloak.models.RealmModel; import org.keycloak.models.RealmModel;
import org.keycloak.models.credential.OTPCredentialModel;
import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.models.utils.TimeBasedOTP;
import org.keycloak.representations.idm.ClientRepresentation; import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest; import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.arquillian.annotation.EnableFeature; import org.keycloak.testsuite.arquillian.annotation.EnableFeature;
@ -47,7 +43,6 @@ import org.keycloak.testsuite.authentication.PushButtonAuthenticatorFactory;
import org.keycloak.testsuite.pages.AppPage; import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage; import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.UserBuilder;
import org.keycloak.util.BasicAuthHelper; import org.keycloak.util.BasicAuthHelper;
import org.openqa.selenium.By; import org.openqa.selenium.By;
@ -71,8 +66,6 @@ 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";
public static final String TEST_APP_HTTP_CHALLENGE_OTP = "http-challenge-otp-client";
@Rule @Rule
public AssertEvents events = new AssertEvents(this); public AssertEvents events = new AssertEvents(this);
@ -180,22 +173,6 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
realm.addAuthenticatorExecution(execution); realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel challengeOTP = new AuthenticationFlowModel();
challengeOTP.setAlias("challenge-override-flow");
challengeOTP.setDescription("challenge grant based authentication");
challengeOTP.setProviderId("basic-flow");
challengeOTP.setTopLevel(true);
challengeOTP.setBuiltIn(true);
challengeOTP = realm.addAuthenticationFlow(challengeOTP);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(challengeOTP.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
execution.setAuthenticator(BasicAuthOTPAuthenticatorFactory.PROVIDER_ID);
execution.setPriority(10);
realm.addAuthenticatorExecution(execution);
client = realm.addClient(TEST_APP_DIRECT_OVERRIDE); client = realm.addClient(TEST_APP_DIRECT_OVERRIDE);
client.setSecret("password"); client.setSecret("password");
client.setBaseUrl(serializedApplicationData.applicationBaseUrl); client.setBaseUrl(serializedApplicationData.applicationBaseUrl);
@ -206,29 +183,6 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
client.setDirectAccessGrantsEnabled(true); client.setDirectAccessGrantsEnabled(true);
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId()); client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.getId());
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(serializedApplicationData.applicationBaseUrl);
client.setManagementUrl(serializedApplicationData.applicationManagementUrl);
client.setEnabled(true);
client.addRedirectUri(serializedApplicationData.applicationRedirectUrl);
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());
client = realm.addClient(TEST_APP_HTTP_CHALLENGE_OTP);
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("challenge-override-flow").getId());
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, realm.getFlowByAlias("challenge-override-flow").getId());
}); });
} }
@ -371,181 +325,6 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
events.clear(); events.clear();
} }
@Test
public void testClientOverrideFlowUsingDirectGrantHttpChallenge() {
Client httpClient = AdminClientUtil.createResteasyClient();
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 testDirectGrantHttpChallengeOTP() {
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost").get(0);
UserRepresentation userUpdate = UserBuilder.edit(user).totpSecret("totpSecret").otpEnabled().build();
adminClient.realm("test").users().get(user.getId()).update(userUpdate);
CredentialRepresentation totpCredential = adminClient.realm("test").users()
.get(user.getId()).credentials().stream().filter(c -> OTPCredentialModel.TYPE.equals(c.getType())).findFirst().get();
setupBruteForce();
Client httpClient = AdminClientUtil.createResteasyClient();
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param(OAuth2Constants.CLIENT_ID, TEST_APP_HTTP_CHALLENGE_OTP);
// correct password + totp
String totpCode = totp.generateTOTP("totpSecret");
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password" + totpCode))
.post(Entity.form(form));
assertEquals(200, response.getStatus());
response.close();
// correct password + wrong totp 2x
response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password123456"))
.post(Entity.form(form));
assertEquals(401, response.getStatus());
response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password123456"))
.post(Entity.form(form));
assertEquals(401, response.getStatus());
// correct password + totp but user is temporarily locked
totpCode = totp.generateTOTP("totpSecret");
response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password" + totpCode))
.post(Entity.form(form));
assertEquals(401, response.getStatus());
response.close();
clearBruteForce();
adminClient.realm("test").users().get(user.getId()).removeCredential(totpCredential.getId());
}
@Test
public void testDirectGrantHttpChallengeUserDisabled() {
setupBruteForce();
Client httpClient = AdminClientUtil.createResteasyClient();
String grantUri = oauth.getResourceOwnerPasswordCredentialGrantUrl();
WebTarget grantTarget = httpClient.target(grantUri);
Form form = new Form();
form.param(OAuth2Constants.GRANT_TYPE, OAuth2Constants.PASSWORD);
form.param(OAuth2Constants.CLIENT_ID, TEST_APP_HTTP_CHALLENGE);
UserRepresentation user = adminClient.realm("test").users().search("test-user@localhost").get(0);
user.setEnabled(false);
adminClient.realm("test").users().get(user.getId()).update(user);
// user disabled
Response response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password"))
.post(Entity.form(form));
assertEquals(401, response.getStatus());
assertEquals("Unauthorized", response.getStatusInfo().getReasonPhrase());
response.close();
user.setEnabled(true);
adminClient.realm("test").users().get(user.getId()).update(user);
// lock the user account
grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "wrongpassword"))
.post(Entity.form(form));
grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "wrongpassword"))
.post(Entity.form(form));
// user is temporarily disabled
response = grantTarget.request()
.header(HttpHeaders.AUTHORIZATION, BasicAuthHelper.createHeader("test-user@localhost", "password"))
.post(Entity.form(form));
assertEquals(401, response.getStatus());
assertEquals("Unauthorized", response.getStatusInfo().getReasonPhrase());
response.close();
clearBruteForce();
httpClient.close();
events.clear();
}
@Test
public void testClientOverrideFlowUsingBrowserHttpChallenge() {
Client httpClient = AdminClientUtil.createResteasyClient();
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, oauth.APP_AUTH_ROOT);
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();
}
// TODO remove this once DYNAMIC_SCOPES feature is enabled by default
@Test
@EnableFeature(value = Profile.Feature.DYNAMIC_SCOPES, skipRestart = true)
public void testClientOverrideFlowUsingBrowserHttpChallengeWithDynamicScope() {
// Just use existing test with DYNAMIC_SCOPES feature enabled as it was failing with DYNAMIC_SCOPES
testClientOverrideFlowUsingBrowserHttpChallenge();
}
@Test @Test
public void testRestInterface() throws Exception { public void testRestInterface() throws Exception {
ClientsResource clients = adminClient.realm("test").clients(); ClientsResource clients = adminClient.realm("test").clients();
@ -592,21 +371,4 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
Assert.assertEquals(browserFlowId, clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.BROWSER_BINDING)); Assert.assertEquals(browserFlowId, clientRep.getAuthenticationFlowBindingOverrides().get(AuthenticationFlowBindings.BROWSER_BINDING));
} }
private void setupBruteForce() {
RealmRepresentation testRealm = adminClient.realm("test").toRepresentation();
testRealm.setBruteForceProtected(true);
testRealm.setFailureFactor(2);
testRealm.setMaxDeltaTimeSeconds(20);
testRealm.setMaxFailureWaitSeconds(100);
testRealm.setWaitIncrementSeconds(5);
adminClient.realm("test").update(testRealm);
}
private void clearBruteForce() {
RealmRepresentation testRealm = adminClient.realm("test").toRepresentation();
testRealm.setBruteForceProtected(false);
adminClient.realm("test").attackDetection().clearAllBruteForce();
adminClient.realm("test").update(testRealm);
}
} }

View file

@ -86,6 +86,7 @@ import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static net.bytebuddy.matcher.ElementMatchers.is;
import static org.hamcrest.CoreMatchers.not; import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.allOf; import static org.hamcrest.Matchers.allOf;
@ -168,6 +169,13 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
Assert.assertNull("migration-saml-client login theme was not removed", client.get(0).getAttributes().get(DefaultThemeSelectorProvider.LOGIN_THEME_KEY)); Assert.assertNull("migration-saml-client login theme was not removed", client.get(0).getAttributes().get(DefaultThemeSelectorProvider.LOGIN_THEME_KEY));
} }
protected void testHttpChallengeFlow(RealmResource realm) {
log.info("testing 'http challenge' flow not present");
Assert.assertFalse(realm.flows().getFlows()
.stream()
.anyMatch(authFlow -> authFlow.getAlias().equalsIgnoreCase("http challenge")));
}
/** /**
* @see org.keycloak.migration.migrators.MigrateTo2_0_0 * @see org.keycloak.migration.migrators.MigrateTo2_0_0
*/ */
@ -349,6 +357,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
protected void testMigrationTo22_0_0() { protected void testMigrationTo22_0_0() {
testRhssoThemes(migrationRealm); testRhssoThemes(migrationRealm);
testHttpChallengeFlow(migrationRealm);
} }
protected void testDeleteAccount(RealmResource realm) { protected void testDeleteAccount(RealmResource realm) {

View file

@ -76,6 +76,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo20_x(); testMigrationTo20_x();
testMigrationTo21_x(); testMigrationTo21_x();
testMigrationTo22_x();
} }
@Override @Override

View file

@ -70,6 +70,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo20_x(); testMigrationTo20_x();
testMigrationTo21_x(); testMigrationTo21_x();
testMigrationTo22_x();
} }
} }

View file

@ -65,6 +65,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo20_x(); testMigrationTo20_x();
testMigrationTo21_x(); testMigrationTo21_x();
testMigrationTo22_x();
} }
} }

View file

@ -59,6 +59,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo20_x(); testMigrationTo20_x();
testMigrationTo21_x(); testMigrationTo21_x();
testMigrationTo22_x();
} }
} }

View file

@ -52,6 +52,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat
testMigrationTo18_x(); testMigrationTo18_x();
testMigrationTo20_x(); testMigrationTo20_x();
testMigrationTo21_x(); testMigrationTo21_x();
testMigrationTo22_x();
} }
} }

View file

@ -40,17 +40,6 @@
"/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" : {