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:
parent
4d0fa6796f
commit
8080085cc1
25 changed files with 136 additions and 1014 deletions
|
@ -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
|
||||
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
|
||||
|
||||
The removal of openshift-integration allows us to remove few thirdparty dependencies from Keycloak distribution. This includes
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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_2;
|
||||
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_1_0;
|
||||
import org.keycloak.migration.migrators.MigrateTo2_2_0;
|
||||
|
@ -108,7 +109,8 @@ public class LegacyMigrationManager implements MigrationManager {
|
|||
new MigrateTo14_0_0(),
|
||||
new MigrateTo18_0_0(),
|
||||
new MigrateTo20_0_0(),
|
||||
new MigrateTo21_0_0()
|
||||
new MigrateTo21_0_0(),
|
||||
new MigrateTo22_0_0()
|
||||
};
|
||||
|
||||
private final KeycloakSession session;
|
||||
|
|
|
@ -40,7 +40,6 @@ public class DefaultAuthenticationFlows {
|
|||
public static final String LOGIN_FORMS_FLOW = "forms";
|
||||
public static final String SAML_ECP_FLOW = "saml ecp";
|
||||
public static final String DOCKER_AUTH = "docker auth";
|
||||
public static final String HTTP_CHALLENGE_FLOW = "http challenge";
|
||||
|
||||
public static final String CLIENT_AUTHENTICATION_FLOW = "clients";
|
||||
public static final String FIRST_BROKER_LOGIN_FLOW = "first broker login";
|
||||
|
@ -58,7 +57,6 @@ public class DefaultAuthenticationFlows {
|
|||
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, false);
|
||||
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
|
||||
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
|
||||
if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
|
||||
}
|
||||
public static void migrateFlows(RealmModel realm) {
|
||||
if (realm.getFlowByAlias(BROWSER_FLOW) == null) browserFlow(realm, true);
|
||||
|
@ -69,7 +67,6 @@ public class DefaultAuthenticationFlows {
|
|||
if (realm.getFlowByAlias(FIRST_BROKER_LOGIN_FLOW) == null) firstBrokerLoginFlow(realm, true);
|
||||
if (realm.getFlowByAlias(SAML_ECP_FLOW) == null) samlEcpProfile(realm);
|
||||
if (realm.getFlowByAlias(DOCKER_AUTH) == null) dockerAuthenticationFlow(realm);
|
||||
if (realm.getFlowByAlias(HTTP_CHALLENGE_FLOW) == null) httpChallengeFlow(realm);
|
||||
}
|
||||
|
||||
public static void registrationFlow(RealmModel realm) {
|
||||
|
@ -676,61 +673,4 @@ public class DefaultAuthenticationFlows {
|
|||
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
}
|
||||
|
||||
public static void httpChallengeFlow(RealmModel realm) {
|
||||
AuthenticationFlowModel challengeFlow = new AuthenticationFlowModel();
|
||||
challengeFlow.setAlias(HTTP_CHALLENGE_FLOW);
|
||||
challengeFlow.setDescription("An authentication flow based on challenge-response HTTP Authentication Schemes");
|
||||
challengeFlow.setProviderId("basic-flow");
|
||||
challengeFlow.setTopLevel(true);
|
||||
challengeFlow.setBuiltIn(true);
|
||||
challengeFlow = realm.addAuthenticationFlow(challengeFlow);
|
||||
|
||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||
execution.setParentFlow(challengeFlow.getId());
|
||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
|
||||
execution.setAuthenticator("no-cookie-redirect");
|
||||
execution.setPriority(10);
|
||||
execution.setAuthenticatorFlow(false);
|
||||
realm.addAuthenticatorExecution(execution);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -848,6 +848,32 @@ public final class KeycloakModelUtils {
|
|||
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) {
|
||||
return realm.getClientScopesStream()
|
||||
.filter(clientScope -> Objects.equals(clientScopeName, clientScope.getName()))
|
||||
|
|
|
@ -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() + "\"";
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -39,6 +39,7 @@ import org.keycloak.models.RealmModel;
|
|||
import org.keycloak.models.RequiredActionProviderModel;
|
||||
import org.keycloak.models.utils.Base32;
|
||||
import org.keycloak.models.utils.DefaultAuthenticationFlows;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.models.utils.ModelToRepresentation;
|
||||
import org.keycloak.models.utils.RepresentationToModel;
|
||||
import org.keycloak.provider.ConfiguredProvider;
|
||||
|
@ -308,28 +309,18 @@ public class AuthenticationManagementResource {
|
|||
@NoCache
|
||||
public void deleteFlow(@PathParam("id") String id) {
|
||||
auth.realm().requireManageRealm();
|
||||
|
||||
deleteFlow(id, true);
|
||||
}
|
||||
|
||||
private void deleteFlow(String id, boolean isTopMostLevel) {
|
||||
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(id);
|
||||
if (flow == null) {
|
||||
throw new NotFoundException("Could not find flow with id");
|
||||
}
|
||||
if (flow.isBuiltIn()) {
|
||||
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);
|
||||
KeycloakModelUtils.deepDeleteAuthenticationFlow(realm, realm.getAuthenticationFlowById(id),
|
||||
() -> {
|
||||
throw new NotFoundException("Could not find flow with id");
|
||||
},
|
||||
() -> {
|
||||
throw new BadRequestException("Can't delete built in flow");
|
||||
}
|
||||
);
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -45,9 +45,6 @@ org.keycloak.protocol.saml.profile.ecp.authenticator.HttpBasicAuthenticatorFacto
|
|||
org.keycloak.authentication.authenticators.x509.X509ClientCertificateAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.x509.ValidateX509CertificateUsernameFactory
|
||||
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.WebAuthnPasswordlessAuthenticatorFactory
|
||||
org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory
|
||||
|
|
|
@ -22,7 +22,6 @@ import org.junit.Test;
|
|||
import org.keycloak.authentication.AuthenticationFlow;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
|
||||
import org.keycloak.authentication.authenticators.browser.WebAuthnAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.challenge.NoCookieFlowRedirectAuthenticatorFactory;
|
||||
import org.keycloak.authentication.authenticators.client.ClientIdAndSecretAuthenticator;
|
||||
import org.keycloak.events.admin.OperationType;
|
||||
import org.keycloak.events.admin.ResourceType;
|
||||
|
@ -330,7 +329,6 @@ public class ExecutionTest extends AbstractAuthenticationTest {
|
|||
|
||||
addExecutionCheckReq(newBrowserFlow, UsernameFormFactory.PROVIDER_ID, params, REQUIRED);
|
||||
addExecutionCheckReq(newBrowserFlow, WebAuthnAuthenticatorFactory.PROVIDER_ID, params, DISABLED);
|
||||
addExecutionCheckReq(newBrowserFlow, NoCookieFlowRedirectAuthenticatorFactory.PROVIDER_ID, params, REQUIRED);
|
||||
|
||||
AuthenticationFlowRepresentation rep = findFlowByAlias(newBrowserFlow, authMgmtResource.getFlows());
|
||||
Assert.assertNotNull(rep);
|
||||
|
|
|
@ -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, "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});
|
||||
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));
|
||||
|
||||
flow = newFlow("registration", "registration flow", "basic-flow", true, true);
|
||||
|
|
|
@ -154,8 +154,6 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"Validates a username and password from login form.");
|
||||
addProviderInfo(result, "auth-x509-client-username-form", "X509/Validate Username Form",
|
||||
"Validates username and password from X509 client certificate received as a part of mutual SSL handshake.");
|
||||
addProviderInfo(result, "basic-auth", "Basic Auth Challenge", "Challenge-response authentication using HTTP BASIC scheme.");
|
||||
addProviderInfo(result, "basic-auth-otp", "Basic Auth Password+OTP", "Challenge-response authentication using HTTP BASIC scheme. Password param should contain a combination of password + otp. Realm's OTP policy is used to determine how to parse this. This SHOULD NOT BE USED in conjection with regular basic auth provider.");
|
||||
addProviderInfo(result, "direct-grant-auth-x509-username", "X509/Validate Username",
|
||||
"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");
|
||||
|
@ -179,7 +177,6 @@ public class ProvidersTest extends AbstractAuthenticationTest {
|
|||
"User reviews and updates profile data retrieved from Identity Provider in the displayed form");
|
||||
addProviderInfo(result, "idp-username-password-form", "Username Password Form for identity provider reauthentication",
|
||||
"Validates a password from login form. Username 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",
|
||||
"Just press the button to login.");
|
||||
addProviderInfo(result, "reset-credential-email", "Send Reset Email", "Send email to user and wait for response.");
|
||||
|
|
|
@ -17,9 +17,7 @@
|
|||
|
||||
package org.keycloak.testsuite.federation.kerberos;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
|
@ -30,10 +28,6 @@ import org.junit.Test;
|
|||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.federation.kerberos.CommonKerberosConfig;
|
||||
import org.keycloak.models.AuthenticationFlowBindings;
|
||||
import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
|
||||
import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.ComponentRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.storage.UserStorageProvider;
|
||||
|
@ -87,36 +81,6 @@ public class KerberosLdapTest extends AbstractKerberosSingleRealmTest {
|
|||
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
|
||||
public void validatePasswordPolicyTest() throws Exception{
|
||||
updateProviderEditMode(UserStorageProvider.EditMode.WRITABLE);
|
||||
|
|
|
@ -25,7 +25,6 @@ import org.junit.Test;
|
|||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.ClientsResource;
|
||||
import org.keycloak.authentication.authenticators.browser.UsernamePasswordFormFactory;
|
||||
import org.keycloak.authentication.authenticators.challenge.BasicAuthOTPAuthenticatorFactory;
|
||||
import org.keycloak.common.Profile;
|
||||
import org.keycloak.events.Details;
|
||||
import org.keycloak.models.AuthenticationExecutionModel;
|
||||
|
@ -33,12 +32,9 @@ import org.keycloak.models.AuthenticationFlowBindings;
|
|||
import org.keycloak.models.AuthenticationFlowModel;
|
||||
import org.keycloak.models.ClientModel;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.credential.OTPCredentialModel;
|
||||
import org.keycloak.models.utils.TimeBasedOTP;
|
||||
import org.keycloak.representations.idm.ClientRepresentation;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.representations.idm.RealmRepresentation;
|
||||
import org.keycloak.representations.idm.UserRepresentation;
|
||||
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||
import org.keycloak.testsuite.AssertEvents;
|
||||
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.ErrorPage;
|
||||
import org.keycloak.testsuite.pages.LoginPage;
|
||||
import org.keycloak.testsuite.util.UserBuilder;
|
||||
import org.keycloak.util.BasicAuthHelper;
|
||||
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_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
|
||||
public AssertEvents events = new AssertEvents(this);
|
||||
|
@ -180,22 +173,6 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
|
|||
|
||||
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.setSecret("password");
|
||||
client.setBaseUrl(serializedApplicationData.applicationBaseUrl);
|
||||
|
@ -206,29 +183,6 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
|
|||
client.setDirectAccessGrantsEnabled(true);
|
||||
client.setAuthenticationFlowBindingOverride(AuthenticationFlowBindings.BROWSER_BINDING, browser.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();
|
||||
}
|
||||
|
||||
@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
|
||||
public void testRestInterface() throws Exception {
|
||||
ClientsResource clients = adminClient.realm("test").clients();
|
||||
|
@ -592,21 +371,4 @@ public class FlowOverrideTest extends AbstractTestRealmKeycloakTest {
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -86,6 +86,7 @@ import java.util.regex.Matcher;
|
|||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static net.bytebuddy.matcher.ElementMatchers.is;
|
||||
import static org.hamcrest.CoreMatchers.not;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
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));
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
|
@ -349,6 +357,7 @@ public abstract class AbstractMigrationTest extends AbstractKeycloakTest {
|
|||
|
||||
protected void testMigrationTo22_0_0() {
|
||||
testRhssoThemes(migrationRealm);
|
||||
testHttpChallengeFlow(migrationRealm);
|
||||
}
|
||||
|
||||
protected void testDeleteAccount(RealmResource realm) {
|
||||
|
|
|
@ -76,6 +76,7 @@ public class JsonFileImport198MigrationTest extends AbstractJsonFileImportMigrat
|
|||
testMigrationTo18_x();
|
||||
testMigrationTo20_x();
|
||||
testMigrationTo21_x();
|
||||
testMigrationTo22_x();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -70,6 +70,7 @@ public class JsonFileImport255MigrationTest extends AbstractJsonFileImportMigrat
|
|||
testMigrationTo18_x();
|
||||
testMigrationTo20_x();
|
||||
testMigrationTo21_x();
|
||||
testMigrationTo22_x();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -65,6 +65,7 @@ public class JsonFileImport343MigrationTest extends AbstractJsonFileImportMigrat
|
|||
testMigrationTo18_x();
|
||||
testMigrationTo20_x();
|
||||
testMigrationTo21_x();
|
||||
testMigrationTo22_x();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ public class JsonFileImport483MigrationTest extends AbstractJsonFileImportMigrat
|
|||
testMigrationTo18_x();
|
||||
testMigrationTo20_x();
|
||||
testMigrationTo21_x();
|
||||
testMigrationTo22_x();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -52,6 +52,7 @@ public class JsonFileImport903MigrationTest extends AbstractJsonFileImportMigrat
|
|||
testMigrationTo18_x();
|
||||
testMigrationTo20_x();
|
||||
testMigrationTo21_x();
|
||||
testMigrationTo22_x();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -40,17 +40,6 @@
|
|||
"/auth/realms/master/app/*"
|
||||
],
|
||||
"secret": "password"
|
||||
},
|
||||
{
|
||||
"clientId": "kerberos-app-challenge",
|
||||
"enabled": true,
|
||||
"adminUrl": "/kerberos-portal/logout",
|
||||
"baseUrl": "/kerberos-portal",
|
||||
"redirectUris": [
|
||||
"/kerberos-portal/*",
|
||||
"/auth/realms/master/app/*"
|
||||
],
|
||||
"secret": "password"
|
||||
}
|
||||
],
|
||||
"roles" : {
|
||||
|
|
Loading…
Reference in a new issue