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 c5d28a1691..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 @@ -25,6 +25,7 @@
+
diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java index 9b02eb89d0..681e76c484 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationFlowContext.java @@ -71,6 +71,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon */ void cancelLogin(); + /** + * Reset the current flow to the beginning and restarts it. + * + */ + void resetFlow(); + /** * Fork the current flow. The client session will be cloned and set to point at the realm's browser login flow. The Response will be the result * of this fork. The previous flow will still be set at the current execution. This is used by reset password when it sends an email. diff --git a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java index 409b0e7311..b6b3272ca6 100755 --- a/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java +++ b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java @@ -454,6 +454,11 @@ public class AuthenticationProcessor { forceChallenge(response); } + @Override + public void resetFlow() { + this.status = FlowStatus.FLOW_RESET; + } + @Override public void fork() { this.status = FlowStatus.FORK; diff --git a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java index cf825c0a07..d9ca0b094f 100755 --- a/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java +++ b/services/src/main/java/org/keycloak/authentication/DefaultAuthenticationFlow.java @@ -4,6 +4,9 @@ import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.ClientSessionModel; import org.keycloak.models.UserModel; +import org.omg.PortableInterceptor.SUCCESSFUL; + +import static org.keycloak.authentication.FlowStatus.*; import javax.ws.rs.core.Response; import java.util.Iterator; @@ -153,62 +156,65 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow { public Response processResult(AuthenticationProcessor.Result result) { AuthenticationExecutionModel execution = result.getExecution(); FlowStatus status = result.getStatus(); - if (status == FlowStatus.SUCCESS) { - AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); - if (execution.isAlternative()) alternativeSuccessful = true; - return null; - } else if (status == FlowStatus.FAILED) { - AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); - processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); - if (result.getChallenge() != null) { - return sendChallenge(result, execution); - } - throw new AuthenticationFlowException(result.getError()); - } else if (status == FlowStatus.FORK) { - AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); - processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); - throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); - } else if (status == FlowStatus.FORCE_CHALLENGE) { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); - return sendChallenge(result, execution); - } else if (status == FlowStatus.CHALLENGE) { - AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); - if (execution.isRequired()) { + switch (status) { + case SUCCESS: + AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); + if (execution.isAlternative()) alternativeSuccessful = true; + return null; + case FAILED: + AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); + processor.logFailure(); + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); + if (result.getChallenge() != null) { + return sendChallenge(result, execution); + } + throw new AuthenticationFlowException(result.getError()); + case FORK: + AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); + processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); + throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); + case FORCE_CHALLENGE: processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); - } - UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); - if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { + case CHALLENGE: + AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator()); + if (execution.isRequired()) { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + return sendChallenge(result, execution); + } + UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); + if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); + return sendChallenge(result, execution); + } + if (execution.isAlternative()) { + alternativeChallenge = result.getChallenge(); + challengedAlternativeExecution = execution; + } else { + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); + } + return null; + case FAILURE_CHALLENGE: + AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); + processor.logFailure(); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); return sendChallenge(result, execution); - } - if (execution.isAlternative()) { - alternativeChallenge = result.getChallenge(); - challengedAlternativeExecution = execution; - } else { - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); - } - return null; - } else if (status == FlowStatus.FAILURE_CHALLENGE) { - AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); - processor.logFailure(); - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); - return sendChallenge(result, execution); - } else if (status == FlowStatus.ATTEMPTED) { - AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); - if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { - throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); - } - processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED); - return null; - } else { - AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); - AuthenticationProcessor.logger.error("Unknown result status"); - throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); + case ATTEMPTED: + AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator()); + if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) { + throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS); + } + processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED); + return null; + case FLOW_RESET: + AuthenticationProcessor.resetFlow(processor.getClientSession()); + return processor.authenticate(); + default: + AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator()); + AuthenticationProcessor.logger.error("Unknown result status"); + throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR); } - } public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) { diff --git a/services/src/main/java/org/keycloak/authentication/FlowStatus.java b/services/src/main/java/org/keycloak/authentication/FlowStatus.java index 6e6ecbd517..c8acbc7465 100755 --- a/services/src/main/java/org/keycloak/authentication/FlowStatus.java +++ b/services/src/main/java/org/keycloak/authentication/FlowStatus.java @@ -48,6 +48,13 @@ public enum FlowStatus { * This flow is being forked. The current client session is being cloned, reset, and redirected to browser login. * */ - FORK + FORK, + + /** + * This flow was reset to the beginning. An example is hitting cancel on the OTP page which will bring you back to the + * username password page. + * + */ + FLOW_RESET } 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 e3427e0a48..4a0c48dc48 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 @@ -37,6 +37,10 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl public void validateOTP(AuthenticationFlowContext context) { MultivaluedMap inputData = context.getHttpRequest().getDecodedFormParameters(); + if (inputData.containsKey("cancel")) { + context.resetFlow(); + return; + } List credentials = new LinkedList<>(); String password = inputData.getFirst(CredentialRepresentation.TOTP); if (password == null) { diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java index b37401ca94..6eda3fa30d 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/forms/LoginTotpTest.java @@ -153,6 +153,16 @@ public class LoginTotpTest { events.expectLogin().assertEvent(); } + @Test + public void loginWithTotpCancel() throws Exception { + loginPage.open(); + loginPage.login("test-user@localhost", "password"); + + loginTotpPage.assertCurrent(); + loginTotpPage.cancel(); + loginPage.assertCurrent(); + } + @Test public void loginWithTotpInvalidPassword() throws Exception { loginPage.open(); diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java index b725a169b9..232b102663 100755 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/pages/LoginTotpPage.java @@ -39,6 +39,9 @@ public class LoginTotpPage extends AbstractPage { @FindBy(css = "input[type=\"submit\"]") private WebElement submitButton; + @FindBy(id = "kc-cancel") + private WebElement cancelButton; + @FindBy(className = "feedback-error") private WebElement loginErrorMessage; @@ -49,6 +52,10 @@ public class LoginTotpPage extends AbstractPage { submitButton.click(); } + public void cancel() { + cancelButton.click(); + } + public String getError() { return loginErrorMessage != null ? loginErrorMessage.getText() : null; }