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 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.kcButtonDefaultClass!} ${properties.kcButtonLargeClass!}" name="cancel" id="kc-cancel" type="submit" value="${msg("doCancel")}"/>
</div>
</div>
</div>

View file

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

View file

@ -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;

View file

@ -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,12 +156,13 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
public Response processResult(AuthenticationProcessor.Result result) {
AuthenticationExecutionModel execution = result.getExecution();
FlowStatus status = result.getStatus();
if (status == FlowStatus.SUCCESS) {
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;
} else if (status == FlowStatus.FAILED) {
case FAILED:
AuthenticationProcessor.logger.debugv("authenticator FAILED: {0}", execution.getAuthenticator());
processor.logFailure();
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.FAILED);
@ -166,14 +170,14 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
return sendChallenge(result, execution);
}
throw new AuthenticationFlowException(result.getError());
} else if (status == FlowStatus.FORK) {
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());
} else if (status == FlowStatus.FORCE_CHALLENGE) {
case FORCE_CHALLENGE:
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
return sendChallenge(result, execution);
} else if (status == FlowStatus.CHALLENGE) {
case CHALLENGE:
AuthenticationProcessor.logger.debugv("authenticator CHALLENGE: {0}", execution.getAuthenticator());
if (execution.isRequired()) {
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.CHALLENGED);
@ -191,24 +195,26 @@ public class DefaultAuthenticationFlow implements AuthenticationFlow {
processor.getClientSession().setExecutionStatus(execution.getId(), ClientSessionModel.ExecutionStatus.SKIPPED);
}
return null;
} else if (status == FlowStatus.FAILURE_CHALLENGE) {
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);
} else if (status == FlowStatus.ATTEMPTED) {
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;
} else {
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) {

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.
*
*/
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) {
MultivaluedMap<String, String> inputData = context.getHttpRequest().getDecodedFormParameters();
if (inputData.containsKey("cancel")) {
context.resetFlow();
return;
}
List<UserCredentialModel> credentials = new LinkedList<>();
String password = inputData.getFirst(CredentialRepresentation.TOTP);
if (password == null) {

View file

@ -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();

View file

@ -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;
}