1st phase auth/required action spi
This commit is contained in:
1 parent
e4204a56f5
commit
338300df32
41 files changed
+1574
-58
No files matched your search
+358
@@ -0,0 +1,358 @@
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.events.EventBuilder;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.protocol.oidc.TokenManager;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
|
||||
//
|
||||
// setup
|
||||
// cookie: master, alternative
|
||||
// CERT_AUTH: alternative
|
||||
// UserPassword: alternative
|
||||
// OTP: optional
|
||||
// CAPTHA: required
|
||||
//
|
||||
// scenario: username password
|
||||
// * cookie, attempted
|
||||
// * cert, attempated
|
||||
// * usernamepassord, doesn't see form, sets challenge to form
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
|
||||
public class AuthenticationProcessor {
|
||||
protected RealmModel realm;
|
||||
protected UserSessionModel userSession;
|
||||
protected ClientSessionModel clientSession;
|
||||
protected ClientConnection connection;
|
||||
protected UriInfo uriInfo;
|
||||
protected KeycloakSession session;
|
||||
protected List<AuthenticatorModel> authenticators;
|
||||
protected BruteForceProtector protector;
|
||||
protected EventBuilder eventBuilder;
|
||||
protected HttpRequest request;
|
||||
|
||||
|
||||
public static enum Status {
|
||||
SUCCESS,
|
||||
CHALLENGE,
|
||||
FAILURE_CHALLENGE,
|
||||
FAILED,
|
||||
ATTEMPTED
|
||||
|
||||
}
|
||||
public static enum Error {
|
||||
INVALID_USER,
|
||||
INVALID_CREDENTIALS,
|
||||
CREDENTIAL_SETUP_REQUIRED,
|
||||
USER_DISABLED,
|
||||
USER_CONFLICT,
|
||||
USER_TEMPORARILY_DISABLED,
|
||||
INTERNAL_ERROR,
|
||||
UNKNOWN_USER
|
||||
}
|
||||
|
||||
public RealmModel getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
public ClientSessionModel getClientSession() {
|
||||
return clientSession;
|
||||
}
|
||||
|
||||
public ClientConnection getConnection() {
|
||||
return connection;
|
||||
}
|
||||
|
||||
public UriInfo getUriInfo() {
|
||||
return uriInfo;
|
||||
}
|
||||
|
||||
public KeycloakSession getSession() {
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
private class Result implements AuthenticatorContext {
|
||||
AuthenticatorModel model;
|
||||
Authenticator authenticator;
|
||||
Status status;
|
||||
Response challenge;
|
||||
Error error;
|
||||
|
||||
private Result(AuthenticatorModel model, Authenticator authenticator) {
|
||||
this.model = model;
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public AuthenticatorModel getModel() {
|
||||
return model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setModel(AuthenticatorModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator getAuthenticator() {
|
||||
return authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAuthenticator(Authenticator authenticator) {
|
||||
this.authenticator = authenticator;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Status getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void success() {
|
||||
this.status = Status.SUCCESS;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void failure(Error error) {
|
||||
status = Status.FAILED;
|
||||
this.error = error;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void challenge(Response challenge) {
|
||||
this.status = Status.CHALLENGE;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
@Override
|
||||
public void failureChallenge(Error error, Response challenge) {
|
||||
this.error = error;
|
||||
this.status = Status.FAILURE_CHALLENGE;
|
||||
this.challenge = challenge;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attempted() {
|
||||
this.status = Status.ATTEMPTED;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserModel getUser() {
|
||||
return getClientSession().getAuthenticatedUser();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUser(UserModel user) {
|
||||
UserModel previousUser = getUser();
|
||||
if (previousUser != null && !user.getId().equals(previousUser.getId())) throw new AuthException(Error.USER_CONFLICT);
|
||||
validateUser(user);
|
||||
getClientSession().setAuthenticatedUser(user);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RealmModel getRealm() {
|
||||
return AuthenticationProcessor.this.getRealm();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientSessionModel getClientSession() {
|
||||
return AuthenticationProcessor.this.getClientSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientConnection getConnection() {
|
||||
return AuthenticationProcessor.this.getConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public UriInfo getUriInfo() {
|
||||
return AuthenticationProcessor.this.getUriInfo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public KeycloakSession getSession() {
|
||||
return AuthenticationProcessor.this.getSession();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequest getHttpRequest() {
|
||||
return AuthenticationProcessor.this.request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attachUserSession(UserSessionModel userSession) {
|
||||
AuthenticationProcessor.this.userSession = userSession;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BruteForceProtector getProtector() {
|
||||
return AuthenticationProcessor.this.protector;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AuthException extends RuntimeException {
|
||||
private Error error;
|
||||
|
||||
public AuthException(Error error) {
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public AuthException(String message, Error error) {
|
||||
super(message);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public AuthException(String message, Throwable cause, Error error) {
|
||||
super(message, cause);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public AuthException(Throwable cause, Error error) {
|
||||
super(cause);
|
||||
this.error = error;
|
||||
}
|
||||
|
||||
public AuthException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace, Error error) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
this.error = error;
|
||||
}
|
||||
}
|
||||
|
||||
public void logUserFailure() {
|
||||
|
||||
}
|
||||
|
||||
protected boolean isProcessed(UserSessionModel.AuthenticatorStatus status) {
|
||||
return status == UserSessionModel.AuthenticatorStatus.SUCCESS || status == UserSessionModel.AuthenticatorStatus.SKIPPED
|
||||
|| status == UserSessionModel.AuthenticatorStatus.ATTEMPTED
|
||||
|| status == UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED;
|
||||
}
|
||||
|
||||
public Response authenticate() {
|
||||
UserModel authUser = clientSession.getAuthenticatedUser();
|
||||
validateUser(authUser);
|
||||
Response challenge = null;
|
||||
Map<String, UserSessionModel.AuthenticatorStatus> previousAttempts = clientSession.getAuthenticators();
|
||||
for (AuthenticatorModel model : authenticators) {
|
||||
UserSessionModel.AuthenticatorStatus oldStatus = previousAttempts.get(model.getAlias());
|
||||
if (isProcessed(oldStatus)) continue;
|
||||
|
||||
AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, model.getProviderId());
|
||||
Authenticator authenticator = factory.create(model);
|
||||
if (authenticator.requiresUser() && authUser == null){
|
||||
if ( authenticator.requiresUser()) {
|
||||
if (challenge != null) return challenge;
|
||||
throw new AuthException(Error.UNKNOWN_USER);
|
||||
}
|
||||
}
|
||||
if (authUser != null && model.getRequirement() == AuthenticatorModel.Requirement.ALTERNATIVE) {
|
||||
clientSession.setAuthenticatorStatus(model.getAlias(), UserSessionModel.AuthenticatorStatus.SKIPPED);
|
||||
continue;
|
||||
}
|
||||
authUser = clientSession.getAuthenticatedUser();
|
||||
|
||||
if (authenticator.requiresUser() && authUser != null && !authenticator.configuredFor(authUser)) {
|
||||
if (model.getRequirement() == AuthenticatorModel.Requirement.REQUIRED) {
|
||||
if (model.isUserSetupAllowed()) {
|
||||
clientSession.setAuthenticatorStatus(model.getAlias(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED);
|
||||
authUser.addRequiredAction(authenticator.getRequiredAction());
|
||||
|
||||
} else {
|
||||
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
Result context = new Result(model, authenticator);
|
||||
authenticator.authenticate(context);
|
||||
Status result = context.getStatus();
|
||||
if (result == Status.SUCCESS){
|
||||
clientSession.setAuthenticatorStatus(model.getAlias(), UserSessionModel.AuthenticatorStatus.SUCCESS);
|
||||
if (model.isMasterAuthenticator()) return authenticationComplete();
|
||||
continue;
|
||||
} else if (result == Status.FAILED) {
|
||||
throw new AuthException(context.error);
|
||||
} else if (result == Status.CHALLENGE) {
|
||||
if (model.getRequirement() == AuthenticatorModel.Requirement.REQUIRED) return context.challenge;
|
||||
if (challenge != null) challenge = context.challenge;
|
||||
continue;
|
||||
} else if (result == Status.FAILURE_CHALLENGE) {
|
||||
logUserFailure();
|
||||
return context.challenge;
|
||||
} else if (result == Status.ATTEMPTED) {
|
||||
if (model.getRequirement() == AuthenticatorModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS);
|
||||
clientSession.setAuthenticatorStatus(model.getAlias(), UserSessionModel.AuthenticatorStatus.ATTEMPTED);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (authUser == null) {
|
||||
if (challenge != null) return challenge;
|
||||
throw new AuthException(Error.UNKNOWN_USER);
|
||||
}
|
||||
|
||||
|
||||
return authenticationComplete();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public void validateUser(UserModel authenticatedUser) {
|
||||
if (authenticatedUser != null) {
|
||||
if (!clientSession.getAuthenticatedUser().isEnabled()) throw new AuthException(Error.USER_DISABLED);
|
||||
}
|
||||
if (realm.isBruteForceProtected()) {
|
||||
if (protector.isTemporarilyDisabled(session, realm, authenticatedUser.getUsername())) {
|
||||
throw new AuthException(Error.USER_TEMPORARILY_DISABLED);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected Response authenticationComplete() {
|
||||
if (userSession == null) { // if no authenticator attached a usersession
|
||||
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), clientSession.getAuthenticatedUser().getUsername(), connection.getRemoteAddr(), "form", false, null, null);
|
||||
userSession.setState(UserSessionModel.State.LOGGING_IN);
|
||||
}
|
||||
TokenManager.attachClientSession(userSession, clientSession);
|
||||
return processRequiredActions();
|
||||
|
||||
}
|
||||
|
||||
public Response processRequiredActions() {
|
||||
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, eventBuilder);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.provider.Provider;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface Authenticator extends Provider {
|
||||
boolean requiresUser();
|
||||
void authenticate(AuthenticatorContext context);
|
||||
boolean configuredFor(UserModel user);
|
||||
String getRequiredAction();
|
||||
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.jboss.resteasy.spi.HttpRequest;
|
||||
import org.keycloak.ClientConnection;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.ClientSessionModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.UserSessionModel;
|
||||
import org.keycloak.services.managers.BruteForceProtector;
|
||||
|
||||
import javax.ws.rs.core.Response;
|
||||
import javax.ws.rs.core.UriInfo;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface AuthenticatorContext {
|
||||
AuthenticatorModel getModel();
|
||||
|
||||
void setModel(AuthenticatorModel model);
|
||||
|
||||
Authenticator getAuthenticator();
|
||||
|
||||
void setAuthenticator(Authenticator authenticator);
|
||||
|
||||
AuthenticationProcessor.Status getStatus();
|
||||
|
||||
UserModel getUser();
|
||||
|
||||
void setUser(UserModel user);
|
||||
|
||||
RealmModel getRealm();
|
||||
|
||||
ClientSessionModel getClientSession();
|
||||
void attachUserSession(UserSessionModel userSession);
|
||||
|
||||
ClientConnection getConnection();
|
||||
|
||||
UriInfo getUriInfo();
|
||||
|
||||
KeycloakSession getSession();
|
||||
|
||||
HttpRequest getHttpRequest();
|
||||
BruteForceProtector getProtector();
|
||||
|
||||
void success();
|
||||
void failure(AuthenticationProcessor.Error error);
|
||||
void challenge(Response challenge);
|
||||
void failureChallenge(AuthenticationProcessor.Error error, Response challenge);
|
||||
void attempted();
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
package org.keycloak.authentication;
|
||||
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.provider.ProviderFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public interface AuthenticatorFactory extends ProviderFactory<Authenticator> {
|
||||
Authenticator create(AuthenticatorModel model);
|
||||
|
||||
}
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorContext;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class CookieAuthenticator implements Authenticator {
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticatorContext context) {
|
||||
AuthenticationManager.AuthResult authResult = AuthenticationManager.authenticateIdentityCookie(context.getSession(),
|
||||
context.getRealm(), context.getUriInfo(), context.getConnection(), context.getHttpRequest().getHttpHeaders(), true);
|
||||
if (authResult == null) {
|
||||
context.attempted();
|
||||
} else {
|
||||
context.setUser(authResult.getUser());
|
||||
context.attachUserSession(authResult.getSession());
|
||||
context.success();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(UserModel user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequiredAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
Executable
+45
@@ -0,0 +1,45 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.keycloak.Config;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorFactory;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.KeycloakSessionFactory;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class CookieAuthenticatorFactory implements AuthenticatorFactory {
|
||||
static CookieAuthenticator SINGLETON = new CookieAuthenticator();
|
||||
@Override
|
||||
public Authenticator create(AuthenticatorModel model) {
|
||||
return SINGLETON;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Authenticator create(KeycloakSession session) {
|
||||
throw new IllegalStateException("illegal call");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(Config.Scope config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void postInit(KeycloakSessionFactory factory) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId() {
|
||||
return "auth-cookie";
|
||||
}
|
||||
}
|
||||
Executable
+71
@@ -0,0 +1,71 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.AuthenticatorContext;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator {
|
||||
|
||||
public LoginFormOTPAuthenticator(AuthenticatorModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticatorContext context) {
|
||||
if (!isActionUrl(context)) {
|
||||
context.failure(AuthenticationProcessor.Error.INTERNAL_ERROR);
|
||||
return;
|
||||
}
|
||||
validateOTP(context);
|
||||
}
|
||||
|
||||
public void validateOTP(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getFormParameters();
|
||||
List<UserCredentialModel> credentials = new LinkedList<>();
|
||||
String password = inputData.getFirst(CredentialRepresentation.TOTP);
|
||||
if (password == null) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
credentials.add(UserCredentialModel.totp(password));
|
||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||
if (!valid) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(UserModel user) {
|
||||
return user.configuredForCredentialType(UserCredentialModel.TOTP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequiredAction() {
|
||||
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.AuthenticatorContext;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticator {
|
||||
|
||||
public LoginFormPasswordAuthenticator(AuthenticatorModel model) {
|
||||
super(model);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticatorContext context) {
|
||||
if (!isActionUrl(context)) {
|
||||
context.failure(AuthenticationProcessor.Error.INTERNAL_ERROR);
|
||||
return;
|
||||
}
|
||||
validatePassword(context);
|
||||
}
|
||||
|
||||
public void validatePassword(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getFormParameters();
|
||||
List<UserCredentialModel> credentials = new LinkedList<>();
|
||||
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
|
||||
if (password == null) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
credentials.add(UserCredentialModel.password(password));
|
||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||
if (!valid) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(UserModel user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequiredAction() {
|
||||
return UserModel.RequiredAction.UPDATE_PASSWORD.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
+121
@@ -0,0 +1,121 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorContext;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class LoginFormUsernameAuthenticator implements Authenticator {
|
||||
protected AuthenticatorModel model;
|
||||
|
||||
public LoginFormUsernameAuthenticator(AuthenticatorModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticatorContext context) {
|
||||
if (!isActionUrl(context)) {
|
||||
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
|
||||
String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
|
||||
if (loginHint == null) {
|
||||
loginHint = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders());
|
||||
if (loginHint != null) {
|
||||
formData.add("rememberMe", "on");
|
||||
}
|
||||
}
|
||||
if (loginHint != null) formData.add(AuthenticationManager.FORM_USERNAME, loginHint);
|
||||
Response challengeResponse = challenge(context, formData);
|
||||
context.challenge(challengeResponse);
|
||||
return;
|
||||
}
|
||||
validateUser(context);
|
||||
}
|
||||
|
||||
protected boolean isActionUrl(AuthenticatorContext context) {
|
||||
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
|
||||
return expected.getPath().equals(context.getUriInfo().getPath());
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return false;
|
||||
}
|
||||
|
||||
protected Response challenge(AuthenticatorContext context, MultivaluedMap<String, String> formData) {
|
||||
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode());
|
||||
|
||||
if (formData.size() > 0) forms.setFormData(formData);
|
||||
|
||||
return forms.createLogin();
|
||||
}
|
||||
|
||||
public void validateUser(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getFormParameters();
|
||||
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
|
||||
if (username == null) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
||||
return;
|
||||
}
|
||||
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
|
||||
if (invalidUser(context, user)) return;
|
||||
context.setUser(user);
|
||||
}
|
||||
|
||||
public boolean invalidUser(AuthenticatorContext context, UserModel user) {
|
||||
if (user == null) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
||||
return true;
|
||||
}
|
||||
if (!user.isEnabled()) {
|
||||
context.failure(AuthenticationProcessor.Error.USER_DISABLED);
|
||||
return true;
|
||||
}
|
||||
if (context.getRealm().isBruteForceProtected()) {
|
||||
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) {
|
||||
context.failure(AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public Response challenge(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
|
||||
return challenge(context, formData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(UserModel user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequiredAction() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
package org.keycloak.authentication.authenticators;
|
||||
|
||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||
import org.keycloak.authentication.AuthenticationProcessor;
|
||||
import org.keycloak.authentication.Authenticator;
|
||||
import org.keycloak.authentication.AuthenticatorContext;
|
||||
import org.keycloak.login.LoginFormsProvider;
|
||||
import org.keycloak.models.AuthenticatorModel;
|
||||
import org.keycloak.models.UserCredentialModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||
import org.keycloak.services.managers.AuthenticationManager;
|
||||
import org.keycloak.services.managers.ClientSessionCode;
|
||||
import org.keycloak.services.resources.LoginActionsService;
|
||||
|
||||
import javax.ws.rs.core.MultivaluedMap;
|
||||
import javax.ws.rs.core.Response;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
public class OTPFormAuthenticator implements Authenticator {
|
||||
protected AuthenticatorModel model;
|
||||
|
||||
public OTPFormAuthenticator(AuthenticatorModel model) {
|
||||
this.model = model;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void authenticate(AuthenticatorContext context) {
|
||||
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
|
||||
if (!expected.getPath().equals(context.getUriInfo().getPath())) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.challenge(challengeResponse);
|
||||
return;
|
||||
}
|
||||
validateOTP(context);
|
||||
}
|
||||
|
||||
public void validateOTP(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> inputData = context.getHttpRequest().getFormParameters();
|
||||
List<UserCredentialModel> credentials = new LinkedList<>();
|
||||
String password = inputData.getFirst(CredentialRepresentation.TOTP);
|
||||
if (password == null) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
credentials.add(UserCredentialModel.totp(password));
|
||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||
if (!valid) {
|
||||
Response challengeResponse = challenge(context);
|
||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean requiresUser() {
|
||||
return true;
|
||||
}
|
||||
|
||||
protected Response challenge(AuthenticatorContext context, MultivaluedMap<String, String> formData) {
|
||||
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
|
||||
.setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode());
|
||||
|
||||
if (formData.size() > 0) forms.setFormData(formData);
|
||||
|
||||
return forms.createLoginTotp();
|
||||
}
|
||||
|
||||
public Response challenge(AuthenticatorContext context) {
|
||||
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
|
||||
return challenge(context, formData);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean configuredFor(UserModel user) {
|
||||
return user.configuredForCredentialType(UserCredentialModel.TOTP);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getRequiredAction() {
|
||||
return UserModel.RequiredAction.CONFIGURE_TOTP.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
|
||||
}
|
||||
}
|
||||
@@ -406,7 +406,7 @@ public class AuthenticationManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (userSession.getState() != UserSessionModel.State.LOGGED_IN) userSession.setState(UserSessionModel.State.LOGGED_IN);
|
||||
// refresh the cookies!
|
||||
createLoginCookie(realm, userSession.getUser(), userSession, uriInfo, clientConnection);
|
||||
if (userSession.isRememberMe()) createRememberMeCookie(realm, userSession.getUser().getUsername(), uriInfo, clientConnection);
|
||||
@@ -434,12 +434,12 @@ public class AuthenticationManager {
|
||||
|
||||
event.detail(Details.CODE_ID, clientSession.getId());
|
||||
|
||||
Set<UserModel.RequiredAction> requiredActions = user.getRequiredActions();
|
||||
Set<String> requiredActions = user.getRequiredActions();
|
||||
if (!requiredActions.isEmpty()) {
|
||||
Iterator<RequiredAction> i = user.getRequiredActions().iterator();
|
||||
UserModel.RequiredAction action = i.next();
|
||||
Iterator<String> i = user.getRequiredActions().iterator();
|
||||
String action = i.next();
|
||||
|
||||
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL) && Validation.isEmpty(user.getEmail())) {
|
||||
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name()) && Validation.isEmpty(user.getEmail())) {
|
||||
if (i.hasNext())
|
||||
action = i.next();
|
||||
else
|
||||
@@ -447,16 +447,16 @@ public class AuthenticationManager {
|
||||
}
|
||||
|
||||
if (action != null) {
|
||||
accessCode.setRequiredAction(action);
|
||||
accessCode.setRequiredAction(RequiredAction.valueOf(action));
|
||||
|
||||
LoginFormsProvider loginFormsProvider = session.getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode())
|
||||
.setUser(user);
|
||||
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL)) {
|
||||
if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name())) {
|
||||
event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success();
|
||||
LoginActionsService.createActionCookie(realm, uriInfo, clientConnection, userSession.getId());
|
||||
}
|
||||
|
||||
return loginFormsProvider.createResponse(action);
|
||||
return loginFormsProvider.createResponse(RequiredAction.valueOf(action));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,10 @@ public class LoginActionsService {
|
||||
return loginActionsBaseUrl(baseUriBuilder);
|
||||
}
|
||||
|
||||
public static UriBuilder authenticationFormProcessor(UriInfo uriInfo) {
|
||||
return loginActionsBaseUrl(uriInfo).path("auth-form");
|
||||
}
|
||||
|
||||
public static UriBuilder loginActionsBaseUrl(UriBuilder baseUriBuilder) {
|
||||
return baseUriBuilder.path(RealmsResource.class).path(RealmsResource.class, "getLoginActionsService");
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user