Merge pull request #1404 from patriot1burke/master

handle page refresh better
This commit is contained in:
Bill Burke 2015-06-18 20:16:29 -04:00
commit 8d885d14ed
19 changed files with 450 additions and 319 deletions

View file

@ -32,9 +32,9 @@ public interface ClientSessionModel {
public Set<String> getProtocolMappers();
public void setProtocolMappers(Set<String> protocolMappers);
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators();
public void setAuthenticatorStatus(String authenticator, UserSessionModel.AuthenticatorStatus status);
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> status);
public Map<String, ExecutionStatus> getExecutionStatus();
public void setExecutionStatus(String authenticator, ExecutionStatus status);
public void clearExecutionStatus();
public UserModel getAuthenticatedUser();
public void setAuthenticatedUser(UserModel user);
@ -67,6 +67,8 @@ public interface ClientSessionModel {
*/
public Map<String, String> getUserSessionNotes();
public void clearUserSessionNotes();
public static enum Action {
OAUTH_GRANT,
CODE_TO_TOKEN,
@ -80,4 +82,12 @@ public interface ClientSessionModel {
LOGGED_OUT
}
public enum ExecutionStatus {
FAILED,
SUCCESS,
SETUP_REQUIRED,
ATTEMPTED,
SKIPPED,
CHALLENGED
}
}

View file

@ -37,15 +37,6 @@ public interface UserSessionModel {
List<ClientSessionModel> getClientSessions();
public static enum AuthenticatorStatus {
FAILED,
SUCCESS,
SETUP_REQUIRED,
ATTEMPTED,
SKIPPED,
CHALLENGED
}
public String getNote(String name);
public void setNote(String name, String value);
public void removeNote(String name);

View file

@ -185,23 +185,32 @@ public class ClientSessionAdapter implements ClientSessionModel {
return copy;
}
@Override
public void clearUserSessionNotes() {
entity.setUserSessionNotes(new HashMap<String, String>());
update();
}
void update() {
provider.getTx().replace(cache, entity.getId(), entity);
}
@Override
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
public Map<String, ExecutionStatus> getExecutionStatus() {
return entity.getAuthenticatorStatus();
}
@Override
public void setAuthenticatorStatus(String authenticator, UserSessionModel.AuthenticatorStatus status) {
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
entity.getAuthenticatorStatus().put(authenticator, status);
update();
}
@Override
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> status) {
entity.setAuthenticatorStatus(status);
public void clearExecutionStatus() {
entity.getAuthenticatorStatus().clear();
update();
}
@Override
@ -211,6 +220,7 @@ public class ClientSessionAdapter implements ClientSessionModel {
@Override
public void setAuthenticatedUser(UserModel user) {
entity.setAuthUserId(user.getId());
update();
}

View file

@ -1,7 +1,6 @@
package org.keycloak.models.sessions.infinispan.entities;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserSessionModel;
import java.util.HashMap;
import java.util.Map;
@ -30,7 +29,7 @@ public class ClientSessionEntity extends SessionEntity {
private Set<String> protocolMappers;
private Map<String, String> notes;
private Map<String, String> userSessionNotes;
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
private String authUserId;
public String getClient() {
@ -113,11 +112,11 @@ public class ClientSessionEntity extends SessionEntity {
this.notes = notes;
}
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticatorStatus() {
public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
return authenticatorStatus;
}
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}

View file

@ -6,6 +6,7 @@ import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.jpa.entities.ClientSessionAuthStatusEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity;
@ -106,6 +107,17 @@ public class ClientSessionAdapter implements ClientSessionModel {
return copy;
}
@Override
public void clearUserSessionNotes() {
Iterator<ClientUserSessionNoteEntity> it = entity.getUserSessionNotes().iterator();
while (it.hasNext()) {
ClientUserSessionNoteEntity attr = it.next();
it.remove();
em.remove(attr);
}
}
@Override
public String getId() {
return entity.getId();
@ -242,27 +254,44 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
@Override
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
return null;
public Map<String, ExecutionStatus> getExecutionStatus() {
Map<String, ExecutionStatus> result = new HashMap<>();
for (ClientSessionAuthStatusEntity status : entity.getAuthanticatorStatus()) {
result.put(status.getAuthenticator(), status.getStatus());
}
return result;
}
@Override
public void setAuthenticatorStatus(String authenticator, UserSessionModel.AuthenticatorStatus status) {
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
ClientSessionAuthStatusEntity authStatus = new ClientSessionAuthStatusEntity();
authStatus.setAuthenticator(authenticator);
authStatus.setClientSession(entity);
authStatus.setStatus(status);
em.persist(authStatus);
entity.getAuthanticatorStatus().add(authStatus);
em.flush();
}
@Override
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> status) {
public void clearExecutionStatus() {
Iterator<ClientSessionAuthStatusEntity> iterator = entity.getAuthanticatorStatus().iterator();
while (iterator.hasNext()) {
ClientSessionAuthStatusEntity authStatus = iterator.next();
iterator.remove();
em.remove(authStatus);
}
}
@Override
public UserModel getAuthenticatedUser() {
return null;
return session.users().getUserById(entity.getUserId(), realm);
}
@Override
public void setAuthenticatedUser(UserModel user) {
entity.setUserId(user.getId());
}
}

View file

@ -1,6 +1,6 @@
package org.keycloak.models.sessions.jpa.entities;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.ClientSessionModel;
import javax.persistence.Column;
import javax.persistence.Entity;
@ -39,7 +39,7 @@ public class ClientSessionAuthStatusEntity {
@Column(name = "AUTHENTICATOR")
protected String authenticator;
@Column(name = "STATUS")
protected UserSessionModel.AuthenticatorStatus status;
protected ClientSessionModel.ExecutionStatus status;
public String getAuthenticator() {
return authenticator;
@ -49,11 +49,11 @@ public class ClientSessionAuthStatusEntity {
this.authenticator = authenticator;
}
public UserSessionModel.AuthenticatorStatus getStatus() {
public ClientSessionModel.ExecutionStatus getStatus() {
return status;
}
public void setStatus(UserSessionModel.AuthenticatorStatus status) {
public void setStatus(ClientSessionModel.ExecutionStatus status) {
this.status = status;
}

View file

@ -155,19 +155,24 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
@Override
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
public Map<String, ExecutionStatus> getExecutionStatus() {
return entity.getAuthenticatorStatus();
}
@Override
public void setAuthenticatorStatus(String authenticator, UserSessionModel.AuthenticatorStatus status) {
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
entity.getAuthenticatorStatus().put(authenticator, status);
}
@Override
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> status) {
entity.setAuthenticatorStatus(status);
public void clearExecutionStatus() {
entity.getAuthenticatorStatus().clear();
}
@Override
public void clearUserSessionNotes() {
entity.getUserSessionNotes().clear();
}
@Override

View file

@ -1,7 +1,6 @@
package org.keycloak.models.sessions.mem.entities;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserSessionModel;
import java.util.HashMap;
import java.util.Map;
@ -15,7 +14,7 @@ public class ClientSessionEntity {
private String id;
private String clientId;
private String realmId;
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
private String authUserId;
private UserSessionEntity session;
@ -122,11 +121,11 @@ public class ClientSessionEntity {
this.authUserId = authUserId;
}
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticatorStatus() {
public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
return authenticatorStatus;
}
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}

View file

@ -171,22 +171,26 @@ public class ClientSessionAdapter extends AbstractMongoAdapter<MongoClientSessio
}
@Override
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
public Map<String, ExecutionStatus> getExecutionStatus() {
return entity.getAuthenticatorStatus();
}
@Override
public void setAuthenticatorStatus(String authenticator, UserSessionModel.AuthenticatorStatus status) {
public void setExecutionStatus(String authenticator, ExecutionStatus status) {
entity.getAuthenticatorStatus().put(authenticator, status);
updateMongoEntity();
}
@Override
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> status) {
entity.setAuthenticatorStatus(status);
public void clearExecutionStatus() {
entity.getAuthenticatorStatus().clear();
updateMongoEntity();
}
@Override
public void clearUserSessionNotes() {
entity.getUserSessionNotes().clear();
}
@Override

View file

@ -4,7 +4,6 @@ import org.keycloak.connections.mongo.api.MongoCollection;
import org.keycloak.connections.mongo.api.MongoIdentifiableEntity;
import org.keycloak.connections.mongo.api.context.MongoStoreInvocationContext;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.entities.AbstractIdentifiableEntity;
import java.util.HashMap;
@ -31,7 +30,7 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
private List<String> protocolMappers;
private Map<String, String> notes = new HashMap<String, String>();
private Map<String, String> userSessionNotes = new HashMap<String, String>();
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
private Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus = new HashMap<>();
private String authUserId;
public String getId() {
@ -130,11 +129,11 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
this.sessionId = sessionId;
}
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticatorStatus() {
public Map<String, ClientSessionModel.ExecutionStatus> getAuthenticatorStatus() {
return authenticatorStatus;
}
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
public void setAuthenticatorStatus(Map<String, ClientSessionModel.ExecutionStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}

View file

@ -3,6 +3,7 @@ package org.keycloak.authentication;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.authentication.authenticators.AbstractFormAuthenticator;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
@ -21,17 +22,19 @@ import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.BruteForceProtector;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.util.Time;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;
import java.util.Collections;
import java.util.List;
import java.util.HashMap;
import java.util.Iterator;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationProcessor {
public static final String CURRENT_AUTHENTICATION_EXECUTION = "current.authentication.execution";
protected static Logger logger = Logger.getLogger(AuthenticationProcessor.class);
protected RealmModel realm;
protected UserSessionModel userSession;
@ -323,7 +326,7 @@ public class AuthenticationProcessor {
@Override
public String generateAccessCode() {
ClientSessionCode accessCode = new ClientSessionCode(getRealm(), getClientSession());
accessCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
clientSession.setTimestamp(Time.currentTime());
return accessCode.getCode();
}
}
@ -360,23 +363,32 @@ public class AuthenticationProcessor {
}
}
public void logUserFailure() {
public void logFailure() {
if (realm.isBruteForceProtected()) {
String username = clientSession.getNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME);
// todo need to handle non form failures
if (username == null) {
} else {
protector.failedLogin(realm, username, connection);
}
}
}
protected boolean isProcessed(AuthenticationExecutionModel model) {
if (model.isDisabled()) return true;
UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId());
ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().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;
return status == ClientSessionModel.ExecutionStatus.SUCCESS || status == ClientSessionModel.ExecutionStatus.SKIPPED
|| status == ClientSessionModel.ExecutionStatus.ATTEMPTED
|| status == ClientSessionModel.ExecutionStatus.SETUP_REQUIRED;
}
public boolean isSuccessful(AuthenticationExecutionModel model) {
UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId());
ClientSessionModel.ExecutionStatus status = clientSession.getExecutionStatus().get(model.getId());
if (status == null) return false;
return status == UserSessionModel.AuthenticatorStatus.SUCCESS;
return status == ClientSessionModel.ExecutionStatus.SUCCESS;
}
public Response handleBrowserException(Exception failure) {
@ -414,6 +426,16 @@ public class AuthenticationProcessor {
}
public FlowExecution createFlowExecution(String flowId) {
AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId);
if (flow == null) {
logger.error("Unknown flow to execute with");
throw new AuthException(Error.INTERNAL_ERROR);
}
FlowExecution flowExecution = new FlowExecution();
flowExecution.executions = realm.getAuthenticationExecutions(flow.getId()).iterator();
return flowExecution;
}
public Response authenticate() throws AuthException {
checkClientSession();
@ -428,7 +450,53 @@ public class AuthenticationProcessor {
}
UserModel authUser = clientSession.getAuthenticatedUser();
validateUser(authUser);
Response challenge = processFlow(flowId);
FlowExecution flowExecution = createFlowExecution(this.flowId);
Response challenge = flowExecution.processFlow();
if (challenge != null) return challenge;
if (clientSession.getAuthenticatedUser() == null) {
throw new AuthException(Error.UNKNOWN_USER);
}
return authenticationComplete();
}
protected void resetFlow() {
clientSession.clearExecutionStatus();
clientSession.clearUserSessionNotes();
clientSession.removeNote(CURRENT_AUTHENTICATION_EXECUTION);
}
public Response authenticationAction(String execution) {
checkClientSession();
String current = clientSession.getNote(CURRENT_AUTHENTICATION_EXECUTION);
if (!execution.equals(current)) {
logger.debug("Current execution does not equal executed execution. Might be a page refresh");
logFailure();
resetFlow();
return authenticate();
}
AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(execution);
if (model == null) {
logger.debug("Cannot find execution, reseting flow");
logFailure();
resetFlow();
return 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);
}
AuthenticatorModel authenticatorModel = realm.getAuthenticatorById(model.getAuthenticator());
AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, authenticatorModel.getProviderId());
Authenticator authenticator = factory.create(authenticatorModel);
Result context = new Result(model, authenticatorModel, authenticator);
authenticator.action(context);
FlowExecution flowExecution = createFlowExecution(this.flowId);
Response challenge = flowExecution.action(execution, context);
if (challenge != null) return challenge;
if (clientSession.getAuthenticatedUser() == null) {
throw new AuthException(Error.UNKNOWN_USER);
@ -444,6 +512,7 @@ public class AuthenticationProcessor {
if (!code.isActionActive(ClientSessionModel.Action.AUTHENTICATE.name())) {
throw new AuthException(Error.EXPIRED_CODE);
}
clientSession.setTimestamp(Time.currentTime());
}
public Response authenticateOnly() throws AuthException {
@ -458,7 +527,8 @@ public class AuthenticationProcessor {
}
UserModel authUser = clientSession.getAuthenticatedUser();
validateUser(authUser);
Response challenge = processFlow(flowId);
FlowExecution flowExecution = createFlowExecution(this.flowId);
Response challenge = flowExecution.processFlow();
if (challenge != null) return challenge;
String username = clientSession.getAuthenticatedUser().getUsername();
@ -482,125 +552,6 @@ public class AuthenticationProcessor {
}
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);
authenticator.setRequiredActions(session, realm, clientSession.getAuthenticatedUser());
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);
@ -632,5 +583,170 @@ public class AuthenticationProcessor {
}
class FlowExecution {
Response alternativeChallenge = null;
AuthenticationExecutionModel challengedAlternativeExecution = null;
boolean alternativeSuccessful = false;
Iterator<AuthenticationExecutionModel> executions;
public Response action(String actionExecution, Result actionResult) {
while (executions.hasNext()) {
AuthenticationExecutionModel model = executions.next();
if (isProcessed(model)) {
logger.debug("execution is processed");
if (!alternativeSuccessful && model.isAlternative() && isSuccessful(model)) alternativeSuccessful = true;
continue;
}
if (!model.getId().equals(actionExecution)) {
if (model.isAutheticatorFlow()) {
FlowExecution flowExecution = createFlowExecution(model.getAuthenticator());
return flowExecution.action(actionExecution, actionResult);
} else {
throw new AuthException("action is not current execution", Error.INTERNAL_ERROR);
}
} else { // we found the action
Response response = processResult(actionResult);
if (response == null) return processFlow();
else return response;
}
}
throw new AuthException("action is not in current execution", Error.INTERNAL_ERROR);
}
public Response processFlow() {
while (executions.hasNext()) {
AuthenticationExecutionModel model = executions.next();
if (isProcessed(model)) {
logger.debug("execution is processed");
if (!alternativeSuccessful && model.isAlternative() && isSuccessful(model)) alternativeSuccessful = true;
continue;
}
if (model.isAlternative() && alternativeSuccessful) {
clientSession.setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
continue;
}
if (model.isAutheticatorFlow()) {
FlowExecution flowExecution = createFlowExecution(model.getAuthenticator());
Response flowResponse = flowExecution.processFlow();
if (flowResponse == null) {
clientSession.setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.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.setExecutionStatus(challengedAlternativeExecution.getId(), ClientSessionModel.ExecutionStatus.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.setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SETUP_REQUIRED);
authenticator.setRequiredActions(session, realm, clientSession.getAuthenticatedUser());
continue;
} else {
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
}
} else if (model.isOptional()) {
clientSession.setExecutionStatus(model.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
continue;
}
}
}
Result context = new Result(model, authenticatorModel, authenticator);
authenticator.authenticate(context);
Response response = processResult(context);
if (response != null) return response;
}
return null;
}
public Response processResult(Result result) {
AuthenticationExecutionModel execution = result.getExecution();
AuthenticatorModel authenticatorModel = result.getAuthenticatorModel();
Status status = result.getStatus();
if (status == Status.SUCCESS){
logger.debugv("authenticator SUCCESS: {0}", authenticatorModel.getProviderId());
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
if (execution.isAlternative()) alternativeSuccessful = true;
return null;
} else if (status == Status.FAILED) {
logger.debugv("authenticator FAILED: {0}", authenticatorModel.getProviderId());
logFailure();
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
if (result.challenge != null) {
return sendChallenge(result, execution);
}
throw new AuthException(result.error);
} else if (status == Status.FORCE_CHALLENGE) {
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
} else if (status == Status.CHALLENGE) {
logger.debugv("authenticator CHALLENGE: {0}", authenticatorModel.getProviderId());
if (execution.isRequired()) {
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
}
UserModel authenticatedUser = clientSession.getAuthenticatedUser();
if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(session, realm, authenticatedUser)) {
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
}
if (execution.isAlternative()) {
alternativeChallenge = result.challenge;
challengedAlternativeExecution = execution;
} else {
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
}
return null;
} else if (status == Status.FAILURE_CHALLENGE) {
logger.debugv("authenticator FAILURE_CHALLENGE: {0}", authenticatorModel.getProviderId());
logFailure();
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
} else if (status == Status.ATTEMPTED) {
logger.debugv("authenticator ATTEMPTED: {0}", authenticatorModel.getProviderId());
if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
throw new AuthException(Error.INVALID_CREDENTIALS);
}
clientSession.setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED);
return null;
} else {
logger.debugv("authenticator INTERNAL_ERROR: {0}", authenticatorModel.getProviderId());
logger.error("Unknown result status");
throw new AuthException(Error.INTERNAL_ERROR);
}
}
public Response sendChallenge(Result result, AuthenticationExecutionModel execution) {
clientSession.setNote(CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
return result.challenge;
}
}
}

View file

@ -20,5 +20,7 @@ public interface Authenticator extends Provider {
*/
void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user);
void action(AuthenticatorContext context);
}

View file

@ -2,6 +2,7 @@ package org.keycloak.authentication.authenticators;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -24,20 +25,25 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AbstractFormAuthenticator {
public abstract class AbstractFormAuthenticator implements Authenticator {
public static final String LOGIN_FORM_ACTION = "login_form";
public static final String REGISTRATION_FORM_ACTION = "registration_form";
public static final String ACTION = "action";
public static final String FORM_USERNAME = "FORM_USERNAME";
public static final String EXECUTION = "execution";
public static final String ATTEMPTED_USERNAME = "ATTEMPTED_USERNAME";
@Override
public void action(AuthenticatorContext context) {
}
@Override
public void close() {
protected boolean isAction(AuthenticatorContext context, String action) {
return action.equals(context.getAction());
}
protected LoginFormsProvider loginForm(AuthenticatorContext context) {
String accessCode = context.generateAccessCode();
URI action = getActionUrl(context, accessCode, LOGIN_FORM_ACTION);
URI action = getActionUrl(context, accessCode);
LoginFormsProvider provider = context.getSession().getProvider(LoginFormsProvider.class)
.setUser(context.getUser())
.setActionUri(action)
@ -48,10 +54,10 @@ public class AbstractFormAuthenticator {
return provider;
}
public static URI getActionUrl(AuthenticatorContext context, String code, String action) {
public URI getActionUrl(AuthenticatorContext context, String code) {
return LoginActionsService.authenticationFormProcessor(context.getUriInfo())
.queryParam(OAuth2Constants.CODE, code)
.queryParam(ACTION, action)
.queryParam(EXECUTION, context.getExecution().getId())
.build(context.getRealm().getName());
}
@ -111,7 +117,7 @@ public class AbstractFormAuthenticator {
return false;
}
context.getEvent().detail(Details.USERNAME, username);
context.getClientSession().setNote(AbstractFormAuthenticator.FORM_USERNAME, username);
context.getClientSession().setNote(AbstractFormAuthenticator.ATTEMPTED_USERNAME, username);
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
if (invalidUser(context, user)) return false;
String rememberMe = inputData.getFirst("rememberMe");
@ -119,6 +125,8 @@ public class AbstractFormAuthenticator {
if (remember) {
context.getClientSession().setNote(Details.REMEMBER_ME, "true");
context.getEvent().detail(Details.REMEMBER_ME, "true");
} else {
context.getClientSession().removeNote(Details.REMEMBER_ME);
}
context.setUser(user);
return true;

View file

@ -32,6 +32,11 @@ public class CookieAuthenticator implements Authenticator {
}
@Override
public void action(AuthenticatorContext context) {
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;

View file

@ -31,14 +31,15 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A
this.model = model;
}
@Override
public void action(AuthenticatorContext context) {
validateOTP(context);
}
@Override
public void authenticate(AuthenticatorContext context) {
if (!isAction(context, TOTP_FORM_ACTION)) {
Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse);
return;
}
validateOTP(context);
}
public void validateOTP(AuthenticatorContext context) {
@ -69,7 +70,7 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A
protected Response challenge(AuthenticatorContext context, String error) {
String accessCode = context.generateAccessCode();
URI action = AbstractFormAuthenticator.getActionUrl(context, accessCode, TOTP_FORM_ACTION);
URI action = getActionUrl(context, accessCode);
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
.setActionUri(action)
.setClientSessionCode(accessCode);
@ -91,6 +92,8 @@ public class OTPFormAuthenticator extends AbstractFormAuthenticator implements A
}
@Override
public void close() {

View file

@ -39,26 +39,17 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au
return false;
}
protected boolean isAlreadyChallenged(AuthenticatorContext context) {
UserSessionModel.AuthenticatorStatus status = context.getClientSession().getAuthenticators().get(context.getExecution().getId());
if (status == null) return false;
return status == UserSessionModel.AuthenticatorStatus.CHALLENGED;
@Override
public void action(AuthenticatorContext context) {
context.attempted();
return;
}
@Override
public void authenticate(AuthenticatorContext context) {
HttpRequest request = context.getHttpRequest();
String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (isAction(context, KERBEROS_DISABLED)) {
context.attempted();
return;
}
// Case when we don't yet have any Negotiate header
if (authHeader == null) {
if (isAlreadyChallenged(context)) {
context.attempted();
return;
}
Response challenge = challengeNegotiation(context, null);
context.forceChallenge(challenge);
return;
@ -131,7 +122,7 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au
*/
protected Response optionalChallengeRedirect(AuthenticatorContext context, String negotiateHeader) {
String accessCode = context.generateAccessCode();
URI action = getActionUrl(context, accessCode, KERBEROS_DISABLED);
URI action = getActionUrl(context, accessCode);
StringBuilder builder = new StringBuilder();
@ -159,18 +150,6 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au
.entity(builder.toString()).build();
}
protected Response formChallenge(AuthenticatorContext context, String negotiateHeader) {
String accessCode = context.generateAccessCode();
URI action = getActionUrl(context, accessCode, KERBEROS_DISABLED);
return context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(accessCode)
.setActionUri(action)
.setStatus(Response.Status.UNAUTHORIZED)
.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader)
.setUser(context.getUser())
.createForm("bypass_kerberos.ftl", new HashMap<String, Object>());
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {

View file

@ -4,24 +4,18 @@ import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.AuthenticationManager;
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>
@ -34,13 +28,34 @@ public class UsernamePasswordForm extends AbstractFormAuthenticator implements A
this.model = model;
}
@Override
public void action(AuthenticatorContext context) {
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.getEvent().error(Errors.REJECTED_BY_USER);
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo());
Response response = protocol.cancelLogin(context.getClientSession());
context.forceChallenge(response);
return;
}
if (!validateUser(context, formData)) {
return;
}
if (!validatePassword(context, formData)) {
return;
}
context.success();
}
@Override
public void authenticate(AuthenticatorContext context) {
if (isAction(context, REGISTRATION_FORM_ACTION) && context.getUser() != null) {
if (REGISTRATION_FORM_ACTION.equals(context.getAction()) && context.getUser() != null) {
context.success();
return;
}
if (!isAction(context, LOGIN_FORM_ACTION)) {
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM);
@ -55,29 +70,8 @@ public class UsernamePasswordForm extends AbstractFormAuthenticator implements A
}
}
Response challengeResponse = challenge(context, formData);
context.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, context.getExecution().getId());
context.challenge(challengeResponse);
return;
}
MultivaluedMap<String, String> formData = context.getHttpRequest().getDecodedFormParameters();
if (formData.containsKey("cancel")) {
context.getEvent().error(Errors.REJECTED_BY_USER);
LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod());
protocol.setRealm(context.getRealm())
.setHttpHeaders(context.getHttpRequest().getHttpHeaders())
.setUriInfo(context.getUriInfo());
Response response = protocol.cancelLogin(context.getClientSession());
context.challenge(response);
return;
}
if (!validateUser(context, formData)) {
return;
}
if (!validatePassword(context, formData)) {
return;
}
context.success();
}
@Override

View file

@ -142,25 +142,6 @@ public class ClientSessionCode {
clientSession.setTimestamp(Time.currentTime());
}
public void setRequiredAction(RequiredAction requiredAction) {
setAction(convertToAction(requiredAction));
}
private String convertToAction(RequiredAction requiredAction) {
switch (requiredAction) {
case CONFIGURE_TOTP:
return ClientSessionModel.Action.CONFIGURE_TOTP.name();
case UPDATE_PASSWORD:
return ClientSessionModel.Action.UPDATE_PASSWORD.name();
case UPDATE_PROFILE:
return ClientSessionModel.Action.UPDATE_PROFILE.name();
case VERIFY_EMAIL:
return ClientSessionModel.Action.VERIFY_EMAIL.name();
default:
throw new IllegalArgumentException("Unknown required action " + requiredAction);
}
}
public String getCode() {
return generateCode(realm, clientSession);
}

View file

@ -23,6 +23,7 @@ package org.keycloak.services.resources;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.InternalServerErrorException;
import org.keycloak.ClientConnection;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorUtil;
@ -65,6 +66,7 @@ import org.keycloak.services.ErrorPage;
import org.keycloak.services.Urls;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.services.validation.Validation;
import org.keycloak.util.Time;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
@ -234,7 +236,7 @@ public class LoginActionsService {
@Path("authenticate")
@GET
public Response authenticate(@QueryParam("code") String code,
@QueryParam("action") String action) {
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
if (!checks.check(code)) {
@ -249,27 +251,57 @@ public class LoginActionsService {
clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name());
}
AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW);
String flowId = flow.getId();
return processAuthentication(execution, clientSession);
}
protected Response processAuthentication(String execution, ClientSessionModel clientSession) {
String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW;
AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flowId)
.setFlowId(flow.getId())
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setAction(action)
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
try {
if (execution != null) {
return processor.authenticationAction(execution);
} else {
return processor.authenticate();
}
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
/**
* URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY!
*
* @param code
* @return
*/
@Path("authenticate")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("execution") String execution) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
if (!checks.check(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
final ClientSessionModel clientSession = clientCode.getClientSession();
return processAuthentication(execution, clientSession);
}
/**
* protocol independent registration page entry point
*
@ -302,46 +334,6 @@ public class LoginActionsService {
.createRegistration();
}
/**
* URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY!
*
* @param code
* @return
*/
@Path("authenticate")
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response authenticateForm(@QueryParam("code") String code,
@QueryParam("action") String action) {
event.event(EventType.LOGIN);
Checks checks = new Checks();
if (!checks.check(code, ClientSessionModel.Action.AUTHENTICATE.name())) {
return checks.response;
}
final ClientSessionCode clientCode = checks.clientCode;
final ClientSessionModel clientSession = clientCode.getClientSession();
String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW;
AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias);
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flow.getId())
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setAction(action)
.setRequest(request);
try {
return processor.authenticate();
} catch (Exception e) {
return processor.handleBrowserException(e);
}
}
/**
* Registration
@ -946,7 +938,7 @@ public class LoginActionsService {
}
@Path("required-actions/{action}")
public Object requiredAction(@QueryParam("code") String code,
public Object requiredAction(@QueryParam("code") final String code,
@PathParam("action") String action) {
event.event(EventType.LOGIN);
if (action == null) {
@ -1024,6 +1016,11 @@ public class LoginActionsService {
@Override
public String generateAccessCode(String action) {
String clientSessionAction = clientSession.getAction();
if (action.equals(clientSessionAction)) {
clientSession.setTimestamp(Time.currentTime());
return code;
}
ClientSessionCode code = new ClientSessionCode(getRealm(), getClientSession());
code.setAction(action);
return code.getCode();