From a1f7cfaf3ac81890411867ef1ffedc393124dcda Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 27 May 2015 10:39:46 -0400 Subject: [PATCH] auth spi initialization --- .../migration/MigrationModelManager.java | 7 + .../migrators/MigrateTo1_3_0_Beta1.java | 27 +++ .../models/AuthenticationExecutionModel.java | 19 +- .../utils/DefaultAuthenticationFlows.java | 92 ++++++++++ .../AuthenticationProcessor.java | 162 ++++++++++++------ .../services/managers/RealmManager.java | 8 + 6 files changed, 262 insertions(+), 53 deletions(-) create mode 100755 model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_3_0_Beta1.java create mode 100755 model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java diff --git a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java index 62dcd68dd5..7f52ab38e5 100755 --- a/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java +++ b/model/api/src/main/java/org/keycloak/migration/MigrationModelManager.java @@ -1,6 +1,7 @@ package org.keycloak.migration; import org.jboss.logging.Logger; +import org.keycloak.migration.migrators.MigrateTo1_3_0_Beta1; import org.keycloak.migration.migrators.MigrationTo1_2_0_CR1; import org.keycloak.models.KeycloakSession; @@ -24,6 +25,12 @@ public class MigrationModelManager { } new MigrationTo1_2_0_CR1().migrate(session); } + if (stored == null || stored.lessThan(MigrateTo1_3_0_Beta1.VERSION)) { + if (stored != null) { + logger.debug("Migrating older model to 1.3.0.Beta1 updates"); + } + new MigrateTo1_3_0_Beta1().migrate(session); + } model.setStoredVersion(MigrationModel.LATEST_VERSION); } diff --git a/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_3_0_Beta1.java b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_3_0_Beta1.java new file mode 100755 index 0000000000..ccf3c75373 --- /dev/null +++ b/model/api/src/main/java/org/keycloak/migration/migrators/MigrateTo1_3_0_Beta1.java @@ -0,0 +1,27 @@ +package org.keycloak.migration.migrators; + +import org.keycloak.migration.ModelVersion; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; + +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class MigrateTo1_3_0_Beta1 { + public static final ModelVersion VERSION = new ModelVersion("1.3.0.Beta1"); + + + public void migrate(KeycloakSession session) { + List realms = session.realms().getRealms(); + for (RealmModel realm : realms) { + if (realm.getAuthenticationFlows().size() == 0) { + DefaultAuthenticationFlows.addFlows(realm); + } + } + + } +} diff --git a/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java b/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java index 8cea65bc80..79a2b67e77 100755 --- a/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java +++ b/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java @@ -78,6 +78,23 @@ public class AuthenticationExecutionModel { public enum Requirement { REQUIRED, OPTIONAL, - ALTERNATIVE + ALTERNATIVE, + DISABLED + } + + public boolean isRequired() { + return requirement == Requirement.REQUIRED; + } + public boolean isOptional() { + return requirement == Requirement.OPTIONAL; + } + public boolean isAlternative() { + return requirement == Requirement.ALTERNATIVE; + } + public boolean isDisabled() { + return requirement == Requirement.DISABLED; + } + public boolean isEnabled() { + return requirement != Requirement.DISABLED; } } 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 new file mode 100755 index 0000000000..05a5a4a71c --- /dev/null +++ b/model/api/src/main/java/org/keycloak/models/utils/DefaultAuthenticationFlows.java @@ -0,0 +1,92 @@ +package org.keycloak.models.utils; + +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.RealmModel; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class DefaultAuthenticationFlows { + public static void addFlows(RealmModel realm) { + AuthenticatorModel model = new AuthenticatorModel(); + model.setProviderId("auth-cookie"); + model.setAlias("Cookie"); + AuthenticatorModel cookieAuth = realm.addAuthenticator(model); + model = new AuthenticatorModel(); + model.setProviderId("auth-login-form-otp"); + model.setAlias("Login Form OTP"); + AuthenticatorModel loginFormOtp = realm.addAuthenticator(model); + model = new AuthenticatorModel(); + model.setProviderId("auth-login-form-password"); + model.setAlias("Login Form Password"); + AuthenticatorModel password = realm.addAuthenticator(model); + model = new AuthenticatorModel(); + model.setProviderId("auth-login-form-username"); + model.setAlias("Login Form Username"); + AuthenticatorModel username = realm.addAuthenticator(model); + model = new AuthenticatorModel(); + model.setProviderId("auth-otp-form"); + model.setAlias("Single OTP Form"); + AuthenticatorModel otp = realm.addAuthenticator(model); + + AuthenticationFlowModel browser = new AuthenticationFlowModel(); + browser.setAlias("browser"); + browser.setDescription("browser based authentication"); + browser = realm.addAuthenticationFlow(browser); + AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); + execution.setParentFlow(browser.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE); + execution.setAuthenticator(cookieAuth.getId()); + execution.setPriority(0); + execution.setUserSetupAllowed(false); + execution.setAutheticatorFlow(false); + realm.addAuthenticatorExecution(execution); + AuthenticationFlowModel forms = new AuthenticationFlowModel(); + forms.setAlias("forms"); + 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.setUserSetupAllowed(false); + execution.setAutheticatorFlow(true); + realm.addAuthenticatorExecution(execution); + + // forms + // Username processing + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(forms.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(username.getId()); + execution.setPriority(10); + execution.setUserSetupAllowed(false); + execution.setAutheticatorFlow(false); + realm.addAuthenticatorExecution(execution); + + // password processing + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(forms.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); + execution.setAuthenticator(password.getId()); + execution.setPriority(11); + execution.setUserSetupAllowed(false); + execution.setAutheticatorFlow(false); + realm.addAuthenticatorExecution(execution); + + // otp processing + execution = new AuthenticationExecutionModel(); + execution.setParentFlow(forms.getId()); + execution.setRequirement(AuthenticationExecutionModel.Requirement.OPTIONAL); + execution.setAuthenticator(otp.getId()); + execution.setPriority(12); + execution.setUserSetupAllowed(true); + execution.setAutheticatorFlow(false); + realm.addAuthenticatorExecution(execution); + + } +} diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 5b20edf43c..5920e101f8 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -1,9 +1,11 @@ package org.keycloak.authentication; +import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.events.EventBuilder; import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; @@ -23,40 +25,18 @@ import java.util.Map; * @author Bill Burke * @version $Revision: 1 $ */ - -// -// setup -// cookie: master, alternative -// CERT_AUTH: alternative -// UserPassword: alternative -// OTP: optional -// CAPTHA: required -// -// scenario: username password -// * cookie, attempted -// * cert, attempated -// * usernamepassord, doesn't see form, sets challenge to form -// -// -// -// -// -// -// - - - public class AuthenticationProcessor { + protected static Logger logger = Logger.getLogger(AuthenticationProcessor.class); protected RealmModel realm; protected UserSessionModel userSession; protected ClientSessionModel clientSession; protected ClientConnection connection; protected UriInfo uriInfo; protected KeycloakSession session; - protected List executions; protected BruteForceProtector protector; protected EventBuilder eventBuilder; protected HttpRequest request; + protected String flowId; public static enum Status { @@ -98,6 +78,50 @@ public class AuthenticationProcessor { return session; } + public AuthenticationProcessor setRealm(RealmModel realm) { + this.realm = realm; + return this; + } + + public AuthenticationProcessor setClientSession(ClientSessionModel clientSession) { + this.clientSession = clientSession; + return this; + } + + public AuthenticationProcessor setConnection(ClientConnection connection) { + this.connection = connection; + return this; + } + + public AuthenticationProcessor setUriInfo(UriInfo uriInfo) { + this.uriInfo = uriInfo; + return this; + } + + public AuthenticationProcessor setSession(KeycloakSession session) { + this.session = session; + return this; + } + + public AuthenticationProcessor setProtector(BruteForceProtector protector) { + this.protector = protector; + return this; + } + + public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) { + this.eventBuilder = eventBuilder; + return this; + } + + public AuthenticationProcessor setRequest(HttpRequest request) { + this.request = request; + return this; + } + + public AuthenticationProcessor setFlowId(String flowId) { + this.flowId = flowId; + return this; + } private class Result implements AuthenticatorContext { AuthenticatorModel model; @@ -261,35 +285,74 @@ public class AuthenticationProcessor { } - protected boolean isProcessed(UserSessionModel.AuthenticatorStatus status) { + protected boolean isProcessed(AuthenticationExecutionModel model) { + if (model.isDisabled()) return true; + UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId()); + if (status == null) return false; return status == UserSessionModel.AuthenticatorStatus.SUCCESS || status == UserSessionModel.AuthenticatorStatus.SKIPPED || status == UserSessionModel.AuthenticatorStatus.ATTEMPTED || status == UserSessionModel.AuthenticatorStatus.SETUP_REQUIRED; } - public Response authenticate() { + public boolean isSuccessful(AuthenticationExecutionModel model) { + UserSessionModel.AuthenticatorStatus status = clientSession.getAuthenticators().get(model.getId()); + if (status == null) return false; + return status == UserSessionModel.AuthenticatorStatus.SUCCESS; + } + + public Response authenticate() throws AuthException { UserModel authUser = clientSession.getAuthenticatedUser(); validateUser(authUser); - Response challenge = null; - Map previousAttempts = clientSession.getAuthenticators(); + Response challenge = processFlow(flowId); + if (challenge != null) return challenge; + if (clientSession.getAuthenticatedUser() == null) { + throw new AuthException(Error.UNKNOWN_USER); + } + return authenticationComplete(); + + } + + public Response processFlow(String flowId) { + AuthenticationFlowModel flow = realm.getAuthenticationFlowById(flowId); + if (flow == null) { + logger.error("Unknown flow to execute with"); + throw new AuthException(Error.INTERNAL_ERROR); + } + List executions = realm.getAuthenticationExecutions(flowId); + if (executions == null) return null; + Response alternativeChallenge = null; + boolean alternativeSuccessful = false; for (AuthenticationExecutionModel model : executions) { - UserSessionModel.AuthenticatorStatus oldStatus = previousAttempts.get(model.getId()); - if (isProcessed(oldStatus)) continue; + if (isProcessed(model)) { + if (!alternativeSuccessful && model.isAlternative() && isSuccessful(model)) alternativeSuccessful = true; + continue; + } + Result context = null; + if (model.isAlternative() && alternativeSuccessful) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + continue; + } + if (model.isAutheticatorFlow()) { + Response flowResponse = processFlow(model.getAuthenticator()); + if (flowResponse == null) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SUCCESS); + if (model.isAlternative()) alternativeSuccessful = true; + continue; + } else { + return flowResponse; + } + + } AuthenticatorModel authenticatorModel = realm.getAuthenticatorById(model.getAuthenticator()); AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, authenticatorModel.getProviderId()); Authenticator authenticator = factory.create(authenticatorModel); + UserModel authUser = clientSession.getAuthenticatedUser(); + if (authenticator.requiresUser() && authUser == null){ - if ( authenticator.requiresUser()) { - if (challenge != null) return challenge; - throw new AuthException(Error.UNKNOWN_USER); - } + if (alternativeChallenge != null) return alternativeChallenge; + throw new AuthException(Error.UNKNOWN_USER); } - if (authUser != null && model.getRequirement() == AuthenticationExecutionModel.Requirement.ALTERNATIVE) { - clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); - continue; - } - authUser = clientSession.getAuthenticatedUser(); if (authenticator.requiresUser() && authUser != null && !authenticator.configuredFor(authUser)) { if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { @@ -303,19 +366,21 @@ public class AuthenticationProcessor { } continue; } - Result context = new Result(authenticatorModel, authenticator); + context = new Result(authenticatorModel, authenticator); authenticator.authenticate(context); Status result = context.getStatus(); if (result == Status.SUCCESS){ clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SUCCESS); - //if (model.isMasterAuthenticator()) return authenticationComplete(); + if (model.isAlternative()) alternativeSuccessful = true; continue; } else if (result == Status.FAILED) { + logUserFailure(); if (context.challenge != null) return context.challenge; throw new AuthException(context.error); } else if (result == Status.CHALLENGE) { - if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) return context.challenge; - if (challenge != null) challenge = context.challenge; + if (model.isRequired()) return context.challenge; + else if (model.isAlternative()) alternativeChallenge = context.challenge; + else clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); continue; } else if (result == Status.FAILURE_CHALLENGE) { logUserFailure(); @@ -326,14 +391,7 @@ public class AuthenticationProcessor { continue; } } - - if (authUser == null) { - if (challenge != null) return challenge; - throw new AuthException(Error.UNKNOWN_USER); - } - - - return authenticationComplete(); + return null; } @@ -349,7 +407,7 @@ public class AuthenticationProcessor { } } - protected Response authenticationComplete() { + protected Response authenticationComplete() { 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.setState(UserSessionModel.State.LOGGING_IN); diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java index f2caef83f9..41f5ac6b49 100755 --- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java +++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java @@ -16,6 +16,7 @@ import org.keycloak.models.RoleModel; import org.keycloak.models.UserFederationProviderModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionProvider; +import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.representations.idm.ClientRepresentation; @@ -86,10 +87,15 @@ public class RealmManager { setupAccountManagement(realm); setupBrokerService(realm); setupAdminConsole(realm); + setupAuthenticationFlows(realm); return realm; } + protected void setupAuthenticationFlows(RealmModel realm) { + if (realm.getAuthenticationFlows().size() == 0) DefaultAuthenticationFlows.addFlows(realm); + } + protected void setupAdminConsole(RealmModel realm) { ClientModel adminConsole = realm.getClientByClientId(Constants.ADMIN_CONSOLE_CLIENT_ID); if (adminConsole == null) adminConsole = new ClientManager(this).createClient(realm, Constants.ADMIN_CONSOLE_CLIENT_ID); @@ -254,6 +260,8 @@ public class RealmManager { RepresentationToModel.importRealm(session, rep, realm); + setupAuthenticationFlows(realm); + // Refresh periodic sync tasks for configured federationProviders List federationProviders = realm.getUserFederationProviders(); UsersSyncManager usersSyncManager = new UsersSyncManager();