phased auth spi introduction

This commit is contained in:
Bill Burke 2015-06-03 10:55:03 -04:00
parent 106bdc3c52
commit c12fe28b2d
45 changed files with 846 additions and 244 deletions

View file

@ -129,6 +129,17 @@
<column name="REQUIRED_ACTION" value="UPDATE_PASSWORD"/>
<where>ACTION = 3</where>
</update>
<createTable tableName="CLIENT_USER_SESSION_NOTE">
<column name="NAME" type="VARCHAR(255)">
<constraints nullable="false"/>
</column>
<column name="VALUE" type="VARCHAR(255)"/>
<column name="CLIENT_SESSION" type="VARCHAR(36)">
<constraints nullable="false"/>
</column>
</createTable>
<addPrimaryKey columnNames="CLIENT_SESSION, NAME" constraintName="CONSTRAINT_CLIENT_USER_SESSION_NOTE" tableName="CLIENT_USER_SESSION_NOTE"/>
<addForeignKeyConstraint baseColumnNames="CLIENT_SESSION" baseTableName="CLIENT_USER_SESSION_NOTE" constraintName="FK_CLIENT_USER_SESSION_NOTE" referencedColumnNames="ID" referencedTableName="CLIENT_SESSION"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATOR_PK" tableName="AUTHENTICATOR"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_FLOW_PK" tableName="AUTHENTICATION_FLOW"/>
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_EXECUTION_PK" tableName="AUTHENTICATION_EXECUTION"/>

View file

@ -35,6 +35,7 @@
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionAuthStatusEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.ClientUserSessionNoteEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
<class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>

View file

