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(); + } +}