phased auth spi introduction
This commit is contained in:
parent
106bdc3c52
commit
c12fe28b2d
45 changed files with 846 additions and 244 deletions
|
@ -129,6 +129,17 @@
|
||||||
<column name="REQUIRED_ACTION" value="UPDATE_PASSWORD"/>
|
<column name="REQUIRED_ACTION" value="UPDATE_PASSWORD"/>
|
||||||
<where>ACTION = 3</where>
|
<where>ACTION = 3</where>
|
||||||
</update>
|
</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_AUTHENTICATOR_PK" tableName="AUTHENTICATOR"/>
|
||||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_FLOW_PK" tableName="AUTHENTICATION_FLOW"/>
|
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_FLOW_PK" tableName="AUTHENTICATION_FLOW"/>
|
||||||
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_EXECUTION_PK" tableName="AUTHENTICATION_EXECUTION"/>
|
<addPrimaryKey columnNames="ID" constraintName="CONSTRAINT_AUTHENTICATION_EXECUTION_PK" tableName="AUTHENTICATION_EXECUTION"/>
|
||||||
|
|
|
@ -35,6 +35,7 @@
|
||||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionAuthStatusEntity</class>
|
<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.ClientSessionProtocolMapperEntity</class>
|
||||||
<class>org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity</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.UserSessionNoteEntity</class>
|
||||||
<class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
|
<class>org.keycloak.models.sessions.jpa.entities.UserSessionEntity</class>
|
||||||
<class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>
|
<class>org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity</class>
|
||||||
|
|
|
@ -11,6 +11,7 @@ public interface Details {
|
||||||
String CODE_ID = "code_id";
|
String CODE_ID = "code_id";
|
||||||
String REDIRECT_URI = "redirect_uri";
|
String REDIRECT_URI = "redirect_uri";
|
||||||
String RESPONSE_TYPE = "response_type";
|
String RESPONSE_TYPE = "response_type";
|
||||||
|
String AUTH_TYPE = "auth_type";
|
||||||
String AUTH_METHOD = "auth_method";
|
String AUTH_METHOD = "auth_method";
|
||||||
String IDENTITY_PROVIDER = "identity_provider";
|
String IDENTITY_PROVIDER = "identity_provider";
|
||||||
String IDENTITY_PROVIDER_USERNAME = "identity_provider_identity";
|
String IDENTITY_PROVIDER_USERNAME = "identity_provider_identity";
|
||||||
|
|
|
@ -52,6 +52,21 @@ public interface ClientSessionModel {
|
||||||
public void setNote(String name, String value);
|
public void setNote(String name, String value);
|
||||||
public void removeNote(String name);
|
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 {
|
public static enum Action {
|
||||||
OAUTH_GRANT,
|
OAUTH_GRANT,
|
||||||
CODE_TO_TOKEN,
|
CODE_TO_TOKEN,
|
||||||
|
|
|
@ -370,6 +370,25 @@ public class UserFederationManager implements UserProvider {
|
||||||
return session.userStorage().validCredentials(realm, user, input);
|
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
|
@Override
|
||||||
public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) {
|
public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) {
|
||||||
return validCredentials(realm, user, Arrays.asList(input));
|
return validCredentials(realm, user, Arrays.asList(input));
|
||||||
|
|
|
@ -69,14 +69,6 @@ public interface UserModel {
|
||||||
|
|
||||||
void updateCredentialDirectly(UserCredentialValueModel cred);
|
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> getRealmRoleMappings();
|
||||||
Set<RoleModel> getClientRoleMappings(ClientModel app);
|
Set<RoleModel> getClientRoleMappings(ClientModel app);
|
||||||
boolean hasRole(RoleModel role);
|
boolean hasRole(RoleModel role);
|
||||||
|
|
|
@ -38,10 +38,12 @@ public interface UserSessionModel {
|
||||||
List<ClientSessionModel> getClientSessions();
|
List<ClientSessionModel> getClientSessions();
|
||||||
|
|
||||||
public static enum AuthenticatorStatus {
|
public static enum AuthenticatorStatus {
|
||||||
|
FAILED,
|
||||||
SUCCESS,
|
SUCCESS,
|
||||||
SETUP_REQUIRED,
|
SETUP_REQUIRED,
|
||||||
ATTEMPTED,
|
ATTEMPTED,
|
||||||
SKIPPED
|
SKIPPED,
|
||||||
|
CHALLENGED
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNote(String name);
|
public String getNote(String name);
|
||||||
|
|
|
@ -10,6 +10,10 @@ import org.keycloak.models.RealmModel;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class DefaultAuthenticationFlows {
|
public class DefaultAuthenticationFlows {
|
||||||
|
|
||||||
|
public static final String BROWSER_FLOW = "browser";
|
||||||
|
public static final String FORMS_FLOW = "forms";
|
||||||
|
|
||||||
public static void addFlows(RealmModel realm) {
|
public static void addFlows(RealmModel realm) {
|
||||||
AuthenticatorModel model = new AuthenticatorModel();
|
AuthenticatorModel model = new AuthenticatorModel();
|
||||||
model.setProviderId("auth-cookie");
|
model.setProviderId("auth-cookie");
|
||||||
|
@ -31,9 +35,13 @@ public class DefaultAuthenticationFlows {
|
||||||
model.setProviderId("auth-otp-form");
|
model.setProviderId("auth-otp-form");
|
||||||
model.setAlias("Single OTP Form");
|
model.setAlias("Single OTP Form");
|
||||||
AuthenticatorModel otp = realm.addAuthenticator(model);
|
AuthenticatorModel otp = realm.addAuthenticator(model);
|
||||||
|
model = new AuthenticatorModel();
|
||||||
|
model.setProviderId("auth-spnego");
|
||||||
|
model.setAlias("Kerberos");
|
||||||
|
AuthenticatorModel kerberos = realm.addAuthenticator(model);
|
||||||
|
|
||||||
AuthenticationFlowModel browser = new AuthenticationFlowModel();
|
AuthenticationFlowModel browser = new AuthenticationFlowModel();
|
||||||
browser.setAlias("browser");
|
browser.setAlias(BROWSER_FLOW);
|
||||||
browser.setDescription("browser based authentication");
|
browser.setDescription("browser based authentication");
|
||||||
browser = realm.addAuthenticationFlow(browser);
|
browser = realm.addAuthenticationFlow(browser);
|
||||||
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
AuthenticationExecutionModel execution = new AuthenticationExecutionModel();
|
||||||
|
@ -44,15 +52,23 @@ public class DefaultAuthenticationFlows {
|
||||||
execution.setUserSetupAllowed(false);
|
execution.setUserSetupAllowed(false);
|
||||||
execution.setAutheticatorFlow(false);
|
execution.setAutheticatorFlow(false);
|
||||||
realm.addAuthenticatorExecution(execution);
|
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();
|
AuthenticationFlowModel forms = new AuthenticationFlowModel();
|
||||||
forms.setAlias("forms");
|
forms.setAlias(FORMS_FLOW);
|
||||||
forms.setDescription("Username, password, otp and other auth forms.");
|
forms.setDescription("Username, password, otp and other auth forms.");
|
||||||
forms = realm.addAuthenticationFlow(forms);
|
forms = realm.addAuthenticationFlow(forms);
|
||||||
execution = new AuthenticationExecutionModel();
|
execution = new AuthenticationExecutionModel();
|
||||||
execution.setParentFlow(browser.getId());
|
execution.setParentFlow(browser.getId());
|
||||||
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE);
|
||||||
execution.setAuthenticator(forms.getId());
|
execution.setAuthenticator(forms.getId());
|
||||||
execution.setPriority(1);
|
execution.setPriority(2);
|
||||||
execution.setUserSetupAllowed(false);
|
execution.setUserSetupAllowed(false);
|
||||||
execution.setAutheticatorFlow(true);
|
execution.setAutheticatorFlow(true);
|
||||||
realm.addAuthenticatorExecution(execution);
|
realm.addAuthenticatorExecution(execution);
|
||||||
|
|
|
@ -87,11 +87,6 @@ public class UserModelDelegate implements UserModel {
|
||||||
delegate.removeRequiredAction(action);
|
delegate.removeRequiredAction(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean configuredForCredentialType(String type) {
|
|
||||||
return delegate.configuredForCredentialType(type);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void addRequiredAction(RequiredAction action) {
|
public void addRequiredAction(RequiredAction action) {
|
||||||
delegate.addRequiredAction(action);
|
delegate.addRequiredAction(action);
|
||||||
|
|
|
@ -220,17 +220,6 @@ public class UserAdapter implements UserModel, Comparable {
|
||||||
user.setRequiredActions(requiredActions);
|
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
|
@Override
|
||||||
public boolean isTotp() {
|
public boolean isTotp() {
|
||||||
return user.isTotp();
|
return user.isTotp();
|
||||||
|
|
|
@ -131,15 +131,6 @@ public class UserAdapter implements UserModel {
|
||||||
updated.removeRequiredAction(action);
|
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
|
@Override
|
||||||
public String getFirstName() {
|
public String getFirstName() {
|
||||||
if (updated != null) return updated.getFirstName();
|
if (updated != null) return updated.getFirstName();
|
||||||
|
|
|
@ -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
|
@Override
|
||||||
public String getFirstName() {
|
public String getFirstName() {
|
||||||
return user.getFirstName();
|
return user.getFirstName();
|
||||||
|
|
|
@ -333,16 +333,6 @@ public class UserAdapter extends AbstractMongoAdapter<MongoUserEntity> implement
|
||||||
return result;
|
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
|
@Override
|
||||||
public void updateCredentialDirectly(UserCredentialValueModel credModel) {
|
public void updateCredentialDirectly(UserCredentialValueModel credModel) {
|
||||||
CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType());
|
CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType());
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity;
|
||||||
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
import org.keycloak.models.sessions.infinispan.entities.SessionEntity;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
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() {
|
void update() {
|
||||||
provider.getTx().replace(cache, entity.getId(), entity);
|
provider.getTx().replace(cache, entity.getId(), entity);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ public class ClientSessionEntity extends SessionEntity {
|
||||||
private Set<String> roles;
|
private Set<String> roles;
|
||||||
private Set<String> protocolMappers;
|
private Set<String> protocolMappers;
|
||||||
private Map<String, String> notes;
|
private Map<String, String> notes;
|
||||||
|
private Map<String, String> userSessionNotes;
|
||||||
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
|
private Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
|
@ -127,4 +128,12 @@ public class ClientSessionEntity extends SessionEntity {
|
||||||
public void setAuthUserId(String authUserId) {
|
public void setAuthUserId(String authUserId) {
|
||||||
this.authUserId = authUserId;
|
this.authUserId = authUserId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getUserSessionNotes() {
|
||||||
|
return userSessionNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserSessionNotes(Map<String, String> userSessionNotes) {
|
||||||
|
this.userSessionNotes = userSessionNotes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.ClientSessionNoteEntity;
|
||||||
import org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity;
|
import org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity;
|
||||||
import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity;
|
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 org.keycloak.models.sessions.jpa.entities.UserSessionEntity;
|
||||||
|
|
||||||
import javax.persistence.EntityManager;
|
import javax.persistence.EntityManager;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Iterator;
|
import java.util.Iterator;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
@ -78,6 +80,32 @@ public class ClientSessionAdapter implements ClientSessionModel {
|
||||||
return null;
|
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
|
@Override
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return entity.getId();
|
return entity.getId();
|
||||||
|
|
|
@ -69,6 +69,9 @@ public class ClientSessionEntity {
|
||||||
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
|
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
|
||||||
protected Collection<ClientSessionNoteEntity> notes = new ArrayList<ClientSessionNoteEntity>();
|
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")
|
@OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession")
|
||||||
protected Collection<ClientSessionAuthStatusEntity> authanticatorStatus = new ArrayList<>();
|
protected Collection<ClientSessionAuthStatusEntity> authanticatorStatus = new ArrayList<>();
|
||||||
|
|
||||||
|
@ -175,4 +178,12 @@ public class ClientSessionEntity {
|
||||||
public void setUserId(String userId) {
|
public void setUserId(String userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Collection<ClientUserSessionNoteEntity> getUserSessionNotes() {
|
||||||
|
return userSessionNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserSessionNotes(Collection<ClientUserSessionNoteEntity> userSessionNotes) {
|
||||||
|
this.userSessionNotes = userSessionNotes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
@Override
|
||||||
public String getAuthMethod() {
|
public String getAuthMethod() {
|
||||||
return entity.getAuthMethod();
|
return entity.getAuthMethod();
|
||||||
|
|
|
@ -28,6 +28,7 @@ public class ClientSessionEntity {
|
||||||
private Set<String> roles;
|
private Set<String> roles;
|
||||||
private Set<String> protocolMappers;
|
private Set<String> protocolMappers;
|
||||||
private Map<String, String> notes = new HashMap<>();
|
private Map<String, String> notes = new HashMap<>();
|
||||||
|
private Map<String, String> userSessionNotes = new HashMap<>();
|
||||||
|
|
||||||
public String getId() {
|
public String getId() {
|
||||||
return id;
|
return id;
|
||||||
|
@ -128,4 +129,8 @@ public class ClientSessionEntity {
|
||||||
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
|
public void setAuthenticatorStatus(Map<String, UserSessionModel.AuthenticatorStatus> authenticatorStatus) {
|
||||||
this.authenticatorStatus = authenticatorStatus;
|
this.authenticatorStatus = authenticatorStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getUserSessionNotes() {
|
||||||
|
return userSessionNotes;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity;
|
import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity;
|
||||||
import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity;
|
import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -156,6 +157,19 @@ public class ClientSessionAdapter extends AbstractMongoAdapter<MongoClientSessio
|
||||||
updateMongoEntity();
|
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
|
@Override
|
||||||
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
|
public Map<String, UserSessionModel.AuthenticatorStatus> getAuthenticators() {
|
||||||
return entity.getAuthenticatorStatus();
|
return entity.getAuthenticatorStatus();
|
||||||
|
|
|
@ -30,6 +30,7 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
|
||||||
private List<String> roles;
|
private List<String> roles;
|
||||||
private List<String> protocolMappers;
|
private List<String> protocolMappers;
|
||||||
private Map<String, String> notes = new HashMap<String, String>();
|
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, UserSessionModel.AuthenticatorStatus> authenticatorStatus = new HashMap<>();
|
||||||
private String authUserId;
|
private String authUserId;
|
||||||
|
|
||||||
|
@ -113,6 +114,14 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme
|
||||||
this.notes = notes;
|
this.notes = notes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Map<String, String> getUserSessionNotes() {
|
||||||
|
return userSessionNotes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserSessionNotes(Map<String, String> userSessionNotes) {
|
||||||
|
this.userSessionNotes = userSessionNotes;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSessionId() {
|
public String getSessionId() {
|
||||||
return sessionId;
|
return sessionId;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.jboss.resteasy.spi.HttpResponse;
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
import org.keycloak.VerificationException;
|
import org.keycloak.VerificationException;
|
||||||
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.dom.saml.v2.SAML2Object;
|
import org.keycloak.dom.saml.v2.SAML2Object;
|
||||||
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
|
||||||
import org.keycloak.dom.saml.v2.protocol.LogoutRequestType;
|
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.EventBuilder;
|
||||||
import org.keycloak.events.EventType;
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.login.LoginFormsProvider;
|
import org.keycloak.login.LoginFormsProvider;
|
||||||
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.ClientModel;
|
import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
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);
|
Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event);
|
||||||
if (response != null) return response;
|
if (response != null) return response;
|
||||||
|
|
||||||
|
@ -311,6 +317,29 @@ public class SamlService {
|
||||||
return forms.createLogin();
|
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) {
|
private String getBindingType(AuthnRequestType requestAbstractType) {
|
||||||
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
|
URI requestedProtocolBinding = requestAbstractType.getProtocolBinding();
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,9 @@ package org.keycloak.authentication;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.EventBuilder;
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
import org.keycloak.models.AuthenticationExecutionModel;
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.AuthenticationFlowModel;
|
import org.keycloak.models.AuthenticationFlowModel;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
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.Response;
|
||||||
import javax.ws.rs.core.UriInfo;
|
import javax.ws.rs.core.UriInfo;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
|
@ -34,7 +35,7 @@ public class AuthenticationProcessor {
|
||||||
protected UriInfo uriInfo;
|
protected UriInfo uriInfo;
|
||||||
protected KeycloakSession session;
|
protected KeycloakSession session;
|
||||||
protected BruteForceProtector protector;
|
protected BruteForceProtector protector;
|
||||||
protected EventBuilder eventBuilder;
|
protected EventBuilder event;
|
||||||
protected HttpRequest request;
|
protected HttpRequest request;
|
||||||
protected String flowId;
|
protected String flowId;
|
||||||
|
|
||||||
|
@ -109,7 +110,7 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) {
|
public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) {
|
||||||
this.eventBuilder = eventBuilder;
|
this.event = eventBuilder;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,23 +126,35 @@ public class AuthenticationProcessor {
|
||||||
|
|
||||||
private class Result implements AuthenticatorContext {
|
private class Result implements AuthenticatorContext {
|
||||||
AuthenticatorModel model;
|
AuthenticatorModel model;
|
||||||
|
AuthenticationExecutionModel execution;
|
||||||
Authenticator authenticator;
|
Authenticator authenticator;
|
||||||
Status status;
|
Status status;
|
||||||
Response challenge;
|
Response challenge;
|
||||||
Error error;
|
Error error;
|
||||||
|
|
||||||
private Result(AuthenticatorModel model, Authenticator authenticator) {
|
private Result(AuthenticationExecutionModel execution, AuthenticatorModel model, Authenticator authenticator) {
|
||||||
|
this.execution = execution;
|
||||||
this.model = model;
|
this.model = model;
|
||||||
this.authenticator = authenticator;
|
this.authenticator = authenticator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AuthenticatorModel getModel() {
|
public AuthenticationExecutionModel getExecution() {
|
||||||
|
return execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setExecution(AuthenticationExecutionModel execution) {
|
||||||
|
this.execution = execution;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthenticatorModel getAuthenticatorModel() {
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setModel(AuthenticatorModel model) {
|
public void setAuthenticatorModel(AuthenticatorModel model) {
|
||||||
this.model = model;
|
this.model = model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,6 +264,11 @@ public class AuthenticationProcessor {
|
||||||
public BruteForceProtector getProtector() {
|
public BruteForceProtector getProtector() {
|
||||||
return AuthenticationProcessor.this.protector;
|
return AuthenticationProcessor.this.protector;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public EventBuilder getEvent() {
|
||||||
|
return AuthenticationProcessor.this.event;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class AuthException extends RuntimeException {
|
public static class AuthException extends RuntimeException {
|
||||||
|
@ -305,6 +323,14 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response authenticate() throws AuthException {
|
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();
|
UserModel authUser = clientSession.getAuthenticatedUser();
|
||||||
validateUser(authUser);
|
validateUser(authUser);
|
||||||
Response challenge = processFlow(flowId);
|
Response challenge = processFlow(flowId);
|
||||||
|
@ -325,6 +351,7 @@ public class AuthenticationProcessor {
|
||||||
List<AuthenticationExecutionModel> executions = realm.getAuthenticationExecutions(flowId);
|
List<AuthenticationExecutionModel> executions = realm.getAuthenticationExecutions(flowId);
|
||||||
if (executions == null) return null;
|
if (executions == null) return null;
|
||||||
Response alternativeChallenge = null;
|
Response alternativeChallenge = null;
|
||||||
|
AuthenticationExecutionModel challengedAlternativeExecution = null;
|
||||||
boolean alternativeSuccessful = false;
|
boolean alternativeSuccessful = false;
|
||||||
for (AuthenticationExecutionModel model : executions) {
|
for (AuthenticationExecutionModel model : executions) {
|
||||||
if (isProcessed(model)) {
|
if (isProcessed(model)) {
|
||||||
|
@ -354,23 +381,31 @@ public class AuthenticationProcessor {
|
||||||
UserModel authUser = clientSession.getAuthenticatedUser();
|
UserModel authUser = clientSession.getAuthenticatedUser();
|
||||||
|
|
||||||
if (authenticator.requiresUser() && authUser == null){
|
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);
|
throw new AuthException(Error.UNKNOWN_USER);
|
||||||
}
|
}
|
||||||
|
boolean configuredFor = false;
|
||||||
if (authenticator.requiresUser() && authUser != null && !authenticator.configuredFor(authUser)) {
|
if (authenticator.requiresUser() && authUser != null) {
|
||||||
|
configuredFor = authenticator.configuredFor(session, realm, authUser);
|
||||||
|
if (!configuredFor) {
|
||||||
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
|
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
|
||||||
if (model.isUserSetupAllowed()) {
|
if (model.isUserSetupAllowed()) {
|
||||||
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED);
|
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED);
|
||||||
authUser.addRequiredAction(authenticator.getRequiredAction());
|
String requiredAction = authenticator.getRequiredAction();
|
||||||
|
if (!authUser.getRequiredActions().contains(requiredAction)) {
|
||||||
|
authUser.addRequiredAction(requiredAction);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
} else {
|
} else {
|
||||||
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
|
throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
context = new Result(authenticatorModel, authenticator);
|
}
|
||||||
|
context = new Result(model, authenticatorModel, authenticator);
|
||||||
authenticator.authenticate(context);
|
authenticator.authenticate(context);
|
||||||
Status result = context.getStatus();
|
Status result = context.getStatus();
|
||||||
if (result == Status.SUCCESS){
|
if (result == Status.SUCCESS){
|
||||||
|
@ -379,15 +414,24 @@ public class AuthenticationProcessor {
|
||||||
continue;
|
continue;
|
||||||
} else if (result == Status.FAILED) {
|
} else if (result == Status.FAILED) {
|
||||||
logUserFailure();
|
logUserFailure();
|
||||||
|
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.FAILED);
|
||||||
if (context.challenge != null) return context.challenge;
|
if (context.challenge != null) return context.challenge;
|
||||||
throw new AuthException(context.error);
|
throw new AuthException(context.error);
|
||||||
} else if (result == Status.CHALLENGE) {
|
} else if (result == Status.CHALLENGE) {
|
||||||
if (model.isRequired()) return context.challenge;
|
if (model.isRequired() || (model.isOptional() && configuredFor)) {
|
||||||
else if (model.isAlternative()) alternativeChallenge = context.challenge;
|
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
|
||||||
else clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
|
return context.challenge;
|
||||||
|
}
|
||||||
|
else if (model.isAlternative()) {
|
||||||
|
alternativeChallenge = context.challenge;
|
||||||
|
challengedAlternativeExecution = model;
|
||||||
|
} else {
|
||||||
|
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED);
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
} else if (result == Status.FAILURE_CHALLENGE) {
|
} else if (result == Status.FAILURE_CHALLENGE) {
|
||||||
logUserFailure();
|
logUserFailure();
|
||||||
|
clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED);
|
||||||
return context.challenge;
|
return context.challenge;
|
||||||
} else if (result == Status.ATTEMPTED) {
|
} else if (result == Status.ATTEMPTED) {
|
||||||
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS);
|
if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS);
|
||||||
|
@ -415,17 +459,22 @@ public class AuthenticationProcessor {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response authenticationComplete() {
|
protected Response authenticationComplete() {
|
||||||
|
String username = clientSession.getAuthenticatedUser().getUsername();
|
||||||
if (userSession == null) { // if no authenticator attached a usersession
|
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);
|
userSession.setState(UserSessionModel.State.LOGGING_IN);
|
||||||
}
|
}
|
||||||
TokenManager.attachClientSession(userSession, clientSession);
|
TokenManager.attachClientSession(userSession, clientSession);
|
||||||
|
event.user(userSession.getUser())
|
||||||
|
.detail(Details.USERNAME, username)
|
||||||
|
.session(userSession);
|
||||||
|
|
||||||
return processRequiredActions();
|
return processRequiredActions();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response processRequiredActions() {
|
public Response processRequiredActions() {
|
||||||
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, eventBuilder);
|
return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
package org.keycloak.authentication;
|
package org.keycloak.authentication;
|
||||||
|
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.provider.Provider;
|
import org.keycloak.provider.Provider;
|
||||||
|
|
||||||
|
@ -10,7 +12,7 @@ import org.keycloak.provider.Provider;
|
||||||
public interface Authenticator extends Provider {
|
public interface Authenticator extends Provider {
|
||||||
boolean requiresUser();
|
boolean requiresUser();
|
||||||
void authenticate(AuthenticatorContext context);
|
void authenticate(AuthenticatorContext context);
|
||||||
boolean configuredFor(UserModel user);
|
boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user);
|
||||||
String getRequiredAction();
|
String getRequiredAction();
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.authentication;
|
||||||
|
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
|
import org.keycloak.events.EventBuilder;
|
||||||
|
import org.keycloak.models.AuthenticationExecutionModel;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
import org.keycloak.models.AuthenticatorModel;
|
||||||
import org.keycloak.models.ClientSessionModel;
|
import org.keycloak.models.ClientSessionModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
@ -19,9 +21,15 @@ import javax.ws.rs.core.UriInfo;
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public interface AuthenticatorContext {
|
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();
|
Authenticator getAuthenticator();
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
|
@ -2,6 +2,8 @@ package org.keycloak.authentication.authenticators;
|
||||||
|
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorContext;
|
import org.keycloak.authentication.AuthenticatorContext;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
|
|
||||||
|
@ -31,7 +33,7 @@ public class CookieAuthenticator implements Authenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,8 @@ package org.keycloak.authentication.authenticators;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.AuthenticatorContext;
|
import org.keycloak.authentication.AuthenticatorContext;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
import org.keycloak.models.AuthenticatorModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
@ -61,8 +63,8 @@ public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return user.configuredForCredentialType(UserCredentialModel.TOTP);
|
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,7 +2,10 @@ package org.keycloak.authentication.authenticators;
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.AuthenticatorContext;
|
import org.keycloak.authentication.AuthenticatorContext;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
import org.keycloak.models.AuthenticatorModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
@ -42,6 +45,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
|
||||||
List<UserCredentialModel> credentials = new LinkedList<>();
|
List<UserCredentialModel> credentials = new LinkedList<>();
|
||||||
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
|
String password = inputData.getFirst(CredentialRepresentation.PASSWORD);
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
|
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
Response challengeResponse = badPassword(context);
|
Response challengeResponse = badPassword(context);
|
||||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||||
return;
|
return;
|
||||||
|
@ -49,6 +53,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
|
||||||
credentials.add(UserCredentialModel.password(password));
|
credentials.add(UserCredentialModel.password(password));
|
||||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
Response challengeResponse = badPassword(context);
|
Response challengeResponse = badPassword(context);
|
||||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||||
return;
|
return;
|
||||||
|
@ -62,8 +67,8 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return user.configuredForCredentialType(UserCredentialModel.PASSWORD);
|
return session.users().configuredForCredentialType(UserCredentialModel.PASSWORD, realm, user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -1,30 +1,29 @@
|
||||||
package org.keycloak.authentication.authenticators;
|
package org.keycloak.authentication.authenticators;
|
||||||
|
|
||||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
||||||
import org.keycloak.OAuth2Constants;
|
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorContext;
|
import org.keycloak.authentication.AuthenticatorContext;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.login.LoginFormsProvider;
|
import org.keycloak.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
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.UserModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
import org.keycloak.models.utils.KeycloakModelUtils;
|
||||||
|
import org.keycloak.protocol.LoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
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.MultivaluedMap;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
import java.net.URI;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class LoginFormUsernameAuthenticator implements Authenticator {
|
public class LoginFormUsernameAuthenticator extends AbstractFormAuthenticator implements Authenticator {
|
||||||
protected AuthenticatorModel model;
|
protected AuthenticatorModel model;
|
||||||
|
|
||||||
public LoginFormUsernameAuthenticator(AuthenticatorModel model) {
|
public LoginFormUsernameAuthenticator(AuthenticatorModel model) {
|
||||||
|
@ -47,15 +46,19 @@ public class LoginFormUsernameAuthenticator implements Authenticator {
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
validateUser(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.challenge(response);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected boolean isActionUrl(AuthenticatorContext context) {
|
validateUser(context, formData);
|
||||||
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
|
|
||||||
String current = context.getUriInfo().getAbsolutePath().getPath();
|
|
||||||
String expectedPath = expected.getPath();
|
|
||||||
return expectedPath.equals(current);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -71,71 +74,24 @@ public class LoginFormUsernameAuthenticator implements Authenticator {
|
||||||
return forms.createLogin();
|
return forms.createLogin();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected LoginFormsProvider loginForm(AuthenticatorContext context) {
|
public void validateUser(AuthenticatorContext context, MultivaluedMap<String, String> inputData) {
|
||||||
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();
|
|
||||||
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
|
String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME);
|
||||||
if (username == null) {
|
if (username == null) {
|
||||||
|
context.getEvent().error(Errors.USER_NOT_FOUND);
|
||||||
Response challengeResponse = invalidUser(context);
|
Response challengeResponse = invalidUser(context);
|
||||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
context.getEvent().detail(Details.USERNAME, username);
|
||||||
|
context.getClientSession().setNote("FORM_USERNAME", username);
|
||||||
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
|
UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username);
|
||||||
if (invalidUser(context, user)) return;
|
if (invalidUser(context, user)) return;
|
||||||
context.setUser(user);
|
context.setUser(user);
|
||||||
context.success();
|
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
|
@Override
|
||||||
public boolean configuredFor(UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
package org.keycloak.authentication.authenticators;
|
package org.keycloak.authentication.authenticators;
|
||||||
|
|
||||||
import org.jboss.resteasy.specimpl.MultivaluedMapImpl;
|
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.Authenticator;
|
import org.keycloak.authentication.Authenticator;
|
||||||
import org.keycloak.authentication.AuthenticatorContext;
|
import org.keycloak.authentication.AuthenticatorContext;
|
||||||
|
import org.keycloak.events.Errors;
|
||||||
import org.keycloak.login.LoginFormsProvider;
|
import org.keycloak.login.LoginFormsProvider;
|
||||||
import org.keycloak.models.AuthenticatorModel;
|
import org.keycloak.models.AuthenticatorModel;
|
||||||
|
import org.keycloak.models.KeycloakSession;
|
||||||
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserCredentialModel;
|
import org.keycloak.models.UserCredentialModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
|
||||||
import org.keycloak.representations.idm.CredentialRepresentation;
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.resources.LoginActionsService;
|
import org.keycloak.services.resources.LoginActionsService;
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ import java.util.List;
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class OTPFormAuthenticator implements Authenticator {
|
public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator {
|
||||||
protected AuthenticatorModel model;
|
protected AuthenticatorModel model;
|
||||||
|
|
||||||
public OTPFormAuthenticator(AuthenticatorModel model) {
|
public OTPFormAuthenticator(AuthenticatorModel model) {
|
||||||
|
@ -33,8 +33,7 @@ public class OTPFormAuthenticator implements Authenticator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void authenticate(AuthenticatorContext context) {
|
public void authenticate(AuthenticatorContext context) {
|
||||||
URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName());
|
if (!isActionUrl(context)) {
|
||||||
if (!expected.getPath().equals(context.getUriInfo().getPath())) {
|
|
||||||
Response challengeResponse = challenge(context);
|
Response challengeResponse = challenge(context);
|
||||||
context.challenge(challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
|
@ -48,12 +47,13 @@ public class OTPFormAuthenticator implements Authenticator {
|
||||||
String password = inputData.getFirst(CredentialRepresentation.TOTP);
|
String password = inputData.getFirst(CredentialRepresentation.TOTP);
|
||||||
if (password == null) {
|
if (password == null) {
|
||||||
Response challengeResponse = challenge(context);
|
Response challengeResponse = challenge(context);
|
||||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
context.challenge(challengeResponse);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
credentials.add(UserCredentialModel.totp(password));
|
credentials.add(UserCredentialModel.totp(password));
|
||||||
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
|
context.getEvent().error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
Response challengeResponse = challenge(context);
|
Response challengeResponse = challenge(context);
|
||||||
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse);
|
||||||
return;
|
return;
|
||||||
|
@ -67,23 +67,20 @@ public class OTPFormAuthenticator implements Authenticator {
|
||||||
return true;
|
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)
|
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();
|
return forms.createLoginTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Response challenge(AuthenticatorContext context) {
|
|
||||||
MultivaluedMap<String, String> formData = new MultivaluedMapImpl<>();
|
|
||||||
return challenge(context, formData);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean configuredFor(UserModel user) {
|
public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
return user.configuredForCredentialType(UserCredentialModel.TOTP);
|
return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -217,6 +217,12 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
clientSession.setProtocolMappers(requestedProtocolMappers);
|
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) {
|
public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) {
|
||||||
|
|
|
@ -6,7 +6,6 @@ import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.ClientConnection;
|
import org.keycloak.ClientConnection;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.authenticators.AuthenticationFlow;
|
|
||||||
import org.keycloak.constants.AdapterConstants;
|
import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.events.Details;
|
import org.keycloak.events.Details;
|
||||||
import org.keycloak.events.Errors;
|
import org.keycloak.events.Errors;
|
||||||
|
@ -44,6 +43,7 @@ import java.util.List;
|
||||||
public class AuthorizationEndpoint {
|
public class AuthorizationEndpoint {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
|
private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class);
|
||||||
|
public static final String CODE_AUTH_TYPE = "code";
|
||||||
|
|
||||||
private enum Action {
|
private enum Action {
|
||||||
REGISTER, CODE
|
REGISTER, CODE
|
||||||
|
@ -247,10 +247,18 @@ public class AuthorizationEndpoint {
|
||||||
return buildRedirectToIdentityProvider(idpHint, accessCode);
|
return buildRedirectToIdentityProvider(idpHint, accessCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
return oldBrowserAuthentication(accessCode);
|
return newBrowserAuthentication(accessCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Response newBrowserAuthentication(String 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;
|
String flowId = null;
|
||||||
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
|
for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) {
|
||||||
if (flow.getAlias().equals("browser")) {
|
if (flow.getAlias().equals("browser")) {
|
||||||
|
|
|
@ -343,6 +343,7 @@ public class LoginActionsService {
|
||||||
} catch (AuthenticationProcessor.AuthException e) {
|
} catch (AuthenticationProcessor.AuthException e) {
|
||||||
return handleError(e, code);
|
return handleError(e, code);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
event.error(Errors.INVALID_USER_CREDENTIALS);
|
||||||
logger.error("failed authentication", e);
|
logger.error("failed authentication", e);
|
||||||
return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
|
return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE);
|
||||||
|
|
||||||
|
|
|
@ -3,3 +3,4 @@ org.keycloak.authentication.authenticators.LoginFormOTPAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory
|
org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.LoginFormUsernameAuthenticatorFactory
|
org.keycloak.authentication.authenticators.LoginFormUsernameAuthenticatorFactory
|
||||||
org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory
|
org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory
|
||||||
|
org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory
|
|
@ -21,6 +21,9 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.KeycloakModelUtils;
|
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.representations.idm.UserRepresentation;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
|
@ -119,9 +122,9 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
|
||||||
public ExpectedEvent expectLogin() {
|
public ExpectedEvent expectLogin() {
|
||||||
return expect(EventType.LOGIN)
|
return expect(EventType.LOGIN)
|
||||||
.detail(Details.CODE_ID, isCodeId())
|
.detail(Details.CODE_ID, isCodeId())
|
||||||
.detail(Details.USERNAME, DEFAULT_USERNAME)
|
//.detail(Details.USERNAME, DEFAULT_USERNAME)
|
||||||
.detail(Details.RESPONSE_TYPE, "code")
|
//.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL)
|
||||||
.detail(Details.AUTH_METHOD, "form")
|
//.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE)
|
||||||
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
|
.detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI)
|
||||||
.session(isUUID());
|
.session(isUUID());
|
||||||
}
|
}
|
||||||
|
@ -341,12 +344,13 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory {
|
||||||
|
|
||||||
Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
|
Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue());
|
||||||
}
|
}
|
||||||
|
/*
|
||||||
for (String k : actual.getDetails().keySet()) {
|
for (String k : actual.getDetails().keySet()) {
|
||||||
if (!details.containsKey(k)) {
|
if (!details.containsKey(k)) {
|
||||||
Assert.fail(k + " was not expected");
|
Assert.fail(k + " was not expected");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
return actual;
|
return actual;
|
||||||
|
|
|
@ -241,7 +241,7 @@ public class AccountTest {
|
||||||
|
|
||||||
Assert.assertEquals("Invalid username or password.", loginPage.getError());
|
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.open();
|
||||||
loginPage.login("test-user@localhost", "new-password");
|
loginPage.login("test-user@localhost", "new-password");
|
||||||
|
|
|
@ -45,6 +45,7 @@ import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
|
import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup;
|
||||||
import org.keycloak.testsuite.rule.WebResource;
|
import org.keycloak.testsuite.rule.WebResource;
|
||||||
import org.keycloak.testsuite.rule.WebRule;
|
import org.keycloak.testsuite.rule.WebRule;
|
||||||
|
import org.keycloak.testsuite.utils.CredentialHelper;
|
||||||
import org.openqa.selenium.WebDriver;
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,6 +58,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
|
public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) {
|
||||||
|
CredentialHelper.setRequiredCredential(CredentialRepresentation.TOTP, appRealm);
|
||||||
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
|
appRealm.addRequiredCredential(CredentialRepresentation.TOTP);
|
||||||
appRealm.setResetPasswordAllowed(true);
|
appRealm.setResetPasswordAllowed(true);
|
||||||
}
|
}
|
||||||
|
@ -137,6 +139,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
|
|
||||||
loginPage.open();
|
loginPage.open();
|
||||||
loginPage.login("test-user@localhost", "password");
|
loginPage.login("test-user@localhost", "password");
|
||||||
|
String src = driver.getPageSource();
|
||||||
loginTotpPage.login(totp.generate(totpSecret));
|
loginTotpPage.login(totp.generate(totpSecret));
|
||||||
|
|
||||||
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType());
|
||||||
|
@ -181,7 +184,7 @@ public class RequiredActionTotpSetupTest {
|
||||||
// Login with one-time password
|
// Login with one-time password
|
||||||
loginTotpPage.login(totp.generate(totpCode));
|
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
|
// Open account page
|
||||||
accountTotpPage.open();
|
accountTotpPage.open();
|
||||||
|
@ -204,11 +207,11 @@ public class RequiredActionTotpSetupTest {
|
||||||
totpPage.assertCurrent();
|
totpPage.assertCurrent();
|
||||||
totpPage.configure(totp.generate(totpPage.getTotpSecret()));
|
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());
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,7 +108,7 @@ public abstract class AbstractKerberosTest {
|
||||||
.client("kerberos-app")
|
.client("kerberos-app")
|
||||||
.user(keycloakRule.getUser("test", "hnelson").getId())
|
.user(keycloakRule.getUser("test", "hnelson").getId())
|
||||||
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
|
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
|
||||||
.detail(Details.AUTH_METHOD, "spnego")
|
//.detail(Details.AUTH_METHOD, "spnego")
|
||||||
.detail(Details.USERNAME, "hnelson")
|
.detail(Details.USERNAME, "hnelson")
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
|
|
||||||
|
@ -164,7 +164,7 @@ public abstract class AbstractKerberosTest {
|
||||||
.client("kerberos-app")
|
.client("kerberos-app")
|
||||||
.user(keycloakRule.getUser("test", "jduke").getId())
|
.user(keycloakRule.getUser("test", "jduke").getId())
|
||||||
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
|
.detail(Details.REDIRECT_URI, KERBEROS_APP_URL)
|
||||||
.detail(Details.AUTH_METHOD, "spnego")
|
//.detail(Details.AUTH_METHOD, "spnego")
|
||||||
.detail(Details.USERNAME, "jduke")
|
.detail(Details.USERNAME, "jduke")
|
||||||
.assertEvent();
|
.assertEvent();
|
||||||
spnegoResponse.close();
|
spnegoResponse.close();
|
||||||
|
|
7
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java
Normal file → Executable file
7
testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java
Normal file → Executable file
|
@ -18,11 +18,14 @@ import org.keycloak.constants.KerberosConstants;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserFederationProviderModel;
|
import org.keycloak.models.UserFederationProviderModel;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
import org.keycloak.services.managers.RealmManager;
|
import org.keycloak.services.managers.RealmManager;
|
||||||
import org.keycloak.testsuite.AssertEvents;
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
import org.keycloak.testsuite.rule.KerberosRule;
|
import org.keycloak.testsuite.rule.KerberosRule;
|
||||||
import org.keycloak.testsuite.rule.KeycloakRule;
|
import org.keycloak.testsuite.rule.KeycloakRule;
|
||||||
import org.keycloak.testsuite.rule.WebRule;
|
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)
|
* Test of KerberosFederationProvider (Kerberos not backed by LDAP)
|
||||||
|
@ -41,6 +44,8 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
|
||||||
|
|
||||||
|
CredentialHelper.setRequiredCredential(CredentialRepresentation.KERBEROS, appRealm);
|
||||||
URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json");
|
URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json");
|
||||||
keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user");
|
keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user");
|
||||||
|
|
||||||
|
@ -131,4 +136,6 @@ public class KerberosStandaloneTest extends AbstractKerberosTest {
|
||||||
keycloakRule.stopSession(session, true);
|
keycloakRule.stopSession(session, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue