package org.keycloak.authentication;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
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.ErrorPage;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.messages.Messages;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collections;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationProcessor {
protected static Logger logger = Logger.getLogger(AuthenticationProcessor.class);
protected RealmModel realm;
protected UserSessionModel userSession;
protected ClientSessionModel clientSession;
protected ClientConnection connection;
protected UriInfo uriInfo;
protected KeycloakSession session;
protected BruteForceProtector protector;
protected EventBuilder event;
protected HttpRequest request;
protected String flowId;
protected String action;
protected boolean userSessionCreated;
public static enum Status {
SUCCESS,
CHALLENGE,
FORCE_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;
}
public UserSessionModel getUserSession() {
return userSession;
}
public boolean isUserSessionCreated() {
return userSessionCreated;
}
public AuthenticationProcessor setRealm(RealmModel realm) {
this.realm = realm;
return this;
}
public AuthenticationProcessor setClientSession(ClientSessionModel clientSession) {
this.clientSession = clientSession;
return this;
}
public AuthenticationProcessor setConnection(ClientConnection connection) {
this.connection = connection;
return this;
}
public AuthenticationProcessor setUriInfo(UriInfo uriInfo) {
this.uriInfo = uriInfo;
return this;
}
public AuthenticationProcessor setSession(KeycloakSession session) {
this.session = session;
return this;
}
public AuthenticationProcessor setProtector(BruteForceProtector protector) {
this.protector = protector;
return this;
}
public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) {
this.event = eventBuilder;
return this;
}
public AuthenticationProcessor setRequest(HttpRequest request) {
this.request = request;
return this;
}
public AuthenticationProcessor setFlowId(String flowId) {
this.flowId = flowId;
return this;
}
public AuthenticationProcessor setAction(String action) {
this.action = action;
return this;
}
private class Result implements AuthenticatorContext {
AuthenticatorModel model;
AuthenticationExecutionModel execution;
Authenticator authenticator;
Status status;
Response challenge;
Error error;
private Result(AuthenticationExecutionModel execution, AuthenticatorModel model, Authenticator authenticator) {
this.execution = execution;
this.model = model;
this.authenticator = authenticator;
}
@Override
public AuthenticationExecutionModel getExecution() {
return execution;
}
@Override
public void setExecution(AuthenticationExecutionModel execution) {
this.execution = execution;
}
@Override
public AuthenticatorModel getAuthenticatorModel() {
return model;
}
@Override
public void setAuthenticatorModel(AuthenticatorModel model) {
this.model = model;
}
@Override
public String getAction() {
return AuthenticationProcessor.this.action;
}
@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 forceChallenge(Response challenge) {
this.status = Status.FORCE_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 failure(Error error, Response challenge) {
this.error = error;
this.status = Status.FAILED;
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;
}
@Override
public EventBuilder getEvent() {
return AuthenticationProcessor.this.event;
}
}
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 Error getError() {
return error;
}
}
public void logUserFailure() {
}
protected boolean isProcessed(AuthenticationExecutionModel model) {
if (model.isDisabled()) return true;
UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId());
if (status == null) return false;
return status == UserSessionModel.AuthenticatorStatus.SUCCESS || status == UserSessionModel.AuthenticatorStatus.SKIPPED
|| status == UserSessionModel.AuthenticatorStatus.ATTEMPTED
|| status == UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED;
}
public boolean isSuccessful(AuthenticationExecutionModel model) {
UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId());
if (status == null) return false;
return status == UserSessionModel.AuthenticatorStatus.SUCCESS;
}
public Response handleBrowserException(Exception failure) {
if (failure instanceof AuthException) {
AuthException e = (AuthException)failure;
logger.error("failed authentication: " + e.getError().toString(), e);
if (e.getError() == AuthenticationProcessor.Error.INVALID_USER) {
event.error(Errors.USER_NOT_FOUND);
return ErrorPage.error(session, Messages.INVALID_USER);
} else if (e.getError() == AuthenticationProcessor.Error.USER_DISABLED) {
event.error(Errors.USER_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_DISABLED);
} else if (e.getError() == AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED) {
event.error(Errors.USER_TEMPORARILY_DISABLED);
return ErrorPage.error(session, Messages.ACCOUNT_TEMPORARILY_DISABLED);
} else {
event.error(Errors.INVALID_USER_CREDENTIALS);
return ErrorPage.error(session, Messages.INVALID_USER);
}
} else {
logger.error("failed authentication", failure);
event.error(Errors.INVALID_USER_CREDENTIALS);
return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST);
}
}
public Response authenticate() throws AuthException {
logger.debug("AUTHENTICATE");
event.event(EventType.LOGIN);
event.client(clientSession.getClient().getClientId())
.detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.AUTH_METHOD, clientSession.getAuthMethod());
String authType = clientSession.getNote(Details.AUTH_TYPE);
if (authType != null) {
event.detail(Details.AUTH_TYPE, authType);
}
UserModel authUser = clientSession.getAuthenticatedUser();
validateUser(authUser);
Response challenge = processFlow(flowId);
if (challenge != null) return challenge;
if (clientSession.getAuthenticatedUser() == null) {
throw new AuthException(Error.UNKNOWN_USER);
}
return authenticationComplete();
}
public Response authenticateOnly() throws AuthException {
event.event(EventType.LOGIN);
event.client(clientSession.getClient().getClientId())
.detail(Details.REDIRECT_URI, clientSession.getRedirectUri())
.detail(Details.AUTH_METHOD, clientSession.getAuthMethod());
String authType = clientSession.getNote(Details.AUTH_TYPE);
if (authType != null) {
event.detail(Details.AUTH_TYPE, authType);
}
UserModel authUser = clientSession.getAuthenticatedUser();
validateUser(authUser);
Response challenge = processFlow(flowId);
if (challenge != null) return challenge;
String username = clientSession.getAuthenticatedUser().getUsername();
if (userSession == null) { // if no authenticator attached a usersession
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null);
userSession.setState(UserSessionModel.State.LOGGING_IN);
userSessionCreated = true;
}
TokenManager.attachClientSession(userSession, clientSession);
event.user(userSession.getUser())
.detail(Details.USERNAME, username)
.session(userSession);
return AuthenticationManager.actionRequired(session, userSession, clientSession, connection, request, uriInfo, event);
}
public Response finishAuthentication() {
event.success();
RealmModel realm = clientSession.getRealm();
return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection);
}
public Response processFlow(String flowId) {
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId);
if (flow == null) {
logger.error("Unknown flow to execute with");
throw new AuthException(Error.INTERNAL_ERROR);
}
List<AuthenticationExecutionModel> executions = realm.getAuthenticationExecutions(flowId);
if (executions == null) return null;
Response alternativeChallenge = null;
AuthenticationExecutionModel challengedAlternativeExecution = null;
boolean alternativeSuccessful = false;
for (AuthenticationExecutionModel model : executions) {
if (isProcessed(model)) {
logger.debug("execution is processed");
if (!alternativeSuccessful && model.isAlternative() && isSuccessful(model)) alternativeSuccessful = true;
continue;
}
Result context = null;
if (model.isAlternative() && alternativeSuccessful) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
continue;
}
if (model.isAutheticatorFlow()) {
Response flowResponse = processFlow(model.getAuthenticator());
if (flowResponse == null) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SUCCESS);
if (model.isAlternative()) alternativeSuccessful = true;
continue;
} else {
return flowResponse;
}
}
AuthenticatorModel authenticatorModel = realm.getAuthenticatorById(model.getAuthenticator());
AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, authenticatorModel.getProviderId());
Authenticator authenticator = factory.create(authenticatorModel);
logger.debugv("authenticator: {0}", authenticatorModel.getProviderId());
UserModel authUser = clientSession.getAuthenticatedUser();
if (authenticator.requiresUser() && authUser == null){
if (alternativeChallenge != null) {
clientSession.setAuthenticatorStatus(challengedAlternativeExecution.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return alternativeChallenge;
}
throw new AuthException("authenticator: " + authenticatorModel.getProviderId(), Error.UNKNOWN_USER);
}
boolean configuredFor = false;
if (authenticator.requiresUser() && authUser != null) {
configuredFor = authenticator.configuredFor(session, realm, authUser);
if (!configuredFor) {
if (model.isRequired()) {
if (model.isUserSetupAllowed()) {
logger.debugv("authenticator SETUP_REQUIRED: {0}", authenticatorModel.getProviderId());
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED);
String requiredAction = authenticator.getRequiredAction();
if (!authUser.getRequiredActions().contains(requiredAction)) {
authUser.addRequiredAction(requiredAction);
}
continue;
} else {
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
}
} else if (model.isOptional()) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
continue;
}
}
}
context = new Result(model, authenticatorModel, authenticator);
authenticator.authenticate(context);
Status result = context.getStatus();
if (result == Status.SUCCESS){
logger.debugv("authenticator SUCCESS: {0}", authenticatorModel.getProviderId());
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SUCCESS);
if (model.isAlternative()) alternativeSuccessful = true;
continue;
} else if (result == Status.FAILED) {
logger.debugv("authenticator FAILED: {0}", authenticatorModel.getProviderId());
logUserFailure();
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.FAILED);
if (context.challenge != null) return context.challenge;
throw new AuthException(context.error);
} else if (result == Status.FORCE_CHALLENGE) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return context.challenge;
} else if (result == Status.CHALLENGE) {
logger.debugv("authenticator CHALLENGE: {0}", authenticatorModel.getProviderId());
if (model.isRequired() || (model.isOptional() && configuredFor)) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return context.challenge;
}
else if (model.isAlternative()) {
alternativeChallenge = context.challenge;
challengedAlternativeExecution = model;
} else {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
}
continue;
} else if (result == Status.FAILURE_CHALLENGE) {
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", authenticatorModel.getProviderId());
logUserFailure();
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return context.challenge;
} else if (result == Status.ATTEMPTED) {
logger.debugv("authenticator ATTEMPTED: {0}", authenticatorModel.getProviderId());
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
throw new AuthException(Error.INVALID_CREDENTIALS);
}
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.ATTEMPTED);
continue;
} else {
logger.debugv("authenticator INTERNAL_ERROR: {0}", authenticatorModel.getProviderId());
logger.error("Unknown result status");
throw new AuthException(Error.INTERNAL_ERROR);
}
}
return null;
}
public void validateUser(UserModel authenticatedUser) {
if (authenticatedUser != null) {
if (!authenticatedUser.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() {
String username = clientSession.getAuthenticatedUser().getUsername();
String rememberMe = clientSession.getNote(Details.REMEMBER_ME);
boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true");
if (userSession == null) { // if no authenticator attached a usersession
userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), clientSession.getAuthMethod(), remember, null, null);
userSession.setState(UserSessionModel.State.LOGGING_IN);
}
if (remember) {
event.detail(Details.REMEMBER_ME, "true");
}
TokenManager.attachClientSession(userSession, clientSession);
event.user(userSession.getUser())
.detail(Details.USERNAME, username)
.session(userSession);
return processRequiredActions();
}
public Response processRequiredActions() {
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event);
}
}