From c12fe28b2d1bd52eec66c29664557d9bc06d8632 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 3 Jun 2015 10:55:03 -0400 Subject: [PATCH] phased auth spi introduction --- .../META-INF/jpa-changelog-1.3.0.Beta1.xml | 11 ++ .../main/resources/META-INF/persistence.xml | 1 + .../java/org/keycloak/events/Details.java | 1 + .../keycloak/models/ClientSessionModel.java | 15 +++ .../models/UserFederationManager.java | 19 +++ .../java/org/keycloak/models/UserModel.java | 8 -- .../org/keycloak/models/UserSessionModel.java | 4 +- .../utils/DefaultAuthenticationFlows.java | 22 +++- .../models/utils/UserModelDelegate.java | 5 - .../models/file/adapter/UserAdapter.java | 11 -- .../keycloak/models/cache/UserAdapter.java | 9 -- .../org/keycloak/models/jpa/UserAdapter.java | 9 -- .../mongo/keycloak/adapters/UserAdapter.java | 10 -- .../infinispan/ClientSessionAdapter.java | 21 +++ .../entities/ClientSessionEntity.java | 9 ++ .../sessions/jpa/ClientSessionAdapter.java | 28 ++++ .../jpa/entities/ClientSessionEntity.java | 11 ++ .../entities/ClientUserSessionNoteEntity.java | 109 ++++++++++++++++ .../sessions/mem/ClientSessionAdapter.java | 10 ++ .../mem/entities/ClientSessionEntity.java | 5 + .../sessions/mongo/ClientSessionAdapter.java | 14 ++ .../entities/MongoClientSessionEntity.java | 9 ++ .../keycloak/protocol/saml/SamlService.java | 29 +++++ .../AuthenticationProcessor.java | 95 ++++++++++---- .../authentication/Authenticator.java | 4 +- .../authentication/AuthenticatorContext.java | 12 +- .../AbstractFormAuthenticator.java | 80 ++++++++++++ .../authenticators/AuthenticationFlow.java | 60 --------- .../authenticators/CookieAuthenticator.java | 4 +- .../LoginFormOTPAuthenticator.java | 6 +- .../LoginFormPasswordAuthenticator.java | 9 +- .../LoginFormUsernameAuthenticator.java | 90 ++++--------- .../authenticators/OTPFormAuthenticator.java | 31 ++--- .../authenticators/SpnegoAuthenticator.java | 123 ++++++++++++++++++ .../SpnegoAuthenticatorFactory.java | 70 ++++++++++ .../keycloak/protocol/oidc/TokenManager.java | 6 + .../oidc/endpoints/AuthorizationEndpoint.java | 12 +- .../resources/LoginActionsService.java | 1 + ...ycloak.authentication.AuthenticatorFactory | 3 +- .../org/keycloak/testsuite/AssertEvents.java | 12 +- .../testsuite/account/AccountTest.java | 2 +- .../actions/RequiredActionTotpSetupTest.java | 9 +- .../federation/AbstractKerberosTest.java | 4 +- .../federation/KerberosStandaloneTest.java | 7 + .../testsuite/utils/CredentialHelper.java | 80 ++++++++++++ 45 files changed, 846 insertions(+), 244 deletions(-) create mode 100755 model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionNoteEntity.java create mode 100755 services/src/main/java/org/keycloak/authentication/authenticators/AbstractFormAuthenticator.java delete mode 100755 services/src/main/java/org/keycloak/authentication/authenticators/AuthenticationFlow.java create mode 100755 services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java create mode 100755 services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java mode change 100644 => 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java create mode 100755 testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml index c469581b7a..c1b385abef 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.3.0.Beta1.xml @@ -129,6 +129,17 @@ ACTION = 3 + + + + + + + + + + + diff --git a/connections/jpa/src/main/resources/META-INF/persistence.xml b/connections/jpa/src/main/resources/META-INF/persistence.xml index be9c2811f3..bb3891477a 100755 --- a/connections/jpa/src/main/resources/META-INF/persistence.xml +++ b/connections/jpa/src/main/resources/META-INF/persistence.xml @@ -35,6 +35,7 @@ org.keycloak.models.sessions.jpa.entities.ClientSessionAuthStatusEntity org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity + org.keycloak.models.sessions.jpa.entities.ClientUserSessionNoteEntity org.keycloak.models.sessions.jpa.entities.UserSessionNoteEntity org.keycloak.models.sessions.jpa.entities.UserSessionEntity org.keycloak.models.sessions.jpa.entities.UsernameLoginFailureEntity diff --git a/events/api/src/main/java/org/keycloak/events/Details.java b/events/api/src/main/java/org/keycloak/events/Details.java index cee7475ec6..d1ef4a5178 100755 --- a/events/api/src/main/java/org/keycloak/events/Details.java +++ b/events/api/src/main/java/org/keycloak/events/Details.java @@ -11,6 +11,7 @@ public interface Details { String CODE_ID = "code_id"; String REDIRECT_URI = "redirect_uri"; String RESPONSE_TYPE = "response_type"; + String AUTH_TYPE = "auth_type"; String AUTH_METHOD = "auth_method"; String IDENTITY_PROVIDER = "identity_provider"; String IDENTITY_PROVIDER_USERNAME = "identity_provider_identity"; diff --git a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java index 2c66df3f4e..5d6823770d 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -52,6 +52,21 @@ public interface ClientSessionModel { public void setNote(String name, String value); public void removeNote(String name); + /** + * These are notes you want applied to the UserSessionModel when the client session is attached to it. + * + * @param name + * @param value + */ + public void setUserSessionNote(String name, String value); + + /** + * These are notes you want applied to the UserSessionModel when the client session is attached to it. + * + * @return + */ + public Map getUserSessionNotes(); + public static enum Action { OAUTH_GRANT, CODE_TO_TOKEN, diff --git a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java index bd07b772bf..d094b821be 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -370,6 +370,25 @@ public class UserFederationManager implements UserProvider { return session.userStorage().validCredentials(realm, user, input); } + /** + * Is the user configured to use this credential type + * + * @return + */ + public boolean configuredForCredentialType(String type, RealmModel realm, UserModel user) { + UserFederationProvider link = getFederationLink(realm, user); + if (link != null) { + Set supportedCredentialTypes = link.getSupportedCredentialTypes(user); + if (supportedCredentialTypes.contains(type)) return true; + } + List creds = user.getCredentialsDirectly(); + for (UserCredentialValueModel cred : creds) { + if (cred.getType().equals(type)) return true; + } + return false; + } + + @Override public boolean validCredentials(RealmModel realm, UserModel user, UserCredentialModel... input) { return validCredentials(realm, user, Arrays.asList(input)); diff --git a/model/api/src/main/java/org/keycloak/models/UserModel.java b/model/api/src/main/java/org/keycloak/models/UserModel.java index 2088abce2d..645250e91b 100755 --- a/model/api/src/main/java/org/keycloak/models/UserModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserModel.java @@ -69,14 +69,6 @@ public interface UserModel { void updateCredentialDirectly(UserCredentialValueModel cred); - /** - * Is the use configured to use this credential type - * - * @param type - * @return - */ - boolean configuredForCredentialType(String type); - Set getRealmRoleMappings(); Set getClientRoleMappings(ClientModel app); boolean hasRole(RoleModel role); diff --git a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java index 769fbcaef3..6af29cf295 100755 --- a/model/api/src/main/java/org/keycloak/models/UserSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/UserSessionModel.java @@ -38,10 +38,12 @@ public interface UserSessionModel { List getClientSessions(); public static enum AuthenticatorStatus { + FAILED, SUCCESS, SETUP_REQUIRED, ATTEMPTED, - SKIPPED + SKIPPED, + CHALLENGED } public String getNote(String name); diff --git a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java index 05a5a4a71c..af057b6fa0 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -10,6 +10,10 @@ import org.keycloak.models.RealmModel; * @version $Revision: 1 $ */ public class DefaultAuthenticationFlows { + + public static final String BROWSER_FLOW = "browser"; + public static final String FORMS_FLOW = "forms"; + public static void addFlows(RealmModel realm) { AuthenticatorModel model = new AuthenticatorModel(); model.setProviderId("auth-cookie"); @@ -31,9 +35,13 @@ public class DefaultAuthenticationFlows { model.setProviderId("auth-otp-form"); model.setAlias("Single OTP Form"); AuthenticatorModel otp = realm.addAuthenticator(model); + model = new AuthenticatorModel(); + model.setProviderId("auth-spnego"); + model.setAlias("Kerberos"); + AuthenticatorModel kerberos = realm.addAuthenticator(model); AuthenticationFlowModel browser = new AuthenticationFlowModel(); - browser.setAlias("browser"); + browser.setAlias(BROWSER_FLOW); browser.setDescription("browser based authentication"); browser = realm.addAuthenticationFlow(browser); AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); @@ -44,15 +52,23 @@ public class DefaultAuthenticationFlows { execution.setUserSetupAllowed(false); execution.setAutheticatorFlow(false); realm.addAuthenticatorExecution(execution); + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.DISABLED); + execution.setAuthenticator(kerberos.getId()); + execution.setPriority(1); + execution.setUserSetupAllowed(false); + execution.setAutheticatorFlow(false); + realm.addAuthenticatorExecution(execution); AuthenticationFlowModel forms = new AuthenticationFlowModel(); - forms.setAlias("forms"); + forms.setAlias(FORMS_FLOW); forms.setDescription("Username, password, otp and other auth forms."); forms = realm.addAuthenticationFlow(forms); execution = new AuthenticationExecutionModel(); execution.setParentFlow(browser.getId()); execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); execution.setAuthenticator(forms.getId()); - execution.setPriority(1); + execution.setPriority(2); execution.setUserSetupAllowed(false); execution.setAutheticatorFlow(true); realm.addAuthenticatorExecution(execution); diff --git a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java index 3f6bec44a0..7123c3e1d5 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java +++ b/model/api/src/main/java/org/keycloak/models/utils/UserModelDelegate.java @@ -87,11 +87,6 @@ public class UserModelDelegate implements UserModel { delegate.removeRequiredAction(action); } - @Override - public boolean configuredForCredentialType(String type) { - return delegate.configuredForCredentialType(type); - } - @Override public void addRequiredAction(RequiredAction action) { delegate.addRequiredAction(action); diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java index b39fc6de7c..39024c1100 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/UserAdapter.java @@ -220,17 +220,6 @@ public class UserAdapter implements UserModel, Comparable { user.setRequiredActions(requiredActions); } - @Override - public boolean configuredForCredentialType(String type) { - List creds = getCredentialsDirectly(); - for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) return true; - } - return false; - } - - - @Override public boolean isTotp() { return user.isTotp(); diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java index dc159ceb67..aa80a25fc1 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/UserAdapter.java @@ -131,15 +131,6 @@ public class UserAdapter implements UserModel { updated.removeRequiredAction(action); } - @Override - public boolean configuredForCredentialType(String type) { - List creds = getCredentialsDirectly(); - for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) return true; - } - return false; - } - @Override public String getFirstName() { if (updated != null) return updated.getFirstName(); diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java index 977a4f5175..670f5f039b 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/UserAdapter.java @@ -185,15 +185,6 @@ public class UserAdapter implements UserModel { } } - @Override - public boolean configuredForCredentialType(String type) { - List creds = getCredentialsDirectly(); - for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) return true; - } - return false; - } - @Override public String getFirstName() { return user.getFirstName(); diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java index 813faab887..79a6260b9d 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/UserAdapter.java @@ -333,16 +333,6 @@ public class UserAdapter extends AbstractMongoAdapter implement return result; } - @Override - public boolean configuredForCredentialType(String type) { - List creds = getCredentialsDirectly(); - for (UserCredentialValueModel cred : creds) { - if (cred.getType().equals(type)) return true; - } - return false; - } - - @Override public void updateCredentialDirectly(UserCredentialValueModel credModel) { CredentialEntity credentialEntity = getCredentialEntity(user, credModel.getType()); diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java index ddf42d58f4..226a2ec9cc 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/ClientSessionAdapter.java @@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.infinispan.entities.ClientSessionEntity; import org.keycloak.models.sessions.infinispan.entities.SessionEntity; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Set; @@ -164,6 +165,26 @@ public class ClientSessionAdapter implements ClientSessionModel { } } + @Override + public void setUserSessionNote(String name, String value) { + if (entity.getUserSessionNotes() == null) { + entity.setUserSessionNotes(new HashMap()); + } + entity.getNotes().put(name, value); + update(); + + } + + @Override + public Map getUserSessionNotes() { + if (entity.getUserSessionNotes() == null) { + return Collections.EMPTY_MAP; + } + HashMap copy = new HashMap<>(); + copy.putAll(entity.getUserSessionNotes()); + return copy; + } + void update() { provider.getTx().replace(cache, entity.getId(), entity); } diff --git a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java index cc8ce200ba..53c82181c7 100755 --- a/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java +++ b/model/sessions-infinispan/src/main/java/org/keycloak/models/sessions/infinispan/entities/ClientSessionEntity.java @@ -29,6 +29,7 @@ public class ClientSessionEntity extends SessionEntity { private Set roles; private Set protocolMappers; private Map notes; + private Map userSessionNotes; private Map authenticatorStatus = new HashMap<>(); private String authUserId; @@ -127,4 +128,12 @@ public class ClientSessionEntity extends SessionEntity { public void setAuthUserId(String authUserId) { this.authUserId = authUserId; } + + public Map getUserSessionNotes() { + return userSessionNotes; + } + + public void setUserSessionNotes(Map userSessionNotes) { + this.userSessionNotes = userSessionNotes; + } } diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java index 68e3b53ee5..0ce21fa348 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/ClientSessionAdapter.java @@ -10,9 +10,11 @@ import org.keycloak.models.sessions.jpa.entities.ClientSessionEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionNoteEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionProtocolMapperEntity; import org.keycloak.models.sessions.jpa.entities.ClientSessionRoleEntity; +import org.keycloak.models.sessions.jpa.entities.ClientUserSessionNoteEntity; import org.keycloak.models.sessions.jpa.entities.UserSessionEntity; import javax.persistence.EntityManager; +import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -78,6 +80,32 @@ public class ClientSessionAdapter implements ClientSessionModel { return null; } + @Override + public void setUserSessionNote(String name, String value) { + for (ClientUserSessionNoteEntity attr : entity.getUserSessionNotes()) { + if (attr.getName().equals(name)) { + attr.setValue(value); + return; + } + } + ClientUserSessionNoteEntity attr = new ClientUserSessionNoteEntity(); + attr.setName(name); + attr.setValue(value); + attr.setClientSession(entity); + em.persist(attr); + entity.getUserSessionNotes().add(attr); + + } + + @Override + public Map getUserSessionNotes() { + Map copy = new HashMap<>(); + for (ClientUserSessionNoteEntity attr : entity.getUserSessionNotes()) { + copy.put(attr.getName(), attr.getValue()); + } + return copy; + } + @Override public String getId() { return entity.getId(); diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java index 7cc9062b99..bf0c520b78 100755 --- a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientSessionEntity.java @@ -69,6 +69,9 @@ public class ClientSessionEntity { @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession") protected Collection notes = new ArrayList(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession") + protected Collection userSessionNotes = new ArrayList<>(); + @OneToMany(cascade = CascadeType.REMOVE, orphanRemoval = true, mappedBy="clientSession") protected Collection authanticatorStatus = new ArrayList<>(); @@ -175,4 +178,12 @@ public class ClientSessionEntity { public void setUserId(String userId) { this.userId = userId; } + + public Collection getUserSessionNotes() { + return userSessionNotes; + } + + public void setUserSessionNotes(Collection userSessionNotes) { + this.userSessionNotes = userSessionNotes; + } } diff --git a/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionNoteEntity.java b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionNoteEntity.java new file mode 100755 index 0000000000..9051925c6a --- /dev/null +++ b/model/sessions-jpa/src/main/java/org/keycloak/models/sessions/jpa/entities/ClientUserSessionNoteEntity.java @@ -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 Bill Burke + * @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; + } + } + +} diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java index cff4ca373d..cbea1d6ac7 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/ClientSessionAdapter.java @@ -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 getUserSessionNotes() { + return entity.getUserSessionNotes(); + } + @Override public String getAuthMethod() { return entity.getAuthMethod(); diff --git a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java index 9823570c4c..bd6eac91d0 100755 --- a/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java +++ b/model/sessions-mem/src/main/java/org/keycloak/models/sessions/mem/entities/ClientSessionEntity.java @@ -28,6 +28,7 @@ public class ClientSessionEntity { private Set roles; private Set protocolMappers; private Map notes = new HashMap<>(); + private Map userSessionNotes = new HashMap<>(); public String getId() { return id; @@ -128,4 +129,8 @@ public class ClientSessionEntity { public void setAuthenticatorStatus(Map authenticatorStatus) { this.authenticatorStatus = authenticatorStatus; } + + public Map getUserSessionNotes() { + return userSessionNotes; + } } diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java index e5fd346d55..52fa0aa7c7 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/ClientSessionAdapter.java @@ -10,6 +10,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.sessions.mongo.entities.MongoClientSessionEntity; import org.keycloak.models.sessions.mongo.entities.MongoUserSessionEntity; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; @@ -156,6 +157,19 @@ public class ClientSessionAdapter extends AbstractMongoAdapter getUserSessionNotes() { + Map copy = new HashMap<>(); + copy.putAll(entity.getUserSessionNotes()); + return copy; + } + @Override public Map getAuthenticators() { return entity.getAuthenticatorStatus(); diff --git a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java index ed8099dd90..10a211f014 100755 --- a/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java +++ b/model/sessions-mongo/src/main/java/org/keycloak/models/sessions/mongo/entities/MongoClientSessionEntity.java @@ -30,6 +30,7 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme private List roles; private List protocolMappers; private Map notes = new HashMap(); + private Map userSessionNotes = new HashMap(); private Map authenticatorStatus = new HashMap<>(); private String authUserId; @@ -113,6 +114,14 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme this.notes = notes; } + public Map getUserSessionNotes() { + return userSessionNotes; + } + + public void setUserSessionNotes(Map userSessionNotes) { + this.userSessionNotes = userSessionNotes; + } + public String getSessionId() { return sessionId; } diff --git a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java index d11ffb0583..dbbde2972c 100755 --- a/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java +++ b/saml/saml-protocol/src/main/java/org/keycloak/protocol/saml/SamlService.java @@ -6,6 +6,7 @@ import org.jboss.resteasy.spi.HttpRequest; import org.jboss.resteasy.spi.HttpResponse; import org.keycloak.ClientConnection; import org.keycloak.VerificationException; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.dom.saml.v2.SAML2Object; import org.keycloak.dom.saml.v2.protocol.AuthnRequestType; import org.keycloak.dom.saml.v2.protocol.LogoutRequestType; @@ -17,6 +18,7 @@ import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -282,6 +284,10 @@ public class SamlService { } } + return newBrowserAuthentication(clientSession); + } + + private Response oldBrowserAuthentication(ClientSessionModel clientSession) { Response response = authManager.checkNonFormAuthentication(session, clientSession, realm, uriInfo, request, clientConnection, headers, event); if (response != null) return response; @@ -311,6 +317,29 @@ public class SamlService { return forms.createLogin(); } + protected Response newBrowserAuthentication(ClientSessionModel clientSession) { + String flowId = null; + for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { + if (flow.getAlias().equals("browser")) { + flowId = flow.getId(); + break; + } + } + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowId(flowId) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + + return processor.authenticate(); + } + + private String getBindingType(AuthnRequestType requestAbstractType) { URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 1ebfa50abe..570b9ca59b 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -3,7 +3,9 @@ package org.keycloak.authentication; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; +import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; +import org.keycloak.events.EventType; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorModel; @@ -19,7 +21,6 @@ import org.keycloak.services.managers.BruteForceProtector; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; import java.util.List; -import java.util.Map; /** * @author Bill Burke @@ -34,7 +35,7 @@ public class AuthenticationProcessor { protected UriInfo uriInfo; protected KeycloakSession session; protected BruteForceProtector protector; - protected EventBuilder eventBuilder; + protected EventBuilder event; protected HttpRequest request; protected String flowId; @@ -109,7 +110,7 @@ public class AuthenticationProcessor { } public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) { - this.eventBuilder = eventBuilder; + this.event = eventBuilder; return this; } @@ -125,23 +126,35 @@ public class AuthenticationProcessor { private class Result implements AuthenticatorContext { AuthenticatorModel model; + AuthenticationExecutionModel execution; Authenticator authenticator; Status status; Response challenge; Error error; - private Result(AuthenticatorModel model, Authenticator authenticator) { + private Result(AuthenticationExecutionModel execution, AuthenticatorModel model, Authenticator authenticator) { + this.execution = execution; this.model = model; this.authenticator = authenticator; } @Override - public AuthenticatorModel getModel() { + public AuthenticationExecutionModel getExecution() { + return execution; + } + + @Override + public void setExecution(AuthenticationExecutionModel execution) { + this.execution = execution; + } + + @Override + public AuthenticatorModel getAuthenticatorModel() { return model; } @Override - public void setModel(AuthenticatorModel model) { + public void setAuthenticatorModel(AuthenticatorModel model) { this.model = model; } @@ -251,6 +264,11 @@ public class AuthenticationProcessor { public BruteForceProtector getProtector() { return AuthenticationProcessor.this.protector; } + + @Override + public EventBuilder getEvent() { + return AuthenticationProcessor.this.event; + } } public static class AuthException extends RuntimeException { @@ -305,6 +323,14 @@ public class AuthenticationProcessor { } public Response authenticate() throws AuthException { + event.event(EventType.LOGIN); + event.client(clientSession.getClient().getClientId()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.AUTH_METHOD, clientSession.getAuthMethod()); + String authType = clientSession.getNote(Details.AUTH_TYPE); + if (authType != null) { + event.detail(Details.AUTH_TYPE, authType); + } UserModel authUser = clientSession.getAuthenticatedUser(); validateUser(authUser); Response challenge = processFlow(flowId); @@ -325,6 +351,7 @@ public class AuthenticationProcessor { List executions = realm.getAuthenticationExecutions(flowId); if (executions == null) return null; Response alternativeChallenge = null; + AuthenticationExecutionModel challengedAlternativeExecution = null; boolean alternativeSuccessful = false; for (AuthenticationExecutionModel model : executions) { if (isProcessed(model)) { @@ -354,23 +381,31 @@ public class AuthenticationProcessor { UserModel authUser = clientSession.getAuthenticatedUser(); if (authenticator.requiresUser() && authUser == null){ - if (alternativeChallenge != null) return alternativeChallenge; + if (alternativeChallenge != null) { + clientSession.setAuthenticatorStatus(challengedAlternativeExecution.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); + return alternativeChallenge; + } throw new AuthException(Error.UNKNOWN_USER); } - - if (authenticator.requiresUser() && authUser != null && !authenticator.configuredFor(authUser)) { - if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { - if (model.isUserSetupAllowed()) { - clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED); - authUser.addRequiredAction(authenticator.getRequiredAction()); - - } else { - throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED); + boolean configuredFor = false; + if (authenticator.requiresUser() && authUser != null) { + configuredFor = authenticator.configuredFor(session, realm, authUser); + if (!configuredFor) { + if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { + if (model.isUserSetupAllowed()) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED); + String requiredAction = authenticator.getRequiredAction(); + if (!authUser.getRequiredActions().contains(requiredAction)) { + authUser.addRequiredAction(requiredAction); + } + continue; + } else { + throw new AuthException(Error.CREDENTIAL_SETUP_REQUIRED); + } } } - continue; } - context = new Result(authenticatorModel, authenticator); + context = new Result(model, authenticatorModel, authenticator); authenticator.authenticate(context); Status result = context.getStatus(); if (result == Status.SUCCESS){ @@ -379,15 +414,24 @@ public class AuthenticationProcessor { continue; } else if (result == Status.FAILED) { logUserFailure(); + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.FAILED); if (context.challenge != null) return context.challenge; throw new AuthException(context.error); } else if (result == Status.CHALLENGE) { - if (model.isRequired()) return context.challenge; - else if (model.isAlternative()) alternativeChallenge = context.challenge; - else clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + if (model.isRequired() || (model.isOptional() && configuredFor)) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); + return context.challenge; + } + else if (model.isAlternative()) { + alternativeChallenge = context.challenge; + challengedAlternativeExecution = model; + } else { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + } continue; } else if (result == Status.FAILURE_CHALLENGE) { logUserFailure(); + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); return context.challenge; } else if (result == Status.ATTEMPTED) { if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS); @@ -415,17 +459,22 @@ public class AuthenticationProcessor { } protected Response authenticationComplete() { + String username = clientSession.getAuthenticatedUser().getUsername(); if (userSession == null) { // if no authenticator attached a usersession - userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), clientSession.getAuthenticatedUser().getUsername(), connection.getRemoteAddr(), "form", false, null, null); + userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null); userSession.setState(UserSessionModel.State.LOGGING_IN); } TokenManager.attachClientSession(userSession, clientSession); + event.user(userSession.getUser()) + .detail(Details.USERNAME, username) + .session(userSession); + return processRequiredActions(); } public Response processRequiredActions() { - return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, eventBuilder); + return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, connection, request, uriInfo, event); } diff --git a/services/src/main/java/org/keycloak/authentication/Authenticator.java b/services/src/main/java/org/keycloak/authentication/Authenticator.java index 3d5c64eeba..ee9d43500b 100755 --- a/services/src/main/java/org/keycloak/authentication/Authenticator.java +++ b/services/src/main/java/org/keycloak/authentication/Authenticator.java @@ -1,5 +1,7 @@ package org.keycloak.authentication; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.provider.Provider; @@ -10,7 +12,7 @@ import org.keycloak.provider.Provider; public interface Authenticator extends Provider { boolean requiresUser(); void authenticate(AuthenticatorContext context); - boolean configuredFor(UserModel user); + boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user); String getRequiredAction(); diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java index 8a91d5e08a..91e9514861 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java @@ -2,6 +2,8 @@ package org.keycloak.authentication; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; +import org.keycloak.events.EventBuilder; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -19,9 +21,15 @@ import javax.ws.rs.core.UriInfo; * @version $Revision: 1 $ */ public interface AuthenticatorContext { - AuthenticatorModel getModel(); + EventBuilder getEvent(); - void setModel(AuthenticatorModel model); + AuthenticationExecutionModel getExecution(); + + void setExecution(AuthenticationExecutionModel execution); + + AuthenticatorModel getAuthenticatorModel(); + + void setAuthenticatorModel(AuthenticatorModel model); Authenticator getAuthenticator(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/AbstractFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/AbstractFormAuthenticator.java new file mode 100755 index 0000000000..0657e8a8b2 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/AbstractFormAuthenticator.java @@ -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 Bill Burke + * @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; + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/AuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/authenticators/AuthenticationFlow.java deleted file mode 100755 index 98e5826554..0000000000 --- a/services/src/main/java/org/keycloak/authentication/authenticators/AuthenticationFlow.java +++ /dev/null @@ -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 Bill Burke - * @version $Revision: 1 $ - */ -public class AuthenticationFlow { - - /** - * Hardcoded models just to test this stuff. It is temporary - */ - static List 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); - } - */ -} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticator.java index b508024828..1455b2f6c9 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticator.java @@ -2,6 +2,8 @@ package org.keycloak.authentication.authenticators; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorContext; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.services.managers.AuthenticationManager; @@ -31,7 +33,7 @@ public class CookieAuthenticator implements Authenticator { } @Override - public boolean configuredFor(UserModel user) { + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java index c678c4265f..a75015876c 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticator.java @@ -3,6 +3,8 @@ package org.keycloak.authentication.authenticators; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticatorContext; import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; @@ -61,8 +63,8 @@ public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator { } @Override - public boolean configuredFor(UserModel user) { - return user.configuredForCredentialType(UserCredentialModel.TOTP); + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp(); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java index 17419a1368..98f532af01 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java @@ -2,7 +2,10 @@ package org.keycloak.authentication.authenticators; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.AuthenticatorContext; +import org.keycloak.events.Errors; import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.representations.idm.CredentialRepresentation; @@ -42,6 +45,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.PASSWORD); if (password == null) { + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); Response challengeResponse = badPassword(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; @@ -49,6 +53,7 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat credentials.add(UserCredentialModel.password(password)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); Response challengeResponse = badPassword(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; @@ -62,8 +67,8 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat } @Override - public boolean configuredFor(UserModel user) { - return user.configuredForCredentialType(UserCredentialModel.PASSWORD); + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return session.users().configuredForCredentialType(UserCredentialModel.PASSWORD, realm, user); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java index 56707f9a3a..14ae6a1661 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticator.java @@ -1,30 +1,29 @@ package org.keycloak.authentication.authenticators; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; -import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorContext; +import org.keycloak.events.Details; +import org.keycloak.events.Errors; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticatorModel; -import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.services.managers.AuthenticationManager; -import org.keycloak.services.managers.ClientSessionCode; -import org.keycloak.services.messages.Messages; -import org.keycloak.services.resources.LoginActionsService; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import java.net.URI; /** * @author Bill Burke * @version $Revision: 1 $ */ -public class LoginFormUsernameAuthenticator implements Authenticator { +public class LoginFormUsernameAuthenticator extends AbstractFormAuthenticator implements Authenticator { protected AuthenticatorModel model; public LoginFormUsernameAuthenticator(AuthenticatorModel model) { @@ -47,15 +46,19 @@ public class LoginFormUsernameAuthenticator implements Authenticator { context.challenge(challengeResponse); return; } - validateUser(context); - } - - protected boolean isActionUrl(AuthenticatorContext context) { - URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName()); - String current = context.getUriInfo().getAbsolutePath().getPath(); - String expectedPath = expected.getPath(); - return expectedPath.equals(current); + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + if (formData.containsKey("cancel")) { + context.getEvent().error(Errors.REJECTED_BY_USER); + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod()); + protocol.setRealm(context.getRealm()) + .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) + .setUriInfo(context.getUriInfo()); + Response response = protocol.cancelLogin(context.getClientSession()); + context.challenge(response); + return; + } + validateUser(context, formData); } @Override @@ -71,71 +74,24 @@ public class LoginFormUsernameAuthenticator implements Authenticator { return forms.createLogin(); } - protected LoginFormsProvider loginForm(AuthenticatorContext context) { - ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession()); - code.setAction(ClientSessionModel.Action.AUTHENTICATE); - URI action = LoginActionsService.authenticationFormProcessor(context.getUriInfo()) - .queryParam(OAuth2Constants.CODE, code.getCode()) - .build(context.getRealm().getName()); - return context.getSession().getProvider(LoginFormsProvider.class) - .setActionUri(action) - .setClientSessionCode(code.getCode()); - } - - protected Response invalidUser(AuthenticatorContext context) { - return loginForm(context).setError(Messages.INVALID_USER).createLogin(); - } - - protected Response disabledUser(AuthenticatorContext context) { - return loginForm(context).setError(Messages.ACCOUNT_DISABLED).createLogin(); - } - - protected Response temporarilyDisabledUser(AuthenticatorContext context) { - return loginForm(context).setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin(); - } - - public void validateUser(AuthenticatorContext context) { - MultivaluedMap inputData = context.getHttpRequest().getFormParameters(); + public void validateUser(AuthenticatorContext context, MultivaluedMap inputData) { String username = inputData.getFirst(AuthenticationManager.FORM_USERNAME); if (username == null) { + context.getEvent().error(Errors.USER_NOT_FOUND); Response challengeResponse = invalidUser(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse); return; } + context.getEvent().detail(Details.USERNAME, username); + context.getClientSession().setNote("FORM_USERNAME", username); UserModel user = KeycloakModelUtils.findUserByNameOrEmail(context.getSession(), context.getRealm(), username); if (invalidUser(context, user)) return; context.setUser(user); context.success(); } - public boolean invalidUser(AuthenticatorContext context, UserModel user) { - if (user == null) { - Response challengeResponse = invalidUser(context); - context.failureChallenge(AuthenticationProcessor.Error.INVALID_USER, challengeResponse); - return true; - } - if (!user.isEnabled()) { - Response challengeResponse = disabledUser(context); - context.failureChallenge(AuthenticationProcessor.Error.USER_DISABLED, challengeResponse); - return true; - } - if (context.getRealm().isBruteForceProtected()) { - if (context.getProtector().isTemporarilyDisabled(context.getSession(), context.getRealm(), user.getUsername())) { - Response challengeResponse = temporarilyDisabledUser(context); - context.failureChallenge(AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED, challengeResponse); - return true; - } - } - return false; - } - - public Response challenge(AuthenticatorContext context) { - MultivaluedMap formData = new MultivaluedMapImpl<>(); - return challenge(context, formData); - } - @Override - public boolean configuredFor(UserModel user) { + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { return true; } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java index d93836f8c3..b5bf2d00ee 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java @@ -1,16 +1,16 @@ package org.keycloak.authentication.authenticators; -import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorContext; +import org.keycloak.events.Errors; import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; -import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.resources.LoginActionsService; @@ -24,7 +24,7 @@ import java.util.List; * @author Bill Burke * @version $Revision: 1 $ */ -public class OTPFormAuthenticator implements Authenticator { +public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator { protected AuthenticatorModel model; public OTPFormAuthenticator(AuthenticatorModel model) { @@ -33,8 +33,7 @@ public class OTPFormAuthenticator implements Authenticator { @Override public void authenticate(AuthenticatorContext context) { - URI expected = LoginActionsService.authenticationFormProcessor(context.getUriInfo()).build(context.getRealm().getName()); - if (!expected.getPath().equals(context.getUriInfo().getPath())) { + if (!isActionUrl(context)) { Response challengeResponse = challenge(context); context.challenge(challengeResponse); return; @@ -48,12 +47,13 @@ public class OTPFormAuthenticator implements Authenticator { String password = inputData.getFirst(CredentialRepresentation.TOTP); if (password == null) { Response challengeResponse = challenge(context); - context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); + context.challenge(challengeResponse); return; } credentials.add(UserCredentialModel.totp(password)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); Response challengeResponse = challenge(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; @@ -67,23 +67,20 @@ public class OTPFormAuthenticator implements Authenticator { return true; } - protected Response challenge(AuthenticatorContext context, MultivaluedMap formData) { + protected Response challenge(AuthenticatorContext context) { + ClientSessionCode clientSessionCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + URI action = AbstractFormAuthenticator.getActionUrl(context, clientSessionCode); LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class) - .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()); + .setActionUri(action) + .setClientSessionCode(clientSessionCode.getCode()); - if (formData.size() > 0) forms.setFormData(formData); return forms.createLoginTotp(); } - public Response challenge(AuthenticatorContext context) { - MultivaluedMap formData = new MultivaluedMapImpl<>(); - return challenge(context, formData); - } - @Override - public boolean configuredFor(UserModel user) { - return user.configuredForCredentialType(UserCredentialModel.TOTP); + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return session.users().configuredForCredentialType(UserCredentialModel.TOTP, realm, user) && user.isTotp(); } @Override diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java new file mode 100755 index 0000000000..1045b558f2 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java @@ -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 Bill Burke + * @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 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() { + + } +} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java new file mode 100755 index 0000000000..a541305d17 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java @@ -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 Bill Burke + * @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 getConfigProperties() { + return null; + } +} diff --git a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java index 82ed245b41..99528c8316 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java @@ -217,6 +217,12 @@ public class TokenManager { } } clientSession.setProtocolMappers(requestedProtocolMappers); + + Map transferredNotes = clientSession.getUserSessionNotes(); + for (Map.Entry entry : transferredNotes.entrySet()) { + session.setNote(entry.getKey(), entry.getValue()); + } + } public static void dettachClientSession(UserSessionProvider sessions, RealmModel realm, ClientSessionModel clientSession) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java index fdb029eeae..85e4067f97 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java @@ -6,7 +6,6 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.OAuth2Constants; import org.keycloak.authentication.AuthenticationProcessor; -import org.keycloak.authentication.authenticators.AuthenticationFlow; import org.keycloak.constants.AdapterConstants; import org.keycloak.events.Details; import org.keycloak.events.Errors; @@ -44,6 +43,7 @@ import java.util.List; public class AuthorizationEndpoint { private static final Logger logger = Logger.getLogger(AuthorizationEndpoint.class); + public static final String CODE_AUTH_TYPE = "code"; private enum Action { REGISTER, CODE @@ -247,10 +247,18 @@ public class AuthorizationEndpoint { return buildRedirectToIdentityProvider(idpHint, accessCode); } - return oldBrowserAuthentication(accessCode); + return newBrowserAuthentication(accessCode); } protected Response newBrowserAuthentication(String accessCode) { + List identityProviders = realm.getIdentityProviders(); + for (IdentityProviderModel identityProvider : identityProviders) { + if (identityProvider.isAuthenticateByDefault()) { + return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode); + } + } + clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); + String flowId = null; for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { if (flow.getAlias().equals("browser")) { diff --git a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java index 4329591394..9221268e8a 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -343,6 +343,7 @@ public class LoginActionsService { } catch (AuthenticationProcessor.AuthException e) { return handleError(e, code); } catch (Exception e) { + event.error(Errors.INVALID_USER_CREDENTIALS); logger.error("failed authentication", e); return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE); diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory index 20dff3bd8f..8fbf02e5c7 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -2,4 +2,5 @@ org.keycloak.authentication.authenticators.CookieAuthenticatorFactory org.keycloak.authentication.authenticators.LoginFormOTPAuthenticatorFactory org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory org.keycloak.authentication.authenticators.LoginFormUsernameAuthenticatorFactory -org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory \ No newline at end of file +org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory +org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java index ce805271c6..ea8fab233e 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/AssertEvents.java @@ -21,6 +21,9 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocolService; +import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint; import org.keycloak.representations.idm.UserRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.rule.KeycloakRule; @@ -119,9 +122,9 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { public ExpectedEvent expectLogin() { return expect(EventType.LOGIN) .detail(Details.CODE_ID, isCodeId()) - .detail(Details.USERNAME, DEFAULT_USERNAME) - .detail(Details.RESPONSE_TYPE, "code") - .detail(Details.AUTH_METHOD, "form") + //.detail(Details.USERNAME, DEFAULT_USERNAME) + //.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL) + //.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE) .detail(Details.REDIRECT_URI, DEFAULT_REDIRECT_URI) .session(isUUID()); } @@ -341,12 +344,13 @@ public class AssertEvents implements TestRule, EventListenerProviderFactory { Assert.assertThat("Unexpected value for " + d.getKey(), actualValue, d.getValue()); } - + /* for (String k : actual.getDetails().keySet()) { if (!details.containsKey(k)) { Assert.fail(k + " was not expected"); } } + */ } return actual; diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java index b8cf2a8dba..5bfabae083 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/account/AccountTest.java @@ -241,7 +241,7 @@ public class AccountTest { Assert.assertEquals("Invalid username or password.", loginPage.getError()); - events.expectLogin().session((String) null).error("invalid_user_credentials").assertEvent(); + events.expectLogin().session((String) null).user((String)null).error("invalid_user_credentials").assertEvent(); loginPage.open(); loginPage.login("test-user@localhost", "new-password"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java index 5b12823c62..b759b3e843 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/actions/RequiredActionTotpSetupTest.java @@ -45,6 +45,7 @@ import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.KeycloakRule.KeycloakSetup; import org.keycloak.testsuite.rule.WebResource; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.testsuite.utils.CredentialHelper; import org.openqa.selenium.WebDriver; /** @@ -57,6 +58,7 @@ public class RequiredActionTotpSetupTest { @Override public void config(RealmManager manager, RealmModel defaultRealm, RealmModel appRealm) { + CredentialHelper.setRequiredCredential(CredentialRepresentation.TOTP, appRealm); appRealm.addRequiredCredential(CredentialRepresentation.TOTP); appRealm.setResetPasswordAllowed(true); } @@ -137,6 +139,7 @@ public class RequiredActionTotpSetupTest { loginPage.open(); loginPage.login("test-user@localhost", "password"); + String src = driver.getPageSource(); loginTotpPage.login(totp.generate(totpSecret)); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); @@ -181,7 +184,7 @@ public class RequiredActionTotpSetupTest { // Login with one-time password loginTotpPage.login(totp.generate(totpCode)); - loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + loginEvent = events.expectLogin().user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent(); // Open account page accountTotpPage.open(); @@ -204,11 +207,11 @@ public class RequiredActionTotpSetupTest { totpPage.assertCurrent(); totpPage.configure(totp.generate(totpPage.getTotpSecret())); - String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setupTotp2").assertEvent().getSessionId(); + String sessionId = events.expectRequiredAction(EventType.UPDATE_TOTP).user(userId).detail(Details.USERNAME, "setuptotp2").assertEvent().getSessionId(); Assert.assertEquals(RequestType.AUTH_RESPONSE, appPage.getRequestType()); - events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setupTotp2").assertEvent(); + events.expectLogin().user(userId).session(sessionId).detail(Details.USERNAME, "setuptotp2").assertEvent(); } } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index 5e1930e3ee..c5b25fdd90 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -108,7 +108,7 @@ public abstract class AbstractKerberosTest { .client("kerberos-app") .user(keycloakRule.getUser("test", "hnelson").getId()) .detail(Details.REDIRECT_URI, KERBEROS_APP_URL) - .detail(Details.AUTH_METHOD, "spnego") + //.detail(Details.AUTH_METHOD, "spnego") .detail(Details.USERNAME, "hnelson") .assertEvent(); @@ -164,7 +164,7 @@ public abstract class AbstractKerberosTest { .client("kerberos-app") .user(keycloakRule.getUser("test", "jduke").getId()) .detail(Details.REDIRECT_URI, KERBEROS_APP_URL) - .detail(Details.AUTH_METHOD, "spnego") + //.detail(Details.AUTH_METHOD, "spnego") .detail(Details.USERNAME, "jduke") .assertEvent(); spnegoResponse.close(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java old mode 100644 new mode 100755 index 699d85be87..2841e25af9 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java @@ -18,11 +18,14 @@ import org.keycloak.constants.KerberosConstants; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserFederationProviderModel; +import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.rule.KerberosRule; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebRule; +import org.keycloak.testsuite.utils.CredentialHelper; +import org.picketlink.idm.credential.util.CredentialUtils; /** * Test of KerberosFederationProvider (Kerberos not backed by LDAP) @@ -41,6 +44,8 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + + CredentialHelper.setRequiredCredential(CredentialRepresentation.KERBEROS, appRealm); URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json"); keycloakRule.deployApplication("kerberos-portal", "/kerberos-portal", KerberosCredDelegServlet.class, url.getPath(), "user"); @@ -131,4 +136,6 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { keycloakRule.stopSession(session, true); } } + + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java new file mode 100755 index 0000000000..3018fac287 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java @@ -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 Bill Burke + * @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; + + } +}