disabledReason as read-only attribute, AuthenticatorUtils

This commit is contained in:
Václav Muzikář 2021-04-28 17:23:33 +02:00 committed by Marek Posolda
parent 315b9e3c29
commit 5a33ec2244
7 changed files with 89 additions and 58 deletions

View file

@ -34,12 +34,12 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.messages.Messages; import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
import static org.keycloak.services.validation.Validation.FIELD_PASSWORD; import static org.keycloak.services.validation.Validation.FIELD_PASSWORD;
import static org.keycloak.services.validation.Validation.FIELD_USERNAME; import static org.keycloak.services.validation.Validation.FIELD_USERNAME;
@ -240,17 +240,13 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth
} }
protected boolean isDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) { protected boolean isDisabledByBruteForce(AuthenticationFlowContext context, UserModel user) {
if (context.getRealm().isBruteForceProtected()) { String bruteForceError = getDisabledByBruteForceEventError(context.getProtector(), context.getSession(), context.getRealm(), user);
BruteForceProtector protector = context.getProtector(); if (bruteForceError != null) {
boolean isPermanentlyLockedOut = protector.isPermanentlyLockedOut(context.getSession(), context.getRealm(), user); context.getEvent().user(user);
context.getEvent().error(bruteForceError);
if (isPermanentlyLockedOut || protector.isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) { Response challengeResponse = challenge(context, disabledByBruteForceError(), disabledByBruteForceFieldError());
context.getEvent().user(user); context.forceChallenge(challengeResponse);
context.getEvent().error(isPermanentlyLockedOut ? Errors.USER_DISABLED : Errors.USER_TEMPORARILY_DISABLED); return true;
Response challengeResponse = challenge(context, disabledByBruteForceError(), disabledByBruteForceFieldError());
context.forceChallenge(challengeResponse);
return true;
}
} }
return false; return false;
} }

View file

@ -31,13 +31,14 @@ import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.provider.ProviderConfigProperty; import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.LinkedList; import java.util.LinkedList;
import java.util.List; import java.util.List;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
@ -75,18 +76,16 @@ public class ValidateUsername extends AbstractDirectGrantAuthenticator {
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse); context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
return; return;
} }
if (context.getRealm().isBruteForceProtected()) {
BruteForceProtector protector = context.getProtector();
boolean isPermanentlyLockedOut = protector.isPermanentlyLockedOut(context.getSession(), context.getRealm(), user);
if (isPermanentlyLockedOut || protector.isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) { String bruteForceError = getDisabledByBruteForceEventError(context.getProtector(), context.getSession(), context.getRealm(), user);
context.getEvent().user(user); if (bruteForceError != null) {
context.getEvent().error(isPermanentlyLockedOut ? Errors.USER_DISABLED : Errors.USER_TEMPORARILY_DISABLED); context.getEvent().user(user);
Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials"); context.getEvent().error(bruteForceError);
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse); Response challengeResponse = errorResponse(Response.Status.UNAUTHORIZED.getStatusCode(), "invalid_grant", "Invalid user credentials");
return; context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
} return;
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED); context.getEvent().error(Errors.USER_DISABLED);

View file

@ -0,0 +1,42 @@
/*
* Copyright 2021 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.util;
import org.keycloak.events.Errors;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.BruteForceProtector;
/**
* @author Vaclav Muzikar <vmuzikar@redhat.com>
*/
public final class AuthenticatorUtils {
public static String getDisabledByBruteForceEventError(BruteForceProtector protector, KeycloakSession session, RealmModel realm, UserModel user) {
if (realm.isBruteForceProtected()) {
if (protector.isPermanentlyLockedOut(session, realm, user)) {
return Errors.USER_DISABLED;
}
else if (protector.isTemporarilyDisabled(session, realm, user)) {
return Errors.USER_TEMPORARILY_DISABLED;
}
return null;
}
return null;
}
}

View file

@ -30,7 +30,8 @@ import org.keycloak.events.Errors;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.services.ServicesLogger; import org.keycloak.services.ServicesLogger;
import org.keycloak.services.managers.BruteForceProtector;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
/** /**
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a> * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
@ -119,18 +120,16 @@ public class ValidateX509CertificateUsername extends AbstractX509ClientCertifica
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse); context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
return; return;
} }
if (context.getRealm().isBruteForceProtected()) {
BruteForceProtector protector = context.getProtector();
boolean isPermanentlyLockedOut = protector.isPermanentlyLockedOut(context.getSession(), context.getRealm(), user);
if (isPermanentlyLockedOut || protector.isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) { String bruteForceError = getDisabledByBruteForceEventError(context.getProtector(), context.getSession(), context.getRealm(), user);
context.getEvent().user(user); if (bruteForceError != null) {
context.getEvent().error(isPermanentlyLockedOut ? Errors.USER_DISABLED : Errors.USER_TEMPORARILY_DISABLED); context.getEvent().user(user);
Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Invalid user credentials"); context.getEvent().error(bruteForceError);
context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse); Response challengeResponse = errorResponse(Response.Status.BAD_REQUEST.getStatusCode(), "invalid_grant", "Invalid user credentials");
return; context.failure(AuthenticationFlowError.INVALID_USER, challengeResponse);
} return;
} }
if (!user.isEnabled()) { if (!user.isEnabled()) {
context.getEvent().user(user); context.getEvent().user(user);
context.getEvent().error(Errors.USER_DISABLED); context.getEvent().error(Errors.USER_DISABLED);

View file

@ -35,7 +35,8 @@ import org.keycloak.forms.login.LoginFormsProvider;
import org.keycloak.models.ModelDuplicateException; import org.keycloak.models.ModelDuplicateException;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.FormMessage;
import org.keycloak.services.managers.BruteForceProtector;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
/** /**
* @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a> * @author <a href="mailto:pnalyvayko@agi.com">Peter Nalyvayko</a>
@ -136,21 +137,17 @@ public class X509ClientCertificateAuthenticator extends AbstractX509ClientCertif
return; return;
} }
if (context.getRealm().isBruteForceProtected()) { String bruteForceError = getDisabledByBruteForceEventError(context.getProtector(), context.getSession(), context.getRealm(), user);
BruteForceProtector protector = context.getProtector(); if (bruteForceError != null) {
boolean isPermanentlyLockedOut = protector.isPermanentlyLockedOut(context.getSession(), context.getRealm(), user); context.getEvent().user(user);
context.getEvent().error(bruteForceError);
if (isPermanentlyLockedOut || protector.isTemporarilyDisabled(context.getSession(), context.getRealm(), user)) { // TODO use specific locale to load error messages
context.getEvent().user(user); String errorMessage = "X509 certificate authentication's failed.";
context.getEvent().error(isPermanentlyLockedOut ? Errors.USER_DISABLED : Errors.USER_TEMPORARILY_DISABLED); // TODO is calling form().setErrors enough to show errors on login screen?
// TODO use specific locale to load error messages context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(),
String errorMessage = "X509 certificate authentication's failed."; errorMessage, "Invalid user"));
// TODO is calling form().setErrors enough to show errors on login screen? context.attempted();
context.challenge(createErrorResponse(context, certs[0].getSubjectDN().getName(), return;
errorMessage, "Invalid user"));
context.attempted();
return;
}
} }
if (!userEnabled(context, user)) { if (!userEnabled(context, user)) {

View file

@ -136,6 +136,7 @@ import java.util.regex.Pattern;
import java.util.stream.Stream; import java.util.stream.Stream;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.keycloak.authentication.authenticators.util.AuthenticatorUtils.getDisabledByBruteForceEventError;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID; import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_ID;
import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME; import static org.keycloak.models.ImpersonationSessionNote.IMPERSONATOR_USERNAME;
@ -1234,14 +1235,11 @@ public class TokenEndpoint {
event.error(Errors.USER_DISABLED); event.error(Errors.USER_DISABLED);
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST); throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
} }
if (realm.isBruteForceProtected()) {
BruteForceProtector protector = session.getProvider(BruteForceProtector.class);
boolean isPermanentlyLockedOut = protector.isPermanentlyLockedOut(session, realm, user);
if (isPermanentlyLockedOut || protector.isTemporarilyDisabled(session, realm, user)) { String bruteForceError = getDisabledByBruteForceEventError(session.getProvider(BruteForceProtector.class), session, realm, user);
event.error(isPermanentlyLockedOut ? Errors.USER_DISABLED : Errors.USER_TEMPORARILY_DISABLED); if (bruteForceError != null) {
throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST); event.error(bruteForceError);
} throw new CorsErrorResponseException(cors, Errors.INVALID_TOKEN, "Invalid Token", Response.Status.BAD_REQUEST);
} }
context.getIdp().updateBrokeredUser(session, realm, user, context); context.getIdp().updateBrokeredUser(session, realm, user, context);

View file

@ -44,7 +44,7 @@ public class LegacyUserProfileProviderFactory implements UserProfileProviderFact
// Attributes, which can't be updated by administrator // Attributes, which can't be updated by administrator
private Pattern adminReadOnlyAttributesPattern; private Pattern adminReadOnlyAttributesPattern;
private String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED" }; private String[] DEFAULT_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp", "userCertificate", "saml.persistent.name.id.for.*", "ENABLED", "EMAIL_VERIFIED", "disabledReason" };
private String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" }; private String[] DEFAULT_ADMIN_READ_ONLY_ATTRIBUTES = { "KERBEROS_PRINCIPAL", "LDAP_ID", "LDAP_ENTRY_DN", "CREATED_TIMESTAMP", "createTimestamp", "modifyTimestamp" };
@Override @Override