KEYCLOAK-1908

This commit is contained in:
Bill Burke 2015-10-14 11:49:36 -04:00
parent aadd63e2b0
commit 5563118d79
8 changed files with 98 additions and 52 deletions

View file

@ -25,6 +25,7 @@
<div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}"> <div id="kc-form-buttons" class="${properties.kcFormButtonsClass!}">
<div class="${properties.kcFormButtonsWrapperClass!}"> <div class="${properties.kcFormButtonsWrapperClass!}">
<input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/> <input class="${properties.kcButtonClass!} ${properties.kcButtonPrimaryClass!} ${properties.kcButtonLargeClass!}" name="login" id="kc-login" type="submit" value="${msg("doLogIn")}"/>
<input class="${properties.kcButtonClass!} ${properties.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
</div> </div>
</div> </div>
</div> </div>

View file

@ -71,6 +71,12 @@ public interface AuthenticationFlowContext extends AbstractAuthenticationFlowCon
*/ */
void cancelLogin(); 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 * 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. * 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.

View file

@ -454,6 +454,11 @@ public class AuthenticationProcessor {
forceChallenge(response); forceChallenge(response);
} }
@Override
public void resetFlow() {
this.status = FlowStatus.FLOW_RESET;
}
@Override @Override
public void fork() { public void fork() {
this.status = FlowStatus.FORK; this.status = FlowStatus.FORK;

View file

@ -4,6 +4,9 @@ import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.AuthenticationFlowModel;
import org.keycloak.models.ClientSessionModel; import org.keycloak.models.ClientSessionModel;
import org.keycloak.models.UserModel; import org.keycloak.models.UserModel;
import org.omg.PortableInterceptor.SUCCESSFUL;
import static org.keycloak.authentication.FlowStatus.*;
import javax.ws.rs.core.Response; import javax.ws.rs.core.Response;
import java.util.Iterator; import java.util.Iterator;
@ -153,62 +156,65 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
public Response processResult(AuthenticationProcessor.Result result) { public Response processResult(AuthenticationProcessor.Result result) {
AuthenticationExecutionModel execution = result.getExecution(); AuthenticationExecutionModel execution = result.getExecution();
FlowStatus status = result.getStatus(); FlowStatus status = result.getStatus();
if (status == FlowStatus.SUCCESS) { switch (status) {
AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator()); case SUCCESS:
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS); AuthenticationProcessor.logger.debugv("authenticator SUCCESS: {0}", execution.getAuthenticator());
if (execution.isAlternative()) alternativeSuccessful = true; processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SUCCESS);
return null; if (execution.isAlternative()) alternativeSuccessful = true;
} else if (status == FlowStatus.FAILED) { return null;
AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator()); case FAILED:
processor.logFailure(); AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED); processor.logFailure();
if (result.getChallenge() != null) { processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
return sendChallenge(result, execution); if (result.getChallenge() != null) {
} return sendChallenge(result, execution);
throw new AuthenticationFlowException(result.getError()); }
} else if (status == FlowStatus.FORK) { throw new AuthenticationFlowException(result.getError());
AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator()); case FORK:
processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId()); AuthenticationProcessor.logger.debugv("reset browser login from authenticator: {0}", execution.getAuthenticator());
throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage()); processor.getClientSession().setNote(AuthenticationProcessor.CURRENT_AUTHENTICATION_EXECUTION, execution.getId());
} else if (status == FlowStatus.FORCE_CHALLENGE) { throw new ForkFlowException(result.getSuccessMessage(), result.getErrorMessage());
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); case FORCE_CHALLENGE:
return sendChallenge(result, execution);
} else if (status == FlowStatus.CHALLENGE) {
AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
if (execution.isRequired()) {
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution); return sendChallenge(result, execution);
} case CHALLENGE:
UserModel authenticatedUser = processor.getClientSession().getAuthenticatedUser(); AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
if (execution.isOptional() && authenticatedUser != null && result.getAuthenticator().configuredFor(processor.getSession(), processor.getRealm(), authenticatedUser)) { 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); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution); return sendChallenge(result, execution);
} case ATTEMPTED:
if (execution.isAlternative()) { AuthenticationProcessor.logger.debugv("authenticator ATTEMPTED: {0}", execution.getAuthenticator());
alternativeChallenge = result.getChallenge(); if (execution.getRequirement() == AuthenticationExecutionModel.Requirement.REQUIRED) {
challengedAlternativeExecution = execution; throw new AuthenticationFlowException(AuthenticationFlowError.INVALID_CREDENTIALS);
} else { }
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED); processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.ATTEMPTED);
} return null;
return null; case FLOW_RESET:
} else if (status == FlowStatus.FAILURE_CHALLENGE) { AuthenticationProcessor.resetFlow(processor.getClientSession());
AuthenticationProcessor.logger.debugv("authenticator FAILURE_CHALLENGE: {0}", execution.getAuthenticator()); return processor.authenticate();
processor.logFailure(); default:
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED); AuthenticationProcessor.logger.debugv("authenticator INTERNAL_ERROR: {0}", execution.getAuthenticator());
return sendChallenge(result, execution); AuthenticationProcessor.logger.error("Unknown result status");
} else if (status == FlowStatus.ATTEMPTED) { throw new AuthenticationFlowException(AuthenticationFlowError.INTERNAL_ERROR);
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);
} }
} }
public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) { public Response sendChallenge(AuthenticationProcessor.Result result, AuthenticationExecutionModel execution) {

View file

@ -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. * 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
} }

View file

@ -37,6 +37,10 @@ public class OTPFormAuthenticator extends AbstractUsernameFormAuthenticator impl
public void validateOTP(AuthenticationFlowContext context) { public void validateOTP(AuthenticationFlowContext context) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters(); MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
if (inputData.containsKey("cancel")) {
context.resetFlow();
return;
}
List<UserCredentialModel> credentials = new LinkedList<>(); List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.TOTP); String password = inputData.getFirst(CredentialRepresentation.TOTP);
if (password == null) { if (password == null) {

View file

@ -153,6 +153,16 @@ public class LoginTotpTest {
events.expectLogin().assertEvent(); events.expectLogin().assertEvent();
} }
@Test
public void loginWithTotpCancel() throws Exception {
loginPage.open();
loginPage.login("test-user@localhost", "password");
loginTotpPage.assertCurrent();
loginTotpPage.cancel();
loginPage.assertCurrent();
}
@Test @Test
public void loginWithTotpInvalidPassword() throws Exception { public void loginWithTotpInvalidPassword() throws Exception {
loginPage.open(); loginPage.open();

View file

@ -39,6 +39,9 @@ public class LoginTotpPage extends AbstractPage {
@FindBy(css = "input[type=\"submit\"]") @FindBy(css = "input[type=\"submit\"]")
private WebElement submitButton; private WebElement submitButton;
@FindBy(id = "kc-cancel")
private WebElement cancelButton;
@FindBy(className = "feedback-error") @FindBy(className = "feedback-error")
private WebElement loginErrorMessage; private WebElement loginErrorMessage;
@ -49,6 +52,10 @@ public class LoginTotpPage extends AbstractPage {
submitButton.click(); submitButton.click();
} }
public void cancel() {
cancelButton.click();
}
public String getError() { public String getError() {
return loginErrorMessage != null ? loginErrorMessage.getText() : null; return loginErrorMessage != null ? loginErrorMessage.getText() : null;
} }