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