diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/LoginForm.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/LoginForm.java
index 85ad4b6c10..ad914cdafb 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/LoginForm.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/LoginForm.java
@@ -36,6 +36,8 @@ public class LoginForm extends Form {
private AccountFields accountFields;
@Page
private PasswordFields passwordFields;
+ @Page
+ private TotpSetupForm totpForm;
@FindBy(name = "login")
private WebElement loginButton;
@@ -119,5 +121,35 @@ public class LoginForm extends Form {
public void waitForLoginButtonPresent() {
waitUntilElement(loginButton).is().present();
}
-
+
+ public TotpSetupForm totpForm() {
+ return totpForm;
+ }
+
+ public class TotpSetupForm extends Form {
+ @FindBy(id = "totp")
+ private WebElement totpInputField;
+
+ @FindBy(id = "totpSecret")
+ private WebElement totpSecret;
+
+ @FindBy(xpath = ".//input[@value='Submit']")
+ private WebElement submit;
+
+ public void waitForTotpInputFieldPresent() {
+ waitUntilElement(totpInputField).is().present();
+ }
+
+ public void setTotp(String value) {
+ setInputValue(totpInputField, value);
+ }
+
+ public String getTotpSecret() {
+ return totpSecret.getAttribute(VALUE);
+ }
+
+ public void submit() {
+ submit.click();
+ }
+ }
}
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OneTimeCode.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OneTimeCode.java
new file mode 100644
index 0000000000..ccc23723aa
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/auth/page/login/OneTimeCode.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.auth.page.login;
+
+import org.keycloak.testsuite.auth.page.login.LoginForm.TotpSetupForm;
+import org.openqa.selenium.support.FindBy;
+
+/**
+ *
+ * @author Vlastislav Ramik
+ */
+public class OneTimeCode extends Authenticate {
+
+ @FindBy(id = "kc-totp-login-form")
+ private TotpSetupForm form;
+
+ public TotpSetupForm form() {
+ return form;
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/AbstractCustomAccountManagementTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/AbstractCustomAccountManagementTest.java
new file mode 100644
index 0000000000..36ff0f69bd
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/AbstractCustomAccountManagementTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.keycloak.testsuite.account.custom;
+
+import java.util.List;
+import javax.ws.rs.core.GenericType;
+import javax.ws.rs.core.Response;
+import org.junit.Before;
+import org.keycloak.admin.client.resource.AuthenticationManagementResource;
+import org.keycloak.models.AuthenticationExecutionModel;
+import org.keycloak.representations.idm.AuthenticationExecutionInfoRepresentation;
+import org.keycloak.testsuite.account.AbstractAccountManagementTest;
+
+/**
+ *
+ * @author Vlastislav Ramik
+ */
+public abstract class AbstractCustomAccountManagementTest extends AbstractAccountManagementTest {
+
+ private AuthenticationManagementResource authMgmtResource;
+
+ @Override
+ public void setDefaultPageUriParameters() {
+ super.setDefaultPageUriParameters();
+ }
+
+ @Before
+ public void beforeTest() {
+ authMgmtResource = testRealmResource().flows();
+ }
+
+ protected AuthenticationManagementResource getAuthMgmtResource() {
+ return authMgmtResource;
+ }
+
+ protected void updateRequirement(String flowAlias, String provider, AuthenticationExecutionModel.Requirement requirement) {
+ AuthenticationExecutionInfoRepresentation exec = getExecution(flowAlias, provider);
+
+ exec.setRequirement(requirement.name());
+ authMgmtResource.updateExecutions(flowAlias, exec);
+ }
+
+ protected AuthenticationExecutionInfoRepresentation getExecution(String flowAlias, String provider) {
+ Response response = authMgmtResource.getExecutions(flowAlias);
+
+ List executionReps = response.readEntity(
+ new GenericType>() {
+ });
+
+ response.close();
+
+ for (AuthenticationExecutionInfoRepresentation exec : executionReps) {
+ if (provider.equals(exec.getProviderId())) {
+ return exec;
+ }
+ }
+ return null;
+ }
+
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowCookieTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowCookieTest.java
new file mode 100644
index 0000000000..beef6ccbc7
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowCookieTest.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.account.custom;
+
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Test;
+
+import org.junit.Before;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import static org.keycloak.testsuite.auth.page.AuthRealm.TEST;
+import org.keycloak.testsuite.console.page.AdminConsole;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWithLoginUrlOf;
+
+/**
+ *
+ * @author Vlastislav Ramik
+ */
+public class CustomAuthFlowCookieTest extends AbstractCustomAccountManagementTest {
+
+ @Page
+ private AdminConsole testRealmAdminConsolePage;
+
+ @Override
+ public void setDefaultPageUriParameters() {
+ super.setDefaultPageUriParameters();
+ }
+
+ @Before
+ @Override
+ public void beforeTest() {
+ super.beforeTest();
+ testRealmAdminConsolePage.setAdminRealm(TEST);
+ }
+
+ @Test
+ public void cookieAlternative() {
+ //test default setting of cookie provider
+ //login to account management
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+
+ //check SSO is working
+ //navigate to realm-management (different client of the same realm) and verify user is logged in
+ testRealmAdminConsolePage.navigateTo();
+ assertCurrentUrlStartsWith(testRealmAdminConsolePage);
+ }
+
+ @Test
+ public void disabledCookie() {
+ //disable cookie
+ updateRequirement("browser", "auth-cookie", Requirement.DISABLED);
+
+ //login to account management
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+
+ //SSO shouln't work
+ //navigate to realm-management and verify user is not logged in
+ testRealmAdminConsolePage.navigateTo();
+ assertCurrentUrlStartsWithLoginUrlOf(testRealmLoginPage);
+ }
+}
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
new file mode 100644
index 0000000000..9334bb968a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/account/custom/CustomAuthFlowOTPTest.java
@@ -0,0 +1,338 @@
+/*
+ * Copyright 2016 Red Hat, Inc. and/or its affiliates
+ * and other contributors as indicated by the @author tags.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.keycloak.testsuite.account.custom;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.ws.rs.core.Response;
+import org.jboss.arquillian.graphene.page.Page;
+import org.junit.Assert;
+import static org.junit.Assert.assertTrue;
+import org.junit.Test;
+
+import org.junit.Before;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.DEFAULT_OTP_OUTCOME;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE_OTP_FOR_HTTP_HEADER;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.FORCE_OTP_ROLE;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OTP_CONTROL_USER_ATTRIBUTE;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP_OTP_FOR_HTTP_HEADER;
+import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.SKIP_OTP_ROLE;
+import org.keycloak.models.AuthenticationExecutionModel.Requirement;
+import static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP;
+import org.keycloak.models.utils.TimeBasedOTP;
+import org.keycloak.representations.idm.AuthenticationFlowRepresentation;
+import org.keycloak.representations.idm.AuthenticatorConfigRepresentation;
+import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.testsuite.admin.Users;
+import org.keycloak.testsuite.auth.page.login.OneTimeCode;
+import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith;
+
+/**
+ *
+ * @author Vlastislav Ramik
+ */
+public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
+
+ private final TimeBasedOTP totp = new TimeBasedOTP();
+
+ @Page
+ private OneTimeCode testLoginOneTimeCodePage;
+
+ @Override
+ public void setDefaultPageUriParameters() {
+ super.setDefaultPageUriParameters();
+ testLoginOneTimeCodePage.setAuthRealm(testRealmPage);
+ }
+
+ @Before
+ @Override
+ public void beforeTest() {
+ super.beforeTest();
+ //set configure TOTP as required action to test user
+ List requiredActions = new ArrayList<>();
+ requiredActions.add(CONFIGURE_TOTP.name());
+ testUser.setRequiredActions(requiredActions);
+ testRealmResource().users().get(testUser.getId()).update(testUser);
+
+ //configure OTP for test user
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+ String totpSecret = testRealmLoginPage.form().totpForm().getTotpSecret();
+ testRealmLoginPage.form().totpForm().setTotp(totp.generateTOTP(totpSecret));
+ testRealmLoginPage.form().totpForm().submit();
+ testRealmAccountManagementPage.signOut();
+
+ //verify that user has OTP configured
+ testUser = testRealmResource().users().get(testUser.getId()).toRepresentation();
+ Users.setPasswordFor(testUser, PASSWORD);
+ assertTrue(testUser.getRequiredActions().isEmpty());
+ }
+
+ @Test
+ public void requireOTPTest() {
+
+ updateRequirement("browser", "auth-otp-form", Requirement.REQUIRED);
+
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ @Test
+ public void conditionalOTPNoDefault() {
+ //prepare config - no configuration specified
+ Map config = new HashMap<>();
+ setConditionalOTPForm(config);
+
+ //test OTP is required
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ @Test
+ public void conditionalOTPDefaultSkip() {
+ //prepare config - default skip
+ Map config = new HashMap<>();
+ config.put(DEFAULT_OTP_OUTCOME, SKIP);
+
+ setConditionalOTPForm(config);
+
+ //test OTP is skipped
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ assertCurrentUrlStartsWith(testRealmAccountManagementPage);
+ }
+
+ @Test
+ public void conditionalOTPDefaultForce() {
+ //prepare config - default force
+ Map config = new HashMap<>();
+ config.put(DEFAULT_OTP_OUTCOME, FORCE);
+
+ setConditionalOTPForm(config);
+
+ //test OTP is forced
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ @Test
+ public void conditionalOTPUserAttributeSkip() {
+ //prepare config - user attribute, default to force
+ Map config = new HashMap<>();
+ config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute");
+ config.put(DEFAULT_OTP_OUTCOME, FORCE);
+
+ setConditionalOTPForm(config);
+
+ //add skip user attribute to user
+ Map userAttributes = new HashMap<>();
+ List attributeValues = new ArrayList<>();
+ attributeValues.add("skip");
+ userAttributes.put("userSkipAttribute", attributeValues);
+ testUser.setAttributes(userAttributes);
+ testRealmResource().users().get(testUser.getId()).update(testUser);
+
+ //test OTP is skipped
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ assertCurrentUrlStartsWith(testRealmAccountManagementPage);
+ }
+
+ @Test
+ public void conditionalOTPUserAttributeForce() {
+ //prepare config - user attribute, default to skip
+ Map config = new HashMap<>();
+ config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute");
+ config.put(DEFAULT_OTP_OUTCOME, SKIP);
+
+ setConditionalOTPForm(config);
+
+ //add force user attribute to user
+ Map userAttributes = new HashMap<>();
+ List attributeValues = new ArrayList<>();
+ attributeValues.add("force");
+ userAttributes.put("userSkipAttribute", attributeValues);
+ testUser.setAttributes(userAttributes);
+ testRealmResource().users().get(testUser.getId()).update(testUser);
+
+ //test OTP is required
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ @Test
+ public void conditionalOTPRoleSkip() {
+ //prepare config - role, default to force
+ Map config = new HashMap<>();
+ config.put(SKIP_OTP_ROLE, "otp_role");
+ config.put(DEFAULT_OTP_OUTCOME, FORCE);
+
+ setConditionalOTPForm(config);
+
+ //create role
+ RoleRepresentation role = new RoleRepresentation("otp_role", "", false);
+ testRealmResource().roles().create(role);
+ //obtain id
+ role = testRealmResource().roles().get("otp_role").toRepresentation();
+ //add role to user
+ List realmRoles = new ArrayList<>();
+ realmRoles.add(role);
+ testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles);
+
+ //test OTP is skipped
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ assertCurrentUrlStartsWith(testRealmAccountManagementPage);
+ }
+
+ @Test
+ public void conditionalOTPRoleForce() {
+ //prepare config - role, default to skip
+ Map config = new HashMap<>();
+ config.put(FORCE_OTP_ROLE, "otp_role");
+ config.put(DEFAULT_OTP_OUTCOME, SKIP);
+
+ setConditionalOTPForm(config);
+
+ //create role
+ RoleRepresentation role = new RoleRepresentation("otp_role", "", false);
+ testRealmResource().roles().create(role);
+ //obtain id
+ role = testRealmResource().roles().get("otp_role").toRepresentation();
+ //add role to user
+ List realmRoles = new ArrayList<>();
+ realmRoles.add(role);
+ testRealmResource().users().get(testUser.getId()).roles().realmLevel().add(realmRoles);
+
+ //test OTP is required
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ @Test
+ public void conditionalOTPRequestHeaderSkip() {
+ //prepare config - request header skip, default to force
+ Map config = new HashMap<>();
+ String port = System.getProperty("auth.server.http.port", "8180");
+ config.put(SKIP_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port);
+ config.put(DEFAULT_OTP_OUTCOME, FORCE);
+
+ setConditionalOTPForm(config);
+
+ //test OTP is skipped
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ assertCurrentUrlStartsWith(testRealmAccountManagementPage);
+ }
+
+ @Test
+ public void conditionalOTPRequestHeaderForce() {
+ //prepare config - equest header force, default to skip
+ Map config = new HashMap<>();
+ String port = System.getProperty("auth.server.http.port", "8180");
+ config.put(FORCE_OTP_FOR_HTTP_HEADER, "Host: localhost:" + port);
+ config.put(DEFAULT_OTP_OUTCOME, SKIP);
+
+ setConditionalOTPForm(config);
+
+ //test OTP is required
+ testRealmAccountManagementPage.navigateTo();
+ testRealmLoginPage.form().login(testUser);
+ testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent();
+
+ //verify that the page is login page, not totp setup
+ assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
+ }
+
+ private void setConditionalOTPForm(Map config) {
+ String flowAlias = "ConditionalOTPFlow";
+ String provider = "auth-conditional-otp-form";
+
+ //create flow
+ AuthenticationFlowRepresentation flow = new AuthenticationFlowRepresentation();
+ flow.setAlias(flowAlias);
+ flow.setDescription("");
+ flow.setProviderId("basic-flow");
+ flow.setTopLevel(true);
+ flow.setBuiltIn(false);
+
+ Response response = getAuthMgmtResource().createFlow(flow);
+ Assert.assertEquals(flowAlias + " create success", 201, response.getStatus());
+ response.close();
+
+ //add execution - username-password form
+ Map data = new HashMap<>();
+ data.put("provider", "auth-username-password-form");
+ getAuthMgmtResource().addExecution(flowAlias, data);
+
+ //set username-password requirement to required
+ updateRequirement(flowAlias, "auth-username-password-form", Requirement.REQUIRED);
+
+ //add execution - conditional OTP
+ data.clear();
+ data.put("provider", provider);
+ getAuthMgmtResource().addExecution(flowAlias, data);
+
+ //set Conditional OTP requirement to required
+ updateRequirement(flowAlias, provider, Requirement.REQUIRED);
+
+ //update realm browser flow
+ RealmRepresentation realm = testRealmResource().toRepresentation();
+ realm.setBrowserFlow(flowAlias);
+ testRealmResource().update(realm);
+
+ //get executionId
+ String executionId = getExecution(flowAlias, provider).getId();
+
+ //prepare auth config
+ AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
+ authConfig.setAlias("Config alias");
+ authConfig.setConfig(config);
+
+ //add auth config to the execution
+ response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig);
+ Assert.assertEquals("new execution success", 201, response.getStatus());
+ response.close();
+ }
+}