diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml new file mode 100755 index 0000000000..80f0e3672a --- /dev/null +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-1.4.0.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + ACTION = 0 + + + + ACTION = 1 + + + + ACTION = 2 + + + + ACTION = 3 + + + + ACTION = 4 + + + + ACTION = 5 + + + + ACTION = 6 + + + + ACTION = 7 + + + + ACTION = 8 + + + + ACTION = 9 + + + + + + + + + + + + + + + + + diff --git a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml index f3f3f9039b..efba42ce0f 100755 --- a/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml +++ b/connections/jpa-liquibase/src/main/resources/META-INF/jpa-changelog-master.xml @@ -7,4 +7,5 @@ + diff --git a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java index ad4101678e..809f2f3aa4 100755 --- a/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java +++ b/connections/jpa/src/main/java/org/keycloak/connections/jpa/updater/JpaUpdaterProvider.java @@ -12,7 +12,7 @@ public interface JpaUpdaterProvider extends Provider { public String FIRST_VERSION = "1.0.0.Final"; - public String LAST_VERSION = "1.3.0.Beta1"; + public String LAST_VERSION = "1.4.0"; public String getCurrentVersionSql(); 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/distribution/subsystem-war/src/main/webapp/WEB-INF/web.xml b/distribution/subsystem-war/src/main/webapp/WEB-INF/web.xml index 7228afe95f..4257449a98 100755 --- a/distribution/subsystem-war/src/main/webapp/WEB-INF/web.xml +++ b/distribution/subsystem-war/src/main/webapp/WEB-INF/web.xml @@ -25,11 +25,6 @@ org.keycloak.services.listeners.KeycloakSessionDestroyListener - - Keycloak Client Connection Filter - org.keycloak.services.filters.ClientConnectionFilter - - Keycloak Session Management org.keycloak.services.filters.KeycloakSessionServletFilter 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 4a0a1ad007..73d98ffb70 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/events/api/src/main/java/org/keycloak/events/EventBuilder.java b/events/api/src/main/java/org/keycloak/events/EventBuilder.java old mode 100644 new mode 100755 index ee7a70a7cf..641f2335f3 --- a/events/api/src/main/java/org/keycloak/events/EventBuilder.java +++ b/events/api/src/main/java/org/keycloak/events/EventBuilder.java @@ -140,7 +140,9 @@ public class EventBuilder { } public void error(String error) { - event.setType(EventType.valueOf(event.getType().name() + "_ERROR")); + if (!event.getType().name().endsWith("_ERROR")) { + event.setType(EventType.valueOf(event.getType().name() + "_ERROR")); + } event.setError(error); send(); } diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js index cc3092fa1a..822e688ccc 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/app.js @@ -1045,6 +1045,16 @@ module.config([ '$routeProvider', function($routeProvider) { }, controller : 'ProtocolListCtrl' }) + .when('/realms/:realm/authentication', { + templateUrl : resourceUrl + '/partials/authentication-flows.html', + resolve : { + realm : function(RealmLoader) { + return RealmLoader(); + } + }, + controller : 'AuthenticationFlowsCtrl' + }) + .when('/server-info', { templateUrl : resourceUrl + '/partials/server-info.html' }) diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js index 53ad7ae531..a88ffd73ab 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/realm.js @@ -1572,6 +1572,45 @@ module.controller('IdentityProviderMapperCreateCtrl', function($scope, realm, id }); +module.controller('AuthenticationFlowsCtrl', function($scope, realm, AuthenticationExecutions, Notifications, Dialog, $location) { + $scope.realm = realm; + var setupForm = function() { + AuthenticationExecutions.query({realm: realm.realm, alias: 'browser'}, function(data) { + $scope.executions = data; + $scope.flowmax = 0; + for (var i = 0; i < $scope.executions.length; i++ ) { + execution = $scope.executions[i]; + if (execution.requirementChoices.length > $scope.flowmax) { + $scope.flowmax = execution.requirementChoices.length; + } + } + for (var i = 0; i < $scope.executions.length; i++ ) { + execution = $scope.executions[i]; + execution.empties = []; + for (j = 0; j < $scope.flowmax - execution.requirementChoices.length; j++) { + execution.empties.push(j); + } + } + }) + }; + + $scope.updateExecution = function(execution) { + var copy = angular.copy(execution); + delete copy.empties; + AuthenticationExecutions.update({realm: realm.realm, alias: 'browser'}, copy, function() { + Notifications.success("Auth requirement updated"); + setupForm(); + }); + + }; + + + setupForm(); + + +}); + + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js index 92e5db2c38..a2573cbd78 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/controllers/users.js @@ -195,7 +195,7 @@ module.controller('UserListCtrl', function($scope, realm, User) { -module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, $location, Dialog, Notifications) { +module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFederationInstances, RequiredActions, $location, Dialog, Notifications) { $scope.realm = realm; $scope.create = !user.id; $scope.editUsername = $scope.create || $scope.realm.editUsernameAllowed; @@ -219,14 +219,29 @@ module.controller('UserDetailCtrl', function($scope, realm, user, User, UserFede } $scope.changed = false; // $scope.create; - + if (user.requiredActions) { + for (var i = 0; i < user.requiredActions.length; i++) { + console.log("user require action: " + user.requiredActions[i]); + } + } // ID - Name map for required actions. IDs are enum names. - $scope.userReqActionList = [ + RequiredActions.query({id: realm.realm}, function(data) { + $scope.userReqActionList = []; + for (var i = 0; i < data.length; i++) { + console.log("listed required action: " + data[i].text); + item = { id: data[i].id, text: data[i].text }; + $scope.userReqActionList.push(item); + } + + }); + + /*[ {id: "VERIFY_EMAIL", text: "Verify Email"}, {id: "UPDATE_PROFILE", text: "Update Profile"}, {id: "CONFIGURE_TOTP", text: "Configure Totp"}, {id: "UPDATE_PASSWORD", text: "Update Password"} ]; + */ $scope.$watch('user', function() { if (!angular.equals($scope.user, user)) { diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index 763d57f004..067bb2077d 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js @@ -186,6 +186,12 @@ module.factory('RealmAdminEvents', function($resource) { }); }); +module.factory('RequiredActions', function($resource) { + return $resource(authUrl + '/admin/realms/:id/required-actions', { + id : '@realm' + }); +}); + module.factory('RealmLDAPConnectionTester', function($resource) { return $resource(authUrl + '/admin/realms/:realm/testLDAPConnection'); }); @@ -1067,3 +1073,15 @@ module.factory('IdentityProviderMapper', function($resource) { }); }); +module.factory('AuthenticationExecutions', function($resource) { + return $resource(authUrl + '/admin/realms/:realm/authentication-flows/flow/:alias/executions', { + realm : '@realm', + alias : '@alias' + }, { + update : { + method : 'PUT' + } + }); +}); + + diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html new file mode 100755 index 0000000000..b3ecf710a4 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/partials/authentication-flows.html @@ -0,0 +1,64 @@ +
+

Authentication Flows {{realm.realm|capitalize}}

+ + + + + + + + + + + + + + + + + + + + + +
Auth TypeRequirement

{{execution.referenceType}}

+ + + + +
No executions available
+
+ + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html index f5521ad7c3..ba09d84c63 100755 --- a/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html +++ b/forms/common-themes/src/main/resources/theme/base/admin/resources/templates/kc-menu.html @@ -16,6 +16,7 @@
  • Roles
  • Identity Providers
  • User Federation
  • +
  • Authentication
  • diff --git a/forms/common-themes/src/main/resources/theme/base/login/bypass_kerberos.ftl b/forms/common-themes/src/main/resources/theme/base/login/bypass_kerberos.ftl new file mode 100755 index 0000000000..d87163c1a1 --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/login/bypass_kerberos.ftl @@ -0,0 +1,25 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "title"> + ${msg("kerberosNotConfiguredTitle")} + <#elseif section = "header"> + ${msg("kerberosNotConfigured")} + <#elseif section = "form"> +
    + +

    ${msg("bypassKerberosDetail")}

    +
    +
    +
    +
    + +
    +
    + <#if client?? && client.baseUrl?has_content> +

    ${msg("backToApplication")}

    + +
    +
    +
    + + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/login/login.ftl b/forms/common-themes/src/main/resources/theme/base/login/login.ftl index e3a3456693..1f8cd86cf1 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login.ftl @@ -1,76 +1,76 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout displayInfo=social.displayInfo; section> - <#if section = "title"> - ${msg("loginTitle",(realm.name!''))} - <#elseif section = "header"> - ${msg("loginTitleHtml",(realm.name!''))} - <#elseif section = "form"> - <#if realm.password> -
    -
    -
    - -
    - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    -
    - -
    -
    - <#if realm.rememberMe> -
    - -
    - -
    - <#if realm.resetPasswordAllowed> - ${msg("doForgotPassword")} - -
    -
    - -
    -
    - - -
    -
    -
    -
    - - <#elseif section = "info" > - <#if realm.password && realm.registrationAllowed> -
    - ${msg("noAccount")} ${msg("doRegister")} -
    - - - <#if realm.password && social.providers??> -
    - -
    - - - +<#import "template.ftl" as layout> +<@layout.registrationLayout displayInfo=social.displayInfo; section> + <#if section = "title"> + ${msg("loginTitle",(realm.name!''))} + <#elseif section = "header"> + ${msg("loginTitleHtml",(realm.name!''))} + <#elseif section = "form"> + <#if realm.password> +
    +
    +
    + +
    + +
    + +
    +
    + +
    +
    + +
    + +
    + +
    +
    + +
    +
    + <#if realm.rememberMe> +
    + +
    + +
    + <#if realm.resetPasswordAllowed> + ${msg("doForgotPassword")} + +
    +
    + +
    +
    + + +
    +
    +
    +
    + + <#elseif section = "info" > + <#if realm.password && realm.registrationAllowed> +
    + ${msg("noAccount")} ${msg("doRegister")} +
    + + + <#if realm.password && social.providers??> +
    + +
    + + + diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties index d99be80159..6cbb05d6ca 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties @@ -4,8 +4,15 @@ doCancel=Abbrechen doSubmit=Absenden doYes=Ja doNo=Nein +doAccept=Accept +doDecline=Decline +doContinue=Continue doForgotPassword=Passwort vergessen? doClickHere=hier klicken +kerberosNotConfigured=Kerberos Not Configured +kerberosNotConfiguredTitle=Kerberos Not Configured +bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means +kerberosNotSetUp=Kerberos is not set up. You cannot login. registerWithTitle=Registrierung bei {0} registerWithTitleHtml=Registrierung bei {0} diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties index 9ee2d07643..7a5237dc48 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_en.properties @@ -4,9 +4,15 @@ doCancel=Cancel doSubmit=Submit doYes=Yes doNo=No +doContinue=Continue +doAccept=Accept +doDecline=Decline doForgotPassword=Forgot Password? doClickHere=Click here - +kerberosNotConfigured=Kerberos Not Configured +kerberosNotConfiguredTitle=Kerberos Not Configured +bypassKerberosDetail=Either you are not logged in via Kerberos or your browser is not set up for Kerberos login. Please click continue to login in through other means +kerberosNotSetUp=Kerberos is not set up. You cannot login. registerWithTitle=Register with {0} registerWithTitleHtml=Register with {0} loginTitle=Log in to {0} @@ -22,6 +28,8 @@ emailForgotTitle=Forgot Your Password? updatePasswordTitle=Update password codeSuccessTitle=Success code codeErrorTitle=Error code\: {0} +termsTitle=Terms and Conditions +termsTitleHtml=Terms and Conditions noAccount=New user? username=Username diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties index 8f7be077ed..5049b68f74 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_it.properties @@ -4,8 +4,15 @@ doCancel=Annulla doSubmit=Invia doYes=Si doNo=No +doAccept=Accept +doDecline=Decline +doContinue=Continue doForgotPassword=Password Dimenticata? doClickHere=Clicca qui +bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means +kerberosNotSetUp=Kerberos is not set up. You cannot login. +kerberosNotConfigured=Kerberos Not Configured +kerberosNotConfiguredTitle=Kerberos Not Configured registerWithTitle=Registrati come {0} registerWithTitleHtml=Registrati come {0} diff --git a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties index 4ed6f4d7da..4959003a7f 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties +++ b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_pt_BR.properties @@ -3,9 +3,16 @@ doRegister=Cadastre-se doCancel=Cancelar doSubmit=Ok doYes=Sim +doAccept=Accept +doDecline=Decline doNo=N\u00E3o +doContinue=Continue doForgotPassword=Esqueceu sua senha? doClickHere=Clique aqui +bypassKerberos=Your browser is not set up for Kerberos login. Please click continue to login in through other means +kerberosNotSetUp=Kerberos is not set up. You cannot login. +kerberosNotConfigured=Kerberos Not Configured +kerberosNotConfiguredTitle=Kerberos Not Configured registerWithTitle=Registre-se com {0} registerWithTitleHtml=Registre-se com {0} diff --git a/forms/common-themes/src/main/resources/theme/base/login/register.ftl b/forms/common-themes/src/main/resources/theme/base/login/register.ftl index aadd022cce..1927378255 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/register.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/register.ftl @@ -1,77 +1,125 @@ -<#import "template.ftl" as layout> -<@layout.registrationLayout; section> - <#if section = "title"> - ${msg("registerWithTitle",(realm.name!''))} - <#elseif section = "header"> - ${msg("registerWithTitleHtml",(realm.name!''))} - <#elseif section = "form"> -
    - <#if !realm.registrationEmailAsUsername> -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    -
    - -
    - - -
    - -
    -
    -
    - +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("registerWithTitle",(realm.name!''))} + <#elseif section = "header"> + ${msg("registerWithTitleHtml",(realm.name!''))} + <#elseif section = "form"> +
    + <#if !realm.registrationEmailAsUsername> +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + + <#if passwordRequired> +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    +
    + +
    + +
    + +
    +
    + + +
    + + +
    + +
    +
    +
    + \ No newline at end of file diff --git a/forms/common-themes/src/main/resources/theme/base/login/terms.ftl b/forms/common-themes/src/main/resources/theme/base/login/terms.ftl new file mode 100755 index 0000000000..2948b0e33e --- /dev/null +++ b/forms/common-themes/src/main/resources/theme/base/login/terms.ftl @@ -0,0 +1,29 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout displayMessage=false; section> + <#if section = "title"> + ${msg("termsTitle")} + <#elseif section = "header"> + ${msg("termsTitleHtml")} + <#elseif section = "form"> +
    + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    + + \ No newline at end of file diff --git a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java index adfd3f87c8..f176555691 100755 --- a/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java +++ b/forms/login-api/src/main/java/org/keycloak/login/LoginFormsProvider.java @@ -2,6 +2,7 @@ package org.keycloak.login; import java.net.URI; import java.util.List; +import java.util.Map; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MultivaluedMap; @@ -24,6 +25,8 @@ public interface LoginFormsProvider extends Provider { public Response createResponse(UserModel.RequiredAction action); + Response createForm(String form, Map attributes); + public Response createLogin(); public Response createPasswordReset(); @@ -68,6 +71,8 @@ public interface LoginFormsProvider extends Provider { public LoginFormsProvider setFormData(MultivaluedMap formData); + LoginFormsProvider setAttribute(String name, Object value); + public LoginFormsProvider setStatus(Response.Status status); LoginFormsProvider setActionUri(URI requestUri); diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/AuthenticatorConfiguredMethod.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/AuthenticatorConfiguredMethod.java new file mode 100755 index 0000000000..7327e793e9 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/AuthenticatorConfiguredMethod.java @@ -0,0 +1,36 @@ +package org.keycloak.login.freemarker; + +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.Urls; + +import java.net.URI; +import java.util.List; + +/** + */ +public class AuthenticatorConfiguredMethod implements TemplateMethodModelEx { + private final RealmModel realm; + private final UserModel user; + private final KeycloakSession session; + + public AuthenticatorConfiguredMethod(RealmModel realm, UserModel user, KeycloakSession session) { + this.realm = realm; + this.user = user; + this.session = session; + } + + @Override + public Object exec(List list) throws TemplateModelException { + String providerId = list.get(0).toString(); + Authenticator authenticator = session.getProvider(Authenticator.class, providerId); + return authenticator.configuredFor(session, realm, user); + } +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java index 3c0f56f8bc..413c701b2b 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/FreeMarkerLoginFormsProvider.java @@ -3,6 +3,7 @@ package org.keycloak.login.freemarker; import org.jboss.logging.Logger; import org.jboss.resteasy.specimpl.MultivaluedMapImpl; import org.keycloak.OAuth2Constants; +import org.keycloak.authentication.AuthenticationProcessor; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.freemarker.BrowserSecurityHeaderSetup; @@ -27,6 +28,7 @@ import org.keycloak.login.freemarker.model.OAuthGrantBean; import org.keycloak.login.freemarker.model.ProfileBean; import org.keycloak.login.freemarker.model.RealmBean; import org.keycloak.login.freemarker.model.RegisterBean; +import org.keycloak.login.freemarker.model.RequiredActionUrlFormatterMethod; import org.keycloak.login.freemarker.model.TotpBean; import org.keycloak.login.freemarker.model.UrlBean; import org.keycloak.models.ClientModel; @@ -84,6 +86,7 @@ import java.util.concurrent.TimeUnit; private UserModel user; private ClientSessionModel clientSession; + private final Map attributes = new HashMap(); public FreeMarkerLoginFormsProvider(KeycloakSession session, FreeMarkerUtil freeMarker) { this.session = session; @@ -159,8 +162,6 @@ import java.util.concurrent.TimeUnit; uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); } - Map attributes = new HashMap(); - ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); Theme theme; try { @@ -205,6 +206,10 @@ import java.util.concurrent.TimeUnit; uriBuilder.replaceQuery(null); } URI baseUri = uriBuilder.build(); + attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); + if (realm != null && user != null && session != null) { + attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session)); + } if (realm != null) { attributes.put("realm", new RealmBean(realm)); @@ -272,6 +277,105 @@ import java.util.concurrent.TimeUnit; } } + @Override + public Response createForm(String form, Map extraAttributes) { + + RealmModel realm = session.getContext().getRealm(); + ClientModel client = session.getContext().getClient(); + UriInfo uriInfo = session.getContext().getUri(); + + MultivaluedMap queryParameterMap = queryParams != null ? queryParams : new MultivaluedMapImpl(); + + String requestURI = uriInfo.getBaseUri().getPath(); + UriBuilder uriBuilder = UriBuilder.fromUri(requestURI); + + for (String k : queryParameterMap.keySet()) { + + Object[] objects = queryParameterMap.get(k).toArray(); + if (objects.length == 1 && objects[0] == null) continue; // + uriBuilder.replaceQueryParam(k, objects); + } + if (accessCode != null) { + uriBuilder.replaceQueryParam(OAuth2Constants.CODE, accessCode); + } + URI baseUri = uriBuilder.build(); + + ThemeProvider themeProvider = session.getProvider(ThemeProvider.class, "extending"); + Theme theme; + try { + theme = themeProvider.getTheme(realm.getLoginTheme(), Theme.Type.LOGIN); + } catch (IOException e) { + logger.error("Failed to create theme", e); + return Response.serverError().build(); + } + + try { + attributes.put("properties", theme.getProperties()); + } catch (IOException e) { + logger.warn("Failed to load properties", e); + } + if (client != null) { + attributes.put("client", new ClientBean(client)); + } + + Properties messagesBundle; + Locale locale = LocaleHelper.getLocale(realm, user, uriInfo, session.getContext().getRequestHeaders()); + try { + messagesBundle = theme.getMessages(locale); + attributes.put("msg", new MessageFormatterMethod(locale, messagesBundle)); + } catch (IOException e) { + logger.warn("Failed to load messages", e); + messagesBundle = new Properties(); + } + + MessagesPerFieldBean messagesPerField = new MessagesPerFieldBean(); + if (messages != null) { + MessageBean wholeMessage = new MessageBean(null, messageType); + for (FormMessage message : this.messages) { + String formattedMessageText = formatMessage(message, messagesBundle, locale); + if (formattedMessageText != null) { + wholeMessage.appendSummaryLine(formattedMessageText); + messagesPerField.addMessage(message.getField(), formattedMessageText, messageType); + } + } + attributes.put("message", wholeMessage); + } + attributes.put("messagesPerField", messagesPerField); + + if (status == null) { + status = Response.Status.OK; + } + + if (realm != null) { + attributes.put("realm", new RealmBean(realm)); + attributes.put("social", new IdentityProviderBean(realm, baseUri, uriInfo)); + attributes.put("url", new UrlBean(realm, theme, baseUri, this.actionUri)); + attributes.put("requiredActionUrl", new RequiredActionUrlFormatterMethod(realm, baseUri)); + + if (realm.isInternationalizationEnabled()) { + UriBuilder b = UriBuilder.fromUri(baseUri).path(uriInfo.getPath()); + attributes.put("locale", new LocaleBean(realm, locale, b, messagesBundle)); + } + } + if (realm != null && user != null && session != null) { + attributes.put("authenticatorConfigured", new AuthenticatorConfiguredMethod(realm, user, session)); + } + try { + String result = freeMarker.processTemplate(attributes, form, theme); + Response.ResponseBuilder builder = Response.status(status).type(MediaType.TEXT_HTML).entity(result); + BrowserSecurityHeaderSetup.headers(builder, realm); + for (Map.Entry entry : httpResponseHeaders.entrySet()) { + builder.header(entry.getKey(), entry.getValue()); + } + LocaleHelper.updateLocaleCookie(builder, locale, realm, uriInfo, Urls.localeCookiePath(baseUri, realm.getName())); + return builder.build(); + } catch (FreeMarkerException e) { + logger.error("Failed to process template", e); + return Response.serverError().build(); + } + } + + public Response createLogin() { return createResponse(LoginFormsPages.LOGIN); } @@ -299,6 +403,7 @@ import java.util.concurrent.TimeUnit; return createResponse(LoginFormsPages.ERROR); } + public Response createOAuthGrant(ClientSessionModel clientSession) { this.clientSession = clientSession; return createResponse(LoginFormsPages.OAUTH_GRANT); @@ -381,6 +486,12 @@ import java.util.concurrent.TimeUnit; return this; } + @Override + public LoginFormsProvider setAttribute(String name, Object value) { + this.attributes.put(name, value); + return this; + } + @Override public LoginFormsProvider setStatus(Response.Status status) { this.status = status; diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RequiredActionUrlFormatterMethod.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RequiredActionUrlFormatterMethod.java new file mode 100755 index 0000000000..3056e26ad5 --- /dev/null +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/RequiredActionUrlFormatterMethod.java @@ -0,0 +1,32 @@ +package org.keycloak.login.freemarker.model; + +import freemarker.template.TemplateMethodModelEx; +import freemarker.template.TemplateModelException; +import org.keycloak.models.RealmModel; +import org.keycloak.services.Urls; + +import java.net.URI; +import java.text.MessageFormat; +import java.util.List; +import java.util.Locale; +import java.util.Properties; + +/** + */ +public class RequiredActionUrlFormatterMethod implements TemplateMethodModelEx { + private final String realm; + private final URI baseUri; + + public RequiredActionUrlFormatterMethod(RealmModel realm, URI baseUri) { + this.realm = realm.getName(); + this.baseUri = baseUri; + } + + @Override + public Object exec(List list) throws TemplateModelException { + String action = list.get(0).toString(); + String relativePath = list.get(1).toString(); + String url = Urls.requiredActionBase(baseUri).path(relativePath).build(realm, action).toString(); + return url; + } +} diff --git a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java index b4b3f16717..c652002ce6 100755 --- a/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java +++ b/forms/login-freemarker/src/main/java/org/keycloak/login/freemarker/model/UrlBean.java @@ -1,102 +1,102 @@ -/* - * JBoss, Home of Professional Open Source. - * Copyright 2012, Red Hat, Inc., and individual contributors - * as indicated by the @author tags. See the copyright.txt file in the - * distribution for a full listing of individual contributors. - * - * This is free software; you can redistribute it and/or modify it - * under the terms of the GNU Lesser General Public License as - * published by the Free Software Foundation; either version 2.1 of - * the License, or (at your option) any later version. - * - * This software is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this software; if not, write to the Free - * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA - * 02110-1301 USA, or see the FSF site: http://www.fsf.org. - */ -package org.keycloak.login.freemarker.model; - -import org.keycloak.freemarker.Theme; -import org.keycloak.models.RealmModel; -import org.keycloak.services.Urls; - -import java.net.URI; - -/** - * @author Stian Thorgersen - */ -public class UrlBean { - - private final URI actionuri; - private URI baseURI; - private Theme theme; - private String realm; - - public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI actionUri) { - this.realm = realm.getName(); - this.theme = theme; - this.baseURI = baseURI; - this.actionuri = actionUri; - } - - public String getLoginAction() { - if (this.actionuri != null) { - return this.actionuri.toString(); - } - return Urls.realmLoginAction(baseURI, realm).toString(); - } - - public String getLoginUrl() { - return Urls.realmLoginPage(baseURI, realm).toString(); - } - - public String getRegistrationAction() { - return Urls.realmRegisterAction(baseURI, realm).toString(); - } - - public String getRegistrationUrl() { - return Urls.realmRegisterPage(baseURI, realm).toString(); - } - - public String getLoginUpdatePasswordUrl() { - return Urls.loginActionUpdatePassword(baseURI, realm).toString(); - } - - public String getLoginUpdateTotpUrl() { - return Urls.loginActionUpdateTotp(baseURI, realm).toString(); - } - - public String getLoginUpdateProfileUrl() { - return Urls.loginActionUpdateProfile(baseURI, realm).toString(); - } - - public String getLoginPasswordResetUrl() { - return Urls.loginPasswordReset(baseURI, realm).toString(); - } - - public String getLoginUsernameReminderUrl() { - return Urls.loginUsernameReminder(baseURI, realm).toString(); - } - - public String getLoginEmailVerificationUrl() { - return Urls.loginActionEmailVerification(baseURI, realm).toString(); - } - - public String getOauthAction() { - if (this.actionuri != null) { - return this.actionuri.getPath(); - } - - return Urls.realmOauthAction(baseURI, realm).toString(); - } - - public String getResourcesPath() { - URI uri = Urls.themeRoot(baseURI); - return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName(); - } -} +/* + * JBoss, Home of Professional Open Source. + * Copyright 2012, Red Hat, Inc., and individual contributors + * as indicated by the @author tags. See the copyright.txt file in the + * distribution for a full listing of individual contributors. + * + * This is free software; you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as + * published by the Free Software Foundation; either version 2.1 of + * the License, or (at your option) any later version. + * + * This software is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this software; if not, write to the Free + * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + * 02110-1301 USA, or see the FSF site: http://www.fsf.org. + */ +package org.keycloak.login.freemarker.model; + +import org.keycloak.freemarker.Theme; +import org.keycloak.models.RealmModel; +import org.keycloak.services.Urls; + +import java.net.URI; + +/** + * @author Stian Thorgersen + */ +public class UrlBean { + + private final URI actionuri; + private URI baseURI; + private Theme theme; + private String realm; + + public UrlBean(RealmModel realm, Theme theme, URI baseURI, URI actionUri) { + this.realm = realm.getName(); + this.theme = theme; + this.baseURI = baseURI; + this.actionuri = actionUri; + } + + public String getLoginAction() { + if (this.actionuri != null) { + return this.actionuri.toString(); + } + return Urls.realmLoginAction(baseURI, realm).toString(); + } + + public String getLoginUrl() { + return Urls.realmLoginPage(baseURI, realm).toString(); + } + + public String getRegistrationAction() { + return Urls.realmRegisterAction(baseURI, realm).toString(); + } + + public String getRegistrationUrl() { + return Urls.realmRegisterPage(baseURI, realm).toString(); + } + + public String getLoginUpdatePasswordUrl() { + return Urls.loginActionUpdatePassword(baseURI, realm).toString(); + } + + public String getLoginUpdateTotpUrl() { + return Urls.loginActionUpdateTotp(baseURI, realm).toString(); + } + + public String getLoginUpdateProfileUrl() { + return Urls.loginActionUpdateProfile(baseURI, realm).toString(); + } + + public String getLoginPasswordResetUrl() { + return Urls.loginPasswordReset(baseURI, realm).toString(); + } + + public String getLoginUsernameReminderUrl() { + return Urls.loginUsernameReminder(baseURI, realm).toString(); + } + + public String getLoginEmailVerificationUrl() { + return Urls.loginActionEmailVerification(baseURI, realm).toString(); + } + + public String getOauthAction() { + if (this.actionuri != null) { + return this.actionuri.getPath(); + } + + return Urls.realmOauthAction(baseURI, realm).toString(); + } + + public String getResourcesPath() { + URI uri = Urls.themeRoot(baseURI); + return uri.getPath() + "/" + theme.getType().toString().toLowerCase() +"/" + theme.getName(); + } +} 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 79a2b67e77..12cb4fde09 100755 --- a/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java +++ b/model/api/src/main/java/org/keycloak/models/AuthenticationExecutionModel.java @@ -1,10 +1,20 @@ package org.keycloak.models; +import java.util.Comparator; + /** * @author Bill Burke * @version $Revision: 1 $ */ public class AuthenticationExecutionModel { + public static class ExecutionComparator implements Comparator { + public static final ExecutionComparator SINGLETON = new ExecutionComparator(); + + @Override + public int compare(AuthenticationExecutionModel o1, AuthenticationExecutionModel o2) { + return o1.priority - o2.priority; + } + } private String id; private String authenticator; 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..01f524a4c4 100755 --- a/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java +++ b/model/api/src/main/java/org/keycloak/models/ClientSessionModel.java @@ -22,9 +22,9 @@ public interface ClientSessionModel { public void setTimestamp(int timestamp); - public Action getAction(); + public String getAction(); - public void setAction(Action action); + public void setAction(String action); public Set getRoles(); public void setRoles(Set roles); @@ -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/KeycloakContext.java b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java old mode 100644 new mode 100755 index ac13d3fbcc..4d33403586 --- a/model/api/src/main/java/org/keycloak/models/KeycloakContext.java +++ b/model/api/src/main/java/org/keycloak/models/KeycloakContext.java @@ -1,5 +1,7 @@ package org.keycloak.models; +import org.keycloak.ClientConnection; + import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.UriInfo; @@ -20,4 +22,8 @@ public interface KeycloakContext { void setClient(ClientModel client); + ClientConnection getConnection(); + + void setConnection(ClientConnection connection); + } diff --git a/model/api/src/main/java/org/keycloak/models/RealmModel.java b/model/api/src/main/java/org/keycloak/models/RealmModel.java index 61bdbee405..6366325364 100755 --- a/model/api/src/main/java/org/keycloak/models/RealmModel.java +++ b/model/api/src/main/java/org/keycloak/models/RealmModel.java @@ -156,6 +156,13 @@ public interface RealmModel extends RoleContainerModel { void updateDefaultRoles(String[] defaultRoles); + Set getDefaultRequiredActions(); + + void addDefaultRequiredAction(String action); + void removeDefaultRequiredAction(String action); + + void setDefaultRequiredActions(Set action); + // Key is clientId Map getClientNameMap(); @@ -180,6 +187,7 @@ public interface RealmModel extends RoleContainerModel { void setSmtpConfig(Map smtpConfig); List getAuthenticationFlows(); + AuthenticationFlowModel getFlowByAlias(String alias); AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model); AuthenticationFlowModel getAuthenticationFlowById(String id); void removeAuthenticationFlow(AuthenticationFlowModel model); 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 4682dddbfa..7d9e7ca434 100755 --- a/model/api/src/main/java/org/keycloak/models/UserFederationManager.java +++ b/model/api/src/main/java/org/keycloak/models/UserFederationManager.java @@ -28,8 +28,8 @@ public class UserFederationManager implements UserProvider { } @Override - public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { - UserModel user = session.userStorage().addUser(realm, id, username.toLowerCase(), addDefaultRoles); + public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { + UserModel user = session.userStorage().addUser(realm, id, username.toLowerCase(), addDefaultRoles, addDefaultRequiredActions); return registerWithFederation(realm, user); } @@ -386,6 +386,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/UserProvider.java b/model/api/src/main/java/org/keycloak/models/UserProvider.java index 5e78cd1784..b9a0079d1b 100755 --- a/model/api/src/main/java/org/keycloak/models/UserProvider.java +++ b/model/api/src/main/java/org/keycloak/models/UserProvider.java @@ -13,7 +13,7 @@ import java.util.Set; public interface UserProvider extends Provider { // Note: The reason there are so many query methods here is for layering a cache on top of an persistent KeycloakSession - UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles); + UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions); UserModel addUser(RealmModel realm, String username); boolean removeUser(RealmModel realm, UserModel user); 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/entities/RealmEntity.java b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java index 827c80bc89..b7f258828d 100755 --- a/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java +++ b/model/api/src/main/java/org/keycloak/models/entities/RealmEntity.java @@ -2,8 +2,10 @@ package org.keycloak.models.entities; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Marek Posolda @@ -76,6 +78,7 @@ public class RealmEntity extends AbstractIdentifiableEntity { private List identityProviderMappers = new ArrayList(); private List authenticationFlows = new ArrayList<>(); private List authenticators = new ArrayList<>(); + private List defaultRequiredActions = new ArrayList<>(); public String getName() { @@ -500,6 +503,14 @@ public class RealmEntity extends AbstractIdentifiableEntity { public void setAuthenticators(List authenticators) { this.authenticators = authenticators; } + + public List getDefaultRequiredActions() { + return defaultRequiredActions; + } + + public void setDefaultRequiredActions(List defaultRequiredActions) { + this.defaultRequiredActions = defaultRequiredActions; + } } 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..f4bb81c2ff 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); @@ -74,7 +90,7 @@ public class DefaultAuthenticationFlows { execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setAuthenticator(password.getId()); execution.setPriority(11); - execution.setUserSetupAllowed(false); + execution.setUserSetupAllowed(true); execution.setAutheticatorFlow(false); realm.addAuthenticatorExecution(execution); @@ -88,5 +104,7 @@ public class DefaultAuthenticationFlows { execution.setAutheticatorFlow(false); realm.addAuthenticatorExecution(execution); + // + } } diff --git a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java index 2adcb2537a..467040f448 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java +++ b/model/api/src/main/java/org/keycloak/models/utils/ModelToRepresentation.java @@ -59,7 +59,8 @@ public class ModelToRepresentation { rep.setFederationLink(user.getFederationLink()); List reqActions = new ArrayList(); - for (String ra : user.getRequiredActions()){ + Set requiredActions = user.getRequiredActions(); + for (String ra : requiredActions){ reqActions.add(ra); } diff --git a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java index c9611af143..245e6c6e8b 100755 --- a/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java +++ b/model/api/src/main/java/org/keycloak/models/utils/RepresentationToModel.java @@ -796,7 +796,7 @@ public class RepresentationToModel { convertDeprecatedSocialProviders(userRep); // Import users just to user storage. Don't federate - UserModel user = session.userStorage().addUser(newRealm, userRep.getId(), userRep.getUsername(), false); + UserModel user = session.userStorage().addUser(newRealm, userRep.getId(), userRep.getUsername(), false, false); user.setEnabled(userRep.isEnabled()); user.setEmail(userRep.getEmail()); user.setEmailVerified(userRep.isEmailVerified()); 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/FileUserProvider.java b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java index 8dd783d985..cf2f21dea6 100755 --- a/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java +++ b/model/file/src/main/java/org/keycloak/models/file/FileUserProvider.java @@ -266,7 +266,7 @@ public class FileUserProvider implements UserProvider { } @Override - public UserAdapter addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { + public UserAdapter addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { if (inMemoryModel.hasUserWithUsername(realm.getId(), username.toLowerCase())) throw new ModelDuplicateException("User with username " + username + " already exists in realm."); @@ -284,6 +284,13 @@ public class FileUserProvider implements UserProvider { } } + if (addDefaultRequiredActions) { + for (String r : realm.getDefaultRequiredActions()) { + userModel.addRequiredAction(r); + } + } + + return userModel; } @@ -358,7 +365,7 @@ public class FileUserProvider implements UserProvider { @Override public UserModel addUser(RealmModel realm, String username) { - return this.addUser(realm, KeycloakModelUtils.generateId(), username.toLowerCase(), true); + return this.addUser(realm, KeycloakModelUtils.generateId(), username.toLowerCase(), true, true); } @Override diff --git a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java index 1a19bad3d6..61cc52be78 100755 --- a/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java +++ b/model/file/src/main/java/org/keycloak/models/file/adapter/RealmAdapter.java @@ -1213,6 +1213,19 @@ public class RealmAdapter implements RealmModel { return models; } + + + @Override + public AuthenticationFlowModel getFlowByAlias(String alias) { + for (AuthenticationFlowModel flow : getAuthenticationFlows()) { + if (flow.getAlias().equals(alias)) { + return flow; + } + } + return null; + } + + protected AuthenticationFlowModel entityToModel(AuthenticationFlowEntity entity) { AuthenticationFlowModel model = new AuthenticationFlowModel(); model.setId(entity.getId()); @@ -1276,6 +1289,7 @@ public class RealmAdapter implements RealmModel { AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } @@ -1548,4 +1562,37 @@ public class RealmAdapter implements RealmModel { mapper.setConfig(config); return mapper; } + + @Override + public Set getDefaultRequiredActions() { + Set result = new HashSet(); + if (realm.getDefaultRequiredActions() != null) { + result.addAll(realm.getDefaultRequiredActions()); + } + return result; + } + + @Override + public void addDefaultRequiredAction(String action) { + Set actions = getDefaultRequiredActions(); + actions.add(action); + setDefaultRequiredActions(actions); + + } + + @Override + public void removeDefaultRequiredAction(String action) { + Set actions = getDefaultRequiredActions(); + actions.remove(action); + setDefaultRequiredActions(actions); + + } + + @Override + public void setDefaultRequiredActions(Set action) { + List result = new ArrayList(); + result.addAll(action); + realm.setDefaultRequiredActions(result); + + } } 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/DefaultCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java index 2f766e0aa4..d7fdbda6dc 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/DefaultCacheUserProvider.java @@ -183,7 +183,6 @@ public class DefaultCacheUserProvider implements CacheUserProvider { if (cached == null) { UserModel model = getDelegate().getUserByEmail(email, realm); if (model == null) return null; - if (managedUsers.containsKey(model.getId())) return managedUsers.get(model.getId()); if (userInvalidations.containsKey(model.getId())) return model; cached = new CachedUser(realm, model); cache.addCachedUser(realm.getId(), cached); @@ -253,8 +252,8 @@ public class DefaultCacheUserProvider implements CacheUserProvider { } @Override - public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { - UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles); + public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { + UserModel user = getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRoles); managedUsers.put(user.getId(), user); return user; } diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java index 5cf96ecb66..aca466b543 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/NoCacheUserProvider.java @@ -119,8 +119,8 @@ public class NoCacheUserProvider implements CacheUserProvider { } @Override - public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { - return getDelegate().addUser(realm, id, username, addDefaultRoles); + public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { + return getDelegate().addUser(realm, id, username, addDefaultRoles, addDefaultRequiredActions); } @Override diff --git a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java index 75b8b82923..2db7ca2be9 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/RealmAdapter.java @@ -1024,6 +1024,16 @@ public class RealmAdapter implements RealmModel { return models; } + @Override + public AuthenticationFlowModel getFlowByAlias(String alias) { + for (AuthenticationFlowModel flow : getAuthenticationFlows()) { + if (flow.getAlias().equals(alias)) { + return flow; + } + } + return null; + } + @Override public AuthenticationFlowModel addAuthenticationFlow(AuthenticationFlowModel model) { getDelegateForUpdate(); @@ -1116,4 +1126,30 @@ public class RealmAdapter implements RealmModel { if (updated != null) return updated.getAuthenticatorById(id); return cached.getAuthenticators().get(id); } + + @Override + public Set getDefaultRequiredActions() { + return cached.getDefaultRequiredActions(); + } + + @Override + public void addDefaultRequiredAction(String action) { + getDelegateForUpdate(); + updated.addDefaultRequiredAction(action); + + } + + @Override + public void removeDefaultRequiredAction(String action) { + getDelegateForUpdate(); + updated.removeDefaultRequiredAction(action); + + } + + @Override + public void setDefaultRequiredActions(Set action) { + getDelegateForUpdate(); + updated.setDefaultRequiredActions(action); + + } } 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/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java index 016870e37c..904081edc2 100755 --- a/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java +++ b/model/invalidation-cache/model-adapters/src/main/java/org/keycloak/models/cache/entities/CachedRealm.java @@ -98,6 +98,7 @@ public class CachedRealm { private Set supportedLocales = new HashSet(); private String defaultLocale; private MultivaluedHashMap identityProviderMappers = new MultivaluedHashMap<>(); + private Set defaultRequiredActions = new HashSet<>(); public CachedRealm() { } @@ -200,6 +201,7 @@ public class CachedRealm { for (AuthenticatorModel authenticator : model.getAuthenticators()) { authenticators.put(authenticator.getId(), authenticator); } + this.defaultRequiredActions.addAll(model.getDefaultRequiredActions()); } @@ -438,4 +440,8 @@ public class CachedRealm { public Map getExecutionsById() { return executionsById; } + + public Set getDefaultRequiredActions() { + return defaultRequiredActions; + } } diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java index dee433e966..1c45bde919 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/JpaUserProvider.java @@ -44,7 +44,7 @@ public class JpaUserProvider implements UserProvider { } @Override - public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { + public UserModel addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { if (id == null) { id = KeycloakModelUtils.generateId(); } @@ -68,13 +68,18 @@ public class JpaUserProvider implements UserProvider { } } } + if (addDefaultRequiredActions) { + for (String r : realm.getDefaultRequiredActions()) { + userModel.addRequiredAction(r); + } + } return userModel; } @Override public UserModel addUser(RealmModel realm, String username) { - return addUser(realm, KeycloakModelUtils.generateId(), username.toLowerCase(), true); + return addUser(realm, KeycloakModelUtils.generateId(), username.toLowerCase(), true, true); } @Override diff --git a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java index 8cd0678960..33ea232574 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/RealmAdapter.java @@ -1525,6 +1525,17 @@ public class RealmAdapter implements RealmModel { return models; } + @Override + public AuthenticationFlowModel getFlowByAlias(String alias) { + for (AuthenticationFlowModel flow : getAuthenticationFlows()) { + if (flow.getAlias().equals(alias)) { + return flow; + } + } + return null; + } + + protected AuthenticationFlowModel entityToModel(AuthenticationFlowEntity entity) { AuthenticationFlowModel model = new AuthenticationFlowModel(); model.setId(entity.getId()); @@ -1583,6 +1594,7 @@ public class RealmAdapter implements RealmModel { AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } @@ -1713,5 +1725,30 @@ public class RealmAdapter implements RealmModel { return authenticators; } + @Override + public Set getDefaultRequiredActions() { + Set result = new HashSet(); + result.addAll(realm.getDefaultRequiredActions()); + return result; + } + + + + @Override + public void setDefaultRequiredActions(Set actions) { + realm.setDefaultRequiredActions(actions); + } + + @Override + public void addDefaultRequiredAction(String action) { + realm.getDefaultRequiredActions().add(action); + } + + @Override + public void removeDefaultRequiredAction(String action) { + realm.getDefaultRequiredActions().remove(action); + } + + } \ No newline at end of file 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/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java index 41be2b16cf..5ccb017fc9 100755 --- a/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java +++ b/model/jpa/src/main/java/org/keycloak/models/jpa/entities/RealmEntity.java @@ -113,6 +113,12 @@ public class RealmEntity { @OneToMany(fetch = FetchType.LAZY, cascade ={CascadeType.REMOVE}, orphanRemoval = true, mappedBy = "realm") Collection roles = new ArrayList(); + @ElementCollection + @Column(name="VALUE") + @CollectionTable(name = "DEFAULT_REQUIRED_ACTIONS", joinColumns={ @JoinColumn(name="REALM_ID") }) + protected Set defaultRequiredActions = new HashSet(); + + @ElementCollection @MapKeyColumn(name="NAME") @Column(name="VALUE") @@ -568,5 +574,13 @@ public class RealmEntity { public void setAuthenticationFlows(Collection authenticationFlows) { this.authenticationFlows = authenticationFlows; } + + public Set getDefaultRequiredActions() { + return defaultRequiredActions; + } + + public void setDefaultRequiredActions(Set defaultRequiredActions) { + this.defaultRequiredActions = defaultRequiredActions; + } } diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java index 39d647ddcc..abf8121a23 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/MongoUserProvider.java @@ -242,7 +242,7 @@ public class MongoUserProvider implements UserProvider { } @Override - public UserAdapter addUser(RealmModel realm, String id, String username, boolean addDefaultRoles) { + public UserAdapter addUser(RealmModel realm, String id, String username, boolean addDefaultRoles, boolean addDefaultRequiredActions) { UserAdapter userModel = addUserEntity(realm, id, username.toLowerCase()); if (addDefaultRoles) { @@ -257,6 +257,13 @@ public class MongoUserProvider implements UserProvider { } } + if (addDefaultRequiredActions) { + for (String r : realm.getDefaultRequiredActions()) { + userModel.addRequiredAction(r); + } + } + + return userModel; } @@ -327,7 +334,7 @@ public class MongoUserProvider implements UserProvider { @Override public UserModel addUser(RealmModel realm, String username) { - return this.addUser(realm, null, username, true); + return this.addUser(realm, null, username, true, true); } @Override diff --git a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java index 2b8c397f04..45456debde 100755 --- a/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java +++ b/model/mongo/src/main/java/org/keycloak/models/mongo/keycloak/adapters/RealmAdapter.java @@ -1290,6 +1290,17 @@ public class RealmAdapter extends AbstractMongoAdapter impleme return models; } + @Override + public AuthenticationFlowModel getFlowByAlias(String alias) { + for (AuthenticationFlowModel flow : getAuthenticationFlows()) { + if (flow.getAlias().equals(alias)) { + return flow; + } + } + return null; + } + + protected AuthenticationFlowModel entityToModel(AuthenticationFlowEntity entity) { AuthenticationFlowModel model = new AuthenticationFlowModel(); model.setId(entity.getId()); @@ -1354,6 +1365,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } @@ -1636,4 +1648,32 @@ public class RealmAdapter extends AbstractMongoAdapter impleme mapper.setConfig(config); return mapper; } + + @Override + public Set getDefaultRequiredActions() { + Set result = new HashSet(); + result.addAll(realm.getDefaultRequiredActions()); + return result; + } + + + + @Override + public void setDefaultRequiredActions(Set actions) { + List result = new ArrayList(); + result.addAll(actions); + getMongoEntity().setDefaultRequiredActions(result); + updateMongoEntity(); + } + + @Override + public void addDefaultRequiredAction(String action) { + getMongoStore().pushItemToList(getMongoEntity(), "defaultRequiredActions", action, true, invocationContext); + } + + @Override + public void removeDefaultRequiredAction(String action) { + getMongoStore().pullItemFromList(getMongoEntity(), "defaultRequiredActions", action, invocationContext); + } + } 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..78be9ee8e5 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; @@ -99,12 +100,12 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public Action getAction() { + public String getAction() { return entity.getAction(); } @Override - public void setAction(Action action) { + public void setAction(String action) { entity.setAction(action); update(); } @@ -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..340bf92dd9 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 @@ -24,11 +24,12 @@ public class ClientSessionEntity extends SessionEntity { private int timestamp; - private ClientSessionModel.Action action; + private String action; private Set roles; private Set protocolMappers; private Map notes; + private Map userSessionNotes; private Map authenticatorStatus = new HashMap<>(); private String authUserId; @@ -80,11 +81,11 @@ public class ClientSessionEntity extends SessionEntity { this.timestamp = timestamp; } - public ClientSessionModel.Action getAction() { + public String getAction() { return action; } - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { this.action = action; } @@ -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..2ce7034363 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(); @@ -161,12 +189,12 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public Action getAction() { + public String getAction() { return entity.getAction(); } @Override - public void setAction(Action action) { + public void setAction(String action) { entity.setAction(action); } 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..300c45a67e 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 @@ -54,8 +54,8 @@ public class ClientSessionEntity { @Column(name="AUTH_METHOD") protected String authMethod; - @Column(name="ACTION") - protected ClientSessionModel.Action action; + @Column(name="CURRENT_ACTION") + protected String action; @Column(name="AUTH_USER_ID") protected String userId; @@ -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<>(); @@ -120,11 +123,11 @@ public class ClientSessionEntity { this.redirectUri = redirectUri; } - public ClientSessionModel.Action getAction() { + public String getAction() { return action; } - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { this.action = action; } @@ -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..0e0647fd8e 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 @@ -93,12 +93,12 @@ public class ClientSessionAdapter implements ClientSessionModel { } @Override - public ClientSessionModel.Action getAction() { + public String getAction() { return entity.getAction(); } @Override - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { entity.setAction(action); } @@ -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..e76f62499d 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 @@ -24,10 +24,11 @@ public class ClientSessionEntity { private String authMethod; private int timestamp; - private ClientSessionModel.Action action; + private String action; private Set roles; private Set protocolMappers; private Map notes = new HashMap<>(); + private Map userSessionNotes = new HashMap<>(); public String getId() { return id; @@ -77,11 +78,11 @@ public class ClientSessionEntity { this.timestamp = timestamp; } - public ClientSessionModel.Action getAction() { + public String getAction() { return action; } - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { this.action = action; } @@ -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..ad1d0d7ee2 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; @@ -107,12 +108,12 @@ 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..21831b6ef8 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 @@ -26,10 +26,11 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme private String authMethod; private int timestamp; - private ClientSessionModel.Action action; + private String action; private List roles; private List protocolMappers; private Map notes = new HashMap(); + private Map userSessionNotes = new HashMap(); private Map authenticatorStatus = new HashMap<>(); private String authUserId; @@ -81,11 +82,11 @@ public class MongoClientSessionEntity extends AbstractIdentifiableEntity impleme this.timestamp = timestamp; } - public ClientSessionModel.Action getAction() { + public String getAction() { return action; } - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { this.action = action; } @@ -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 40ea8fdadb..a557b5a80e 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,11 +18,14 @@ 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.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.utils.RedirectUtils; import org.keycloak.saml.common.constants.GeneralConstants; @@ -30,6 +34,7 @@ import org.keycloak.saml.common.exceptions.ConfigurationException; import org.keycloak.saml.common.exceptions.ProcessingException; import org.keycloak.saml.processing.core.saml.v2.common.SAMLDocumentHolder; import org.keycloak.services.ErrorPage; +import org.keycloak.services.Urls; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.managers.HttpAuthenticationManager; @@ -57,6 +62,7 @@ import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.security.PublicKey; +import java.util.List; /** * Resource class for the oauth/openid connect token service @@ -262,7 +268,7 @@ public class SamlService { ClientSessionModel clientSession = session.sessions().createClientSession(realm, client); clientSession.setAuthMethod(SamlProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirect); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(SamlProtocol.SAML_BINDING, bindingType); clientSession.setNote(GeneralConstants.RELAY_STATE, relayState); @@ -282,6 +288,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 +321,42 @@ public class SamlService { return forms.createLogin(); } + private Response buildRedirectToIdentityProvider(String providerId, String accessCode) { + logger.debug("Automatically redirect to identity provider: " + providerId); + return Response.temporaryRedirect( + Urls.identityProviderAuthnRequest(uriInfo.getBaseUri(), providerId, realm.getName(), accessCode)) + .build(); + } + + + protected Response newBrowserAuthentication(ClientSessionModel clientSession) { + List identityProviders = realm.getIdentityProviders(); + for (IdentityProviderModel identityProvider : identityProviders) { + if (identityProvider.isAuthenticateByDefault()) { + return buildRedirectToIdentityProvider(identityProvider.getAlias(), new ClientSessionCode(realm, clientSession).getCode() ); + } + } + AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); + String flowId = flow.getId(); + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowId(flowId) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); + + try { + return processor.authenticate(); + } catch (Exception e) { + return processor.handleBrowserException(e); + } + } + + private String getBindingType(AuthnRequestType requestAbstractType) { URI requestedProtocolBinding = requestAbstractType.getProtocolBinding(); @@ -365,7 +411,7 @@ public class SamlService { // remove client from logout requests for (ClientSessionModel clientSession : userSession.getClientSessions()) { if (clientSession.getClient().getId().equals(client.getId())) { - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); + clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); } } logger.debug("browser Logout"); @@ -377,13 +423,13 @@ public class SamlService { UserSessionModel userSession = clientSession.getUserSession(); if (clientSession.getClient().getClientId().equals(client.getClientId())) { // remove requesting client from logout - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); + clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); // Remove also other clientSessions of this client as there could be more in this UserSession if (userSession != null) { for (ClientSessionModel clientSession2 : userSession.getClientSessions()) { if (clientSession2.getClient().getId().equals(client.getId())) { - clientSession2.setAction(ClientSessionModel.Action.LOGGED_OUT); + clientSession2.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); } } } @@ -514,7 +560,6 @@ public class SamlService { @QueryParam(GeneralConstants.SAML_RESPONSE_KEY) String samlResponse, @QueryParam(GeneralConstants.RELAY_STATE) String relayState) { logger.debug("SAML GET"); - //String uri = uriInfo.getRequestUri().toString(); return new RedirectBindingProtocol().execute(samlRequest, samlResponse, relayState); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 1ebfa50abe..473bc4fba0 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -3,7 +3,10 @@ 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.Errors; 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; @@ -13,13 +16,15 @@ import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserSessionModel; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.services.ErrorPage; import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; +import org.keycloak.services.messages.Messages; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.util.Collections; import java.util.List; -import java.util.Map; /** * @author Bill Burke @@ -34,14 +39,17 @@ public class AuthenticationProcessor { protected UriInfo uriInfo; protected KeycloakSession session; protected BruteForceProtector protector; - protected EventBuilder eventBuilder; + protected EventBuilder event; protected HttpRequest request; protected String flowId; + protected String action; + protected boolean userSessionCreated; public static enum Status { SUCCESS, CHALLENGE, + FORCE_CHALLENGE, FAILURE_CHALLENGE, FAILED, ATTEMPTED @@ -78,6 +86,14 @@ public class AuthenticationProcessor { return session; } + public UserSessionModel getUserSession() { + return userSession; + } + + public boolean isUserSessionCreated() { + return userSessionCreated; + } + public AuthenticationProcessor setRealm(RealmModel realm) { this.realm = realm; return this; @@ -109,7 +125,7 @@ public class AuthenticationProcessor { } public AuthenticationProcessor setEventBuilder(EventBuilder eventBuilder) { - this.eventBuilder = eventBuilder; + this.event = eventBuilder; return this; } @@ -123,28 +139,50 @@ public class AuthenticationProcessor { return this; } + public AuthenticationProcessor setAction(String action) { + this.action = action; + return this; + } + 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; } + @Override + public String getAction() { + return AuthenticationProcessor.this.action; + } + @Override public Authenticator getAuthenticator() { return authenticator; @@ -177,6 +215,12 @@ public class AuthenticationProcessor { this.status = Status.CHALLENGE; this.challenge = challenge; + } + @Override + public void forceChallenge(Response challenge) { + this.status = Status.FORCE_CHALLENGE; + this.challenge = challenge; + } @Override public void failureChallenge(Error error, Response challenge) { @@ -251,6 +295,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 { @@ -304,7 +353,44 @@ public class AuthenticationProcessor { return status == UserSessionModel.AuthenticatorStatus.SUCCESS; } + public Response handleBrowserException(Exception failure) { + if (failure instanceof AuthException) { + AuthException e = (AuthException)failure; + logger.error("failed authentication: " + e.getError().toString(), e); + if (e.getError() == AuthenticationProcessor.Error.INVALID_USER) { + event.error(Errors.USER_NOT_FOUND); + return ErrorPage.error(session, Messages.INVALID_USER); + } else if (e.getError() == AuthenticationProcessor.Error.USER_DISABLED) { + event.error(Errors.USER_DISABLED); + return ErrorPage.error(session, Messages.ACCOUNT_DISABLED); + } else if (e.getError() == AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED) { + event.error(Errors.USER_TEMPORARILY_DISABLED); + return ErrorPage.error(session, Messages.ACCOUNT_TEMPORARILY_DISABLED); + + } else { + event.error(Errors.INVALID_USER_CREDENTIALS); + return ErrorPage.error(session, Messages.INVALID_USER); + } + + } else { + logger.error("failed authentication", failure); + event.error(Errors.INVALID_USER_CREDENTIALS); + return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_REQUEST); + } + + } + + public Response authenticate() throws AuthException { + logger.debug("AUTHENTICATE"); + event.event(EventType.LOGIN); + event.client(clientSession.getClient().getClientId()) + .detail(Details.REDIRECT_URI, clientSession.getRedirectUri()) + .detail(Details.AUTH_METHOD, clientSession.getAuthMethod()); + String authType = clientSession.getNote(Details.AUTH_TYPE); + if (authType != null) { + event.detail(Details.AUTH_TYPE, authType); + } UserModel authUser = clientSession.getAuthenticatedUser(); validateUser(authUser); Response challenge = processFlow(flowId); @@ -313,6 +399,40 @@ public class AuthenticationProcessor { throw new AuthException(Error.UNKNOWN_USER); } return authenticationComplete(); + } + + public Response authenticateOnly() 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); + if (challenge != null) return challenge; + + String username = clientSession.getAuthenticatedUser().getUsername(); + if (userSession == null) { // if no authenticator attached a usersession + userSession = session.sessions().createUserSession(realm, clientSession.getAuthenticatedUser(), username, connection.getRemoteAddr(), "form", false, null, null); + userSession.setState(UserSessionModel.State.LOGGING_IN); + userSessionCreated = true; + } + TokenManager.attachClientSession(userSession, clientSession); + event.user(userSession.getUser()) + .detail(Details.USERNAME, username) + .session(userSession); + + return AuthenticationManager.actionRequired(session, userSession, clientSession, connection, request, uriInfo, event); + } + + public Response finishAuthentication() { + event.success(); + RealmModel realm = clientSession.getRealm(); + return AuthenticationManager.redirectAfterSuccessfulFlow(session, realm, userSession, clientSession, request, uriInfo, connection); } @@ -325,9 +445,11 @@ 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)) { + logger.debug("execution is processed"); if (!alternativeSuccessful && model.isAlternative() && isSuccessful(model)) alternativeSuccessful = true; continue; } @@ -351,49 +473,82 @@ public class AuthenticationProcessor { AuthenticatorModel authenticatorModel = realm.getAuthenticatorById(model.getAuthenticator()); AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, authenticatorModel.getProviderId()); Authenticator authenticator = factory.create(authenticatorModel); + logger.debugv("authenticator: {0}", authenticatorModel.getProviderId()); UserModel authUser = clientSession.getAuthenticatedUser(); if (authenticator.requiresUser() && authUser == null){ - if (alternativeChallenge != null) return alternativeChallenge; - throw new AuthException(Error.UNKNOWN_USER); + if (alternativeChallenge != null) { + clientSession.setAuthenticatorStatus(challengedAlternativeExecution.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); + return alternativeChallenge; + } + throw new AuthException("authenticator: " + authenticatorModel.getProviderId(), 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.isRequired()) { + if (model.isUserSetupAllowed()) { + logger.debugv("authenticator SETUP_REQUIRED: {0}", authenticatorModel.getProviderId()); + 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); + } + } else if (model.isOptional()) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + continue; } } - continue; } - context = new Result(authenticatorModel, authenticator); + context = new Result(model, authenticatorModel, authenticator); authenticator.authenticate(context); Status result = context.getStatus(); if (result == Status.SUCCESS){ + logger.debugv("authenticator SUCCESS: {0}", authenticatorModel.getProviderId()); clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SUCCESS); if (model.isAlternative()) alternativeSuccessful = true; continue; } else if (result == Status.FAILED) { + logger.debugv("authenticator FAILED: {0}", authenticatorModel.getProviderId()); logUserFailure(); + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.FAILED); if (context.challenge != null) return context.challenge; throw new AuthException(context.error); + } else if (result == Status.FORCE_CHALLENGE) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); + return context.challenge; } else if (result == Status.CHALLENGE) { - if (model.isRequired()) return context.challenge; - else if (model.isAlternative()) alternativeChallenge = context.challenge; - else clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + logger.debugv("authenticator CHALLENGE: {0}", authenticatorModel.getProviderId()); + if (model.isRequired() || (model.isOptional() && configuredFor)) { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); + return context.challenge; + } + else if (model.isAlternative()) { + alternativeChallenge = context.challenge; + challengedAlternativeExecution = model; + } else { + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.SKIPPED); + } continue; } else if (result == Status.FAILURE_CHALLENGE) { + logger.debugv("authenticator FAILURE_CHALLENGE: {0}", authenticatorModel.getProviderId()); logUserFailure(); + clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.CHALLENGED); return context.challenge; } else if (result == Status.ATTEMPTED) { - if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) throw new AuthException(Error.INVALID_CREDENTIALS); + logger.debugv("authenticator ATTEMPTED: {0}", authenticatorModel.getProviderId()); + if (model.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { + throw new AuthException(Error.INVALID_CREDENTIALS); + } clientSession.setAuthenticatorStatus(model.getId(), UserSessionModel.AuthenticatorStatus.ATTEMPTED); continue; } else { + logger.debugv("authenticator INTERNAL_ERROR: {0}", authenticatorModel.getProviderId()); logger.error("Unknown result status"); throw new AuthException(Error.INTERNAL_ERROR); } @@ -415,17 +570,27 @@ public class AuthenticationProcessor { } protected Response authenticationComplete() { + String username = clientSession.getAuthenticatedUser().getUsername(); + String rememberMe = clientSession.getNote(Details.REMEMBER_ME); + boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("true"); 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(), clientSession.getAuthMethod(), remember, null, null); userSession.setState(UserSessionModel.State.LOGGING_IN); } + if (remember) { + event.detail(Details.REMEMBER_ME, "true"); + } 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..248eb7accf 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,17 @@ 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); + + String getAction(); Authenticator getAuthenticator(); @@ -51,6 +61,9 @@ public interface AuthenticatorContext { void failure(AuthenticationProcessor.Error error); void failure(AuthenticationProcessor.Error error, Response response); void challenge(Response challenge); + + void forceChallenge(Response challenge); + void failureChallenge(AuthenticationProcessor.Error error, Response challenge); void attempted(); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java index 04b445e90c..561e60de2c 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorFactory.java @@ -1,9 +1,12 @@ package org.keycloak.authentication; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.provider.ConfiguredProvider; import org.keycloak.provider.ProviderFactory; +import java.util.List; + /** * @author Bill Burke * @version $Revision: 1 $ @@ -13,4 +16,21 @@ public interface AuthenticatorFactory extends ProviderFactory, Co String getDisplayCategory(); String getDisplayType(); + /** + * General authenticator type, i.e. totp, password, cert + * + * @return null if not a referencable type + */ + String getReferenceType(); + + boolean isConfigurable(); + + /** + * What requirement settings are allowed. For example, KERBEROS can only be required because of the way its challenges + * work. + * + * @return + */ + AuthenticationExecutionModel.Requirement[] getRequirementChoices(); + } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java new file mode 100755 index 0000000000..fcf34ba49c --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java @@ -0,0 +1,67 @@ +package org.keycloak.authentication; + +import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory; +import org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory; +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; + +import java.util.LinkedList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class AuthenticatorUtil { + + public static List getEnabledExecutionsRecursively(RealmModel realm, String flowId) { + List executions = new LinkedList<>(); + recurseExecutions(realm, flowId, executions); + return executions; + + } + + public static void recurseExecutions(RealmModel realm, String flowId, List executions) { + for (AuthenticationExecutionModel model : realm.getAuthenticationExecutions(flowId)) { + executions.add(model); + if (model.isAutheticatorFlow() && model.isEnabled()) { + recurseExecutions(realm, model.getAuthenticator(), executions); + } + } + } + + public static AuthenticationExecutionModel findExecutionByAuthenticator(RealmModel realm, String flowId, String authProviderId) { + for (AuthenticationExecutionModel model : realm.getAuthenticationExecutions(flowId)) { + if (model.isAutheticatorFlow()) { + AuthenticationExecutionModel recurse = findExecutionByAuthenticator(realm, model.getAuthenticator(), authProviderId); + if (recurse != null) return recurse; + + } + AuthenticatorModel authenticator = realm.getAuthenticatorById(model.getAuthenticator()); + if (authenticator.getProviderId().equals(authProviderId)) { + return model; + } + } + return null; + } + + public static boolean isEnabled(RealmModel realm, String flowId, String authProviderId) { + AuthenticationExecutionModel execution = findExecutionByAuthenticator(realm, flowId, authProviderId); + if (execution == null) { + return false; + } + return execution.isEnabled(); + } + public static boolean isRequired(RealmModel realm, String flowId, String authProviderId) { + AuthenticationExecutionModel execution = findExecutionByAuthenticator(realm, flowId, authProviderId); + if (execution == null) { + return false; + } + return execution.isRequired(); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java new file mode 100755 index 0000000000..62d4a9314a --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java @@ -0,0 +1,32 @@ +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; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.UserSessionModel; +import org.keycloak.services.managers.BruteForceProtector; + +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriInfo; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface RequiredActionContext { + EventBuilder getEvent(); + UserModel getUser(); + RealmModel getRealm(); + ClientSessionModel getClientSession(); + UserSessionModel getUserSession(); + ClientConnection getConnection(); + UriInfo getUriInfo(); + KeycloakSession getSession(); + HttpRequest getHttpRequest(); +} diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java b/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java new file mode 100755 index 0000000000..b049781df5 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionFactory.java @@ -0,0 +1,11 @@ +package org.keycloak.authentication; + +import org.keycloak.provider.ProviderFactory; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface RequiredActionFactory extends ProviderFactory { + String getDisplayText(); +} diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java new file mode 100755 index 0000000000..e6ff4882b3 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionProvider.java @@ -0,0 +1,15 @@ +package org.keycloak.authentication; + +import org.keycloak.provider.Provider; + +import javax.ws.rs.core.Response; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public interface RequiredActionProvider extends Provider { + void evaluateTriggers(RequiredActionContext context); + Response invokeRequiredAction(RequiredActionContext context); + Object jaxrsService(RequiredActionContext context); +} diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java b/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java new file mode 100755 index 0000000000..65370e5bca --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionSpi.java @@ -0,0 +1,32 @@ +package org.keycloak.authentication; + +import org.keycloak.provider.Provider; +import org.keycloak.provider.ProviderFactory; +import org.keycloak.provider.Spi; + +/** + * @author Stian Thorgersen + */ +public class RequiredActionSpi implements Spi { + + @Override + public boolean isInternal() { + return false; + } + + @Override + public String getName() { + return "required-action"; + } + + @Override + public Class getProviderClass() { + return RequiredActionProvider.class; + } + + @Override + public Class getProviderFactoryClass() { + return RequiredActionFactory.class; + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/actions/TermsAndConditions.java new file mode 100755 index 0000000000..3c0769e25b --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actions/TermsAndConditions.java @@ -0,0 +1,116 @@ +package org.keycloak.authentication.actions; + +import org.keycloak.Config; +import org.keycloak.Version; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.events.Errors; +import org.keycloak.freemarker.BrowserSecurityHeaderSetup; +import org.keycloak.freemarker.FreeMarkerException; +import org.keycloak.freemarker.FreeMarkerUtil; +import org.keycloak.freemarker.Theme; +import org.keycloak.freemarker.ThemeProvider; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.services.Urls; +import org.keycloak.services.managers.AuthenticationManager; +import org.keycloak.services.managers.ClientSessionCode; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.core.Response; +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory { + + public static final String PROVIDER_ID = "terms_and_conditions"; + + public static class Resource { + + public Resource(RequiredActionContext context) { + this.context = context; + } + + protected RequiredActionContext context; + + @POST + @Consumes(MediaType.APPLICATION_FORM_URLENCODED) + public Response agree(final MultivaluedMap formData) throws URISyntaxException, IOException, FreeMarkerException { + if (formData.containsKey("cancel")) { + LoginProtocol protocol = context.getSession().getProvider(LoginProtocol.class, context.getClientSession().getAuthMethod()); + protocol.setRealm(context.getRealm()) + .setHttpHeaders(context.getHttpRequest().getHttpHeaders()) + .setUriInfo(context.getUriInfo()); + context.getEvent().error(Errors.REJECTED_BY_USER); + return protocol.consentDenied(context.getClientSession()); + } + context.getUser().removeRequiredAction(PROVIDER_ID); + return AuthenticationManager.nextActionAfterAuthentication(context.getSession(), context.getUserSession(), context.getClientSession(), context.getConnection(), context.getHttpRequest(), context.getUriInfo(), context.getEvent()); + } + + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public void evaluateTriggers(RequiredActionContext context) { + + } + + @Override + public Response invokeRequiredAction(RequiredActionContext context) { + return context.getSession().getProvider(LoginFormsProvider.class) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .setUser(context.getUser()) + .createForm("terms.ftl", new HashMap()); + } + + @Override + public Object jaxrsService(RequiredActionContext context) { + return new Resource(context); + } + + @Override + public String getDisplayText() { + return "Terms and Conditions"; + } + + @Override + public void close() { + + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java b/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java new file mode 100755 index 0000000000..57ff72b981 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actions/UpdatePassword.java @@ -0,0 +1,99 @@ +package org.keycloak.authentication.actions; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.util.Time; + +import javax.ws.rs.core.Response; +import java.util.concurrent.TimeUnit; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UpdatePassword implements RequiredActionProvider, RequiredActionFactory { + protected static Logger logger = Logger.getLogger(UpdatePassword.class); + @Override + public void evaluateTriggers(RequiredActionContext context) { + int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword(); + if(daysToExpirePassword != -1) { + for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) { + if (entity.getType().equals(UserCredentialModel.PASSWORD)) { + + if(entity.getCreatedDate() == null) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + logger.debug("User is required to update password"); + } else { + long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate(); + long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); + + if(timeElapsed > timeToExpire) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + logger.debug("User is required to update password"); + } + } + break; + } + } + } + } + + @Override + public Response invokeRequiredAction(RequiredActionContext context) { + ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + accessCode.setAction(ClientSessionModel.Action.UPDATE_PASSWORD.name()); + + LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode()) + .setUser(context.getUser()); + return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); + } + + @Override + public Object jaxrsService(RequiredActionContext context) { + // this is handled by LoginActionsService at the moment + return null; + } + + + @Override + public void close() { + + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getDisplayText() { + return "Update Password"; + } + + + @Override + public String getId() { + return UserModel.RequiredAction.UPDATE_PASSWORD.name(); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java b/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java new file mode 100755 index 0000000000..d9aaa73174 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actions/UpdateProfile.java @@ -0,0 +1,81 @@ +package org.keycloak.authentication.actions; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.ClientSessionCode; + +import javax.ws.rs.core.Response; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UpdateProfile implements RequiredActionProvider, RequiredActionFactory { + protected static Logger logger = Logger.getLogger(UpdateProfile.class); + @Override + public void evaluateTriggers(RequiredActionContext context) { + if (context.getRealm().isVerifyEmail() && !context.getUser().isEmailVerified()) { + context.getUser().addRequiredAction(UserModel.RequiredAction.VERIFY_EMAIL); + logger.debug("User is required to verify email"); + } + } + + @Override + public Response invokeRequiredAction(RequiredActionContext context) { + ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + accessCode.setAction(ClientSessionModel.Action.UPDATE_PROFILE.name()); + + LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode()) + .setUser(context.getUser()); + return loginFormsProvider.createResponse(UserModel.RequiredAction.UPDATE_PROFILE); + } + + @Override + public Object jaxrsService(RequiredActionContext context) { + // this is handled by LoginActionsService at the moment + // todo should be refactored to contain it here + return null; + } + + + @Override + public void close() { + + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getDisplayText() { + return "Update Profile"; + } + + + @Override + public String getId() { + return UserModel.RequiredAction.UPDATE_PROFILE.name(); + } +} diff --git a/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java b/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java new file mode 100755 index 0000000000..e378942430 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actions/UpdateTotp.java @@ -0,0 +1,90 @@ +package org.keycloak.authentication.actions; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.util.Time; + +import javax.ws.rs.core.Response; +import java.util.concurrent.TimeUnit; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class UpdateTotp implements RequiredActionProvider, RequiredActionFactory { + protected static Logger logger = Logger.getLogger(UpdateTotp.class); + @Override + public void evaluateTriggers(RequiredActionContext context) { + // I don't think we need this check here. AuthenticationProcessor should be setting the required action + // if OTP changes from required from optional or disabled + for (RequiredCredentialModel c : context.getRealm().getRequiredCredentials()) { + if (c.getType().equals(CredentialRepresentation.TOTP) && !context.getUser().isTotp()) { + context.getUser().addRequiredAction(UserModel.RequiredAction.CONFIGURE_TOTP); + logger.debug("User is required to configure totp"); + } + } + } + + @Override + public Response invokeRequiredAction(RequiredActionContext context) { + ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + accessCode.setAction(ClientSessionModel.Action.CONFIGURE_TOTP.name()); + + LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode()) + .setUser(context.getUser()); + return loginFormsProvider.createResponse(UserModel.RequiredAction.CONFIGURE_TOTP); + } + + @Override + public Object jaxrsService(RequiredActionContext context) { + // this is handled by LoginActionsService at the moment + // todo should be refactored to contain it here + return null; + } + + + @Override + public void close() { + + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getDisplayText() { + return "Configure Totp"; + } + + + @Override + public String getId() { + return UserModel.RequiredAction.CONFIGURE_TOTP.name(); + } + +} diff --git a/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java b/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java new file mode 100755 index 0000000000..9d337f9725 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/actions/VerifyEmail.java @@ -0,0 +1,110 @@ +package org.keycloak.authentication.actions; + +import org.jboss.logging.Logger; +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.events.Details; +import org.keycloak.events.EventType; +import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.ClientSessionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; +import org.keycloak.models.UserCredentialValueModel; +import org.keycloak.models.UserModel; +import org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.resources.LoginActionsService; +import org.keycloak.services.validation.Validation; +import org.keycloak.util.Time; + +import javax.ws.rs.core.Response; +import java.util.concurrent.TimeUnit; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class VerifyEmail implements RequiredActionProvider, RequiredActionFactory { + protected static Logger logger = Logger.getLogger(VerifyEmail.class); + @Override + public void evaluateTriggers(RequiredActionContext context) { + int daysToExpirePassword = context.getRealm().getPasswordPolicy().getDaysToExpirePassword(); + if(daysToExpirePassword != -1) { + for (UserCredentialValueModel entity : context.getUser().getCredentialsDirectly()) { + if (entity.getType().equals(UserCredentialModel.PASSWORD)) { + + if(entity.getCreatedDate() == null) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + logger.debug("User is required to update password"); + } else { + long timeElapsed = Time.toMillis(Time.currentTime()) - entity.getCreatedDate(); + long timeToExpire = TimeUnit.DAYS.toMillis(daysToExpirePassword); + + if(timeElapsed > timeToExpire) { + context.getUser().addRequiredAction(UserModel.RequiredAction.UPDATE_PASSWORD); + logger.debug("User is required to update password"); + } + } + break; + } + } + } + } + + @Override + public Response invokeRequiredAction(RequiredActionContext context) { + if (Validation.isBlank(context.getUser().getEmail())) { + return null; + } + + ClientSessionCode accessCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + accessCode.setAction(ClientSessionModel.Action.VERIFY_EMAIL.name()); + context.getEvent().clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, context.getUser().getEmail()).success(); + LoginActionsService.createActionCookie(context.getRealm(), context.getUriInfo(), context.getConnection(), context.getUserSession().getId()); + + LoginFormsProvider loginFormsProvider = context.getSession().getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode()) + .setUser(context.getUser()); + return loginFormsProvider.createResponse(UserModel.RequiredAction.VERIFY_EMAIL); + } + + @Override + public Object jaxrsService(RequiredActionContext context) { + // this is handled by LoginActionsService at the moment + return null; + } + + + @Override + public void close() { + + } + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return this; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public String getDisplayText() { + return "Verify Email"; + } + + + @Override + public String getId() { + return UserModel.RequiredAction.VERIFY_EMAIL.name(); + } + +} 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..44636626a8 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/AbstractFormAuthenticator.java @@ -0,0 +1,98 @@ +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 { + + public static final String LOGIN_FORM_ACTION = "login_form"; + public static final String REGISTRATION_FORM_ACTION = "registration_form"; + public static final String ACTION = "action"; + + protected boolean isAction(AuthenticatorContext context, String action) { + return action.equals(context.getAction()); + } + + protected LoginFormsProvider loginForm(AuthenticatorContext context) { + ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession()); + code.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + URI action = getActionUrl(context, code, LOGIN_FORM_ACTION); + return context.getSession().getProvider(LoginFormsProvider.class) + .setUser(context.getUser()) + .setActionUri(action) + .setClientSessionCode(code.getCode()); + } + + public static URI getActionUrl(AuthenticatorContext context, ClientSessionCode code, String action) { + return LoginActionsService.authenticationFormProcessor(context.getUriInfo()) + .queryParam(OAuth2Constants.CODE, code.getCode()) + .queryParam(ACTION, action) + .build(context.getRealm().getName()); + } + + protected Response invalidUser(AuthenticatorContext context) { + return loginForm(context) + .setError(Messages.INVALID_USER) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .createLogin(); + } + + protected Response disabledUser(AuthenticatorContext context) { + return loginForm(context) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .setError(Messages.ACCOUNT_DISABLED).createLogin(); + } + + protected Response temporarilyDisabledUser(AuthenticatorContext context) { + return loginForm(context) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin(); + } + + protected Response invalidCredentials(AuthenticatorContext context) { + return loginForm(context) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .setError(Messages.INVALID_USER).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().user(user); + 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().user(user); + 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/CookieAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticatorFactory.java index 0f2ec076a6..525fb0b0ea 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/CookieAuthenticatorFactory.java @@ -3,9 +3,11 @@ package org.keycloak.authentication.authenticators; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; @@ -47,6 +49,23 @@ public class CookieAuthenticatorFactory implements AuthenticatorFactory { return PROVIDER_ID; } + @Override + public String getReferenceType() { + return "cookie"; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {AuthenticationExecutionModel.Requirement.ALTERNATIVE, AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + @Override public String getDisplayCategory() { return "Complete Authenticator"; 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..bac37f7822 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; @@ -25,7 +27,7 @@ public class LoginFormOTPAuthenticator extends LoginFormUsernameAuthenticator { @Override public void authenticate(AuthenticatorContext context) { - if (!isActionUrl(context)) { + if (!isAction(context, LOGIN_FORM_ACTION)) { context.failure(AuthenticationProcessor.Error.INTERNAL_ERROR); return; } @@ -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/LoginFormOTPAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticatorFactory.java index ef240cb052..ff5043a4c1 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormOTPAuthenticatorFactory.java @@ -3,9 +3,11 @@ package org.keycloak.authentication.authenticators; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; @@ -38,6 +40,27 @@ public class LoginFormOTPAuthenticatorFactory implements AuthenticatorFactory { } + @Override + public String getReferenceType() { + return UserCredentialModel.TOTP; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.OPTIONAL, + AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override public void close() { 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..fb393e2fe7 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticator.java @@ -2,11 +2,13 @@ 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; -import org.keycloak.services.messages.Messages; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -25,31 +27,32 @@ public class LoginFormPasswordAuthenticator extends LoginFormUsernameAuthenticat @Override public void authenticate(AuthenticatorContext context) { - if (!isActionUrl(context)) { + if (!isAction(context, LOGIN_FORM_ACTION) && !isAction(context, REGISTRATION_FORM_ACTION)) { context.failure(AuthenticationProcessor.Error.INTERNAL_ERROR); return; } validatePassword(context); } - protected Response badPassword(AuthenticatorContext context) { - return loginForm(context).setError(Messages.INVALID_USER).createLogin(); - } - - public void validatePassword(AuthenticatorContext context) { - MultivaluedMap inputData = context.getHttpRequest().getFormParameters(); + MultivaluedMap inputData = context.getHttpRequest().getDecodedFormParameters(); List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.PASSWORD); if (password == null) { - Response challengeResponse = badPassword(context); + if (context.getUser() != null) { + context.getEvent().user(context.getUser()); + } + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + Response challengeResponse = invalidCredentials(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } credentials.add(UserCredentialModel.password(password)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { - Response challengeResponse = badPassword(context); + context.getEvent().user(context.getUser()); + context.getEvent().error(Errors.INVALID_USER_CREDENTIALS); + Response challengeResponse = invalidCredentials(context); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } @@ -62,8 +65,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/LoginFormPasswordAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticatorFactory.java index 5da119e5c3..e136c442d4 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormPasswordAuthenticatorFactory.java @@ -3,9 +3,11 @@ package org.keycloak.authentication.authenticators; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; @@ -48,6 +50,24 @@ public class LoginFormPasswordAuthenticatorFactory implements AuthenticatorFacto return PROVIDER_ID; } + @Override + public String getReferenceType() { + return UserCredentialModel.PASSWORD; + } + + @Override + public boolean isConfigurable() { + return true; + } + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + @Override public String getDisplayCategory() { return "Credential Validation"; 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..62e964318d 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,30 @@ 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 { + public static final String FORM_USERNAME = "FORM_USERNAME"; protected AuthenticatorModel model; public LoginFormUsernameAuthenticator(AuthenticatorModel model) { @@ -33,29 +33,41 @@ public class LoginFormUsernameAuthenticator implements Authenticator { @Override public void authenticate(AuthenticatorContext context) { - if (!isActionUrl(context)) { + if (isAction(context, REGISTRATION_FORM_ACTION) && context.getUser() != null) { + context.success(); + return; + } + if (!isAction(context, LOGIN_FORM_ACTION)) { MultivaluedMap formData = new MultivaluedMapImpl<>(); String loginHint = context.getClientSession().getNote(OIDCLoginProtocol.LOGIN_HINT_PARAM); - if (loginHint == null) { - loginHint = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders()); + + String rememberMeUsername = AuthenticationManager.getRememberMeUsername(context.getRealm(), context.getHttpRequest().getHttpHeaders()); + + if (loginHint != null || rememberMeUsername != null) { if (loginHint != null) { + formData.add(AuthenticationManager.FORM_USERNAME, loginHint); + } else { + formData.add(AuthenticationManager.FORM_USERNAME, rememberMeUsername); formData.add("rememberMe", "on"); } } - if (loginHint != null) formData.add(AuthenticationManager.FORM_USERNAME, loginHint); Response challengeResponse = challenge(context, formData); 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 +83,30 @@ 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; + String rememberMe = inputData.getFirst("rememberMe"); + boolean remember = rememberMe != null && rememberMe.equalsIgnoreCase("on"); + if (remember) { + context.getClientSession().setNote(Details.REMEMBER_ME, "true"); + context.getEvent().detail(Details.REMEMBER_ME, "true"); + } 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/LoginFormUsernameAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticatorFactory.java index de86b08610..0db3a77579 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/LoginFormUsernameAuthenticatorFactory.java @@ -3,9 +3,11 @@ package org.keycloak.authentication.authenticators; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; @@ -48,6 +50,26 @@ public class LoginFormUsernameAuthenticatorFactory implements AuthenticatorFacto return PROVIDER_ID; } + @Override + public String getReferenceType() { + return null; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + @Override public String getDisplayCategory() { return "User Validation"; 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..325f3cd77d 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticator.java @@ -1,18 +1,18 @@ 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; +import org.keycloak.services.messages.Messages; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -24,7 +24,8 @@ import java.util.List; * @author Bill Burke * @version $Revision: 1 $ */ -public class OTPFormAuthenticator implements Authenticator { +public class OTPFormAuthenticator extends AbstractFormAuthenticator implements Authenticator { + public static final String TOTP_FORM_ACTION = "totp"; protected AuthenticatorModel model; public OTPFormAuthenticator(AuthenticatorModel model) { @@ -33,9 +34,8 @@ 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())) { - Response challengeResponse = challenge(context); + if (!isAction(context, TOTP_FORM_ACTION)) { + Response challengeResponse = challenge(context, null); context.challenge(challengeResponse); return; } @@ -43,47 +43,45 @@ public class OTPFormAuthenticator implements Authenticator { } public void validateOTP(AuthenticatorContext context) { - MultivaluedMap inputData = context.getHttpRequest().getFormParameters(); + MultivaluedMap inputData = context.getHttpRequest().getDecodedFormParameters(); List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.TOTP); if (password == null) { - Response challengeResponse = challenge(context); - context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); + Response challengeResponse = challenge(context, null); + context.challenge(challengeResponse); return; } credentials.add(UserCredentialModel.totp(password)); boolean valid = context.getSession().users().validCredentials(context.getRealm(), context.getUser(), credentials); if (!valid) { - Response challengeResponse = challenge(context); + context.getEvent().user(context.getUser()) + .error(Errors.INVALID_USER_CREDENTIALS); + Response challengeResponse = challenge(context, Messages.INVALID_TOTP); context.failureChallenge(AuthenticationProcessor.Error.INVALID_CREDENTIALS, challengeResponse); return; } context.success(); } - @Override public boolean requiresUser() { return true; } - protected Response challenge(AuthenticatorContext context, MultivaluedMap formData) { + protected Response challenge(AuthenticatorContext context, String error) { + ClientSessionCode clientSessionCode = new ClientSessionCode(context.getRealm(), context.getClientSession()); + URI action = AbstractFormAuthenticator.getActionUrl(context, clientSessionCode, TOTP_FORM_ACTION); LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class) - .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()); - - if (formData.size() > 0) forms.setFormData(formData); + .setActionUri(action) + .setClientSessionCode(clientSessionCode.getCode()); + if (error != null) forms.setError(error); 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/OTPFormAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticatorFactory.java index f5fe0e71a7..f197217835 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/OTPFormAuthenticatorFactory.java @@ -3,9 +3,11 @@ package org.keycloak.authentication.authenticators; import org.keycloak.Config; import org.keycloak.authentication.Authenticator; import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; import org.keycloak.provider.ProviderConfigProperty; import java.util.List; @@ -48,6 +50,25 @@ public class OTPFormAuthenticatorFactory implements AuthenticatorFactory { return PROVIDER_ID; } + @Override + public String getReferenceType() { + return UserCredentialModel.TOTP; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.OPTIONAL, + AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } @Override public String getDisplayCategory() { return "Credential Validation"; 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..3970fa8916 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java @@ -0,0 +1,192 @@ +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.ClientSessionModel; +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 org.keycloak.services.managers.ClientSessionCode; +import org.keycloak.services.messages.Messages; + +import javax.ws.rs.core.HttpHeaders; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.net.URI; +import java.util.HashMap; +import java.util.Map; + +import static org.keycloak.util.HtmlUtils.escapeAttribute; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Authenticator{ + public static final String KERBEROS_DISABLED = "kerberos_disabled"; + 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); + if (isAction(context, KERBEROS_DISABLED)) { + context.attempted(); + return; + } + // Case when we don't yet have any Negotiate header + if (authHeader == null) { + if (isAlreadyChallenged(context)) { + context.attempted(); + return; + } + Response challenge = challengeNegotiation(context, null); + context.forceChallenge(challenge); + return; + } + + 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); + } + if (context.getExecution().isRequired()) { + return context.getSession().getProvider(LoginFormsProvider.class) + .setStatus(Response.Status.UNAUTHORIZED) + .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader) + .setError(Messages.KERBEROS_NOT_ENABLED).createErrorPage(); + } else { + return optionalChallengeRedirect(context, negotiateHeader); + } + } + + // This is used for testing only. Selenium will execute the HTML challenge sent back which results in the javascript + // redirecting. Our old Selenium tests expect that the current URL will be the original openid redirect. + public static boolean bypassChallengeJavascript = false; + + /** + * 401 challenge sent back that bypasses + * @param context + * @param negotiateHeader + * @return + */ + protected Response optionalChallengeRedirect(AuthenticatorContext context, String negotiateHeader) { + ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession()); + code.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + URI action = getActionUrl(context, code, KERBEROS_DISABLED); + + StringBuilder builder = new StringBuilder(); + + builder.append(""); + builder.append(""); + + builder.append("Kerberos Unsupported"); + builder.append(""); + if (bypassChallengeJavascript) { + builder.append(""); + + } else { + builder.append(""); + } + builder.append("
    "); + builder.append(""); + + builder.append("
    "); + return Response.status(Response.Status.UNAUTHORIZED) + .header(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader) + .type(MediaType.TEXT_HTML_TYPE) + .entity(builder.toString()).build(); + } + + protected Response formChallenge(AuthenticatorContext context, String negotiateHeader) { + ClientSessionCode code = new ClientSessionCode(context.getRealm(), context.getClientSession()); + code.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); + URI action = getActionUrl(context, code, KERBEROS_DISABLED); + return context.getSession().getProvider(LoginFormsProvider.class) + .setClientSessionCode(new ClientSessionCode(context.getRealm(), context.getClientSession()).getCode()) + .setActionUri(action) + .setStatus(Response.Status.UNAUTHORIZED) + .setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader) + .setUser(context.getUser()) + .createForm("bypass_kerberos.ftl", new HashMap()); + } + + + @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..21c56fc8d0 --- /dev/null +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java @@ -0,0 +1,93 @@ +package org.keycloak.authentication.authenticators; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.models.UserCredentialModel; +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 getReferenceType() { + return UserCredentialModel.KERBEROS; + } + + @Override + public boolean isConfigurable() { + return false; + } + + public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.ALTERNATIVE, + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.DISABLED}; + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + + @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/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java index b7c7381aa7..4ef8544aab 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java @@ -130,7 +130,7 @@ public class OIDCLoginProtocol implements LoginProtocol { ClientSessionModel clientSession = accessCode.getClientSession(); String redirect = clientSession.getRedirectUri(); String state = clientSession.getNote(OIDCLoginProtocol.STATE_PARAM); - accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN); + accessCode.setAction(ClientSessionModel.Action.CODE_TO_TOKEN.name()); UriBuilder redirectUri = UriBuilder.fromUri(redirect).queryParam(OAuth2Constants.CODE, accessCode.getCode()); log.debugv("redirectAccessCode: state: {0}", state); if (state != 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..5b15cb8366 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; @@ -20,6 +19,7 @@ import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.RequiredCredentialModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.utils.RedirectUtils; @@ -44,6 +44,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 @@ -220,7 +221,7 @@ public class AuthorizationEndpoint { clientSession = session.sessions().createClientSession(realm, client); clientSession.setAuthMethod(OIDCLoginProtocol.LOGIN_PROTOCOL); clientSession.setRedirectUri(redirectUri); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setNote(ClientSessionCode.ACTION_KEY, KeycloakModelUtils.generateCodeSecret()); clientSession.setNote(OIDCLoginProtocol.RESPONSE_TYPE_PARAM, responseType); clientSession.setNote(OIDCLoginProtocol.REDIRECT_URI_PARAM, redirectUriParam); @@ -247,17 +248,20 @@ public class AuthorizationEndpoint { return buildRedirectToIdentityProvider(idpHint, accessCode); } - return oldBrowserAuthentication(accessCode); + return newBrowserAuthentication(accessCode); } protected Response newBrowserAuthentication(String accessCode) { - String flowId = null; - for (AuthenticationFlowModel flow : realm.getAuthenticationFlows()) { - if (flow.getAlias().equals("browser")) { - flowId = flow.getId(); - break; + List identityProviders = realm.getIdentityProviders(); + for (IdentityProviderModel identityProvider : identityProviders) { + if (identityProvider.isAuthenticateByDefault()) { + return buildRedirectToIdentityProvider(identityProvider.getAlias(), accessCode); } } + clientSession.setNote(Details.AUTH_TYPE, CODE_AUTH_TYPE); + + AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); + String flowId = flow.getId(); AuthenticationProcessor processor = new AuthenticationProcessor(); processor.setClientSession(clientSession) .setFlowId(flowId) @@ -269,7 +273,26 @@ public class AuthorizationEndpoint { .setUriInfo(uriInfo) .setRequest(request); - return processor.authenticate(); + Response challenge = null; + try { + challenge = processor.authenticateOnly(); + } catch (Exception e) { + return processor.handleBrowserException(e); + } + + if (challenge != null && prompt != null && prompt.equals("none")) { + if (processor.isUserSessionCreated()) { + session.sessions().removeUserSession(realm, processor.getUserSession()); + } + OIDCLoginProtocol oauth = new OIDCLoginProtocol(session, realm, uriInfo, headers, event); + return oauth.cancelLogin(clientSession); + } + + if (challenge == null) { + return processor.finishAuthentication(); + } else { + return challenge; + } } protected Response oldBrowserAuthentication(String accessCode) { diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java index 66d399592d..bdcbf7369d 100755 --- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java +++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java @@ -191,7 +191,7 @@ public class TokenEndpoint { ClientSessionModel clientSession = accessCode.getClientSession(); event.detail(Details.CODE_ID, clientSession.getId()); - if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN)) { + if (!accessCode.isValid(ClientSessionModel.Action.CODE_TO_TOKEN.name())) { event.error(Errors.INVALID_CODE); throw new ErrorResponseException("invalid_grant", "Code is expired", Response.Status.BAD_REQUEST); } diff --git a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java old mode 100644 new mode 100755 index 1b0e567def..99849a90db --- a/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java +++ b/services/src/main/java/org/keycloak/services/DefaultKeycloakContext.java @@ -1,6 +1,7 @@ package org.keycloak.services; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.ClientConnection; import org.keycloak.models.ClientModel; import org.keycloak.models.KeycloakContext; import org.keycloak.models.RealmModel; @@ -17,6 +18,8 @@ public class DefaultKeycloakContext implements KeycloakContext { private ClientModel client; + private ClientConnection connection; + @Override public UriInfo getUri() { return ResteasyProviderFactory.getContextData(UriInfo.class); @@ -47,4 +50,13 @@ public class DefaultKeycloakContext implements KeycloakContext { this.client = client; } + @Override + public ClientConnection getConnection() { + return connection; + } + + @Override + public void setConnection(ClientConnection connection) { + this.connection = connection; + } } diff --git a/services/src/main/java/org/keycloak/services/Urls.java b/services/src/main/java/org/keycloak/services/Urls.java index 93b479b902..2adbd1e9dc 100755 --- a/services/src/main/java/org/keycloak/services/Urls.java +++ b/services/src/main/java/org/keycloak/services/Urls.java @@ -128,15 +128,20 @@ public class Urls { } public static URI loginActionUpdatePassword(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "updatePassword").build(realmId); } public static URI loginActionUpdateTotp(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "updateTotp").build(realmId); } + public static UriBuilder requiredActionBase(URI baseUri) { + return loginActionsBase(baseUri).path(LoginActionsService.class, "requiredAction"); + } + + public static URI loginActionUpdateProfile(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "updateProfile").build(realmId); } public static URI loginActionEmailVerification(URI baseUri, String realmId) { @@ -144,7 +149,7 @@ public class Urls { } public static UriBuilder loginActionEmailVerificationBuilder(URI baseUri) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "emailVerification"); + return loginActionsBase(baseUri).path(LoginActionsService.class, "emailVerification"); } public static URI loginPasswordReset(URI baseUri, String realmId) { @@ -152,7 +157,7 @@ public class Urls { } public static UriBuilder loginPasswordResetBuilder(URI baseUri) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "passwordReset"); + return loginActionsBase(baseUri).path(LoginActionsService.class, "passwordReset"); } public static URI loginUsernameReminder(URI baseUri, String realmId) { @@ -160,7 +165,7 @@ public class Urls { } public static UriBuilder loginUsernameReminderBuilder(URI baseUri) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "usernameReminder"); + return loginActionsBase(baseUri).path(LoginActionsService.class, "usernameReminder"); } public static String realmIssuer(URI baseUri, String realmId) { @@ -172,11 +177,11 @@ public class Urls { } public static URI realmLoginAction(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "processLogin").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "processLogin").build(realmId); } public static URI realmLoginPage(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "loginPage").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "loginPage").build(realmId); } private static UriBuilder realmLogout(URI baseUri) { @@ -184,11 +189,11 @@ public class Urls { } public static URI realmRegisterAction(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "processRegister").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "processRegister").build(realmId); } public static URI realmRegisterPage(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "registerPage").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "registerPage").build(realmId); } public static URI realmInstalledAppUrnCallback(URI baseUri, String realmId) { @@ -196,7 +201,7 @@ public class Urls { } public static URI realmOauthAction(URI baseUri, String realmId) { - return requiredActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId); + return loginActionsBase(baseUri).path(LoginActionsService.class, "processConsent").build(realmId); } public static String localeCookiePath(URI baseUri, String realmName){ @@ -207,7 +212,7 @@ public class Urls { return themeBase(baseUri).path(Version.RESOURCES_VERSION).build(); } - private static UriBuilder requiredActionsBase(URI baseUri) { + private static UriBuilder loginActionsBase(URI baseUri) { return realmBase(baseUri).path(RealmsResource.class, "getLoginActionsService"); } diff --git a/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java b/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java index f172d39123..e412c0d9c1 100755 --- a/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java +++ b/services/src/main/java/org/keycloak/services/filters/ClientConnectionFilter.java @@ -15,6 +15,7 @@ import java.io.IOException; * @author Bill Burke * @version $Revision: 1 $ */ +@Deprecated public class ClientConnectionFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { diff --git a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java index 411556784f..cfd482e3a9 100755 --- a/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java +++ b/services/src/main/java/org/keycloak/services/filters/KeycloakSessionServletFilter.java @@ -1,6 +1,7 @@ package org.keycloak.services.filters; import org.jboss.resteasy.spi.ResteasyProviderFactory; +import org.keycloak.ClientConnection; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; import org.keycloak.models.KeycloakTransaction; @@ -26,11 +27,29 @@ public class KeycloakSessionServletFilter implements Filter { @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { - HttpServletRequest request = (HttpServletRequest)servletRequest; + final HttpServletRequest request = (HttpServletRequest)servletRequest; KeycloakSessionFactory sessionFactory = (KeycloakSessionFactory) servletRequest.getServletContext().getAttribute(KeycloakSessionFactory.class.getName()); KeycloakSession session = sessionFactory.create(); ResteasyProviderFactory.pushContext(KeycloakSession.class, session); + ClientConnection connection = new ClientConnection() { + @Override + public String getRemoteAddr() { + return request.getRemoteAddr(); + } + + @Override + public String getRemoteHost() { + return request.getRemoteHost(); + } + + @Override + public int getReportPort() { + return request.getRemotePort(); + } + }; + session.getContext().setConnection(connection); + ResteasyProviderFactory.pushContext(ClientConnection.class, connection); KeycloakTransaction tx = session.getTransaction(); ResteasyProviderFactory.pushContext(KeycloakTransaction.class, tx); diff --git a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java index f200130794..a4b0276b50 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -6,6 +6,9 @@ import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.RSATokenVerifier; import org.keycloak.VerificationException; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.broker.provider.IdentityProvider; import org.keycloak.events.Details; import org.keycloak.events.EventBuilder; @@ -28,6 +31,7 @@ import org.keycloak.models.UserSessionModel; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.protocol.LoginProtocol; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.AccessToken; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.services.resources.IdentityBrokerService; @@ -148,7 +152,7 @@ public class AuthenticationManager { public static void backchannelLogoutClientSession(KeycloakSession session, RealmModel realm, ClientSessionModel clientSession, UserSessionModel userSession, UriInfo uriInfo, HttpHeaders headers) { ClientModel client = clientSession.getClient(); - if (client instanceof ClientModel && !client.isFrontchannelLogout() && clientSession.getAction() != ClientSessionModel.Action.LOGGED_OUT) { + if (client instanceof ClientModel && !client.isFrontchannelLogout() && !ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) { String authMethod = clientSession.getAuthMethod(); if (authMethod == null) return; // must be a keycloak service like account LoginProtocol protocol = session.getProvider(LoginProtocol.class, authMethod); @@ -156,7 +160,7 @@ public class AuthenticationManager { .setHttpHeaders(headers) .setUriInfo(uriInfo); protocol.backchannelLogout(userSession, clientSession); - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); + clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); } } @@ -188,7 +192,7 @@ public class AuthenticationManager { List redirectClients = new LinkedList(); for (ClientSessionModel clientSession : userSession.getClientSessions()) { ClientModel client = clientSession.getClient(); - if (clientSession.getAction() == ClientSessionModel.Action.LOGGED_OUT) continue; + if (ClientSessionModel.Action.LOGGED_OUT.name().equals(clientSession.getAction())) continue; if (client.isFrontchannelLogout()) { String authMethod = clientSession.getAuthMethod(); if (authMethod == null) continue; // must be a keycloak service like account @@ -205,7 +209,7 @@ public class AuthenticationManager { try { logger.debugv("backchannel logout to: {0}", client.getClientId()); protocol.backchannelLogout(userSession, clientSession); - clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT); + clientSession.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); } catch (Exception e) { logger.warn("Failed to logout client, continuing", e); } @@ -219,7 +223,7 @@ public class AuthenticationManager { .setHttpHeaders(headers) .setUriInfo(uriInfo); // setting this to logged out cuz I"m not sure protocols can always verify that the client was logged out or not - nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT); + nextRedirectClient.setAction(ClientSessionModel.Action.LOGGED_OUT.name()); try { logger.debugv("frontchannel logout to: {0}", nextRedirectClient.getClient().getClientId()); Response response = protocol.frontchannelLogout(userSession, nextRedirectClient); @@ -425,12 +429,78 @@ public class AuthenticationManager { public static Response nextActionAfterAuthentication(KeycloakSession session, UserSessionModel userSession, ClientSessionModel clientSession, ClientConnection clientConnection, HttpRequest request, UriInfo uriInfo, EventBuilder event) { + Response requiredAction = actionRequired(session, userSession, clientSession, clientConnection, request, uriInfo, event); + if (requiredAction != null) return requiredAction; + event.success(); RealmModel realm = clientSession.getRealm(); - UserModel user = userSession.getUser(); + return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection); + + } + + public static Response actionRequired(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, + final ClientConnection clientConnection, + final HttpRequest request, final UriInfo uriInfo, final EventBuilder event) { + final RealmModel realm = clientSession.getRealm(); + final UserModel user = userSession.getUser(); + /* isForcePasswordUpdateRequired(realm, user); isTotpConfigurationRequired(realm, user); isEmailVerificationRequired(realm, user); - ClientModel client = clientSession.getClient(); + */ + final ClientModel client = clientSession.getClient(); + + RequiredActionContext context = new RequiredActionContext() { + @Override + public EventBuilder getEvent() { + return event; + } + + @Override + public UserModel getUser() { + return user; + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientSessionModel getClientSession() { + return clientSession; + } + + @Override + public UserSessionModel getUserSession() { + return userSession; + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + @Override + public UriInfo getUriInfo() { + return uriInfo; + } + + @Override + public KeycloakSession getSession() { + return session; + } + + @Override + public HttpRequest getHttpRequest() { + return request; + } + }; + + // see if any required actions need triggering, i.e. an expired password + for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) { + RequiredActionProvider provider = ((RequiredActionFactory)factory).create(session); + provider.evaluateTriggers(context); + } ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); @@ -439,37 +509,18 @@ public class AuthenticationManager { event.detail(Details.CODE_ID, clientSession.getId()); Set requiredActions = user.getRequiredActions(); - if (!requiredActions.isEmpty()) { - Iterator i = user.getRequiredActions().iterator(); - String action = i.next(); - - if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name()) && Validation.isBlank(user.getEmail())) { - if (i.hasNext()) - action = i.next(); - else - action = null; - } + for (String action : requiredActions) { + RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, action); + Response challenge = actionProvider.invokeRequiredAction(context); + if (challenge != null) return challenge; - if (action != null) { - accessCode.setRequiredAction(RequiredAction.valueOf(action)); - - LoginFormsProvider loginFormsProvider = session.getProvider(LoginFormsProvider.class).setClientSessionCode(accessCode.getCode()) - .setUser(user); - if (action.equals(UserModel.RequiredAction.VERIFY_EMAIL.name())) { - event.clone().event(EventType.SEND_VERIFY_EMAIL).detail(Details.EMAIL, user.getEmail()).success(); - LoginActionsService.createActionCookie(realm, uriInfo, clientConnection, userSession.getId()); - } - - return loginFormsProvider.createResponse(RequiredAction.valueOf(action)); - } } - if (client.isConsentRequired()) { UserConsentModel grantedConsent = user.getConsentByClient(client.getId()); - List realmRoles = new LinkedList(); - MultivaluedMap resourceRoles = new MultivaluedMapImpl(); + List realmRoles = new LinkedList<>(); + MultivaluedMap resourceRoles = new MultivaluedMapImpl<>(); for (RoleModel r : accessCode.getRequestedRoles()) { // Consent already granted by user @@ -484,7 +535,7 @@ public class AuthenticationManager { } } - List protocolMappers = new LinkedList(); + List protocolMappers = new LinkedList<>(); for (ProtocolMapperModel protocolMapper : accessCode.getRequestedProtocolMappers()) { if (protocolMapper.isConsentRequired() && protocolMapper.getConsentText() != null) { if (grantedConsent == null || !grantedConsent.isProtocolMapperGranted(protocolMapper)) { @@ -495,7 +546,7 @@ public class AuthenticationManager { // Skip grant screen if everything was already approved by this user if (realmRoles.size() > 0 || resourceRoles.size() > 0 || protocolMappers.size() > 0) { - accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT); + accessCode.setAction(ClientSessionModel.Action.OAUTH_GRANT.name()); return session.getProvider(LoginFormsProvider.class) .setClientSessionCode(accessCode.getCode()) @@ -508,12 +559,12 @@ public class AuthenticationManager { } else { event.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED); } - - event.success(); - return redirectAfterSuccessfulFlow(session, realm , userSession, clientSession, request, uriInfo, clientConnection); + return null; } - + + + private static void isForcePasswordUpdateRequired(RealmModel realm, UserModel user) { int daysToExpirePassword = realm.getPasswordPolicy().getDaysToExpirePassword(); if(daysToExpirePassword != -1) { diff --git a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java index 99fc5f2678..8ddc02f74b 100755 --- a/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java +++ b/services/src/main/java/org/keycloak/services/managers/ClientSessionCode.java @@ -80,8 +80,8 @@ public class ClientSessionCode { return clientSession; } - public boolean isValid(ClientSessionModel.Action requestedAction) { - ClientSessionModel.Action action = clientSession.getAction(); + public boolean isValid(String requestedAction) { + String action = clientSession.getAction(); if (action == null) { return false; } @@ -93,18 +93,14 @@ public class ClientSessionCode { } int lifespan; - switch (action) { - case CODE_TO_TOKEN: - lifespan = realm.getAccessCodeLifespan(); - break; - case AUTHENTICATE: - lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction(); - break; - default: - lifespan = realm.getAccessCodeLifespanUserAction(); - break; - } + if (action.equals(ClientSessionModel.Action.CODE_TO_TOKEN.name())) { + lifespan = realm.getAccessCodeLifespan(); + } else if (action.equals(ClientSessionModel.Action.AUTHENTICATE.name())) { + lifespan = realm.getAccessCodeLifespanLogin() > 0 ? realm.getAccessCodeLifespanLogin() : realm.getAccessCodeLifespanUserAction(); + } else { + lifespan = realm.getAccessCodeLifespanUserAction(); + } return timestamp + lifespan > Time.currentTime(); } @@ -132,7 +128,7 @@ public class ClientSessionCode { return requestedProtocolMappers; } - public void setAction(ClientSessionModel.Action action) { + public void setAction(String action) { clientSession.setAction(action); clientSession.setNote(ACTION_KEY, UUID.randomUUID().toString()); clientSession.setTimestamp(Time.currentTime()); @@ -142,16 +138,16 @@ public class ClientSessionCode { setAction(convertToAction(requiredAction)); } - private ClientSessionModel.Action convertToAction(RequiredAction requiredAction) { + private String convertToAction(RequiredAction requiredAction) { switch (requiredAction) { case CONFIGURE_TOTP: - return ClientSessionModel.Action.CONFIGURE_TOTP; + return ClientSessionModel.Action.CONFIGURE_TOTP.name(); case UPDATE_PASSWORD: - return ClientSessionModel.Action.UPDATE_PASSWORD; + return ClientSessionModel.Action.UPDATE_PASSWORD.name(); case UPDATE_PROFILE: - return ClientSessionModel.Action.UPDATE_PROFILE; + return ClientSessionModel.Action.UPDATE_PROFILE.name(); case VERIFY_EMAIL: - return ClientSessionModel.Action.VERIFY_EMAIL; + return ClientSessionModel.Action.VERIFY_EMAIL.name(); default: throw new IllegalArgumentException("Unknown required action " + requiredAction); } diff --git a/services/src/main/java/org/keycloak/services/messages/Messages.java b/services/src/main/java/org/keycloak/services/messages/Messages.java index 8aaa06edb1..9f9070ad58 100755 --- a/services/src/main/java/org/keycloak/services/messages/Messages.java +++ b/services/src/main/java/org/keycloak/services/messages/Messages.java @@ -128,6 +128,8 @@ public class Messages { public static final String COULD_NOT_SEND_AUTHENTICATION_REQUEST = "couldNotSendAuthenticationRequestMessage"; + public static final String KERBEROS_NOT_ENABLED="kerberosNotSetUp"; + public static final String UNEXPECTED_ERROR_HANDLING_REQUEST = "unexpectedErrorHandlingRequestMessage"; public static final String INVALID_ACCESS_CODE = "invalidAccessCodeMessage"; diff --git a/services/src/main/java/org/keycloak/services/resources/AccountService.java b/services/src/main/java/org/keycloak/services/resources/AccountService.java index 697a28fe59..4ceebb399a 100755 --- a/services/src/main/java/org/keycloak/services/resources/AccountService.java +++ b/services/src/main/java/org/keycloak/services/resources/AccountService.java @@ -738,7 +738,7 @@ public class AccountService { try { ClientSessionModel clientSession = auth.getClientSession(); ClientSessionCode clientSessionCode = new ClientSessionCode(realm, clientSession); - clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSessionCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); clientSession.setRedirectUri(redirectUri); clientSession.setNote(OIDCLoginProtocol.STATE_PARAM, UUID.randomUUID().toString()); diff --git a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java index eae77cd780..d45e706a31 100755 --- a/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java +++ b/services/src/main/java/org/keycloak/services/resources/IdentityBrokerService.java @@ -383,7 +383,7 @@ public class IdentityBrokerService implements IdentityProvider.AuthenticationCal private ClientSessionCode parseClientSessionCode(String code) { ClientSessionCode clientCode = ClientSessionCode.parse(code, this.session, this.realmModel); - if (clientCode != null && clientCode.isValid(AUTHENTICATE)) { + if (clientCode != null && clientCode.isValid(AUTHENTICATE.name())) { ClientSessionModel clientSession = clientCode.getClientSession(); if (clientSession != null) { 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 08a05c9d5e..fae9564f6c 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -25,6 +25,11 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.authentication.AuthenticationProcessor; +import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.authentication.authenticators.AbstractFormAuthenticator; +import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.Details; @@ -33,6 +38,7 @@ import org.keycloak.events.EventBuilder; import org.keycloak.events.EventType; import org.keycloak.jose.jws.JWSBuilder; import org.keycloak.login.LoginFormsProvider; +import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientModel; import org.keycloak.models.ClientSessionModel; @@ -47,10 +53,12 @@ import org.keycloak.models.UserCredentialModel; import org.keycloak.models.UserModel; import org.keycloak.models.UserModel.RequiredAction; import org.keycloak.models.UserSessionModel; +import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.FormMessage; import org.keycloak.models.utils.KeycloakModelUtils; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.protocol.LoginProtocol; +import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.OIDCLoginProtocolService; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.representations.PasswordToken; @@ -67,7 +75,9 @@ import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; +import javax.ws.rs.PathParam; import javax.ws.rs.QueryParam; +import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Context; import javax.ws.rs.core.Cookie; import javax.ws.rs.core.HttpHeaders; @@ -166,7 +176,7 @@ public class LoginActionsService { ClientSessionCode clientCode; Response response; - boolean check(String code, ClientSessionModel.Action requiredAction) { + boolean check(String code, String requiredAction) { if (!check(code)) { return false; } else if (!clientCode.isValid(requiredAction)) { @@ -178,7 +188,7 @@ public class LoginActionsService { } } - boolean check(String code, ClientSessionModel.Action requiredAction, ClientSessionModel.Action alternativeRequiredAction) { + boolean check(String code, String requiredAction, String alternativeRequiredAction) { if (!check(code)) { return false; } else if (!(clientCode.isValid(requiredAction) || clientCode.isValid(alternativeRequiredAction))) { @@ -231,9 +241,9 @@ public class LoginActionsService { ClientSessionCode clientSessionCode = checks.clientCode; ClientSessionModel clientSession = clientSessionCode.getClientSession(); - if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD)) { + if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD.name())) { TokenManager.dettachClientSession(session.sessions(), realm, clientSession); - clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE); + clientSession.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); } return session.getProvider(LoginFormsProvider.class) @@ -269,6 +279,7 @@ public class LoginActionsService { return session.getProvider(LoginFormsProvider.class) .setClientSessionCode(clientSessionCode.getCode()) + .setAttribute("passwordRequired", isPasswordRequired()) .createRegistration(); } @@ -281,7 +292,8 @@ public class LoginActionsService { @Path("auth-form") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response authForm(@QueryParam("code") String code) { + public Response authForm(@QueryParam("code") String code, + @QueryParam("action") String action) { event.event(EventType.LOGIN); if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); @@ -301,13 +313,9 @@ public class LoginActionsService { ClientSessionModel clientSession = clientCode.getClientSession(); event.detail(Details.CODE_ID, clientSession.getId()); - if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientSession.getUserSession() != null) { - clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) { event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.EXPIRED_CODE) - .setClientSessionCode(clientCode.getCode()) - .createLogin(); + return ErrorPage.error(session, Messages.EXPIRED_CODE); } ClientModel client = clientSession.getClient(); @@ -315,6 +323,8 @@ public class LoginActionsService { event.error(Errors.CLIENT_NOT_FOUND); return ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER); } + session.getContext().setClient(client); + if (!client.isEnabled()) { event.error(Errors.CLIENT_DISABLED); return ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED); @@ -336,52 +346,17 @@ public class LoginActionsService { .setRealm(realm) .setSession(session) .setUriInfo(uriInfo) + .setAction(action) .setRequest(request); try { return processor.authenticate(); - } catch (AuthenticationProcessor.AuthException e) { - return handleError(e, code); } catch (Exception e) { - logger.error("failed authentication", e); - return ErrorPage.error(session, Messages.UNEXPECTED_ERROR_HANDLING_RESPONSE); - + return processor.handleBrowserException(e); } } - protected Response handleError(AuthenticationProcessor.AuthException e, String code) { - logger.error("failed authentication: " + e.getError().toString(), e); - if (e.getError() == AuthenticationProcessor.Error.INVALID_USER) { - event.error(Errors.USER_NOT_FOUND); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.INVALID_USER) - .setClientSessionCode(code) - .createLogin(); - - } else if (e.getError() == AuthenticationProcessor.Error.USER_DISABLED) { - event.error(Errors.USER_DISABLED); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.ACCOUNT_DISABLED) - .setClientSessionCode(code) - .createLogin(); - - } else if (e.getError() == AuthenticationProcessor.Error.USER_TEMPORARILY_DISABLED) { - event.error(Errors.USER_TEMPORARILY_DISABLED); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED) - .setClientSessionCode(code) - .createLogin(); - - } else { - event.error(Errors.INVALID_USER_CREDENTIALS); - return session.getProvider(LoginFormsProvider.class) - .setError(Messages.INVALID_USER) - .setClientSessionCode(code) - .createLogin(); - } - } - /** * URL called after login page. YOU SHOULD NEVER INVOKE THIS DIRECTLY! * @@ -413,8 +388,8 @@ public class LoginActionsService { ClientSessionModel clientSession = clientCode.getClientSession(); event.detail(Details.CODE_ID, clientSession.getId()); - if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE) || clientSession.getUserSession() != null) { - clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE); + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) { + clientCode.setAction(ClientSessionModel.Action.AUTHENTICATE.name()); event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); return session.getProvider(LoginFormsProvider.class) .setError(Messages.EXPIRED_CODE) @@ -522,14 +497,13 @@ public class LoginActionsService { * Registration * * @param code - * @param formData * @return */ @Path("request/registration") @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) - public Response processRegister(@QueryParam("code") String code, - final MultivaluedMap formData) { + public Response processRegister(@QueryParam("code") String code) { + MultivaluedMap formData = request.getDecodedFormParameters(); event.event(EventType.REGISTER); if (!checkSsl()) { event.error(Errors.SSL_REQUIRED); @@ -549,7 +523,7 @@ public class LoginActionsService { event.error(Errors.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_CODE); } - if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE)) { + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name())) { event.error(Errors.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_CODE); } @@ -585,20 +559,23 @@ public class LoginActionsService { session.getContext().setClient(client); - List requiredCredentialTypes = new LinkedList(); - for (RequiredCredentialModel m : realm.getRequiredCredentials()) { - requiredCredentialTypes.add(m.getType()); + List requiredCredentialTypes = new LinkedList<>(); + boolean passwordRequired = isPasswordRequired(); + if (passwordRequired) { + requiredCredentialTypes.add(CredentialRepresentation.PASSWORD); } // Validate here, so user is not created if password doesn't validate to passwordPolicy of current realm List errors = Validation.validateRegistrationForm(realm, formData, requiredCredentialTypes, realm.getPasswordPolicy()); + if (errors != null && !errors.isEmpty()) { event.error(Errors.INVALID_REGISTRATION); return session.getProvider(LoginFormsProvider.class) .setErrors(errors) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) + .setAttribute("passwordRequired", isPasswordRequired()) .createRegistration(); } @@ -609,6 +586,7 @@ public class LoginActionsService { .setError(Messages.USERNAME_EXISTS) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) + .setAttribute("passwordRequired", isPasswordRequired()) .createRegistration(); } @@ -619,6 +597,7 @@ public class LoginActionsService { .setError(Messages.EMAIL_EXISTS) .setFormData(formData) .setClientSessionCode(clientCode.getCode()) + .setAttribute("passwordRequired", isPasswordRequired()) .createRegistration(); } @@ -629,7 +608,7 @@ public class LoginActionsService { user.setEmail(email); - if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { + if (passwordRequired) { UserCredentialModel credentials = new UserCredentialModel(); credentials.setType(CredentialRepresentation.PASSWORD); credentials.setValue(formData.getFirst("password")); @@ -658,13 +637,35 @@ public class LoginActionsService { .createResponse(UserModel.RequiredAction.UPDATE_PASSWORD); } } - + clientSession.setNote(OIDCLoginProtocol.LOGIN_HINT_PARAM, username); AttributeFormDataProcessor.process(formData, realm, user); event.user(user).success(); event = new EventBuilder(realm, session, clientConnection); + clientSession.setAuthenticatedUser(user); + AuthenticationFlowModel flow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); + AuthenticationProcessor processor = new AuthenticationProcessor(); + processor.setClientSession(clientSession) + .setFlowId(flow.getId()) + .setConnection(clientConnection) + .setEventBuilder(event) + .setProtector(authManager.getProtector()) + .setRealm(realm) + .setAction(AbstractFormAuthenticator.REGISTRATION_FORM_ACTION) + .setSession(session) + .setUriInfo(uriInfo) + .setRequest(request); - return processLogin(code, formData); + try { + return processor.authenticate(); + } catch (Exception e) { + return processor.handleBrowserException(e); + } + } + + public boolean isPasswordRequired() { + AuthenticationFlowModel browserFlow = realm.getFlowByAlias(DefaultAuthenticationFlows.BROWSER_FLOW); + return AuthenticatorUtil.isRequired(realm, browserFlow.getId(), LoginFormPasswordAuthenticatorFactory.PROVIDER_ID); } /** @@ -687,7 +688,7 @@ public class LoginActionsService { String code = formData.getFirst("code"); ClientSessionCode accessCode = ClientSessionCode.parse(code, session, realm); - if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT)) { + if (accessCode == null || !accessCode.isValid(ClientSessionModel.Action.OAUTH_GRANT.name())) { event.error(Errors.INVALID_CODE); return ErrorPage.error(session, Messages.INVALID_ACCESS_CODE); } @@ -717,11 +718,11 @@ public class LoginActionsService { } event.session(userSession); - LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); - protocol.setRealm(realm) - .setHttpHeaders(headers) - .setUriInfo(uriInfo); if (formData.containsKey("cancel")) { + LoginProtocol protocol = session.getProvider(LoginProtocol.class, clientSession.getAuthMethod()); + protocol.setRealm(realm) + .setHttpHeaders(headers) + .setUriInfo(uriInfo); event.error(Errors.REJECTED_BY_USER); return protocol.consentDenied(clientSession); } @@ -754,7 +755,7 @@ public class LoginActionsService { final MultivaluedMap formData) { event.event(EventType.UPDATE_PROFILE); Checks checks = new Checks(); - if (!checks.check(code, ClientSessionModel.Action.UPDATE_PROFILE)) { + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PROFILE.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -806,7 +807,7 @@ public class LoginActionsService { event.clone().event(EventType.UPDATE_EMAIL).detail(Details.PREVIOUS_EMAIL, oldEmail).detail(Details.UPDATED_EMAIL, email).success(); } - return redirectOauth(user, accessCode, clientSession, userSession); + return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); } @Path("totp") @@ -816,7 +817,7 @@ public class LoginActionsService { final MultivaluedMap formData) { event.event(EventType.UPDATE_TOTP); Checks checks = new Checks(); - if (!checks.check(code, ClientSessionModel.Action.CONFIGURE_TOTP)) { + if (!checks.check(code, ClientSessionModel.Action.CONFIGURE_TOTP.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -851,7 +852,7 @@ public class LoginActionsService { event.clone().event(EventType.UPDATE_TOTP).success(); - return redirectOauth(user, accessCode, clientSession, userSession); + return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); } @Path("password") @@ -861,7 +862,7 @@ public class LoginActionsService { final MultivaluedMap formData) { event.event(EventType.UPDATE_PASSWORD); Checks checks = new Checks(); - if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD, ClientSessionModel.Action.RECOVER_PASSWORD)) { + if (!checks.check(code, ClientSessionModel.Action.UPDATE_PASSWORD.name(), ClientSessionModel.Action.RECOVER_PASSWORD.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -902,7 +903,7 @@ public class LoginActionsService { event.event(EventType.UPDATE_PASSWORD).success(); - if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD)) { + if (clientSession.getAction().equals(ClientSessionModel.Action.RECOVER_PASSWORD.name())) { String actionCookieValue = getActionCookie(); if (actionCookieValue == null || !actionCookieValue.equals(userSession.getId())) { return session.getProvider(LoginFormsProvider.class) @@ -913,7 +914,7 @@ public class LoginActionsService { event = event.clone().event(EventType.LOGIN); - return redirectOauth(user, accessCode, clientSession, userSession); + return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); } @@ -923,7 +924,7 @@ public class LoginActionsService { event.event(EventType.VERIFY_EMAIL); if (key != null) { Checks checks = new Checks(); - if (!checks.check(key, ClientSessionModel.Action.VERIFY_EMAIL)) { + if (!checks.check(key, ClientSessionModel.Action.VERIFY_EMAIL.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -946,10 +947,10 @@ public class LoginActionsService { event = event.clone().removeDetail(Details.EMAIL).event(EventType.LOGIN); - return redirectOauth(user, accessCode, clientSession, userSession); + return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); } else { Checks checks = new Checks(); - if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL)) { + if (!checks.check(code, ClientSessionModel.Action.VERIFY_EMAIL.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -972,7 +973,7 @@ public class LoginActionsService { event.event(EventType.RESET_PASSWORD); if (key != null) { Checks checks = new Checks(); - if (!checks.check(key, ClientSessionModel.Action.RECOVER_PASSWORD)) { + if (!checks.check(key, ClientSessionModel.Action.RECOVER_PASSWORD.name())) { return checks.response; } ClientSessionCode accessCode = checks.clientCode; @@ -1050,7 +1051,7 @@ public class LoginActionsService { event.session(userSession); TokenManager.attachClientSession(userSession, clientSession); - accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD); + accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD.name()); try { UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); @@ -1090,10 +1091,6 @@ public class LoginActionsService { CookieHelper.addCookie(ACTION_COOKIE, sessionId, AuthenticationManager.getRealmCookiePath(realm, uriInfo), null, null, -1, realm.getSslRequired().isRequired(clientConnection), true); } - private Response redirectOauth(UserModel user, ClientSessionCode accessCode, ClientSessionModel clientSession, UserSessionModel userSession) { - return AuthenticationManager.nextActionAfterAuthentication(session, userSession, clientSession, clientConnection, request, uriInfo, event); - } - private void initEvent(ClientSessionModel clientSession) { event.event(EventType.LOGIN).client(clientSession.getClient()) .user(clientSession.getUserSession().getUser()) @@ -1112,4 +1109,111 @@ public class LoginActionsService { } } } + + @Path("required-actions/{action}") + public Object requiredAction(@QueryParam("code") String code, + @PathParam("action") String action) { + event.event(EventType.LOGIN); + if (!checkSsl()) { + event.error(Errors.SSL_REQUIRED); + throw new WebApplicationException(ErrorPage.error(session, Messages.HTTPS_REQUIRED)); + } + + if (!realm.isEnabled()) { + event.error(Errors.REALM_DISABLED); + return ErrorPage.error(session, Messages.REALM_NOT_ENABLED); + } + ClientSessionCode clientCode = ClientSessionCode.parse(code, session, realm); + if (clientCode == null) { + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + + final ClientSessionModel clientSession = clientCode.getClientSession(); + event.detail(Details.CODE_ID, clientSession.getId()); + + /* + if (!clientCode.isValid(ClientSessionModel.Action.AUTHENTICATE.name()) || clientSession.getUserSession() != null) { + event.client(clientSession.getClient()).error(Errors.EXPIRED_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.EXPIRED_CODE)); + } + */ + + ClientModel client = clientSession.getClient(); + if (client == null) { + event.error(Errors.CLIENT_NOT_FOUND); + throw new WebApplicationException( ErrorPage.error(session, Messages.UNKNOWN_LOGIN_REQUESTER)); + } + session.getContext().setClient(client); + + if (!client.isEnabled()) { + event.error(Errors.CLIENT_NOT_FOUND); + throw new WebApplicationException( ErrorPage.error(session, Messages.LOGIN_REQUESTER_NOT_ENABLED)); + } + + if (action == null) { + logger.error("required action was null"); + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + + } + + RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, action); + if (provider == null) { + logger.error("required action provider was null"); + event.error(Errors.INVALID_CODE); + throw new WebApplicationException(ErrorPage.error(session, Messages.INVALID_CODE)); + } + RequiredActionContext context = new RequiredActionContext() { + @Override + public EventBuilder getEvent() { + return event; + } + + @Override + public UserModel getUser() { + return getUserSession().getUser(); + } + + @Override + public RealmModel getRealm() { + return realm; + } + + @Override + public ClientSessionModel getClientSession() { + return clientSession; + } + + @Override + public UserSessionModel getUserSession() { + return clientSession.getUserSession(); + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + @Override + public UriInfo getUriInfo() { + return uriInfo; + } + + @Override + public KeycloakSession getSession() { + return session; + } + + @Override + public HttpRequest getHttpRequest() { + return request; + } + }; + return provider.jaxrsService(context); + + + + } + } diff --git a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java index b3dd57044b..dc674f7e0c 100755 --- a/services/src/main/java/org/keycloak/services/resources/RealmsResource.java +++ b/services/src/main/java/org/keycloak/services/resources/RealmsResource.java @@ -4,6 +4,7 @@ import org.jboss.logging.Logger; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; +import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.EventBuilder; import org.keycloak.models.ClientModel; import org.keycloak.models.Constants; diff --git a/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationFlowResource.java b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationFlowResource.java new file mode 100755 index 0000000000..7d2b719f11 --- /dev/null +++ b/services/src/main/java/org/keycloak/services/resources/admin/AuthenticationFlowResource.java @@ -0,0 +1,180 @@ +package org.keycloak.services.resources.admin; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.annotations.cache.NoCache; +import org.jboss.resteasy.spi.NotFoundException; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.authentication.AuthenticatorUtil; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.AuthenticationFlowModel; +import org.keycloak.models.AuthenticatorModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.RealmModel; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import java.util.LinkedList; +import java.util.List; + +import static javax.ws.rs.core.Response.Status.NOT_FOUND; + +/** + * @author Pedro Igor + */ +public class AuthenticationFlowResource { + + private final RealmModel realm; + private final KeycloakSession session; + private RealmAuth auth; + private AdminEventBuilder adminEvent; + private static Logger logger = Logger.getLogger(AuthenticationFlowResource.class); + + public AuthenticationFlowResource(RealmModel realm, KeycloakSession session, RealmAuth auth, AdminEventBuilder adminEvent) { + this.realm = realm; + this.session = session; + this.auth = auth; + this.auth.init(RealmAuth.Resource.IDENTITY_PROVIDER); + this.adminEvent = adminEvent; + } + + public static class AuthenticationExecutionRepresentation { + protected String execution; + protected String referenceType; + protected String requirement; + protected List requirementChoices; + protected Boolean configurable; + protected Boolean subFlow; + + public String getExecution() { + return execution; + } + + public void setExecution(String execution) { + this.execution = execution; + } + + public String getReferenceType() { + return referenceType; + } + + public void setReferenceType(String referenceType) { + this.referenceType = referenceType; + } + + public String getRequirement() { + return requirement; + } + + public void setRequirement(String requirement) { + this.requirement = requirement; + } + + public List getRequirementChoices() { + return requirementChoices; + } + + public void setRequirementChoices(List requirementChoices) { + this.requirementChoices = requirementChoices; + } + + public Boolean getConfigurable() { + return configurable; + } + + public void setConfigurable(Boolean configurable) { + this.configurable = configurable; + } + + public Boolean getSubFlow() { + return subFlow; + } + + public void setSubFlow(Boolean subFlow) { + this.subFlow = subFlow; + } + } + + @Path("/flow/{flowAlias}/executions") + @GET + @NoCache + @Produces(MediaType.APPLICATION_JSON) + public Response getExecutions(@PathParam("flowAlias") String flowAlias) { + this.auth.requireView(); + + AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias); + if (flow == null) { + logger.debug("flow not found: " + flowAlias); + return Response.status(NOT_FOUND).build(); + } + List result = new LinkedList<>(); + List executions = AuthenticatorUtil.getEnabledExecutionsRecursively(realm, flow.getId()); + for (AuthenticationExecutionModel execution : executions) { + AuthenticationExecutionRepresentation rep = new AuthenticationExecutionRepresentation(); + rep.setSubFlow(false); + rep.setRequirementChoices(new LinkedList()); + if (execution.isAutheticatorFlow()) { + AuthenticationFlowModel flowRef = realm.getAuthenticationFlowById(execution.getAuthenticator()); + rep.setReferenceType(flowRef.getAlias()); + rep.setExecution(execution.getId()); + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name()); + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.REQUIRED.name()); + rep.getRequirementChoices().add(AuthenticationExecutionModel.Requirement.DISABLED.name()); + rep.setConfigurable(false); + rep.setExecution(execution.getId()); + rep.setRequirement(execution.getRequirement().name()); + result.add(rep); + } else { + if (!flow.getId().equals(execution.getParentFlow())) { + rep.setSubFlow(true); + } + AuthenticatorModel authenticator = realm.getAuthenticatorById(execution.getAuthenticator()); + AuthenticatorFactory factory = (AuthenticatorFactory)session.getKeycloakSessionFactory().getProviderFactory(Authenticator.class, authenticator.getProviderId()); + if (factory.getReferenceType() == null) continue; + rep.setReferenceType(factory.getReferenceType()); + rep.setConfigurable(factory.isConfigurable()); + for (AuthenticationExecutionModel.Requirement choice : factory.getRequirementChoices()) { + rep.getRequirementChoices().add(choice.name()); + } + rep.setExecution(execution.getId()); + rep.setRequirement(execution.getRequirement().name()); + result.add(rep); + + } + + } + return Response.ok(result).build(); + } + + @Path("/flow/{flowAlias}/executions") + @PUT + @NoCache + @Consumes(MediaType.APPLICATION_JSON) + public void updateExecutions(@PathParam("flowAlias") String flowAlias, AuthenticationExecutionRepresentation rep) { + this.auth.requireManage(); + + AuthenticationFlowModel flow = realm.getFlowByAlias(flowAlias); + if (flow == null) { + logger.debug("flow not found: " + flowAlias); + throw new NotFoundException("flow not found"); + } + + AuthenticationExecutionModel model = realm.getAuthenticationExecutionById(rep.getExecution()); + if (model == null) { + session.getTransaction().setRollbackOnly(); + throw new NotFoundException("Illegal execution"); + + } + if (!model.getRequirement().name().equals(rep.getRequirement())) { + model.setRequirement(AuthenticationExecutionModel.Requirement.valueOf(rep.getRequirement())); + realm.updateAuthenticatorExecution(model); + } + } + +} \ No newline at end of file diff --git a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java index 45fb45e216..2a0a1150cd 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/RealmAdminResource.java @@ -6,6 +6,8 @@ import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.jboss.resteasy.spi.ResteasyProviderFactory; import org.keycloak.ClientConnection; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.events.Event; import org.keycloak.events.EventQuery; import org.keycloak.events.EventStoreProvider; @@ -25,6 +27,7 @@ import org.keycloak.models.cache.CacheUserProvider; import org.keycloak.models.utils.ModelToRepresentation; import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.TokenManager; +import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.adapters.action.GlobalRequestResult; import org.keycloak.representations.idm.RealmEventsConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; @@ -236,6 +239,15 @@ public class RealmAdminResource { return fed; } + @Path("authentication-flows") + public AuthenticationFlowResource flows() { + AuthenticationFlowResource resource = new AuthenticationFlowResource(realm, session, auth, adminEvent); + ResteasyProviderFactory.getInstance().injectProperties(resource); + //resourceContext.initResource(resource); + return resource; + + } + /** * Path for managing all realm-level or client-level roles defined in this realm by it's id. * @@ -553,4 +565,19 @@ public class RealmAdminResource { public IdentityProvidersResource getIdentityProviderResource() { return new IdentityProvidersResource(realm, session, this.auth, adminEvent); } + + @Path("required-actions") + @GET + @Produces(MediaType.APPLICATION_JSON) + public List> getRequiredActions() { + List> list = new LinkedList<>(); + for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) { + RequiredActionFactory actionFactory = (RequiredActionFactory)factory; + Map data = new HashMap<>(); + data.put("id", actionFactory.getId()); + data.put("text", actionFactory.getDisplayText()); + list.add(data); + } + return list; + } } diff --git a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java index c00fb11d36..b27e45353d 100755 --- a/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java +++ b/services/src/main/java/org/keycloak/services/resources/admin/UsersResource.java @@ -5,6 +5,8 @@ import org.jboss.resteasy.annotations.cache.NoCache; import org.jboss.resteasy.spi.BadRequestException; import org.jboss.resteasy.spi.NotFoundException; import org.keycloak.ClientConnection; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; import org.keycloak.email.EmailException; import org.keycloak.email.EmailProvider; import org.keycloak.events.admin.OperationType; @@ -27,6 +29,7 @@ import org.keycloak.models.utils.RepresentationToModel; import org.keycloak.protocol.oidc.OIDCLoginProtocol; import org.keycloak.protocol.oidc.TokenManager; import org.keycloak.protocol.oidc.utils.RedirectUtils; +import org.keycloak.provider.ProviderFactory; import org.keycloak.representations.idm.ClientMappingsRepresentation; import org.keycloak.representations.idm.CredentialRepresentation; import org.keycloak.representations.idm.FederatedIdentityRepresentation; @@ -212,11 +215,15 @@ public class UsersResource { List reqActions = rep.getRequiredActions(); if (reqActions != null) { - for (UserModel.RequiredAction ra : UserModel.RequiredAction.values()) { - if (reqActions.contains(ra.name())) { - user.addRequiredAction(ra); + Set allActions = new HashSet<>(); + for (ProviderFactory factory : session.getKeycloakSessionFactory().getProviderFactories(RequiredActionProvider.class)) { + allActions.add(factory.getId()); + } + for (String action : allActions) { + if (reqActions.contains(action)) { + user.addRequiredAction(action); } else { - user.removeRequiredAction(ra); + user.removeRequiredAction(action); } } } @@ -804,7 +811,7 @@ public class UsersResource { ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId); ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); - accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD); + accessCode.setAction(ClientSessionModel.Action.RECOVER_PASSWORD.name()); try { UriBuilder builder = Urls.loginPasswordResetBuilder(uriInfo.getBaseUri()); @@ -854,7 +861,7 @@ public class UsersResource { ClientSessionModel clientSession = createClientSession(user, redirectUri, clientId); ClientSessionCode accessCode = new ClientSessionCode(realm, clientSession); - accessCode.setAction(ClientSessionModel.Action.VERIFY_EMAIL); + accessCode.setAction(ClientSessionModel.Action.VERIFY_EMAIL.name()); try { UriBuilder builder = Urls.loginActionEmailVerificationBuilder(uriInfo.getBaseUri()); diff --git a/services/src/main/java/org/keycloak/services/validation/Validation.java b/services/src/main/java/org/keycloak/services/validation/Validation.java index d3abc4d830..40e667d6c6 100755 --- a/services/src/main/java/org/keycloak/services/validation/Validation.java +++ b/services/src/main/java/org/keycloak/services/validation/Validation.java @@ -1,134 +1,134 @@ -package org.keycloak.services.validation; - -import org.keycloak.models.PasswordPolicy; -import org.keycloak.models.RealmModel; -import org.keycloak.models.UserModel; -import org.keycloak.models.utils.FormMessage; -import org.keycloak.representations.idm.CredentialRepresentation; -import org.keycloak.services.messages.Messages; - -import javax.ws.rs.core.MultivaluedMap; - -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -public class Validation { - - public static final String FIELD_PASSWORD_CONFIRM = "password-confirm"; - public static final String FIELD_EMAIL = "email"; - public static final String FIELD_LAST_NAME = "lastName"; - public static final String FIELD_FIRST_NAME = "firstName"; - public static final String FIELD_PASSWORD = "password"; - public static final String FIELD_USERNAME = "username"; - - // Actually allow same emails like angular. See ValidationTest.testEmailValidation() - private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*"); - - public static List validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes, PasswordPolicy policy) { - List errors = new ArrayList<>(); - - if (!realm.isRegistrationEmailAsUsername() && isBlank(formData.getFirst(FIELD_USERNAME))) { - addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME); - } - - if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) { - addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); - } - - if (isBlank(formData.getFirst(FIELD_LAST_NAME))) { - addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); - } - - if (isBlank(formData.getFirst(FIELD_EMAIL))) { - addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); - } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { - addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); - } - - if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { - if (isBlank(formData.getFirst(FIELD_PASSWORD))) { - addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD); - } else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) { - addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM); - } - } - - if (formData.getFirst(FIELD_PASSWORD) != null) { - PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD)); - if (err != null) - errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters())); - } - - return errors; - } - - private static void addError(List errors, String field, String message){ - errors.add(new FormMessage(field, message)); - } - - public static List validateUpdateProfileForm(MultivaluedMap formData) { - return validateUpdateProfileForm(null, formData); - } - - public static List validateUpdateProfileForm(RealmModel realm, MultivaluedMap formData) { - List errors = new ArrayList<>(); - - if (realm != null && realm.isEditUsernameAllowed() && isBlank(formData.getFirst(FIELD_USERNAME))) { - addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME); - } - - if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) { - addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); - } - - if (isBlank(formData.getFirst(FIELD_LAST_NAME))) { - addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); - } - - if (isBlank(formData.getFirst(FIELD_EMAIL))) { - addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); - } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { - addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); - } - - return errors; - } - - /** - * Validate if user object contains all mandatory fields. - * - * @param realm user is for - * @param user to validate - * @return true if user object contains all mandatory values, false if some mandatory value is missing - */ - public static boolean validateUserMandatoryFields(RealmModel realm, UserModel user){ - return!(isBlank(user.getFirstName()) || isBlank(user.getLastName()) || isBlank(user.getEmail())); - } - - /** - * Check if string is empty (null or lenght is 0) - * - * @param s to check - * @return true if string is empty - */ - public static boolean isEmpty(String s) { - return s == null || s.length() == 0; - } - - /** - * Check if string is blank (null or lenght is 0 or contains only white characters) - * - * @param s to check - * @return true if string is blank - */ - public static boolean isBlank(String s) { - return s == null || s.trim().length() == 0; - } - - public static boolean isEmailValid(String email) { - return EMAIL_PATTERN.matcher(email).matches(); - } - - -} +package org.keycloak.services.validation; + +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.RealmModel; +import org.keycloak.models.UserModel; +import org.keycloak.models.utils.FormMessage; +import org.keycloak.representations.idm.CredentialRepresentation; +import org.keycloak.services.messages.Messages; + +import javax.ws.rs.core.MultivaluedMap; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +public class Validation { + + public static final String FIELD_PASSWORD_CONFIRM = "password-confirm"; + public static final String FIELD_EMAIL = "email"; + public static final String FIELD_LAST_NAME = "lastName"; + public static final String FIELD_FIRST_NAME = "firstName"; + public static final String FIELD_PASSWORD = "password"; + public static final String FIELD_USERNAME = "username"; + + // Actually allow same emails like angular. See ValidationTest.testEmailValidation() + private static final Pattern EMAIL_PATTERN = Pattern.compile("[a-zA-Z0-9!#$%&'*+/=?^_`{|}~.-]+@[a-zA-Z0-9-]+(\\.[a-zA-Z0-9-]+)*"); + + public static List validateRegistrationForm(RealmModel realm, MultivaluedMap formData, List requiredCredentialTypes, PasswordPolicy policy) { + List errors = new ArrayList<>(); + + if (!realm.isRegistrationEmailAsUsername() && isBlank(formData.getFirst(FIELD_USERNAME))) { + addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME); + } + + if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) { + addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); + } + + if (isBlank(formData.getFirst(FIELD_LAST_NAME))) { + addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); + } + + if (isBlank(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); + } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); + } + + if (requiredCredentialTypes.contains(CredentialRepresentation.PASSWORD)) { + if (isBlank(formData.getFirst(FIELD_PASSWORD))) { + addError(errors, FIELD_PASSWORD, Messages.MISSING_PASSWORD); + } else if (!formData.getFirst(FIELD_PASSWORD).equals(formData.getFirst(FIELD_PASSWORD_CONFIRM))) { + addError(errors, FIELD_PASSWORD_CONFIRM, Messages.INVALID_PASSWORD_CONFIRM); + } + } + + if (formData.getFirst(FIELD_PASSWORD) != null) { + PasswordPolicy.Error err = policy.validate(realm.isRegistrationEmailAsUsername()?formData.getFirst(FIELD_EMAIL):formData.getFirst(FIELD_USERNAME), formData.getFirst(FIELD_PASSWORD)); + if (err != null) + errors.add(new FormMessage(FIELD_PASSWORD, err.getMessage(), err.getParameters())); + } + + return errors; + } + + private static void addError(List errors, String field, String message){ + errors.add(new FormMessage(field, message)); + } + + public static List validateUpdateProfileForm(MultivaluedMap formData) { + return validateUpdateProfileForm(null, formData); + } + + public static List validateUpdateProfileForm(RealmModel realm, MultivaluedMap formData) { + List errors = new ArrayList<>(); + + if (realm != null && realm.isEditUsernameAllowed() && isBlank(formData.getFirst(FIELD_USERNAME))) { + addError(errors, FIELD_USERNAME, Messages.MISSING_USERNAME); + } + + if (isBlank(formData.getFirst(FIELD_FIRST_NAME))) { + addError(errors, FIELD_FIRST_NAME, Messages.MISSING_FIRST_NAME); + } + + if (isBlank(formData.getFirst(FIELD_LAST_NAME))) { + addError(errors, FIELD_LAST_NAME, Messages.MISSING_LAST_NAME); + } + + if (isBlank(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.MISSING_EMAIL); + } else if (!isEmailValid(formData.getFirst(FIELD_EMAIL))) { + addError(errors, FIELD_EMAIL, Messages.INVALID_EMAIL); + } + + return errors; + } + + /** + * Validate if user object contains all mandatory fields. + * + * @param realm user is for + * @param user to validate + * @return true if user object contains all mandatory values, false if some mandatory value is missing + */ + public static boolean validateUserMandatoryFields(RealmModel realm, UserModel user){ + return!(isBlank(user.getFirstName()) || isBlank(user.getLastName()) || isBlank(user.getEmail())); + } + + /** + * Check if string is empty (null or lenght is 0) + * + * @param s to check + * @return true if string is empty + */ + public static boolean isEmpty(String s) { + return s == null || s.length() == 0; + } + + /** + * Check if string is blank (null or lenght is 0 or contains only white characters) + * + * @param s to check + * @return true if string is blank + */ + public static boolean isBlank(String s) { + return s == null || s.trim().length() == 0; + } + + public static boolean isEmailValid(String email) { + return EMAIL_PATTERN.matcher(email).matches(); + } + + +} 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..eb41e5e7fd 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 diff --git a/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory new file mode 100755 index 0000000000..8106ec7fce --- /dev/null +++ b/services/src/main/resources/META-INF/services/org.keycloak.authentication.RequiredActionFactory @@ -0,0 +1,5 @@ +org.keycloak.authentication.actions.UpdatePassword +org.keycloak.authentication.actions.UpdateProfile +org.keycloak.authentication.actions.UpdateTotp +org.keycloak.authentication.actions.VerifyEmail +org.keycloak.authentication.actions.TermsAndConditions \ No newline at end of file diff --git a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi index d2b5ca78ff..050fef24d6 100755 --- a/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi +++ b/services/src/main/resources/META-INF/services/org.keycloak.provider.Spi @@ -3,4 +3,5 @@ org.keycloak.protocol.ProtocolMapperSpi org.keycloak.exportimport.ClientImportSpi org.keycloak.wellknown.WellKnownSpi org.keycloak.messages.MessagesSpi -org.keycloak.authentication.AuthenticatorSpi \ No newline at end of file +org.keycloak.authentication.AuthenticatorSpi +org.keycloak.authentication.RequiredActionSpi \ No newline at end of file diff --git a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java index 64eeb62c3b..d662141681 100755 --- a/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java +++ b/social/twitter/src/main/java/org/keycloak/social/twitter/TwitterIdentityProvider.java @@ -166,7 +166,7 @@ public class TwitterIdentityProvider extends AbstractIdentityProviderStian Thorgersen + */ +public class BypassKerberosPage extends AbstractPage { + + @FindBy(name = "continue") + private WebElement continueButton; + + public boolean isCurrent() { + return driver.getTitle().equals("Log in to test") || driver.getTitle().equals("Anmeldung bei test"); + } + + public void clickContinue() { + continueButton.click(); + } + + @Override + public void open() throws Exception { + + } +} 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..8ac3a158a7 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java @@ -0,0 +1,91 @@ +package org.keycloak.testsuite.utils; + +import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory; +import org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory; +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) { + AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.REQUIRED; + setCredentialRequirement(type, realm, requirement); + } + + public static void setAlternativeCredential(String type, RealmModel realm) { + AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.ALTERNATIVE; + setCredentialRequirement(type, realm, requirement); + } + + public static void setCredentialRequirement(String type, RealmModel realm, AuthenticationExecutionModel.Requirement requirement) { + if (type.equals(CredentialRepresentation.TOTP)) { + String providerId = OTPFormAuthenticatorFactory.PROVIDER_ID; + String flowAlias = DefaultAuthenticationFlows.FORMS_FLOW; + authenticationRequirement(realm, providerId, flowAlias, requirement); + } else if (type.equals(CredentialRepresentation.KERBEROS)) { + String providerId = SpnegoAuthenticatorFactory.PROVIDER_ID; + String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW; + authenticationRequirement(realm, providerId, flowAlias, requirement); + } else if (type.equals(CredentialRepresentation.PASSWORD)) { + String providerId = LoginFormPasswordAuthenticatorFactory.PROVIDER_ID; + String flowAlias = DefaultAuthenticationFlows.FORMS_FLOW; + authenticationRequirement(realm, providerId, flowAlias, requirement); + } + } + + public static AuthenticationExecutionModel.Requirement getRequirement(RealmModel realm, String authenticatorProviderId, String flowAlias) { + AuthenticatorModel authenticator = findAuthenticatorByProviderId(realm, authenticatorProviderId); + AuthenticationFlowModel flow = findAuthenticatorFlowByAlias(realm, flowAlias); + AuthenticationExecutionModel execution = findExecutionByAuthenticator(realm, flow.getId(), authenticator.getId()); + return execution.getRequirement(); + + } + + 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; + + } +}