/* * 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.Before; import org.junit.Test; import org.keycloak.models.AuthenticationExecutionModel.Requirement; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.AuthenticationFlowRepresentation; import org.keycloak.representations.idm.AuthenticatorConfigRepresentation; import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RoleRepresentation; import org.keycloak.testsuite.admin.ApiUtil; import org.keycloak.testsuite.admin.Users; import org.keycloak.testsuite.auth.page.login.OneTimeCode; import org.keycloak.testsuite.pages.LoginConfigTotpPage; import javax.ws.rs.NotFoundException; import javax.ws.rs.core.Response; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; 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 static org.keycloak.models.UserModel.RequiredAction.CONFIGURE_TOTP; import static org.keycloak.representations.idm.CredentialRepresentation.PASSWORD; import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlStartsWith; /** * * @author <a href="mailto:vramik@redhat.com">Vlastislav Ramik</a> */ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest { private final TimeBasedOTP totp = new TimeBasedOTP(); @Page private OneTimeCode testLoginOneTimeCodePage; @Page private LoginConfigTotpPage loginConfigTotpPage; @Override public void setDefaultPageUriParameters() { super.setDefaultPageUriParameters(); testLoginOneTimeCodePage.setAuthRealm(testRealmPage); } @Before @Override public void beforeTest() { super.beforeTest(); } private void configureRequiredActions() { //set configure TOTP as required action to test user List<String> requiredActions = new ArrayList<>(); requiredActions.add(CONFIGURE_TOTP.name()); testUser.setRequiredActions(requiredActions); testRealmResource().users().get(testUser.getId()).update(testUser); } private void configureOTP() { //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() { //update realm browser flow RealmRepresentation realm = testRealmResource().toRepresentation(); realm.setBrowserFlow("browser"); testRealmResource().update(realm); updateRequirement("browser", "auth-otp-form", Requirement.REQUIRED); testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); assertTrue(loginConfigTotpPage.isCurrent()); configureOTP(); testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } @Test public void conditionalOTPNoDefault() { configureRequiredActions(); configureOTP(); //prepare config - no configuration specified Map<String, String> 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<String, String> 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<String, String> config = new HashMap<>(); config.put(DEFAULT_OTP_OUTCOME, FORCE); setConditionalOTPForm(config); //test OTP is forced testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); assertTrue(loginConfigTotpPage.isCurrent()); configureOTP(); 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<String, String> config = new HashMap<>(); config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute"); config.put(DEFAULT_OTP_OUTCOME, FORCE); setConditionalOTPForm(config); //add skip user attribute to user testUser.singleAttribute("userSkipAttribute", "skip"); 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<String, String> config = new HashMap<>(); config.put(OTP_CONTROL_USER_ATTRIBUTE, "userSkipAttribute"); config.put(DEFAULT_OTP_OUTCOME, SKIP); setConditionalOTPForm(config); //add force user attribute to user testUser.singleAttribute("userSkipAttribute", "force"); testRealmResource().users().get(testUser.getId()).update(testUser); //test OTP is required testRealmAccountManagementPage.navigateTo(); testRealmLoginPage.form().login(testUser); assertTrue(loginConfigTotpPage.isCurrent()); configureOTP(); 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<String, String> config = new HashMap<>(); config.put(SKIP_OTP_ROLE, "otp_role"); config.put(DEFAULT_OTP_OUTCOME, FORCE); setConditionalOTPForm(config); //create role RoleRepresentation role = getOrCreateOTPRole(); //add role to user List<RoleRepresentation> 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<String, String> config = new HashMap<>(); config.put(FORCE_OTP_ROLE, "otp_role"); config.put(DEFAULT_OTP_OUTCOME, SKIP); setConditionalOTPForm(config); //create role RoleRepresentation role = getOrCreateOTPRole(); //add role to user List<RoleRepresentation> 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); assertTrue(loginConfigTotpPage.isCurrent()); configureOTP(); testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } private RoleRepresentation getOrCreateOTPRole() { try { return testRealmResource().roles().get("otp_role").toRepresentation(); } catch (NotFoundException ex) { RoleRepresentation role = new RoleRepresentation("otp_role", "", false); testRealmResource().roles().create(role); //obtain id return testRealmResource().roles().get("otp_role").toRepresentation(); } } @Test public void conditionalOTPRequestHeaderSkip() { //prepare config - request header skip, default to force Map<String, String> 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<String, String> 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); assertEquals(driver.getTitle(), "Mobile Authenticator Setup"); configureOTP(); testRealmLoginPage.form().login(testUser); testRealmLoginPage.form().totpForm().waitForTotpInputFieldPresent(); //verify that the page is login page, not totp setup assertCurrentUrlStartsWith(testLoginOneTimeCodePage); } private void setConditionalOTPForm(Map<String, String> config) { List<AuthenticationFlowRepresentation> authFlows = getAuthMgmtResource().getFlows(); for (AuthenticationFlowRepresentation flow : authFlows) { if ("ConditionalOTPFlow".equals(flow.getAlias())) { //update realm browser flow RealmRepresentation realm = testRealmResource().toRepresentation(); realm.setBrowserFlow(DefaultAuthenticationFlows.BROWSER_FLOW); testRealmResource().update(realm); getAuthMgmtResource().deleteFlow(flow.getId()); break; } } 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); assertEquals(flowAlias + " create success", 201, response.getStatus()); response.close(); //add execution - username-password form Map<String, String> 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); assertEquals("new execution success", 201, response.getStatus()); getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response)); response.close(); } }