@ -11,6 +11,7 @@ public interface Details {
String CODE_ID = "code_id";
String REDIRECT_URI = "redirect_uri";
String RESPONSE_TYPE = "response_type";
String AUTH_TYPE = "auth_type";
String AUTH_METHOD = "auth_method";
String IDENTITY_PROVIDER = "identity_provider";
String IDENTITY_PROVIDER_USERNAME = "identity_provider_identity";

View file

@ -52,6 +52,21 @@ public interface ClientSessionModel {
public void setNote(String name, String value);
public void removeNote(String name);
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @param name
* @param value
*/
public void setUserSessionNote(String name, String value);
/**
* These are notes you want applied to the UserSessionModel when the client session is attached to it.
*
* @return
*/
public Map<String, String> getUserSessionNotes();
public static enum Action {
OAUTH_GRANT,
CODE_TO_TOKEN,

View file

@ -370,6 +370,25 @@ public class UserFederationManager implements UserProvider {
return session.userStorage().validCredentials(realm, user, input);
}
/**
* Is the user configured to use this credential type
*
* @return
*/
public boolean configuredForCredentialType(String type, RealmModel realm, UserModel user) {
UserFederationProvider link = getFederationLink(realm, user);
if (link != null) {
Set<String> supportedCredentialTypes = link.getSupportedCredentialTypes(user);
if (supportedCredentialTypes.contains(type)) return true;
}
List<UserCredentialValueModel> creds = user.getCredentialsDirectly();
for (UserCredentialValueModel cred : creds) {
if (cred.getType().equals(type)) return true;
}
return false;
}
@Override
public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) {
return validCredentials(realm, user, Arrays.asList(input));

View file

@ -69,14 +69,6 @@ public interface UserModel {
void updateCredentialDirectly(UserCredentialValueModel cred);
/**
* Is the use configured to use this credential type
*
* @param type
* @return
*/
boolean configuredForCredentialType(String type);
Set<RoleModel> getRealmRoleMappings();
Set<RoleModel> getClientRoleMappings(ClientModel app);
boolean hasRole(RoleModel role);

View file

@ -38,10 +38,12 @@ public interface UserSessionModel {
List<ClientSessionModel> getClientSessions();
public static enum AuthenticatorStatus {
FAILED,
SUCCESS,
SETUP_REQUIRED,
ATTEMPTED,
SKIPPED
SKIPPED,
CHALLENGED
}
public String getNote(String name);

View file

@ -10,6 +10,10 @@ import org.keycloak.models.RealmModel;
* @version $Revision: 1 $
*/
public class DefaultAuthenticationFlows {
public static final String BROWSER_FLOW = "browser";
public static final String FORMS_FLOW = "forms";
public static void addFlows(RealmModel realm) {
AuthenticatorModel model = new AuthenticatorModel();
model.setProviderId("auth-cookie");
@ -31,9 +35,13 @@ public class DefaultAuthenticationFlows {
model.setProviderId("auth-otp-form");
model.setAlias("Single OTP Form");
AuthenticatorModel otp = realm.addAuthenticator(model);
model = new AuthenticatorModel();
model.setProviderId("auth-spnego");
model.setAlias("Kerberos");
AuthenticatorModel kerberos = realm.addAuthenticator(model);
AuthenticationFlowModel browser = new AuthenticationFlowModel();
browser.setAlias("browser");
browser.setAlias(BROWSER_FLOW);
browser.setDescription("browser based authentication");
browser = realm.addAuthenticationFlow(browser);
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
@ -44,15 +52,23 @@ public class DefaultAuthenticationFlows {
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED);
execution.setAuthenticator(kerberos.getId());
execution.setPriority(1);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(false);
realm.addAuthenticatorExecution(execution);
AuthenticationFlowModel forms = new AuthenticationFlowModel();
forms.setAlias("forms");
forms.setAlias(FORMS_FLOW);
forms.setDescription("Username, password, otp and other auth forms.");
forms = realm.addAuthenticationFlow(forms);
execution = new AuthenticationExecutionModel();
execution.setParentFlow(browser.getId());
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
execution.setAuthenticator(forms.getId());
execution.setPriority(1);
execution.setPriority(2);
execution.setUserSetupAllowed(false);
execution.setAutheticatorFlow(true);
realm.addAuthenticatorExecution(execution);

View file

@ -87,11 +87,6 @@ public class UserModelDelegate implements UserModel {
delegate.removeRequiredAction(action);
}
@Override
public boolean configuredForCredentialType(String type) {
return delegate.configuredForCredentialType(type);
}
@Override
public void addRequiredAction(RequiredAction action) {
delegate.addRequiredAction(action);

View file

@ -220,17 +220,6 @@ public class UserAdapter implements UserModel, Comparable {
user.setRequiredActions(requiredActions);
}
@Override
public boolean configuredForCredentialType(String type) {
List<UserCredentialValueModel> creds = getCredentialsDirectly();
for (UserCredentialValueModel cred : creds) {
if (cred.getType().equals(type)) return true;
}
return false;
}
@Override
public boolean isTotp() {
return user.isTotp();

View file

@ -131,15 +131,6 @@ public class UserAdapter implements UserModel {
updated.removeRequiredAction(action);
}
@Override
public boolean configuredForCredentialType(String type) {
List<UserCredentialValueModel> creds = getCredentialsDirectly();
for (UserCredentialValueModel cred : creds) {
if (cred.getType().equals(type)) return true;
}
return false;
}
@Override
public String getFirstName() {
if (updated != null) return updated.getFirstName();

View file

@ -185,15 +185,6 @@ public class UserAdapter implements UserModel {
}
}
@Override
public boolean configuredForCredentialType(String type) {
List<UserCredentialValueModel> creds = getCredentialsDirectly();
for (UserCredentialValueModel cred : creds) {
if (cred.getType().equals(type)) return true;
}
return false;
}
@Override
public String getFirstName() {
return user.getFirstName();

View file

@ -333,16 +333,6 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
return result;
}
@Override
public boolean configuredForCredentialType(String type) {
List<UserCredentialValueModel> creds = getCredentialsDirectly();
for (UserCredentialValueModel cred : creds) {
if (cred.getType().equals(type)) return true;
}
return false;
}
@Override
public void updateCredentialDirectly(UserCredentialValueModel credModel) {
CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType());

View file

@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@ -164,6 +165,26 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
}
@Override
public void setUserSessionNote(String name, String value) {
if (entity.getUserSessionNotes() == null) {
entity.setUserSessionNotes(new HashMap<String, String>());
}
entity.getNotes().put(name, value);
update();
}
@Override
public Map<String, String> getUserSessionNotes() {
if (entity.getUserSessionNotes() == null) {
return Collections.EMPTY_MAP;
}
HashMap<String, String> copy = new HashMap<>();
copy.putAll(entity.getUserSessionNotes());
return copy;
}
void update() {
provider.getTx().replace(cache, entity.getId(), entity);
}

View file

@ -29,6 +29,7 @@ public class ClientSessionEntity extends SessionEntity {
private Set<String> roles;
private Set<String> protocolMappers;
private Map<String, String> notes;
private Map<String, String> userSessionNotes;
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
private String authUserId;
@ -127,4 +128,12 @@ public class ClientSessionEntity extends SessionEntity {
public void setAuthUserId(String authUserId) {
this.authUserId = authUserId;
}
public Map<String, String> getUserSessionNotes() {
return userSessionNotes;
}
public void setUserSessionNotes(Map<String, String> userSessionNotes) {
this.userSessionNotes = userSessionNotes;
}
}

View file

@ -10,9 +10,11 @@ import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity;
import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity;
import org.keycloak.models.sessions.jpa.entities.ClientUserSessionNoteEntity;
import org.keycloak.models.sessions.jpa.entities.UserSessionEntity;
import javax.persistence.EntityManager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
@ -78,6 +80,32 @@ public class ClientSessionAdapter implements ClientSessionModel {
return null;
}
@Override
public void setUserSessionNote(String name, String value) {
for (ClientUserSessionNoteEntity attr : entity.getUserSessionNotes()) {
if (attr.getName().equals(name)) {
attr.setValue(value);
return;
}
}
ClientUserSessionNoteEntity attr = new ClientUserSessionNoteEntity();
attr.setName(name);
attr.setValue(value);
attr.setClientSession(entity);
em.persist(attr);
entity.getUserSessionNotes().add(attr);
}
@Override
public Map<String, String> getUserSessionNotes() {
Map<String, String> copy = new HashMap<>();
for (ClientUserSessionNoteEntity attr : entity.getUserSessionNotes()) {
copy.put(attr.getName(), attr.getValue());
}
return copy;
}
@Override
public String getId() {
return entity.getId();

View file

@ -69,6 +69,9 @@ public class ClientSessionEntity {
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
protected Collection<ClientSessionNoteEntity> notes = new ArrayList<ClientSessionNoteEntity>();
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
protected Collection<ClientUserSessionNoteEntity> userSessionNotes = new ArrayList<>();
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
protected Collection<ClientSessionAuthStatusEntity> authanticatorStatus = new ArrayList<>();
@ -175,4 +178,12 @@ public class ClientSessionEntity {
public void setUserId(String userId) {
this.userId = userId;
}
public Collection<ClientUserSessionNoteEntity> getUserSessionNotes() {
return userSessionNotes;
}
public void setUserSessionNotes(Collection<ClientUserSessionNoteEntity> userSessionNotes) {
this.userSessionNotes = userSessionNotes;
}
}

View file

@ -0,0 +1,109 @@
package org.keycloak.models.sessions.jpa.entities;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.NamedQueries;
import javax.persistence.NamedQuery;
import javax.persistence.Table;
import java.io.Serializable;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@NamedQueries({
@NamedQuery(name = "removeClientUserSessionNoteByUser", query="delete from ClientUserSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IN (select s from UserSessionEntity s where s.realmId = :realmId and s.userId = :userId))"),
@NamedQuery(name = "removeClientUserSessionNoteByClient", query="delete from ClientUserSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.clientId = :clientId and c.realmId = :realmId)"),
@NamedQuery(name = "removeClientUserSessionNoteByRealm", query="delete from ClientUserSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.realmId = :realmId)"),
@NamedQuery(name = "removeClientUserSessionNoteByExpired", query = "delete from ClientUserSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IN (select s from UserSessionEntity s where s.realmId = :realmId and (s.started < :maxTime or s.lastSessionRefresh < :idleTime)))"),
@NamedQuery(name = "removeDetachedUserClientSessionNoteByExpired", query = "delete from ClientUserSessionNoteEntity r where r.clientSession IN (select c from ClientSessionEntity c where c.session IS NULL and c.realmId = :realmId and c.timestamp < :maxTime )")
})
@Table(name="CLIENT_USER_SESSION_NOTE")
@Entity
@IdClass(ClientUserSessionNoteEntity.Key.class)
public class ClientUserSessionNoteEntity {
@Id
@ManyToOne(fetch= FetchType.LAZY)
@JoinColumn(name = "CLIENT_SESSION")
protected ClientSessionEntity clientSession;
@Id
@Column(name = "NAME")
protected String name;
@Column(name = "VALUE")
protected String value;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
public ClientSessionEntity getClientSession() {
return clientSession;
}
public void setClientSession(ClientSessionEntity clientSession) {
this.clientSession = clientSession;
}
public static class Key implements Serializable {
protected ClientSessionEntity clientSession;
protected String name;
public Key() {
}
public Key(ClientSessionEntity clientSession, String name) {
this.clientSession = clientSession;
this.name = name;
}
public ClientSessionEntity getClientSession() {
return clientSession;
}
public String getName() {
return name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Key key = (Key) o;
if (name != null ? !name.equals(key.name) : key.name != null) return false;
if (clientSession != null ? !clientSession.getId().equals(key.clientSession != null ? key.clientSession.getId() : null) : key.clientSession != null) return false;
return true;
}
@Override
public int hashCode() {
int result = clientSession != null ? clientSession.getId().hashCode() : 0;
result = 31 * result + (name != null ? name.hashCode() : 0);
return result;
}
}
}

View file

@ -134,6 +134,16 @@ public class ClientSessionAdapter implements ClientSessionModel {
}
@Override
public void setUserSessionNote(String name, String value) {
entity.getUserSessionNotes().put(name, value);
}
@Override
public Map<String, String> getUserSessionNotes() {
return entity.getUserSessionNotes();
}
@Override
public String getAuthMethod() {
return entity.getAuthMethod();

View file

@ -28,6 +28,7 @@ public class ClientSessionEntity {
private Set<String> roles;
private Set<String> protocolMappers;
private Map<String, String> notes = new HashMap<>();
private Map<String, String> userSessionNotes = new HashMap<>();
public String getId() {
return id;
@ -128,4 +129,8 @@ public class ClientSessionEntity {
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
this.authenticatorStatus = authenticatorStatus;
}
public Map<String, String> getUserSessionNotes() {
return userSessionNotes;
}
}

View file

@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel;
import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity;
import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
@ -156,6 +157,19 @@ public class ClientSessionAdapter extends AbstractMongoAdapter<MongoClientSessio
updateMongoEntity();
}
@Override
public void setUserSessionNote(String name, String value) {
entity.getUserSessionNotes().put(name, value);
updateMongoEntity();
}
@Override
public Map<String, String> getUserSessionNotes() {
Map<String, String> copy = new HashMap<>();
copy.putAll(entity.getUserSessionNotes());
return copy;
}
@Override
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
return entity.getAuthenticatorStatus();

View file

@ -30,6 +30,7 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
private List<String> roles;
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 String authUserId;
@ -113,6 +114,14 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
this.notes = notes;
}
public Map<String, String> getUserSessionNotes() {
return userSessionNotes;
}
public void setUserSessionNotes(Map<String, String> userSessionNotes) {
this.userSessionNotes = userSessionNotes;
}
public String getSessionId() {
return sessionId;
}

View file

@ -6,6 +6,7 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.jboss.resteasy.spi.HttpResponse;
import org.keycloak.ClientConnection;
import org.keycloak.VerificationException;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.dom.saml.v2.SAML2Object;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
@ -17,6 +18,7 @@ import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
@ -282,6 +284,10 @@ public class SamlService {
}
}
return newBrowserAuthentication(clientSession);
}
private Response oldBrowserAuthentication(ClientSessionModel clientSession) {
Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
if (response != null) return response;
@ -311,6 +317,29 @@ public class SamlService {
return forms.createLogin();
}
protected Response newBrowserAuthentication(ClientSessionModel clientSession) {
String flowId = null;
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
if (flow.getAlias().equals("browser")) {
flowId = flow.getId();
break;
}
}
AuthenticationProcessor processor = new AuthenticationProcessor();
processor.setClientSession(clientSession)
.setFlowId(flowId)
.setConnection(clientConnection)
.setEventBuilder(event)
.setProtector(authManager.getProtector())
.setRealm(realm)
.setSession(session)
.setUriInfo(uriInfo)
.setRequest(request);
return processor.authenticate();
}
private String getBindingType(AuthnRequestType requestAbstractType) {
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();

View file

@ -3,7 +3,9 @@ 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.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorModel;
@ -19,7 +21,6 @@ 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>
@ -34,7 +35,7 @@ public class AuthenticationProcessor {
protected UriInfo uriInfo;
protected KeycloakSession session;
protected BruteForceProtector protector;
protected EventBuilder eventBuilder;
protected EventBuilder event;
protected HttpRequest request;
protected String flowId;
@ -109,7 +110,7 @@ public class AuthenticationProcessor {
}
public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) {
this.eventBuilder = eventBuilder;
this.event = eventBuilder;
return this;
}
@ -125,23 +126,35 @@ public class AuthenticationProcessor {
private class Result implements AuthenticatorContext {
AuthenticatorModel model;
AuthenticationExecutionModel execution;
Authenticator authenticator;
Status status;
Response challenge;
Error error;
private Result(AuthenticatorModel model, Authenticator authenticator) {
private Result(AuthenticationExecutionModel execution, AuthenticatorModel model, Authenticator authenticator) {
this.execution = execution;
this.model = model;
this.authenticator = authenticator;
}
@Override
public AuthenticatorModel getModel() {
public AuthenticationExecutionModel getExecution() {
return execution;
}
@Override
public void setExecution(AuthenticationExecutionModel execution) {
this.execution = execution;
}
@Override
public AuthenticatorModel getAuthenticatorModel() {
return model;
}
@Override
public void setModel(AuthenticatorModel model) {
public void setAuthenticatorModel(AuthenticatorModel model) {
this.model = model;
}
@ -251,6 +264,11 @@ public class AuthenticationProcessor {
public BruteForceProtector getProtector() {
return AuthenticationProcessor.this.protector;
}
@Override
public EventBuilder getEvent() {
return AuthenticationProcessor.this.event;
}
}
public static class AuthException extends RuntimeException {
@ -305,6 +323,14 @@ public class AuthenticationProcessor {
}
public Response authenticate() 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);
@ -325,6 +351,7 @@ public class AuthenticationProcessor {
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)) {
@ -354,23 +381,31 @@ public class AuthenticationProcessor {
UserModel authUser = clientSession.getAuthenticatedUser();
if (authenticator.requiresUser() && authUser == null){
if (alternativeChallenge != null) return alternativeChallenge;
if (alternativeChallenge != null) {
clientSession.setAuthenticatorStatus(challengedAlternativeExecution.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return alternativeChallenge;
}
throw new AuthException(Error.UNKNOWN_USER);
}
if (authenticator.requiresUser() && authUser != null && !authenticator.configuredFor(authUser)) {
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
if (model.isUserSetupAllowed()) {
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED);
authUser.addRequiredAction(authenticator.getRequiredAction());
} else {
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
boolean configuredFor = false;
if (authenticator.requiresUser() && authUser != null) {
configuredFor = authenticator.configuredFor(session, realm, authUser);
if (!configuredFor) {
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
if (model.isUserSetupAllowed()) {
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);
}
}
}
continue;
}
context = new Result(authenticatorModel, authenticator);
context = new Result(model, authenticatorModel, authenticator);
authenticator.authenticate(context);
Status result = context.getStatus();
if (result == Status.SUCCESS){
@ -379,15 +414,24 @@ public class AuthenticationProcessor {
continue;
} else if (result == Status.FAILED) {
logUserFailure();
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.FAILED);
if (context.challenge != null) return context.challenge;
throw new AuthException(context.error);
} else if (result == Status.CHALLENGE) {
if (model.isRequired()) return context.challenge;
else if (model.isAlternative()) alternativeChallenge = context.challenge;
else clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
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) {
logUserFailure();
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
return context.challenge;
} else if (result == Status.ATTEMPTED) {
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS);
@ -415,17 +459,22 @@ public class AuthenticationProcessor {
}
protected Response authenticationComplete() {
String username = clientSession.getAuthenticatedUser().getUsername();
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 = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null);
userSession.setState(UserSessionModel.State.LOGGING_IN);
}
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, eventBuilder);
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event);
}

View file

@ -1,5 +1,7 @@
package org.keycloak.authentication;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.provider.Provider;
@ -10,7 +12,7 @@ import org.keycloak.provider.Provider;
public interface Authenticator extends Provider {
boolean requiresUser();
void authenticate(AuthenticatorContext context);
boolean configuredFor(UserModel user);
boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);
String getRequiredAction();

View file

@ -2,6 +2,8 @@ package org.keycloak.authentication;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.events.EventBuilder;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.KeycloakSession;
@ -19,9 +21,15 @@ import javax.ws.rs.core.UriInfo;
* @version $Revision: 1 $
*/
public interface AuthenticatorContext {
AuthenticatorModel getModel();
EventBuilder getEvent();
void setModel(AuthenticatorModel model);
AuthenticationExecutionModel getExecution();
void setExecution(AuthenticationExecutionModel execution);
AuthenticatorModel getAuthenticatorModel();
void setAuthenticatorModel(AuthenticatorModel model);
Authenticator getAuthenticator();

View file

@ -0,0 +1,80 @@
package org.keycloak.authentication.authenticators;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
import org.keycloak.services.resources.LoginActionsService;
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 AbstractFormAuthenticator {
protected boolean isActionUrl(AuthenticatorContext context) {
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
String current = context.getUriInfo().getAbsolutePath().getPath();
String expectedPath = expected.getPath();
return expectedPath.equals(current);
}
protected LoginFormsProvider loginForm(AuthenticatorContext context) {
ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession());
code.setAction(ClientSessionModel.Action.AUTHENTICATE);
URI action = getActionUrl(context, code);
return context.getSession().getProvider(LoginFormsProvider.class)
.setActionUri(action)
.setClientSessionCode(code.getCode());
}
public static URI getActionUrl(AuthenticatorContext context, ClientSessionCode code) {
return LoginActionsService.authenticationFormProcessor(context.getUriInfo())
.queryParam(OAuth2Constants.CODE, code.getCode())
.build(context.getRealm().getName());
}
protected Response invalidUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.INVALID_USER).createLogin();
}
protected Response disabledUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.ACCOUNT_DISABLED).createLogin();
}
protected Response temporarilyDisabledUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin();
}
public boolean invalidUser(AuthenticatorContext context, UserModel user) {
if (user == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return true;
}
if (!user.isEnabled()) {
context.getEvent().error(Errors.USER_DISABLED);
Response challengeResponse = disabledUser(context);
context.failureChallenge(AuthenticationProcessor.Error.USER_DISABLED, challengeResponse);
return true;
}
if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) {
context.getEvent().error(Errors.USER_TEMPORARILY_DISABLED);
Response challengeResponse = temporarilyDisabledUser(context);
context.failureChallenge(AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED, challengeResponse);
return true;
}
}
return false;
}
}

View file

@ -1,60 +0,0 @@
package org.keycloak.authentication.authenticators;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticatorModel;
import java.util.ArrayList;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class AuthenticationFlow {
/**
* Hardcoded models just to test this stuff. It is temporary
*/
static List<AuthenticationExecutionModel> hardcoded = new ArrayList<>();
/*
static {
AuthenticationExecutionModel model = new AuthenticationExecutionModel();
model.setId("1");
model.setAlias("cookie");
model.setMasterAuthenticator(true);
model.setProviderId(CookieAuthenticatorFactory.PROVIDER_ID);
model.setPriority(0);
model.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
model.setUserSetupAllowed(false);
hardcoded.add(model);
model = new AuthenticatorModel();
model.setId("2");
model.setAlias("user form");
model.setMasterAuthenticator(false);
model.setProviderId(LoginFormUsernameAuthenticatorFactory.PROVIDER_ID);
model.setPriority(1);
model.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
model.setUserSetupAllowed(false);
hardcoded.add(model);
model = new AuthenticatorModel();
model.setId("3");
model.setAlias("password form");
model.setMasterAuthenticator(false);
model.setProviderId(LoginFormUsernameAuthenticatorFactory.PROVIDER_ID);
model.setPriority(2);
model.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED);
model.setUserSetupAllowed(false);
hardcoded.add(model);
model = new AuthenticatorModel();
model.setId("4");
model.setAlias("otp form");
model.setMasterAuthenticator(false);
model.setProviderId(OTPFormAuthenticatorFactory.PROVIDER_ID);
model.setPriority(3);
model.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL);
model.setUserSetupAllowed(false);
hardcoded.add(model);
}
*/
}

View file

@ -2,6 +2,8 @@ package org.keycloak.authentication.authenticators;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.services.managers.AuthenticationManager;
@ -31,7 +33,7 @@ public class CookieAuthenticator implements Authenticator {
}
@Override
public boolean configuredFor(UserModel user) {
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}

View file

@ -3,6 +3,8 @@ package org.keycloak.authentication.authenticators;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorContext;
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.representations.idm.CredentialRepresentation;
@ -61,8 +63,8 @@ public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator {
}
@Override
public boolean configuredFor(UserModel user) {
return user.configuredForCredentialType(UserCredentialModel.TOTP);
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
}
@Override

View file

@ -2,7 +2,10 @@ package org.keycloak.authentication.authenticators;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.events.Errors;
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.representations.idm.CredentialRepresentation;
@ -42,6 +45,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
if (password == null) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = badPassword(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
return;
@ -49,6 +53,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
credentials.add(UserCredentialModel.password(password));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
if (!valid) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = badPassword(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
return;
@ -62,8 +67,8 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
}
@Override
public boolean configuredFor(UserModel user) {
return user.configuredForCredentialType(UserCredentialModel.PASSWORD);
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.users().configuredForCredentialType(UserCredentialModel.PASSWORD, realm, user);
}
@Override

View file

@ -1,30 +1,29 @@
package org.keycloak.authentication.authenticators;
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
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;
import org.keycloak.login.LoginFormsProvider;
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.utils.KeycloakModelUtils;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.managers.ClientSessionCode;
import org.keycloak.services.messages.Messages;
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 {
public class LoginFormUsernameAuthenticator extends AbstractFormAuthenticator implements Authenticator {
protected AuthenticatorModel model;
public LoginFormUsernameAuthenticator(AuthenticatorModel model) {
@ -47,15 +46,19 @@ public class LoginFormUsernameAuthenticator implements Authenticator {
context.challenge(challengeResponse);
return;
}
validateUser(context);
}
protected boolean isActionUrl(AuthenticatorContext context) {
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
String current = context.getUriInfo().getAbsolutePath().getPath();
String expectedPath = expected.getPath();
return expectedPath.equals(current);
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;
}
validateUser(context, formData);
}
@Override
@ -71,71 +74,24 @@ public class LoginFormUsernameAuthenticator implements Authenticator {
return forms.createLogin();
}
protected LoginFormsProvider loginForm(AuthenticatorContext context) {
ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession());
code.setAction(ClientSessionModel.Action.AUTHENTICATE);
URI action = LoginActionsService.authenticationFormProcessor(context.getUriInfo())
.queryParam(OAuth2Constants.CODE, code.getCode())
.build(context.getRealm().getName());
return context.getSession().getProvider(LoginFormsProvider.class)
.setActionUri(action)
.setClientSessionCode(code.getCode());
}
protected Response invalidUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.INVALID_USER).createLogin();
}
protected Response disabledUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.ACCOUNT_DISABLED).createLogin();
}
protected Response temporarilyDisabledUser(AuthenticatorContext context) {
return loginForm(context).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin();
}
public void validateUser(AuthenticatorContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getFormParameters();
public void validateUser(AuthenticatorContext context, MultivaluedMap<String, String> inputData) {
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
if (username == null) {
context.getEvent().error(Errors.USER_NOT_FOUND);
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return;
}
context.getEvent().detail(Details.USERNAME, username);
context.getClientSession().setNote("FORM_USERNAME", username);
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
if (invalidUser(context, user)) return;
context.setUser(user);
context.success();
}
public boolean invalidUser(AuthenticatorContext context, UserModel user) {
if (user == null) {
Response challengeResponse = invalidUser(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
return true;
}
if (!user.isEnabled()) {
Response challengeResponse = disabledUser(context);
context.failureChallenge(AuthenticationProcessor.Error.USER_DISABLED, challengeResponse);
return true;
}
if (context.getRealm().isBruteForceProtected()) {
if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) {
Response challengeResponse = temporarilyDisabledUser(context);
context.failureChallenge(AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED, challengeResponse);
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) {
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}

View file

@ -1,16 +1,16 @@
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.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.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;
@ -24,7 +24,7 @@ import java.util.List;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class OTPFormAuthenticator implements Authenticator {
public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator {
protected AuthenticatorModel model;
public OTPFormAuthenticator(AuthenticatorModel model) {
@ -33,8 +33,7 @@ public class OTPFormAuthenticator implements Authenticator {
@Override
public void authenticate(AuthenticatorContext context) {
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
if (!expected.getPath().equals(context.getUriInfo().getPath())) {
if (!isActionUrl(context)) {
Response challengeResponse = challenge(context);
context.challenge(challengeResponse);
return;
@ -48,12 +47,13 @@ public class OTPFormAuthenticator implements Authenticator {
String password = inputData.getFirst(CredentialRepresentation.TOTP);
if (password == null) {
Response challengeResponse = challenge(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
context.challenge(challengeResponse);
return;
}
credentials.add(UserCredentialModel.totp(password));
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
if (!valid) {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
Response challengeResponse = challenge(context);
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
return;
@ -67,23 +67,20 @@ public class OTPFormAuthenticator implements Authenticator {
return true;
}
protected Response challenge(AuthenticatorContext context, MultivaluedMap<String, String> formData) {
protected Response challenge(AuthenticatorContext context) {
ClientSessionCode clientSessionCode = new ClientSessionCode(context.getRealm(), context.getClientSession());
URI action = AbstractFormAuthenticator.getActionUrl(context, clientSessionCode);
LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class)
.setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode());
.setActionUri(action)
.setClientSessionCode(clientSessionCode.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);
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
}
@Override

View file

@ -0,0 +1,123 @@
package org.keycloak.authentication.authenticators;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorContext;
import org.keycloak.constants.KerberosConstants;
import org.keycloak.events.Errors;
import org.keycloak.login.LoginFormsProvider;
import org.keycloak.models.CredentialValidationOutput;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import java.util.Map;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Authenticator{
protected static Logger logger = Logger.getLogger(SpnegoAuthenticator.class);
@Override
public boolean requiresUser() {
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 authenticate(AuthenticatorContext context) {
HttpRequest request = context.getHttpRequest();
String authHeader = request.getHttpHeaders().getRequestHeaders().getFirst(HttpHeaders.AUTHORIZATION);
// 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.challenge(challenge);
return;
}
String[] tokens = authHeader.split(" ");
if (tokens.length == 0) { // assume not supported
logger.debug("Invalid length of tokens: " + tokens.length);
context.attempted();
return;
}
if (!KerberosConstants.NEGOTIATE.equalsIgnoreCase(tokens[0])) {
logger.debug("Unknown scheme " + tokens[0]);
context.attempted();
return;
}
if (tokens.length != 2) {
context.failure(AuthenticationProcessor.Error.INVALID_CREDENTIALS);
return;
}
String spnegoToken = tokens[1];
UserCredentialModel spnegoCredential = UserCredentialModel.kerberos(spnegoToken);
CredentialValidationOutput output = context.getSession().users().validCredentials(context.getRealm(), spnegoCredential);
if (output.getAuthStatus() == CredentialValidationOutput.Status.AUTHENTICATED) {
context.setUser(output.getAuthenticatedUser());
if (output.getState() != null && !output.getState().isEmpty()) {
for (Map.Entry<String, String> entry : output.getState().entrySet()) {
context.getClientSession().setUserSessionNote(entry.getKey(), entry.getValue());
}
}
context.success();
} else if (output.getAuthStatus() == CredentialValidationOutput.Status.CONTINUE) {
String spnegoResponseToken = (String) output.getState().get(KerberosConstants.RESPONSE_TOKEN);
Response challenge = challengeNegotiation(context, spnegoResponseToken);
context.challenge(challenge);
} else {
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
context.failure(AuthenticationProcessor.Error.INVALID_CREDENTIALS);
}
}
private Response challengeNegotiation(AuthenticatorContext context, final String negotiateToken) {
String negotiateHeader = negotiateToken == null ? KerberosConstants.NEGOTIATE : KerberosConstants.NEGOTIATE + " " + negotiateToken;
if (logger.isTraceEnabled()) {
logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader);
}
LoginFormsProvider loginForm = loginForm(context);
loginForm.setStatus(Response.Status.UNAUTHORIZED);
loginForm.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader);
return loginForm.createLogin();
}
@Override
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
return true;
}
@Override
public String getRequiredAction() {
return null;
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,70 @@
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;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class SpnegoAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-spnego";
@Override
public Authenticator create(AuthenticatorModel model) {
return new SpnegoAuthenticator();
}
@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 PROVIDER_ID;
}
@Override
public String getDisplayCategory() {
return "Complete Authenticator";
}
@Override
public String getDisplayType() {
return "SPNEGO";
}
@Override
public String getHelpText() {
return "Initiates the SPNEGO protocol. Most often used with Kerberos.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
return null;
}
}

View file

@ -217,6 +217,12 @@ public class TokenManager {
}
}
clientSession.setProtocolMappers(requestedProtocolMappers);
Map<String, String> transferredNotes = clientSession.getUserSessionNotes();
for (Map.Entry<String, String> entry : transferredNotes.entrySet()) {
session.setNote(entry.getKey(), entry.getValue());
}
}
public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) {

View file

@ -6,7 +6,6 @@ import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.ClientConnection;
import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationProcessor;
import org.keycloak.authentication.authenticators.AuthenticationFlow;
import org.keycloak.constants.AdapterConstants;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
@ -44,6 +43,7 @@ import java.util.List;
public class AuthorizationEndpoint {
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
public static final String CODE_AUTH_TYPE = "code";
private enum Action {
REGISTER, CODE
@ -247,10 +247,18 @@ public class AuthorizationEndpoint {
return buildRedirectToIdentityProvider(idpHint, accessCode);
}
return oldBrowserAuthentication(accessCode);
return newBrowserAuthentication(accessCode);
}
protected Response newBrowserAuthentication(String accessCode) {
List<IdentityProviderModel> identityProviders = realm.getIdentityProviders();
for (IdentityProviderModel identityProvider : identityProviders) {
if (identityProvider.isAuthenticateByDefault()) {
return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode);
}
}
clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE);
String flowId = null;
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
if (flow.getAlias().equals("browser")) {

View file

@ -343,6 +343,7 @@ public class LoginActionsService {
} catch (AuthenticationProcessor.AuthException e) {
return handleError(e, code);
} catch (Exception e) {
event.error(Errors.INVALID_USER_CREDENTIALS);
logger.error("failed authentication", e);
return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);

View file

@ -2,4 +2,5 @@ org.keycloak.authentication.authenticators.CookieAuthenticatorFactory
org.keycloak.authentication.authenticators.LoginFormOTPAuthenticatorFactory
org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory
org.keycloak.authentication.authenticators.LoginFormUsernameAuthenticatorFactory
org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory
org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory
org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory

View file

@ -21,6 +21,9 @@ import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.rule.KeycloakRule;
@ -119,9 +122,9 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
public ExpectedEvent expectLogin() {
return expect(EventType.LOGIN)
.detail(Details.CODE_ID, isCodeId())
.detail(Details.USERNAME, DEFAULT_USERNAME)
.detail(Details.RESPONSE_TYPE, "code")
.detail(Details.AUTH_METHOD, "form")
//.detail(Details.USERNAME, DEFAULT_USERNAME)
//.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL)
//.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE)
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
.session(isUUID());
}
@ -341,12 +344,13 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
}
/*
for (String k : actual.getDetails().keySet()) {
if (!details.containsKey(k)) {
Assert.fail(k + " was not expected");
}
}
*/
}
return actual;

View file

@ -241,7 +241,7 @@ public class AccountTest {
Assert.assertEquals("Invalid username or password.", loginPage.getError());
events.expectLogin().session((String) null).error("invalid_user_credentials").assertEvent();
events.expectLogin().session((String) null).user((String)null).error("invalid_user_credentials").assertEvent();
loginPage.open();
loginPage.login("test-user@localhost", "new-password");

View file

@ -45,6 +45,7 @@ import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.testsuite.utils.CredentialHelper;
import org.openqa.selenium.WebDriver;
/**
@ -57,6 +58,7 @@ public class RequiredActionTotpSetupTest {
@Override
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
CredentialHelper.setRequiredCredential(CredentialRepresentation.TOTP, appRealm);
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
appRealm.setResetPasswordAllowed(true);
}
@ -137,6 +139,7 @@ public class RequiredActionTotpSetupTest {
loginPage.open();
loginPage.login("test-user@localhost", "password");
String src = driver.getPageSource();
loginTotpPage.login(totp.generate(totpSecret));
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
@ -181,7 +184,7 @@ public class RequiredActionTotpSetupTest {
// Login with one-time password
loginTotpPage.login(totp.generate(totpCode));
loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent();
loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent();
// Open account page
accountTotpPage.open();
@ -204,11 +207,11 @@ public class RequiredActionTotpSetupTest {
totpPage.assertCurrent();
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId();
String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent().getSessionId();
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent();
events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setuptotp2").assertEvent();
}
}

View file

@ -108,7 +108,7 @@ public abstract class AbstractKerberosTest {
.client("kerberos-app")
.user(keycloakRule.getUser("test", "hnelson").getId())
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
.detail(Details.AUTH_METHOD, "spnego")
//.detail(Details.AUTH_METHOD, "spnego")
.detail(Details.USERNAME, "hnelson")
.assertEvent();
@ -164,7 +164,7 @@ public abstract class AbstractKerberosTest {
.client("kerberos-app")
.user(keycloakRule.getUser("test", "jduke").getId())
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
.detail(Details.AUTH_METHOD, "spnego")
//.detail(Details.AUTH_METHOD, "spnego")
.detail(Details.USERNAME, "jduke")
.assertEvent();
spnegoResponse.close();

View file

@ -18,11 +18,14 @@ import org.keycloak.constants.KerberosConstants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserFederationProviderModel;
import org.keycloak.representations.idm.CredentialRepresentation;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.rule.KerberosRule;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.testsuite.utils.CredentialHelper;
import org.picketlink.idm.credential.util.CredentialUtils;
/**
* Test of KerberosFederationProvider (Kerberos not backed by LDAP)
@ -41,6 +44,8 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
CredentialHelper.setRequiredCredential(CredentialRepresentation.KERBEROS, appRealm);
URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json");
keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user");
@ -131,4 +136,6 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
keycloakRule.stopSession(session, true);
}
}
}

View file

@ -0,0 +1,80 @@
package org.keycloak.testsuite.utils;
import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory;
import org.keycloak.authentication.authenticators.OTPFormAuthenticator;
import org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory;
import org.keycloak.authentication.authenticators.SpnegoAuthenticator;
import org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.AuthenticatorModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.utils.DefaultAuthenticationFlows;
import org.keycloak.representations.idm.CredentialRepresentation;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class CredentialHelper {
public static void setRequiredCredential(String type, RealmModel realm) {
if (type.equals(CredentialRepresentation.TOTP)) {
String providerId = OTPFormAuthenticatorFactory.PROVIDER_ID;
String flowAlias = DefaultAuthenticationFlows.FORMS_FLOW;
requireAuthentication(realm, providerId, flowAlias);
} else if (type.equals(CredentialRepresentation.KERBEROS)) {
String providerId = SpnegoAuthenticatorFactory.PROVIDER_ID;
String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW;
alternativeAuthentication(realm, providerId, flowAlias);
} else if (type.equals(CredentialRepresentation.PASSWORD)) {
String providerId = LoginFormPasswordAuthenticatorFactory.PROVIDER_ID;
String flowAlias = DefaultAuthenticationFlows.FORMS_FLOW;
requireAuthentication(realm, providerId, flowAlias);
}
}
public static void requireAuthentication(RealmModel realm, String authenticatorProviderId, String flowAlias) {
AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.REQUIRED;
authenticationRequirement(realm, authenticatorProviderId, flowAlias, requirement);
}
public static void alternativeAuthentication(RealmModel realm, String authenticatorProviderId, String flowAlias) {
AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.ALTERNATIVE;
authenticationRequirement(realm, authenticatorProviderId, flowAlias, requirement);
}
public static void authenticationRequirement(RealmModel realm, String authenticatorProviderId, String flowAlias, AuthenticationExecutionModel.Requirement requirement) {
AuthenticatorModel authenticator = findAuthenticatorByProviderId(realm, authenticatorProviderId);
AuthenticationFlowModel flow = findAuthenticatorFlowByAlias(realm, flowAlias);
AuthenticationExecutionModel execution = findExecutionByAuthenticator(realm, flow.getId(), authenticator.getId());
execution.setRequirement(requirement);
realm.updateAuthenticatorExecution(execution);
}
public static AuthenticatorModel findAuthenticatorByProviderId(RealmModel realm, String providerId) {
for (AuthenticatorModel model : realm.getAuthenticators()) {
if (model.getProviderId().equals(providerId)) {
return model;
}
}
return null;
}
public static AuthenticationFlowModel findAuthenticatorFlowByAlias(RealmModel realm, String alias) {
for (AuthenticationFlowModel model : realm.getAuthenticationFlows()) {
if (model.getAlias().equals(alias)) {
return model;
}
}
return null;
}
public static AuthenticationExecutionModel findExecutionByAuthenticator(RealmModel realm, String flowId, String authId) {
for (AuthenticationExecutionModel model : realm.getAuthenticationExecutions(flowId)) {
if (model.getAuthenticator().equals(authId)) {
return model;
}
}
return null;
}
}