/* * 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.broker; import org.junit.Assert; import org.junit.ClassRule; import org.junit.Test; import org.keycloak.authentication.authenticators.broker.IdpEmailVerificationAuthenticatorFactory; import org.keycloak.models.AuthenticationExecutionModel; import org.keycloak.models.AuthenticationFlowModel; import org.keycloak.models.FederatedIdentityModel; import org.keycloak.models.IdentityProviderModel; import org.keycloak.models.KeycloakSession; import org.keycloak.models.RealmModel; import org.keycloak.models.UserModel; import org.keycloak.models.utils.DefaultAuthenticationFlows; import org.keycloak.models.utils.TimeBasedOTP; import org.keycloak.representations.idm.IdentityProviderRepresentation; import org.keycloak.services.managers.RealmManager; import org.keycloak.testsuite.KeycloakServer; import org.keycloak.testsuite.pages.IdpConfirmLinkPage; import org.keycloak.testsuite.pages.LoginConfigTotpPage; import org.keycloak.testsuite.pages.LoginTotpPage; import org.keycloak.testsuite.rule.AbstractKeycloakRule; import org.keycloak.testsuite.rule.WebResource; import java.util.Arrays; import java.util.List; import java.util.Set; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; /** * @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a> */ public class PostBrokerFlowTest extends AbstractIdentityProviderTest { private static final int PORT = 8082; private static String POST_BROKER_FLOW_ID; private static final String APP_REALM_ID = "realm-with-broker"; @ClassRule public static AbstractKeycloakRule samlServerRule = new AbstractKeycloakRule() { @Override protected void configureServer(KeycloakServer server) { server.getConfig().setPort(PORT); } @Override protected void configure(KeycloakSession session, RealmManager manager, RealmModel adminRealm) { server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-kc-oidc.json")); server.importRealm(getClass().getResourceAsStream("/broker-test/test-broker-realm-with-saml.json")); RealmModel realmWithBroker = getRealm(session); // Disable "idp-email-verification" authenticator in firstBrokerLogin flow. Disable updateProfileOnFirstLogin page AbstractFirstBrokerLoginTest.setExecutionRequirement(realmWithBroker, DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_HANDLE_EXISTING_SUBFLOW, IdpEmailVerificationAuthenticatorFactory.PROVIDER_ID, AuthenticationExecutionModel.Requirement.DISABLED); setUpdateProfileFirstLogin(realmWithBroker, IdentityProviderRepresentation.UPFLM_OFF); // Add post-broker flow with OTP authenticator to the realm AuthenticationFlowModel postBrokerFlow = new AuthenticationFlowModel(); postBrokerFlow.setAlias("post-broker"); postBrokerFlow.setDescription("post-broker flow with OTP"); postBrokerFlow.setProviderId("basic-flow"); postBrokerFlow.setTopLevel(true); postBrokerFlow.setBuiltIn(false); postBrokerFlow = realmWithBroker.addAuthenticationFlow(postBrokerFlow); POST_BROKER_FLOW_ID = postBrokerFlow.getId(); AuthenticationExecutionModel execution = new AuthenticationExecutionModel(); execution.setParentFlow(postBrokerFlow.getId()); execution.setRequirement(AuthenticationExecutionModel.Requirement.REQUIRED); execution.setAuthenticator("auth-otp-form"); execution.setPriority(20); execution.setAuthenticatorFlow(false); realmWithBroker.addAuthenticatorExecution(execution); } @Override protected String[] getTestRealms() { return new String[] { "realm-with-oidc-identity-provider", "realm-with-saml-idp-basic" }; } }; @WebResource protected IdpConfirmLinkPage idpConfirmLinkPage; @WebResource protected LoginTotpPage loginTotpPage; @WebResource protected LoginConfigTotpPage totpPage; private TimeBasedOTP totp = new TimeBasedOTP(); @Override protected String getProviderId() { return "kc-oidc-idp"; } @Test public void testPostBrokerLoginWithOTP() { // enable post-broker flow IdentityProviderModel identityProvider = getIdentityProviderModel(); setPostBrokerFlowForProvider(identityProvider, getRealm(), true); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); // login with broker and assert that OTP needs to be set. loginIDP("test-user"); totpPage.assertCurrent(); String totpSecret = totpPage.getTotpSecret(); totpPage.configure(totp.generateTOTP(totpSecret)); assertFederatedUser("test-user", "test-user@localhost", "test-user", getProviderId()); driver.navigate().to("http://localhost:8081/test-app/logout"); // Login again and assert that OTP needs to be provided. loginIDP("test-user"); loginTotpPage.assertCurrent(); loginTotpPage.login(totp.generateTOTP(totpSecret)); assertFederatedUser("test-user", "test-user@localhost", "test-user", getProviderId()); driver.navigate().to("http://localhost:8081/test-app/logout"); // Disable post-broker and ensure that OTP is not required anymore setPostBrokerFlowForProvider(identityProvider, getRealm(), false); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); loginIDP("test-user"); assertFederatedUser("test-user", "test-user@localhost", "test-user", getProviderId()); driver.navigate().to("http://localhost:8081/test-app/logout"); } @Test public void testBrokerReauthentication_samlBrokerWithOTPRequired() throws Exception { RealmModel realmWithBroker = getRealm(); // Enable OTP just for SAML provider IdentityProviderModel samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, true); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); // ensure TOTP setup is required during SAML broker firstLogin and during reauthentication for link OIDC broker too reauthenticateOIDCWithSAMLBroker(true, false); // Disable TOTP for SAML provider realmWithBroker = getRealm(); samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, false); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); } @Test public void testBrokerReauthentication_oidcBrokerWithOTPRequired() throws Exception { // Enable OTP just for OIDC provider IdentityProviderModel oidcIdentityProvider = getIdentityProviderModel(); setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), true); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); // ensure TOTP setup is not required during SAML broker firstLogin, but during reauthentication for link OIDC broker reauthenticateOIDCWithSAMLBroker(false, true); // Disable TOTP for SAML provider oidcIdentityProvider = getIdentityProviderModel(); setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), false); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); } @Test public void testBrokerReauthentication_bothBrokerWithOTPRequired() throws Exception { RealmModel realmWithBroker = getRealm(); // Enable OTP for both OIDC and SAML provider IdentityProviderModel samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, true); IdentityProviderModel oidcIdentityProvider = getIdentityProviderModel(); setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), true); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); // ensure TOTP setup is required during SAML broker firstLogin and during reauthentication for link OIDC broker too reauthenticateOIDCWithSAMLBroker(true, true); // Disable TOTP for both SAML and OIDC provider realmWithBroker = getRealm(); samlIdentityProvider = realmWithBroker.getIdentityProviderByAlias("kc-saml-idp-basic"); setPostBrokerFlowForProvider(samlIdentityProvider, realmWithBroker, false); oidcIdentityProvider = getIdentityProviderModel(); setPostBrokerFlowForProvider(oidcIdentityProvider, getRealm(), false); brokerServerRule.stopSession(this.session, true); this.session = brokerServerRule.startSession(); } private void reauthenticateOIDCWithSAMLBroker(boolean samlBrokerTotpEnabled, boolean oidcBrokerTotpEnabled) { // First login as "testuser" with SAML broker driver.navigate().to("http://localhost:8081/test-app"); this.loginPage.clickSocial("kc-saml-idp-basic"); assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8082/auth/")); Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); this.loginPage.login("test-user", "password"); // Ensure user needs to setup TOTP if SAML broker requires that String totpSecret = null; if (samlBrokerTotpEnabled) { totpPage.assertCurrent(); totpSecret = totpPage.getTotpSecret(); totpPage.configure(totp.generateTOTP(totpSecret)); } assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); driver.navigate().to("http://localhost:8081/test-app/logout"); // login through OIDC broker now loginIDP("test-user"); this.idpConfirmLinkPage.assertCurrent(); Assert.assertEquals("User with email test-user@localhost already exists. How do you want to continue?", this.idpConfirmLinkPage.getMessage()); this.idpConfirmLinkPage.clickLinkAccount(); // assert reauthentication with login page. On login page is link to kc-saml-idp-basic as user has it linked already Assert.assertEquals("Log in to " + APP_REALM_ID, this.driver.getTitle()); Assert.assertEquals("Authenticate as test-user to link your account with " + getProviderId(), this.loginPage.getInfoMessage()); // reauthenticate with SAML broker. OTP authentication is required as well this.loginPage.clickSocial("kc-saml-idp-basic"); Assert.assertEquals("Log in to realm-with-saml-idp-basic", this.driver.getTitle()); this.loginPage.login("test-user", "password"); if (samlBrokerTotpEnabled) { // User already set TOTP during first login with SAML broker loginTotpPage.assertCurrent(); loginTotpPage.login(totp.generateTOTP(totpSecret)); } else if (oidcBrokerTotpEnabled) { // User needs to set TOTP as first login with SAML broker didn't require that totpPage.assertCurrent(); totpSecret = totpPage.getTotpSecret(); totpPage.configure(totp.generateTOTP(totpSecret)); } // authenticated and redirected to app. User is linked with both identity providers assertFederatedUser("test-user", "test-user@localhost", "test-user", getProviderId(), "kc-saml-idp-basic"); } private void setPostBrokerFlowForProvider(IdentityProviderModel identityProvider, RealmModel realm, boolean enable) { if (enable) { identityProvider.setPostBrokerLoginFlowId(POST_BROKER_FLOW_ID); } else { identityProvider.setPostBrokerLoginFlowId(null); } realm.updateIdentityProvider(identityProvider); } private void assertFederatedUser(String expectedUsername, String expectedEmail, String expectedFederatedUsername, String... expectedLinkedProviders) { assertTrue(this.driver.getCurrentUrl().startsWith("http://localhost:8081/test-app")); UserModel federatedUser = getFederatedUser(); assertNotNull(federatedUser); assertEquals(expectedUsername, federatedUser.getUsername()); assertEquals(expectedEmail, federatedUser.getEmail()); RealmModel realmWithBroker = getRealm(); Set<FederatedIdentityModel> federatedIdentities = this.session.users().getFederatedIdentities(federatedUser, realmWithBroker); List<String> expectedProvidersList = Arrays.asList(expectedLinkedProviders); assertEquals(expectedProvidersList.size(), federatedIdentities.size()); for (FederatedIdentityModel federatedIdentityModel : federatedIdentities) { String providerAlias = federatedIdentityModel.getIdentityProvider(); Assert.assertTrue(expectedProvidersList.contains(providerAlias)); assertEquals(expectedFederatedUsername, federatedIdentityModel.getUserName()); } } }