From 1801526f65b4ac9942ef183749427c3e6e6f4ea5 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Tue, 11 Aug 2015 17:44:06 -0400 Subject: [PATCH 1/3] auth spi doco --- .../reference/en/en-US/modules/auth-spi.xml | 195 ++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml index 54bd36d179..8e1ac29d0f 100755 --- a/docbook/reference/en/en-US/modules/auth-spi.xml +++ b/docbook/reference/en/en-US/modules/auth-spi.xml @@ -74,7 +74,202 @@ + + Required Action + + + After authentication completes, the user might have one or more one-time actions he must + complete before he is allowed to login. The user might be required to set up an OTP token + generator or reset an expired password or even accept a Terms and Conditions document. + + + +
+ Algorithm Overview + + Let's talk about how this all works for browser login. Let's assume the following flows, executions and sub flows. + + + + + In the top level of the form we have 3 executions of which all are alternatively required. This means that + if any of these are successful, then the others do not have to execute. The Username/Password form is not executed + if there is an SSO Cookie set or a successful Kerberos login. Let's walk through the steps from when a client + first redirects to keycloak to authenticate the user. + + + + The OpenID Connect or SAML protocol provider unpacks relevent data, verifies the client and any signatures. + It creates a ClientSessionModel. It looks up what the browser flow should be, then starts executing the flow. + + + + + The flow looks at the cookie execution and sees that it is an alternative. It loads the cookie provider. + It checks to see if the cookie provider requires that a user already be associated with the client session. + Cookie provider does not require a user. If it did, the flow would abort and the user would see an error screen. + Cookie provider then executes. Its purpose is to see if there is an SSO cookie set. If there is one set, it is validated + and the UserSessionModel is verified and associated with the ClientSessionModel. The Cookie provider returns a + success() status if the SSO cookie exists and is validated. Since the cookie provider returned success and each execution + at this level of the flow is ALTERNATIVE, no other execution is executed and this results in a successful login. + If there is no SSO cookie, the cookie provider returns with a status of attempted(). This means there was no error condition, + but no success either. The provider tried, but the request just wasn't set up to handle this authenticator. + + + + + Next the flow looks at the Kerberos execution. This is also an alternative. The kerberos provider also does not + require a user to be already set up and associated with the ClientSessionModel so this provider is executed. + Kerberos uses the SPNEGO browser protocol. This requires a series of challenge/responses between the server and client + exchanging negotiation headers. The kerberos provider does not see any negotiate header, so it assumes that this is the + first interaction between the server and client. It therefore creates an HTTP challenge response to the client and sets a + forceChallenge() status. A forceChallenge() means that this HTTP response cannot be ignored by the flow and must be returned to the + client. If instead the provider returned a challenge() status, the flow would hold the challenge response until all other alternatives + are attempted. So, in this initial phase, the flow would stop and the challenge response would be sent back to the browser. If the browser + then responds with a successful negotiate header, the provider associates the user with the ClientSession and the flow ends because + the rest of the executions on this level of the flow are all alternatives. Otherwise, again, the kerberos provider + sets an attempted() status and the flow continues. + + + + + The next execution is a subflow called Forms. The executions for this subflow are loaded and + the same processing logic occurs + + + + + The first execution in the Forms subflow is the UsernamePassword provider. This provider also does not require for a user + to already be associated with the flow. This provider creates challenge HTTP response and sets its status to challenge(). + This execution is required, so the flow honors this challenge and sends the HTTP response back to the browser. This + response is a rendering of the Username/Password HTML page. The user enters in their username and password and clicks submit. + This HTTP request is directed to the UsernamePassword provider. If the user entered an invalid username or password, a new + challenge response is created and a status of failureChallenge() is set for this execution. A failureChallenge() means that + there is a challenge, but that the flow should log this as an error in the error log. This error log can be used to lock accounts + or IP Addresses that have had too many login failures. If the username and password is valid, the provider associated the + UserModel with the ClientSessionModel and returns a status of success() + + + + + The next execution is the OTP Form. This provider requires that a user has been associated with the flow. This requirement is satisfied + because the UsernamePassword provider already associated the user with the flow. Since a user is required for this provider, the provider + is also asked if the user is configured to use this provider. If user is not configured, and this execution is required, then the flow will + then set up a required action that the user must perform after authentication is complete. For OTP, this means the OTP setup page. + If the execution was optional, then this execution is skipped. + + + + + After the flow is complete, the authentication processor creates a UserSEssionModel and associates it with the ClientSEssionModel. + It then checks to see if the user is required to complete any required actions before logging in. + + + + + First, each required action's evaluateTriggers() method is called. This allows the required action provider to figure out if + there is some state that might trigger the action to be fired. For example, if your realm has a password expiration policy, + it might be triggered by this method. + + + + + Each required action associated with the user that has its requiredActionChallenge() method called. Here the provider + sets up an HTTP response which renders the page for the required action. This is done by setting a challenge status. + + + + + If the required action is ultimately successful, then the required action is removed from the user's require actions list. + + + + + After all required actions have been resolved, the user is finally logged in. + + + + +
+
+ Authenticator SPI Walk Through + + In this section, we'll take a look at the Authenticator interface. For this, we are going to implement an authenticator + that requires that a user enter in the answer to a secret question like "What is your mother's maiden name?". This example + is fully implemented and contained in the examples/providers/authenticator directory of the demo distribution of Keycloak. + + + The classes you must implement are the org.keycloak.authentication.AuthenticatorFactory and Authenticator interfaces. The Authenticator + interface defines the logic. The AuthenticatorFactory is responsible for creating instances of an Authenticator. They both extend + a more generic Provider and ProviderFactory set of interfaces that other Keycloak components like User Federation do. + +
+ Packaging Classes and Deployment + + You will package your classes within a single jar. This jar must contain a file named org.keycloak.authentication.AuthenticatorFactory + and must be contained in the META-INF/services/ directory of your jar. This file must list the fully qualified classname + of each AuthenticatorFactory implementation you have in the jar. For example: + + org.keycloak.examples.authenticator.SecretQuestionAuthenticatorFactory + org.keycloak.examples.authenticator.AnotherProviderFactory + + + + This services/ file is used by Keycloak to scan the providers it has to load into the system. + +
+
+ Implementing an Authenticator + + When implementing the Authenticator interface, the first method that needs to be implemented is the + requiresUser() method. For our example, this method must return true as we need to validate the secret question + associated with the user. A provider like kerberos would return false from this method as it can + resolve a user from the negotiate header. This example however is validating a specific credential of a specific + user. + + + The next method to implement is the configuredFor() method. This method is responsible for determining if the + user is configured for this particular authenticator. For this example, we need to check of the answer to the + secret question is been set up by the user or not. In our case we are storing this information, hashed, within + a UserCredentialValueModel within the UserModel (just like passwords are stored). Here's how we do this + very simple check: + + @Override + public boolean configuredFor(KeycloakSession session, RealmModel realm, UserModel user) { + return session.users().configuredForCredentialType("secret_question", realm, user); + } + + + + The configuredForCredentialType() call queries the user to see if it supports that credential type. + + + The next method to implement on the Authenticator is setRequiredActions(). If configuredFor() returns fales + and our example authenticator is required within the flow, this method will be called. It is response for + registering any required actions that must be performed by the user. In our example, we need to register + a required action that will force the user to set up the answer to the secret question. We will implement + this required action provider later in this chapter. Here is the implementation of the setRequiredActions() + method. + + @Override + public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { + user.addRequiredAction("SECRET_QUESTION_CONFIG"); + } + + + + Now we are getting into the meat of the Authenticator implementation. The next method to implement is + authenticate(). This is the initial method the flow invokes when the execution is first visited. + +
+
\ No newline at end of file From 523bdd5ec9a802aed3a6c54aae3598ba316e37ce Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Wed, 12 Aug 2015 20:37:51 -0400 Subject: [PATCH 2/3] doco --- docbook/reference/en/en-US/master.xml | 2 + .../reference/en/en-US/modules/auth-spi.xml | 91 ++++++++++++++++++- .../SecretQuestionAuthenticator.java | 61 ++++++++----- 3 files changed, 131 insertions(+), 23 deletions(-) diff --git a/docbook/reference/en/en-US/master.xml b/docbook/reference/en/en-US/master.xml index 29a49a7bb6..7a798f8fcb 100755 --- a/docbook/reference/en/en-US/master.xml +++ b/docbook/reference/en/en-US/master.xml @@ -46,6 +46,7 @@ + ]> @@ -140,6 +141,7 @@ This one is short &Proxy; &CustomAttributes; &ProtocolMappers; + &AuthSPI; &Migration; diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml index 8e1ac29d0f..e849273876 100755 --- a/docbook/reference/en/en-US/modules/auth-spi.xml +++ b/docbook/reference/en/en-US/modules/auth-spi.xml @@ -268,7 +268,96 @@ Forms Subflow - ALTERNATIVE Now we are getting into the meat of the Authenticator implementation. The next method to implement is - authenticate(). This is the initial method the flow invokes when the execution is first visited. + authenticate(). This is the initial method the flow invokes when the execution is first visited. What we + want is that if a user has answered the secret question already on their browser's machine, that the + user doesn't have to answer the question again. Basically making that machine "trusted". The authenticate() + method isn't responsible for processing the secret question form. Its sole purpose is to render the page + or to continue the flow. + + @Override + public void authenticate(AuthenticationFlowContext context) { + if (hasCookie(context)) { + context.success(); + return; + } + Response challenge = loginForm(context).createForm("secret_question.ftl"); + context.challenge(challenge); + } + + + + If the hasCookie() method checks to see if there is already a cookie set on the browser which indicates + that the secret question has already been answered. If that returns true, we just mark this execution's + status as SUCCESS using the AuthenticationFlowContext.success() method and returning from the authentication() + method. + + + If the hasCookie() method returns false, we must return a response that renders the secret question HTML + form. If your Authenticator classes inherit from the helper class org.keycloak.authentication.AbstractFormAuthenticator + it has a loginForm() method that initializes a Freemarker page builder with appropriate base information needed + to build the form. This page builder is called org.keycloak.login.LoginFormsProvider. + the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme. Additionally + you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the + Freemarker template. We'll go over this later. + + + Calling LoginFormsProvider.createForm() returns a JAX-RS Response object. We then call AuthenticationFlowContext.challenge() + passing in this response. This sets the status of the execution as CHALLENGE and if the execution is Required, this + JAX-RS Response object will be sent to the browser. + + + So, the HTML page asking for the answer to a secret question is displayed to the user and the user + enteres in the answer and clicks submit. The action URL of the HTML form will send an HTTP request to the + flow. The flow will end up invoking the action() method of our Authenticator implementation. + + @Override + public void action(AuthenticationFlowContext context) { + boolean validated = validateAnswer(context); + if (!validated) { + Response challenge = loginForm(context) + .setError("badSecret") + .createForm("secret_question.ftl"); + context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); + return; + } + setCookie(context); + context.success(); + } + + + + + + If the answer is not valid, we rebuild the HTML Form with an additional error message. We then call + AuthenticationFlowContext.failureChallenge() passing in the reason for the value and the JAX-RS response. + failureChallenge() works the same as challenge(), but it also records the failure so it can be analyzed + by any attack detection service. + + + If validation is successful, then we set a cookie to remember that the secret question has been answered + and we call AuthenticationFlowContext.success(). + + + The last thing I want to go over is the setCookie() method. This is an example of providing configuration + for the Authenticator. In this case we want the max age of the cookie to be configurable. + + protected void setCookie(AuthenticationFlowContext context) { + AuthenticatorConfigModel config = context.getAuthenticatorConfig(); + int maxCookieAge = 60 * 60 * 24 * 30; // 30 days + if (config != null) { + maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age")); + + } + ... set the cookie ... + } + + + + + We obtain an AuthenticatorConfigModel from the AuthenticationFlowContext.getAuthenticatorConfig() method. + If configuration exists we pull the max age config out of it. We will see how we can define what should + be configured when we talk about the AuthenticatorFactory implementation. The config values can be + defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation. diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java index 8e60d9b99c..2814a4d55f 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -3,12 +3,15 @@ package org.keycloak.examples.authenticator; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; import org.keycloak.authentication.AbstractFormAuthenticator; +import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.CredentialValidation; +import org.keycloak.services.util.CookieHelper; +import javax.ws.rs.core.Cookie; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; @@ -20,21 +23,52 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { public static final String CREDENTIAL_TYPE = "secret_question"; + protected boolean hasCookie(AuthenticationFlowContext context) { + Cookie cookie = context.getHttpRequest().getHttpHeaders().getCookies().get("SECRET_QUESTION_ANSWERED"); + return cookie != null; + } + @Override public void authenticate(AuthenticationFlowContext context) { + if (hasCookie(context)) { + context.success(); + return; + } Response challenge = loginForm(context).createForm("secret_question.ftl"); context.challenge(challenge); } @Override public void action(AuthenticationFlowContext context) { - MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - String secret = formData.getFirst("secret"); - if (secret == null || secret.trim().equals("")) { - badSecret(context); + boolean validated = validateAnswer(context); + if (!validated) { + Response challenge = loginForm(context) + .setError("badSecret") + .createForm("secret_question.ftl"); + context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); return; } + setCookie(context); + context.success(); + } + protected void setCookie(AuthenticationFlowContext context) { + AuthenticatorConfigModel config = context.getAuthenticatorConfig(); + int maxCookieAge = 60 * 60 * 24 * 30; // 30 days + if (config != null) { + maxCookieAge = Integer.valueOf(config.getConfig().get("cookie.max.age")); + + } + CookieHelper.addCookie("SECRET_QUESTION_ANSWERED", "true", + context.getUriInfo().getBaseUri().getPath() + "/realms/" + context.getRealm().getName(), + null, null, + maxCookieAge, + true, true); + } + + protected boolean validateAnswer(AuthenticationFlowContext context) { + MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); + String secret = formData.getFirst("secret"); UserCredentialValueModel cred = null; for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) { if (model.getType().equals(CREDENTIAL_TYPE)) { @@ -42,25 +76,8 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { break; } } - if (cred == null) { - badSecret(context); - return; - } - boolean validated = CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred); - if (!validated) { - badSecret(context); - return; - } - - context.success(); - } - - private void badSecret(AuthenticationFlowContext context) { - Response challenge = loginForm(context) - .setError("badSecret") - .createForm("secret_question.ftl"); - context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); + return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred); } @Override From 8a23463328ad85f1543f772f3b2d2ae0b6371540 Mon Sep 17 00:00:00 2001 From: Bill Burke Date: Thu, 13 Aug 2015 11:28:11 -0400 Subject: [PATCH 3/3] auth spi refactor and doco --- .../reference/en/en-US/modules/auth-spi.xml | 170 +++++++++++++++++- .../authenticator/secret-question.ftl | 34 ++++ .../SecretQuestionAuthenticator.java | 22 ++- .../SecretQuestionAuthenticatorFactory.java | 56 +++--- .../SecretQuestionRequiredAction.java | 47 +++++ .../SecretQuestionRequiredActionFactory.java | 48 +++++ .../resources/theme/base/login/login-totp.ftl | 3 - .../AbstractFormAuthenticator.java | 52 ------ .../AbstractFormRequiredAction.java | 67 ------- .../AuthenticationFlowContext.java | 24 +++ .../AuthenticationProcessor.java | 31 ++++ .../authentication/RequiredActionContext.java | 57 +++++- .../RequiredActionContextResult.java | 36 +++- .../AbstractUsernameFormAuthenticator.java | 10 +- .../browser/OTPFormAuthenticator.java | 7 +- .../browser/SpnegoAuthenticator.java | 2 +- .../browser/UsernamePasswordForm.java | 2 +- .../requiredactions/TermsAndConditions.java | 9 +- .../managers/AuthenticationManager.java | 45 ++--- .../resources/LoginActionsService.java | 2 +- 20 files changed, 520 insertions(+), 204 deletions(-) create mode 100755 examples/providers/authenticator/secret-question.ftl create mode 100755 examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java create mode 100755 examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java delete mode 100755 services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java diff --git a/docbook/reference/en/en-US/modules/auth-spi.xml b/docbook/reference/en/en-US/modules/auth-spi.xml index e849273876..05e7f4235e 100755 --- a/docbook/reference/en/en-US/modules/auth-spi.xml +++ b/docbook/reference/en/en-US/modules/auth-spi.xml @@ -226,6 +226,9 @@ Forms Subflow - ALTERNATIVE This services/ file is used by Keycloak to scan the providers it has to load into the system. + + To deploy this jar, just copy it to the standalone/configuration/providers directory. +
Implementing an Authenticator @@ -293,8 +296,7 @@ Forms Subflow - ALTERNATIVE If the hasCookie() method returns false, we must return a response that renders the secret question HTML - form. If your Authenticator classes inherit from the helper class org.keycloak.authentication.AbstractFormAuthenticator - it has a loginForm() method that initializes a Freemarker page builder with appropriate base information needed + form. AuthenticationFlowContext has a form() method that initializes a Freemarker page builder with appropriate base information needed to build the form. This page builder is called org.keycloak.login.LoginFormsProvider. the LoginFormsProvider.createForm() method loads a Freemarker template file from your login theme. Additionally you can call the LoginFormsProvider.setAttribute() method if you want to pass additional information to the @@ -314,9 +316,9 @@ Forms Subflow - ALTERNATIVE public void action(AuthenticationFlowContext context) { boolean validated = validateAnswer(context); if (!validated) { - Response challenge = loginForm(context) + Response challenge = context.form() .setError("badSecret") - .createForm("secret_question.ftl"); + .createForm("secret-question.ftl"); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); return; } @@ -360,5 +362,165 @@ Forms Subflow - ALTERNATIVE defined within the admin console if you set up config definitions in your AuthenticatorFactory implementation.
+
+ Implementing an AuthenticatorFactory + + The next step in this process is to implement an AuthenticatorFactory. This factory is responsible + for instantiating an Authenticator. It also provides deployment and configuration metadata about + the Authenticator. + + + The getId() method is just the unique name of the component. The create() methods should also + be self explanatory. The create(KeycloakSession) method will actually never be called. It is just + an artifact of the more generic ProviderFactory interface. + +public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, ConfigurableAuthenticatorFactory { + + public static final String PROVIDER_ID = "secret-question-authenticator"; + private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator(); + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public Authenticator create() { + return SINGLETON; + } + + @Override + public Authenticator create(KeycloakSession session) { + return SINGLETON; + } + + + + + The next thing the factory is responsible for is specify the allowed requirement switches. While there + are four different requirement types: ALTERNATIVE, REQUIRED, OPTIONAL, DISABLED, AuthenticatorFactory + implementations can limit which requirement options are shown in the admin console when defining + a flow. In our example, we're going to limit our requirement options to REQUIRED and DISABLED. + + private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { + AuthenticationExecutionModel.Requirement.REQUIRED, + AuthenticationExecutionModel.Requirement.DISABLED + }; + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return REQUIREMENT_CHOICES; + } + + + + The AuthenticatorFactory.isUserSetupAllowed() is a flag that tells the flow manager whether or not + Authenticator.setRequiredActions() method will be called. If an Authenticator is not configured for a user, + the flow manager checks isUserSetupAllowed(). If it is false, then the flow aborts with an error. If it + returns true, then the flow manager will invoke Authenticator.setRequiredActions(). + + @Override + public boolean isUserSetupAllowed() { + return true; + } + + + + The next few methods define how the Authenticator can be configured. The isConfigurable() method + is a flag which specifies to the admin console on whether the Authenticator can be configured within + a flow. The getConfigProperties() method returns a list of ProviderConfigProperty objects. These + objects define a specific configuration attribute. + getConfigProperties() { + return configProperties; + } + + private static final List configProperties = new ArrayList(); + + static { + ProviderConfigProperty property; + property = new ProviderConfigProperty(); + property.setName("cookie.max.age"); + property.setLabel("Cookie Max Age"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE."); + configProperties.add(property); + } +]]> + + + Each ProviderConfigProperty defines the name of the config property. This is the key used in the config + map stored in AuthenticatorConfigModel. The label defines how the config option will be displayed in the + admin console. The type defines if it is a String, Boolean, or other type. The admin console + will display different UI inputs depending on the type. The help text is what will be shown in the + tooltip for the config attribute in the admin console. Read the javadoc of ProviderConfigProperty + for more detail. + + + The rest of the methods are for the admin console. getHelpText() is the tooltip text that will be + shown when you are picking the Authenticator you want to bind to an execution. getDisplayType() + is what text that will be shown in the admin console when listing the Authenticator. getReferenceCategory() + is just a category the Authenticator belongs to. + +
+
+ Adding Authenticator Form + + Keycloak comes with a Freemarker theme and template engine. The createForm() + method you called within authenticate() of your Authenticator class, builds an HTML page from a file within + your login them: secret-question.ftl. This file should be placed in the login theme with all the other + .ftl files you see for login. + + + Let's take a bigger look at secret-question.ftl Here's a small code snippet: + +
+
+ +
+ +
+ +
+
+ +]]>
+
+ + Any piece of text enclosed in ${} corresponds to an attribute or template funtion. + If you see the form's action, you see it points to ${url.loginAction}. This value + is automatically generated when you invoke the AuthenticationFlowContext.form() method. You can also obtain + this value by calling the AuthenticationFlowContext.getActionURL() method in Java code. + + + You'll also see ${properties.someValue}. These correspond to properties defined + in your theme.properties file of our theme. ${msg("someValue")} corresponds to the + internationalized message bundles (.properties files) included with the login theme messages/ directory. + If you're just using english, you can just add the value of the loginSecretQuestion + value. This should be the question you want to ask the user. + + + When you call AuthenticationFlowContext.form() this gives you a LoginFormsProvider instance. If you called, + LoginFormsProvider.setAttribute("foo", "bar"), the value of "foo" would be available + for reference in your form as ${foo}. The value of an attribute can be any Java + bean as well. + +
+
+ Adding Authenticator to a Flow + + Adding an Authenticator to a flow must be done in the admin console. + If you go to the Authentication menu item and go to the Flow tab, you will be able to view the currently + defined flows. You cannot modify an built in flows, so, to add the Authenticator we've created you + have to copy an existing flow or create your own. I'm hoping the UI is intuitive enough so that you + can figure out for yourself how to create a flow and add the Authenticator. + + + After you've created your flow, you have to bind it to the login action you want to bind it to. If you go + to the Authentication menu and go to the Bindings tab you will see options to bind a flow to + the browser, registration, or direct grant flow. + +
\ No newline at end of file diff --git a/examples/providers/authenticator/secret-question.ftl b/examples/providers/authenticator/secret-question.ftl new file mode 100755 index 0000000000..b69c5206a2 --- /dev/null +++ b/examples/providers/authenticator/secret-question.ftl @@ -0,0 +1,34 @@ +<#import "template.ftl" as layout> +<@layout.registrationLayout; section> + <#if section = "title"> + ${msg("loginTitle",realm.name)} + <#elseif section = "header"> + ${msg("loginTitleHtml",realm.name)} + <#elseif section = "form"> +
+
+
+ +
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ + +
+
+
+
+ + \ No newline at end of file diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java index 2814a4d55f..1f4a8aaa00 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticator.java @@ -2,13 +2,12 @@ package org.keycloak.examples.authenticator; import org.keycloak.authentication.AuthenticationFlowContext; import org.keycloak.authentication.AuthenticationFlowError; -import org.keycloak.authentication.AbstractFormAuthenticator; +import org.keycloak.authentication.Authenticator; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserCredentialValueModel; import org.keycloak.models.UserModel; -import org.keycloak.models.utils.CredentialValidation; import org.keycloak.services.util.CookieHelper; import javax.ws.rs.core.Cookie; @@ -19,7 +18,7 @@ import javax.ws.rs.core.Response; * @author Bill Burke * @version $Revision: 1 $ */ -public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { +public class SecretQuestionAuthenticator implements Authenticator { public static final String CREDENTIAL_TYPE = "secret_question"; @@ -34,7 +33,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { context.success(); return; } - Response challenge = loginForm(context).createForm("secret_question.ftl"); + Response challenge = context.form().createForm("secret_question.ftl"); context.challenge(challenge); } @@ -42,9 +41,9 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { public void action(AuthenticationFlowContext context) { boolean validated = validateAnswer(context); if (!validated) { - Response challenge = loginForm(context) + Response challenge = context.form() .setError("badSecret") - .createForm("secret_question.ftl"); + .createForm("secret-question.ftl"); context.failureChallenge(AuthenticationFlowError.INVALID_CREDENTIALS, challenge); return; } @@ -68,7 +67,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { protected boolean validateAnswer(AuthenticationFlowContext context) { MultivaluedMap formData = context.getHttpRequest().getDecodedFormParameters(); - String secret = formData.getFirst("secret"); + String secret = formData.getFirst("secret_answer"); UserCredentialValueModel cred = null; for (UserCredentialValueModel model : context.getUser().getCredentialsDirectly()) { if (model.getType().equals(CREDENTIAL_TYPE)) { @@ -77,7 +76,7 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { } } - return CredentialValidation.validateHashedCredential(context.getRealm(), context.getUser(), secret, cred); + return cred.getValue().equals(secret); } @Override @@ -92,6 +91,11 @@ public class SecretQuestionAuthenticator extends AbstractFormAuthenticator { @Override public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) { - user.addRequiredAction(CREDENTIAL_TYPE + "_CONFIG"); + user.addRequiredAction(SecretQuestionRequiredAction.PROVIDER_ID); + } + + @Override + public void close() { + } } diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java index 09a83da758..09be4c1747 100755 --- a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionAuthenticatorFactory.java @@ -21,6 +21,11 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, public static final String PROVIDER_ID = "secret-question-authenticator"; private static final SecretQuestionAuthenticator SINGLETON = new SecretQuestionAuthenticator(); + @Override + public String getId() { + return PROVIDER_ID; + } + @Override public Authenticator create() { return SINGLETON; @@ -31,21 +36,6 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, return SINGLETON; } - @Override - public String getDisplayType() { - return "Secret Question"; - } - - @Override - public String getReferenceCategory() { - return "Secret Question"; - } - - @Override - public boolean isConfigurable() { - return true; - } - private static AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = { AuthenticationExecutionModel.Requirement.REQUIRED, AuthenticationExecutionModel.Requirement.DISABLED @@ -61,8 +51,13 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, } @Override - public String getHelpText() { - return "A secret question that a user has to answer. i.e. What is your mother's maiden name."; + public boolean isConfigurable() { + return true; + } + + @Override + public List getConfigProperties() { + return configProperties; } private static final List configProperties = new ArrayList(); @@ -70,17 +65,27 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, static { ProviderConfigProperty property; property = new ProviderConfigProperty(); - property.setName("remember_machine"); - property.setLabel("Remember machine"); - property.setType(ProviderConfigProperty.BOOLEAN_TYPE); - property.setHelpText("If set to true, a checkbox will appear when entering in secret on whether the user wants keycloak to remember the machine. If the user wants to remember, then a persistent cookie is set, and the user will not have to enter in their secret again."); + property.setName("cookie.max.age"); + property.setLabel("Cookie Max Age"); + property.setType(ProviderConfigProperty.STRING_TYPE); + property.setHelpText("Max age in seconds of the SECRET_QUESTION_COOKIE."); configProperties.add(property); } @Override - public List getConfigProperties() { - return configProperties; + public String getHelpText() { + return "A secret question that a user has to answer. i.e. What is your mother's maiden name."; + } + + @Override + public String getDisplayType() { + return "Secret Question"; + } + + @Override + public String getReferenceCategory() { + return "Secret Question"; } @Override @@ -98,8 +103,5 @@ public class SecretQuestionAuthenticatorFactory implements AuthenticatorFactory, } - @Override - public String getId() { - return PROVIDER_ID; - } + } diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java new file mode 100755 index 0000000000..6644b8e2df --- /dev/null +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredAction.java @@ -0,0 +1,47 @@ +package org.keycloak.examples.authenticator; + +import org.keycloak.authentication.RequiredActionContext; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.UserCredentialValueModel; + +import javax.ws.rs.core.Response; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionRequiredAction implements RequiredActionProvider { + public static final String PROVIDER_ID = "secret_question_config"; + + @Override + public void evaluateTriggers(RequiredActionContext context) { + + } + + @Override + public void requiredActionChallenge(RequiredActionContext context) { + Response challenge = context.form().createForm("secret_question_config.ftl"); + context.challenge(challenge); + + } + + @Override + public void processAction(RequiredActionContext context) { + String answer = (context.getHttpRequest().getDecodedFormParameters().getFirst("answer")); + UserCredentialValueModel model = new UserCredentialValueModel(); + model.setValue(answer); + model.setType(SecretQuestionAuthenticator.CREDENTIAL_TYPE); + context.getUser().updateCredentialDirectly(model); + context.success(); + } + + @Override + public String getProviderId() { + return PROVIDER_ID; + } + + @Override + public void close() { + + } +} diff --git a/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java new file mode 100755 index 0000000000..ff699e27ef --- /dev/null +++ b/examples/providers/authenticator/src/main/java/org/keycloak/examples/authenticator/SecretQuestionRequiredActionFactory.java @@ -0,0 +1,48 @@ +package org.keycloak.examples.authenticator; + +import org.keycloak.Config; +import org.keycloak.authentication.RequiredActionFactory; +import org.keycloak.authentication.RequiredActionProvider; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +public class SecretQuestionRequiredActionFactory implements RequiredActionFactory { + + private static final SecretQuestionRequiredAction SINGLETON = new SecretQuestionRequiredAction(); + + @Override + public RequiredActionProvider create(KeycloakSession session) { + return SINGLETON; + } + + + @Override + public String getId() { + return SecretQuestionRequiredAction.PROVIDER_ID; + } + + @Override + public String getDisplayText() { + return "Secret Question"; + } + + @Override + public void init(Config.Scope config) { + + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + + } + + @Override + public void close() { + + } + +} diff --git a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl index b085eeb540..e472fff1ff 100755 --- a/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl +++ b/forms/common-themes/src/main/resources/theme/base/login/login-totp.ftl @@ -6,9 +6,6 @@ ${msg("loginTitleHtml",realm.name)} <#elseif section = "form">
- - -
diff --git a/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java index a3180295fe..1755563553 100755 --- a/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/AbstractFormAuthenticator.java @@ -1,13 +1,5 @@ package org.keycloak.authentication; -import org.keycloak.OAuth2Constants; -import org.keycloak.authentication.AuthenticationFlowContext; -import org.keycloak.authentication.Authenticator; -import org.keycloak.login.LoginFormsProvider; -import org.keycloak.services.resources.LoginActionsService; - -import java.net.URI; - /** * Abstract helper class that Authenticator implementations can leverage * @@ -15,53 +7,9 @@ import java.net.URI; * @version $Revision: 1 $ */ public abstract class AbstractFormAuthenticator implements Authenticator { - public static final String EXECUTION = "execution"; @Override public void close() { } - - /** - * Create a form builder that presets the user, action URI, and a generated access code - * - * @param context - * @return - */ - protected LoginFormsProvider loginForm(AuthenticationFlowContext context) { - String accessCode = context.generateAccessCode(); - URI action = getActionUrl(context, accessCode); - LoginFormsProvider provider = context.getSession().getProvider(LoginFormsProvider.class) - .setUser(context.getUser()) - .setActionUri(action) - .setClientSessionCode(accessCode); - if (context.getForwardedErrorMessage() != null) { - provider.setError(context.getForwardedErrorMessage()); - } - return provider; - } - - /** - * Get the action URL for the required action. - * - * @param context - * @param code client sessino access code - * @return - */ - public URI getActionUrl(AuthenticationFlowContext context, String code) { - return LoginActionsService.authenticationFormProcessor(context.getUriInfo()) - .queryParam(OAuth2Constants.CODE, code) - .queryParam(EXECUTION, context.getExecution().getId()) - .build(context.getRealm().getName()); - } - - /** - * Get the action URL for the required action. This auto-generates the access code. - * - * @param context - * @return - */ - public URI getActionUrl(AuthenticationFlowContext context) { - return getActionUrl(context, context.generateAccessCode()); - } } diff --git a/services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java b/services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java deleted file mode 100755 index ea1443d948..0000000000 --- a/services/src/main/java/org/keycloak/authentication/AbstractFormRequiredAction.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.keycloak.authentication; - -import org.keycloak.OAuth2Constants; -import org.keycloak.authentication.RequiredActionContext; -import org.keycloak.authentication.RequiredActionProvider; -import org.keycloak.login.LoginFormsProvider; -import org.keycloak.services.resources.LoginActionsService; - -import java.net.URI; - -/** - * Abstract helper class that Authenticator implementations can leverage - * - * @author Bill Burke - * @version $Revision: 1 $ - */ -public abstract class AbstractFormRequiredAction implements RequiredActionProvider { - - /** - * Get the action URL for the required action. - * - * @param context - * @param code client sessino access code - * @return - */ - public URI getActionUrl(RequiredActionContext context, String code) { - return LoginActionsService.requiredActionProcessor(context.getUriInfo()) - .queryParam(OAuth2Constants.CODE, code) - .queryParam("action", getProviderId()) - .build(context.getRealm().getName()); - } - - /** - * Get the action URL for the required action. This auto-generates the access code. - * - * @param context - * @return - */ - public URI getActionUrl(RequiredActionContext context) { - String accessCode = context.generateAccessCode(getProviderId()); - return getActionUrl(context, accessCode); - - } - - /** - * Create a form builder that presets the user, action URI, and a generated access code - * - * @param context - * @return - */ - public LoginFormsProvider form(RequiredActionContext context) { - String accessCode = context.generateAccessCode(getProviderId()); - URI action = getActionUrl(context, accessCode); - LoginFormsProvider provider = context.getSession().getProvider(LoginFormsProvider.class) - .setUser(context.getUser()) - .setActionUri(action) - .setClientSessionCode(accessCode); - return provider; - } - - @Override - public void close() { - - } - - -} diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index de694934ac..fd9803af39 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -3,6 +3,7 @@ package org.keycloak.authentication; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.events.EventBuilder; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticatorConfigModel; import org.keycloak.models.ClientSessionModel; @@ -14,6 +15,7 @@ import org.keycloak.services.managers.BruteForceProtector; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.net.URI; /** * This interface encapsulates information about an execution in an AuthenticationFlow. It is also used to set @@ -193,4 +195,26 @@ public interface AuthenticationFlowContext { * @return may return null if there was no error */ AuthenticationFlowError getError(); + + /** + * Create a Freemarker form builder that presets the user, action URI, and a generated access code + * + * @return + */ + LoginFormsProvider form(); + + /** + * Get the action URL for the required action. + * + * @param code client session access code + * @return + */ + URI getActionUrl(String code); + + /** + * Get the action URL for the required action. This auto-generates the access code. + * + * @return + */ + URI getActionUrl(); } diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 552eec8697..aee300e1db 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -3,10 +3,12 @@ package org.keycloak.authentication; import org.jboss.logging.Logger; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; import org.keycloak.authentication.authenticators.browser.AbstractUsernameFormAuthenticator; import org.keycloak.events.Details; import org.keycloak.events.Errors; import org.keycloak.events.EventBuilder; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticatorConfigModel; @@ -21,10 +23,12 @@ import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.BruteForceProtector; import org.keycloak.services.managers.ClientSessionCode; import org.keycloak.services.messages.Messages; +import org.keycloak.services.resources.LoginActionsService; import org.keycloak.util.Time; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.net.URI; import java.util.List; /** @@ -336,6 +340,33 @@ public class AuthenticationProcessor { public AuthenticationFlowError getError() { return error; } + + @Override + public LoginFormsProvider form() { + String accessCode = generateAccessCode(); + URI action = getActionUrl(accessCode); + LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class) + .setUser(getUser()) + .setActionUri(action) + .setClientSessionCode(accessCode); + if (getForwardedErrorMessage() != null) { + provider.setError(getForwardedErrorMessage()); + } + return provider; + } + + @Override + public URI getActionUrl(String code) { + return LoginActionsService.authenticationFormProcessor(getUriInfo()) + .queryParam(OAuth2Constants.CODE, code) + .queryParam("execution", getExecution().getId()) + .build(getRealm().getName()); + } + + @Override + public URI getActionUrl() { + return getActionUrl(generateAccessCode()); + } } public void logFailure() { diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java index 9735ed1c42..9b650c870a 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContext.java @@ -3,6 +3,7 @@ package org.keycloak.authentication; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; import org.keycloak.events.EventBuilder; +import org.keycloak.login.LoginFormsProvider; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; @@ -11,6 +12,7 @@ import org.keycloak.models.UserSessionModel; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.net.URI; /** * Interface that encapsulates current information about the current requred action @@ -19,8 +21,6 @@ import javax.ws.rs.core.UriInfo; * @version $Revision: 1 $ */ public interface RequiredActionContext { - void ignore(); - public static enum Status { CHALLENGE, SUCCESS, @@ -28,6 +28,37 @@ public interface RequiredActionContext { FAILURE } + /** + * Get the action URL for the required action. + * + * @param code client sessino access code + * @return + */ + URI getActionUrl(String code); + + /** + * Get the action URL for the required action. This auto-generates the access code. + * + * @return + */ + URI getActionUrl(); + + /** + * Create a Freemarker form builder that presets the user, action URI, and a generated access code + * + * @return + */ + LoginFormsProvider form(); + + + /** + * If challenge has been sent this returns the JAX-RS Response + * + * @return + */ + Response getChallenge(); + + /** * Current event builder being used * @@ -59,7 +90,29 @@ public interface RequiredActionContext { Status getStatus(); + /** + * Send a challenge Response back to user + * + * @param response + */ void challenge(Response response); + + /** + * Abort the authentication with an error + * + */ void failure(); + + /** + * Mark this required action as successful. The required action will be removed from the UserModel + * + */ void success(); + + /** + * Ignore this required action and go onto the next, or complete the flow. + * + */ + void ignore(); + } diff --git a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java index 10a8a0d92a..279b25cd98 100755 --- a/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java +++ b/services/src/main/java/org/keycloak/authentication/RequiredActionContextResult.java @@ -2,16 +2,20 @@ package org.keycloak.authentication; import org.jboss.resteasy.spi.HttpRequest; import org.keycloak.ClientConnection; +import org.keycloak.OAuth2Constants; import org.keycloak.events.EventBuilder; +import org.keycloak.login.LoginFormsProvider; 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.ClientSessionCode; +import org.keycloak.services.resources.LoginActionsService; import javax.ws.rs.core.Response; import javax.ws.rs.core.UriInfo; +import java.net.URI; /** * @author Bill Burke @@ -27,11 +31,12 @@ public class RequiredActionContextResult implements RequiredActionContext { protected Response challenge; protected HttpRequest httpRequest; protected UserModel user; + protected RequiredActionProvider provider; public RequiredActionContextResult(UserSessionModel userSession, ClientSessionModel clientSession, RealmModel realm, EventBuilder eventBuilder, KeycloakSession session, HttpRequest httpRequest, - UserModel user) { + UserModel user, RequiredActionProvider provider) { this.userSession = userSession; this.clientSession = clientSession; this.realm = realm; @@ -39,6 +44,7 @@ public class RequiredActionContextResult implements RequiredActionContext { this.session = session; this.httpRequest = httpRequest; this.user = user; + this.provider = provider; } @Override @@ -121,6 +127,34 @@ public class RequiredActionContextResult implements RequiredActionContext { status = Status.IGNORE; } + @Override + public URI getActionUrl(String code) { + return LoginActionsService.requiredActionProcessor(getUriInfo()) + .queryParam(OAuth2Constants.CODE, code) + .queryParam("action", provider.getProviderId()) + .build(getRealm().getName()); + } + + @Override + public URI getActionUrl() { + String accessCode = generateAccessCode(provider.getProviderId()); + return getActionUrl(accessCode); + + } + + @Override + public LoginFormsProvider form() { + String accessCode = generateAccessCode(provider.getProviderId()); + URI action = getActionUrl(accessCode); + LoginFormsProvider provider = getSession().getProvider(LoginFormsProvider.class) + .setUser(getUser()) + .setActionUri(action) + .setClientSessionCode(accessCode); + return provider; + } + + + @Override public Response getChallenge() { return challenge; } diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java index 5a96553e7b..062d0ba0c8 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/AbstractUsernameFormAuthenticator.java @@ -36,29 +36,29 @@ public abstract class AbstractUsernameFormAuthenticator extends AbstractFormAuth } protected Response invalidUser(AuthenticationFlowContext context) { - return loginForm(context) + return context.form() .setError(Messages.INVALID_USER) .createLogin(); } protected Response disabledUser(AuthenticationFlowContext context) { - return loginForm(context) + return context.form() .setError(Messages.ACCOUNT_DISABLED).createLogin(); } protected Response temporarilyDisabledUser(AuthenticationFlowContext context) { - return loginForm(context) + return context.form() .setError(Messages.ACCOUNT_TEMPORARILY_DISABLED).createLogin(); } protected Response invalidCredentials(AuthenticationFlowContext context) { - return loginForm(context) + return context.form() .setError(Messages.INVALID_USER).createLogin(); } protected Response setDuplicateUserChallenge(AuthenticationFlowContext context, String eventError, String loginFormError, AuthenticationFlowError authenticatorError) { context.getEvent().error(eventError); - Response challengeResponse = loginForm(context) + Response challengeResponse = context.form() .setError(loginFormError).createLogin(); context.failureChallenge(authenticatorError, challengeResponse); return challengeResponse; diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java index 6f910e98ba..e3427e0a48 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/OTPFormAuthenticator.java @@ -14,7 +14,6 @@ import org.keycloak.services.messages.Messages; import javax.ws.rs.core.MultivaluedMap; import javax.ws.rs.core.Response; -import java.net.URI; import java.util.LinkedList; import java.util.List; @@ -63,11 +62,7 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl } protected Response challenge(AuthenticationFlowContext context, String error) { - String accessCode = context.generateAccessCode(); - URI action = getActionUrl(context, accessCode); - LoginFormsProvider forms = context.getSession().getProvider(LoginFormsProvider.class) - .setActionUri(action) - .setClientSessionCode(accessCode); + LoginFormsProvider forms = context.form(); if (error != null) forms.setError(error); return forms.createLoginTotp(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java index bad2fa2ee9..da2b0aedb2 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/SpnegoAuthenticator.java @@ -117,7 +117,7 @@ public class SpnegoAuthenticator extends AbstractUsernameFormAuthenticator imple */ protected Response optionalChallengeRedirect(AuthenticationFlowContext context, String negotiateHeader) { String accessCode = context.generateAccessCode(); - URI action = getActionUrl(context, accessCode); + URI action = context.getActionUrl(accessCode); StringBuilder builder = new StringBuilder(); diff --git a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java index 47197804d4..9f42cf56cf 100755 --- a/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java +++ b/services/src/main/java/org/keycloak/authentication/authenticators/browser/UsernamePasswordForm.java @@ -71,7 +71,7 @@ public class UsernamePasswordForm extends AbstractUsernameFormAuthenticator impl } protected Response challenge(AuthenticationFlowContext context, MultivaluedMap formData) { - LoginFormsProvider forms = loginForm(context); + LoginFormsProvider forms = context.form(); if (formData.size() > 0) forms.setFormData(formData); diff --git a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java index 627c80f0ed..e3ecc9deb4 100755 --- a/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java +++ b/services/src/main/java/org/keycloak/authentication/requiredactions/TermsAndConditions.java @@ -4,7 +4,6 @@ import org.keycloak.Config; import org.keycloak.authentication.RequiredActionContext; import org.keycloak.authentication.RequiredActionFactory; import org.keycloak.authentication.RequiredActionProvider; -import org.keycloak.authentication.AbstractFormRequiredAction; import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSessionFactory; @@ -14,7 +13,7 @@ import javax.ws.rs.core.Response; * @author Bill Burke * @version $Revision: 1 $ */ -public class TermsAndConditions extends AbstractFormRequiredAction implements RequiredActionFactory { +public class TermsAndConditions implements RequiredActionProvider, RequiredActionFactory { public static final String PROVIDER_ID = "terms_and_conditions"; @@ -54,7 +53,7 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re @Override public void requiredActionChallenge(RequiredActionContext context) { - Response challenge = form(context).createForm("terms.ftl"); + Response challenge = context.form().createForm("terms.ftl"); context.challenge(challenge); } @@ -73,4 +72,8 @@ public class TermsAndConditions extends AbstractFormRequiredAction implements Re return "Terms and Conditions"; } + @Override + public void close() { + + } } 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 9e5cd97fb4..bd50ff004e 100755 --- a/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java +++ b/services/src/main/java/org/keycloak/services/managers/AuthenticationManager.java @@ -423,7 +423,6 @@ public class AuthenticationManager { evaluateRequiredActionTriggers(session, userSession, clientSession, clientConnection, request, uriInfo, event, realm, user); - RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user); logger.debugv("processAccessCode: go to oauth page?: {0}", client.isConsentRequired()); @@ -433,6 +432,7 @@ public class AuthenticationManager { for (String action : requiredActions) { RequiredActionProviderModel model = realm.getRequiredActionProviderByAlias(action); RequiredActionProvider actionProvider = session.getProvider(RequiredActionProvider.class, model.getProviderId()); + RequiredActionContextResult context = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, actionProvider); actionProvider.requiredActionChallenge(context); if (context.getStatus() == RequiredActionContext.Status.FAILURE) { @@ -501,32 +501,33 @@ public class AuthenticationManager { } public static void evaluateRequiredActionTriggers(final KeycloakSession session, final UserSessionModel userSession, final ClientSessionModel clientSession, final ClientConnection clientConnection, final HttpRequest request, final UriInfo uriInfo, final EventBuilder event, final RealmModel realm, final UserModel user) { - RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user) { - @Override - public void challenge(Response response) { - throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()"); - } - - @Override - public void failure() { - throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()"); - } - - @Override - public void success() { - throw new RuntimeException("Not allowed to call success() within evaluateTriggers()"); - } - - @Override - public void ignore() { - throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()"); - } - }; // see if any required actions need triggering, i.e. an expired password for (RequiredActionProviderModel model : realm.getRequiredActionProviders()) { if (!model.isEnabled()) continue; RequiredActionProvider provider = session.getProvider(RequiredActionProvider.class, model.getProviderId()); + RequiredActionContextResult result = new RequiredActionContextResult(userSession, clientSession, realm, event, session, request, user, provider) { + @Override + public void challenge(Response response) { + throw new RuntimeException("Not allowed to call challenge() within evaluateTriggers()"); + } + + @Override + public void failure() { + throw new RuntimeException("Not allowed to call failure() within evaluateTriggers()"); + } + + @Override + public void success() { + throw new RuntimeException("Not allowed to call success() within evaluateTriggers()"); + } + + @Override + public void ignore() { + throw new RuntimeException("Not allowed to call ignore() within evaluateTriggers()"); + } + }; + provider.evaluateTriggers(result); } } 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 e1789393cf..a907237b3b 100755 --- a/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java +++ b/services/src/main/java/org/keycloak/services/resources/LoginActionsService.java @@ -883,7 +883,7 @@ public class LoginActionsService { initEvent(clientSession); - RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser()) { + RequiredActionContextResult context = new RequiredActionContextResult(clientSession.getUserSession(), clientSession, realm, event, session, request, clientSession.getUserSession().getUser(), provider) { @Override public String generateAccessCode(String action) { String clientSessionAction = clientSession.getAction();