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/services.js b/forms/common-themes/src/main/resources/theme/base/admin/resources/js/services.js index e9f877b19c..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 @@ -1073,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/messages/messages_de.properties b/forms/common-themes/src/main/resources/theme/base/login/messages/messages_de.properties index 347ad369b4..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 @@ -6,8 +6,13 @@ 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 8aac70255c..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,11 +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} 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 0fb25d747e..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 @@ -6,8 +6,13 @@ 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 a828f72b00..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 @@ -6,8 +6,13 @@ 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/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 0b36b6810b..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 @@ -314,6 +314,9 @@ import java.util.concurrent.TimeUnit; } 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()); 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 73200e81af..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 @@ -1289,6 +1289,7 @@ public class RealmAdapter implements RealmModel { AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } 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 ecf399addf..9978abaf3c 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 @@ -1588,6 +1588,7 @@ public class RealmAdapter implements RealmModel { AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } 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 2b6b85bc26..4b8090c20e 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 @@ -1319,6 +1319,7 @@ public class RealmAdapter extends AbstractMongoAdapter impleme AuthenticationExecutionModel model = entityToModel(entity); executions.add(model); } + Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); return executions; } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 7d3760ef58..473bc4fba0 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -49,6 +49,7 @@ public class AuthenticationProcessor { public static enum Status { SUCCESS, CHALLENGE, + FORCE_CHALLENGE, FAILURE_CHALLENGE, FAILED, ATTEMPTED @@ -214,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) { @@ -437,7 +444,6 @@ public class AuthenticationProcessor { } List executions = realm.getAuthenticationExecutions(flowId); if (executions == null) return null; - Collections.sort(executions, AuthenticationExecutionModel.ExecutionComparator.SINGLETON); Response alternativeChallenge = null; AuthenticationExecutionModel challengedAlternativeExecution = null; boolean alternativeSuccessful = false; @@ -513,6 +519,9 @@ public class AuthenticationProcessor { 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) { logger.debugv("authenticator CHALLENGE: {0}", authenticatorModel.getProviderId()); if (model.isRequired() || (model.isOptional() && configuredFor)) { diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java index 1569c031ef..248eb7accf 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorContext.java @@ -61,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 index ca7dd4d0e1..fcf34ba49c 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticatorUtil.java @@ -10,12 +10,31 @@ 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()) { 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/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/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/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/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/SetRequiredActionAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/SetRequiredActionAuthenticator.java deleted file mode 100755 index 563d8f27c9..0000000000 --- a/services/src/main/java/org/keycloak/authentication/authenticators/SetRequiredActionAuthenticator.java +++ /dev/null @@ -1,48 +0,0 @@ -package org.keycloak.authentication.authenticators; - -import org.keycloak.authentication.AuthenticationProcessor; -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; - -/** - * No auth, but it sets a required action. - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SetRequiredActionAuthenticator implements Authenticator { - - @Override - public boolean requiresUser() { - return false; - } - - @Override - public void authenticate(AuthenticatorContext context) { - UserModel user = context.getUser(); - if (user == null) { - throw new AuthenticationProcessor.AuthException(AuthenticationProcessor.Error.UNKNOWN_USER); - } - user.addRequiredAction(context.getAuthenticatorModel().getConfig().get("required.action")); - context.success(); - } - - @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/SetRequiredActionAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/SetRequiredActionAuthenticatorFactory.java deleted file mode 100755 index 79f9f551d6..0000000000 --- a/services/src/main/java/org/keycloak/authentication/authenticators/SetRequiredActionAuthenticatorFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -package org.keycloak.authentication.authenticators; - -import org.keycloak.Config; -import org.keycloak.authentication.Authenticator; -import org.keycloak.authentication.AuthenticatorFactory; -import org.keycloak.models.AuthenticatorModel; -import org.keycloak.models.KeycloakSession; -import org.keycloak.models.KeycloakSessionFactory; -import org.keycloak.provider.ProviderConfigProperty; - -import java.util.List; - -/** - * @author Bill Burke - * @version $Revision: 1 $ - */ -public class SetRequiredActionAuthenticatorFactory implements AuthenticatorFactory { - public static final String PROVIDER_ID = "auth-set-required-action"; - static SetRequiredActionAuthenticator SINGLETON = new SetRequiredActionAuthenticator(); - @Override - public Authenticator create(AuthenticatorModel model) { - return SINGLETON; - } - - @Override - public Authenticator create(KeycloakSession session) { - throw new IllegalStateException("illegal call"); - } - - @Override - public void init(Config.Scope config) { - - } - - @Override - public void postInit(KeycloakSessionFactory factory) { - - } - - @Override - public void close() { - - } - - @Override - public String getId() { - return PROVIDER_ID; - } - - @Override - public String getDisplayCategory() { - return "Action"; - } - - @Override - public String getDisplayType() { - return "Set Required Action"; - } - - @Override - public String getHelpText() { - return "Doesn't do any authentication. Instead it just sets a configured required action for the user."; - } - - @Override - public List getConfigProperties() { - return null; - } -} diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java index 1045b558f2..3970fa8916 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticator.java @@ -8,22 +8,31 @@ 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 @@ -41,7 +50,10 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au 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)) { @@ -49,7 +61,7 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au return; } Response challenge = challengeNegotiation(context, null); - context.challenge(challenge); + context.forceChallenge(challenge); return; } @@ -98,11 +110,68 @@ public class SpnegoAuthenticator extends AbstractFormAuthenticator implements Au if (logger.isTraceEnabled()) { logger.trace("Sending back " + HttpHeaders.WWW_AUTHENTICATE + ": " + negotiateHeader); } - LoginFormsProvider loginForm = loginForm(context); + 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); + } + } - loginForm.setStatus(Response.Status.UNAUTHORIZED); - loginForm.setResponseHeader(HttpHeaders.WWW_AUTHENTICATE, negotiateHeader); - return loginForm.createLogin(); + // 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()); } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java index a541305d17..21c56fc8d0 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/SpnegoAuthenticatorFactory.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; @@ -43,6 +45,27 @@ public class SpnegoAuthenticatorFactory implements AuthenticatorFactory { } + @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; 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/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 2410331ef1..f72da8c0c6 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 @@ -238,6 +238,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. * 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 cb6bdc77e2..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 @@ -4,4 +4,3 @@ org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory org.keycloak.authentication.authenticators.LoginFormUsernameAuthenticatorFactory org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory -org.keycloak.authentication.authenticators.SetRequiredActionAuthenticatorFactory \ No newline at end of file diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java index c5b25fdd90..b307baa024 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/AbstractKerberosTest.java @@ -18,6 +18,7 @@ import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.keycloak.adapters.HttpClientBuilder; +import org.keycloak.authentication.authenticators.SpnegoAuthenticator; import org.keycloak.events.Details; import org.keycloak.federation.kerberos.CommonKerberosConfig; import org.keycloak.constants.KerberosConstants; @@ -35,6 +36,7 @@ import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.AssertEvents; import org.keycloak.testsuite.OAuthClient; import org.keycloak.testsuite.pages.AccountPasswordPage; +import org.keycloak.testsuite.pages.BypassKerberosPage; import org.keycloak.testsuite.pages.LoginPage; import org.keycloak.testsuite.rule.KeycloakRule; import org.keycloak.testsuite.rule.WebResource; @@ -59,6 +61,9 @@ public abstract class AbstractKerberosTest { @WebResource protected LoginPage loginPage; + @WebResource + protected BypassKerberosPage bypassPage; + @WebResource protected AccountPasswordPage changePasswordPage; @@ -85,6 +90,7 @@ public abstract class AbstractKerberosTest { public void spnegoNotAvailableTest() throws Exception { initHttpClient(false); + SpnegoAuthenticator.bypassChallengeJavascript = true; driver.navigate().to(KERBEROS_APP_URL); String kcLoginPageLocation = driver.getCurrentUrl(); @@ -94,6 +100,7 @@ public abstract class AbstractKerberosTest { String responseText = response.readEntity(String.class); responseText.contains("Log in to test"); response.close(); + SpnegoAuthenticator.bypassChallengeJavascript = false; } @@ -120,7 +127,7 @@ public abstract class AbstractKerberosTest { spnegoResponse.close(); events.clear(); - } + } @Test @@ -133,6 +140,10 @@ public abstract class AbstractKerberosTest { // Login with username/password from kerberos changePasswordPage.open(); + // Only needed if you are providing a click thru to bypass kerberos. Currently there is a javascript + // to forward the user if kerberos isn't enabled. + //bypassPage.isCurrent(); + //bypassPage.clickContinue(); loginPage.assertCurrent(); loginPage.login("jduke", "theduke"); changePasswordPage.assertCurrent(); @@ -149,6 +160,10 @@ public abstract class AbstractKerberosTest { Assert.assertTrue(driver.getPageSource().contains("Your password has been updated.")); changePasswordPage.logout(); + // Only needed if you are providing a click thru to bypass kerberos. Currently there is a javascript + // to forward the user if kerberos isn't enabled. + //bypassPage.isCurrent(); + //bypassPage.clickContinue(); // Login with old password doesn't work, but with new password works loginPage.login("jduke", "theduke"); loginPage.assertCurrent(); @@ -156,7 +171,7 @@ public abstract class AbstractKerberosTest { changePasswordPage.assertCurrent(); changePasswordPage.logout(); - // Assert SPNEGO login still with the old password as mode is unsynced + // Assert SPNEGO login still with the old password as mode is unsynced events.clear(); Response spnegoResponse = spnegoLogin("jduke", "theduke"); Assert.assertEquals(302, spnegoResponse.getStatus()); @@ -221,12 +236,16 @@ public abstract class AbstractKerberosTest { protected Response spnegoLogin(String username, String password) { + SpnegoAuthenticator.bypassChallengeJavascript = true; driver.navigate().to(KERBEROS_APP_URL); String kcLoginPageLocation = driver.getCurrentUrl(); - + String location = "http://localhost:8081/auth/realms/test/protocol/openid-connect/auth?response_type=code&client_id=kerberos-app&redirect_uri=http%3A%2F%2Flocalhost%3A8081%2Fkerberos-portal&state=0%2F88a96ddd-84fe-4e77-8a46-02394d7b3a7d&login=true"; // Request for SPNEGO login sent with Resteasy client spnegoSchemeFactory.setCredentials(username, password); - return client.target(kcLoginPageLocation).request().get(); + Response response = client.target(kcLoginPageLocation).request().get(); + SpnegoAuthenticator.bypassChallengeJavascript = false; + return response; + } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java index 765c88a529..0192e351d5 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosLdapTest.java @@ -44,7 +44,7 @@ public class KerberosLdapTest extends AbstractKerberosTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - CredentialHelper.setRequiredCredential(CredentialRepresentation.KERBEROS, appRealm); + CredentialHelper.setAlternativeCredential(CredentialRepresentation.KERBEROS, appRealm); URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json"); keycloakRule.createApplicationDeployment() .name("kerberos-portal").contextPath("/kerberos-portal") @@ -109,6 +109,10 @@ public class KerberosLdapTest extends AbstractKerberosTest { // Login with username/password from kerberos changePasswordPage.open(); + // Only needed if you are providing a click thru to bypass kerberos. Currently there is a javascript + // to forward the user if kerberos isn't enabled. + //bypassPage.isCurrent(); + //bypassPage.clickContinue(); loginPage.assertCurrent(); loginPage.login("jduke", "theduke"); changePasswordPage.assertCurrent(); @@ -118,6 +122,11 @@ public class KerberosLdapTest extends AbstractKerberosTest { Assert.assertTrue(driver.getPageSource().contains("Your password has been updated.")); changePasswordPage.logout(); + // Only needed if you are providing a click thru to bypass kerberos. Currently there is a javascript + // to forward the user if kerberos isn't enabled. + //bypassPage.isCurrent(); + //bypassPage.clickContinue(); + // Login with old password doesn't work, but with new password works loginPage.login("jduke", "theduke"); loginPage.assertCurrent(); @@ -139,6 +148,11 @@ public class KerberosLdapTest extends AbstractKerberosTest { // Change password back changePasswordPage.open(); + // Only needed if you are providing a click thru to bypass kerberos. Currently there is a javascript + // to forward the user if kerberos isn't enabled. + //bypassPage.isCurrent(); + //bypassPage.clickContinue(); + loginPage.login("jduke", "newPass"); changePasswordPage.assertCurrent(); changePasswordPage.changePassword("newPass", "theduke", "theduke"); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java index cc0f80e86e..edb5afae91 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/federation/KerberosStandaloneTest.java @@ -45,7 +45,7 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { @Override public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { - CredentialHelper.setRequiredCredential(CredentialRepresentation.KERBEROS, appRealm); + CredentialHelper.setAlternativeCredential(CredentialRepresentation.KERBEROS, appRealm); URL url = getClass().getResource("/kerberos-test/kerberos-app-keycloak.json"); keycloakRule.createApplicationDeployment() .name("kerberos-portal").contextPath("/kerberos-portal") @@ -104,6 +104,11 @@ public class KerberosStandaloneTest extends AbstractKerberosTest { assertUser("hnelson", "hnelson@keycloak.org", null, null, false); } + @Test + @Override + public void usernamePasswordLoginTest() throws Exception { + super.usernamePasswordLoginTest(); + } @Test public void updateProfileEnabledTest() throws Exception { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/BypassKerberosPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/BypassKerberosPage.java new file mode 100755 index 0000000000..10554d3794 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/BypassKerberosPage.java @@ -0,0 +1,50 @@ +/* + * 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.testsuite.pages; + +import org.keycloak.testsuite.OAuthClient; +import org.keycloak.testsuite.rule.WebResource; +import org.openqa.selenium.By; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.FindBy; + +/** + * @author Stian 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 index a88d8b1cd6..8ac3a158a7 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/utils/CredentialHelper.java @@ -1,9 +1,7 @@ package org.keycloak.testsuite.utils; import org.keycloak.authentication.authenticators.LoginFormPasswordAuthenticatorFactory; -import org.keycloak.authentication.authenticators.OTPFormAuthenticator; import org.keycloak.authentication.authenticators.OTPFormAuthenticatorFactory; -import org.keycloak.authentication.authenticators.SpnegoAuthenticator; import org.keycloak.authentication.authenticators.SpnegoAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; @@ -19,18 +17,28 @@ import org.keycloak.representations.idm.CredentialRepresentation; 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; - requireAuthentication(realm, providerId, flowAlias); + authenticationRequirement(realm, providerId, flowAlias, requirement); } else if (type.equals(CredentialRepresentation.KERBEROS)) { String providerId = SpnegoAuthenticatorFactory.PROVIDER_ID; String flowAlias = DefaultAuthenticationFlows.BROWSER_FLOW; - alternativeAuthentication(realm, providerId, flowAlias); + authenticationRequirement(realm, providerId, flowAlias, requirement); } else if (type.equals(CredentialRepresentation.PASSWORD)) { String providerId = LoginFormPasswordAuthenticatorFactory.PROVIDER_ID; String flowAlias = DefaultAuthenticationFlows.FORMS_FLOW; - requireAuthentication(realm, providerId, flowAlias); + authenticationRequirement(realm, providerId, flowAlias, requirement); } } @@ -42,11 +50,6 @@ public class CredentialHelper { } - public static void requireAuthentication(RealmModel realm, String authenticatorProviderId, String flowAlias) { - AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.REQUIRED; - authenticationRequirement(realm, authenticatorProviderId, flowAlias, requirement); - } - public static void alternativeAuthentication(RealmModel realm, String authenticatorProviderId, String flowAlias) { AuthenticationExecutionModel.Requirement requirement = AuthenticationExecutionModel.Requirement.ALTERNATIVE; authenticationRequirement(realm, authenticatorProviderId, flowAlias, requirement);