Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -196,34 +196,6 @@ protected void logoutFromConsumerRealm() {
AccountHelper.logout(getConsumerRealm().admin(), getUserLogin());
}

// Turns off the first-broker-login review-profile step for flows that want to test something else
// without the review-profile page interrupting the redirect back to the consumer. Disabling the flow
// execution alone isn't enough: the new-testsuite consumer realm enables the VERIFY_PROFILE required
// action by default, which would still intercept the incomplete imported user (the provider user has
// no first/last name on purpose), so that is disabled too, mirroring the legacy suite (which never
// enabled VERIFY_PROFILE at all).
protected void disableUpdateProfileOnFirstLogin() {
var flows = getConsumerRealm().admin().flows();
for (AuthenticationExecutionInfoRepresentation execution :
flows.getExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW)) {
if (IdpCreateUserIfUniqueAuthenticatorFactory.PROVIDER_ID.equals(execution.getProviderId())) {
execution.setRequirement(AuthenticationExecutionModel.Requirement.ALTERNATIVE.name());
flows.updateExecutions(DefaultAuthenticationFlows.FIRST_BROKER_LOGIN_FLOW, execution);
} else if (execution.getAlias() != null
&& execution.getAlias().equals(DefaultAuthenticationFlows.IDP_REVIEW_PROFILE_CONFIG_ALIAS)) {
AuthenticatorConfigRepresentation config = flows.getAuthenticatorConfig(execution.getAuthenticationConfig());
config.getConfig().put("update.profile.on.first.login", IdentityProviderRepresentation.UPFLM_OFF);
flows.updateAuthenticatorConfig(config.getId(), config);
}
}
for (RequiredActionProviderRepresentation action : getConsumerRealm().admin().flows().getRequiredActions()) {
if (UserModel.RequiredAction.VERIFY_PROFILE.name().equals(action.getAlias())) {
action.setEnabled(false);
getConsumerRealm().admin().flows().updateRequiredAction(action.getAlias(), action);
}
}
}

protected void assertNumFederatedIdentities(String username, int expected) {
ManagedRealm consumerRealm = getConsumerRealm();
List<UserRepresentation> users = consumerRealm.admin().users().search(username, true);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package org.keycloak.tests.broker.oidc;

import org.keycloak.models.IdentityProviderModel;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.remote.timeoffset.InjectTimeOffSet;
import org.keycloak.testframework.remote.timeoffset.TimeOffSet;
import org.keycloak.testframework.ui.annotations.InjectPage;
import org.keycloak.testframework.ui.page.ErrorPage;
import org.keycloak.tests.broker.AbstractKcOidcBrokerTest;
import org.keycloak.tests.common.CustomProvidersServerConfig;
import org.keycloak.tests.providers.broker.oidc.TestKeycloakOidcIdentityProviderFactory;
import org.keycloak.testsuite.util.AccountHelper;

import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

@KeycloakIntegrationTest(config = CustomProvidersServerConfig.class)
public class KcOidcBrokerPassMaxAgeTest extends AbstractKcOidcBrokerTest {

@InjectRealm(ref = "consumer", lifecycle = LifeCycle.METHOD,
config = PassMaxAgeConsumerRealmConfig.class)
ManagedRealm consumerRealm;

@InjectPage
ErrorPage errorPage;

@InjectTimeOffSet
TimeOffSet timeOffSet;

// The standard login flow doesn't apply cleanly to this config (no client-forwarded prompt, custom
// provider factory) - legacy disabled the same inherited test for this exact configuration and
// overrode the other with its own max_age-specific logic instead. Same shape here.
@Override
@Test
@Disabled("PassMaxAge config needs its own login flow; see testLoginWithExistingUser() below")
public void testLogInAsUserInIDP() {
}

@Override
@Test
public void testLoginWithExistingUser() {
logInAsUserInIDP();
updateAccountInformation();
assertUserCreatedInConsumerRealm();

oauth.openLoginForm();
Assertions.assertTrue(oauth.parseLoginResponse().isSuccess(), "Should be logged in");
logoutFromConsumerRealm();
AccountHelper.logout(providerRealm.admin(), getUserLogin());
oauth.openLoginForm();
loginPage.assertCurrent();

logInWithBroker();

loginPage.fillLogin(getUserLogin(), getUserPassword());
loginPage.submit();

timeOffSet.set(2);

oauth.loginForm().maxAge(1).open();

loginPage.assertCurrent();
Assertions.assertTrue(webDriver.getCurrentUrl().contains("/realms/" + CONSUMER_REALM + "/protocol/openid-connect/auth"),
"Driver should be on the consumer realm page right now");

logInWithBroker();

Assertions.assertTrue(webDriver.getCurrentUrl().contains("/realms/" + PROVIDER_REALM + "/"),
"Driver should be on the provider realm page right now");
loginPage.assertCurrent();

loginPage.fillPassword(getUserPassword());
loginPage.submit();
Assertions.assertTrue(oauth.parseLoginResponse().isSuccess());
}

@Test
void testEnforceReAuthenticationWhenMaxAgeIsSet() {
logInAsUserInIDP();
updateAccountInformation();
assertUserCreatedInConsumerRealm();

oauth.openLoginForm();
Assertions.assertTrue(oauth.parseLoginResponse().isSuccess(), "Should be logged in");
logoutFromConsumerRealm();
AccountHelper.logout(providerRealm.admin(), getUserLogin());
oauth.openLoginForm();
loginPage.assertCurrent();

logInWithBroker();

loginPage.fillLogin(getUserLogin(), getUserPassword());
loginPage.submit();

IdentityProviderRepresentation idpRep = consumerRealm.admin()
.identityProviders().get(getIdpAlias()).toRepresentation();
TestKeycloakOidcIdentityProviderFactory.setIgnoreMaxAgeParam(idpRep);
consumerRealm.admin().identityProviders().get(getIdpAlias()).update(idpRep);

timeOffSet.set(2);

oauth.loginForm().maxAge(1).open();

loginPage.assertCurrent();
Assertions.assertTrue(webDriver.getCurrentUrl().contains("/realms/" + CONSUMER_REALM + "/protocol/openid-connect/auth"),
"Driver should be on the consumer realm page right now");

logInWithBroker();

errorPage.assertCurrent();
Assertions.assertEquals("Unexpected error when authenticating with identity provider",
errorPage.getError());
}

static class PassMaxAgeConsumerRealmConfig implements RealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return configureConsumerRealm(realm,
createOidcIdentityProvider()
.providerId(TestKeycloakOidcIdentityProviderFactory.ID)
.attribute(IdentityProviderModel.LOGIN_HINT, "false")
.attribute(IdentityProviderModel.PASS_MAX_AGE, "true")
// Legacy KcOidcBrokerConfigurationWithPassMaxAge removed the "prompt" attribute so the
// broker does not force an interactive login; without this the provider always shows its
// login page and the ignore-max-age error path is never exercised.
.attribute("prompt", null));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package org.keycloak.tests.providers.broker.oidc;

import java.io.IOException;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import jakarta.ws.rs.core.UriBuilder;

import org.keycloak.broker.oidc.KeycloakOIDCIdentityProvider;
import org.keycloak.broker.oidc.KeycloakOIDCIdentityProviderFactory;
import org.keycloak.broker.oidc.OIDCIdentityProviderConfig;
import org.keycloak.broker.provider.AuthenticationRequest;
import org.keycloak.broker.provider.BrokeredIdentityContext;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.representations.AccessTokenResponse;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.sessions.AuthenticationSessionModel;
import org.keycloak.util.JsonSerialization;

public class TestKeycloakOidcIdentityProviderFactory extends KeycloakOIDCIdentityProviderFactory {

public static final String ID = "test-keycloak-oidc";
public static final String IGNORE_MAX_AGE_PARAM = "ignore-max-age-param";
public static final String USE_SINGLE_REFRESH_TOKEN = "use-single-refresh-token";
public static final String PREFERRED_USERNAME = "preferred-username";

public static void setIgnoreMaxAgeParam(IdentityProviderRepresentation rep) {
rep.getConfig().put(IGNORE_MAX_AGE_PARAM, Boolean.TRUE.toString());
}

@Override
public String getId() {
return ID;
}

@Override
public KeycloakOIDCIdentityProvider create(KeycloakSession session, IdentityProviderModel model) {
return new KeycloakOIDCIdentityProvider(session, new OIDCIdentityProviderConfig(model)) {

private static final Set<String> usernames = ConcurrentHashMap.newKeySet();

@Override
public BrokeredIdentityContext getFederatedIdentity(String response) {
BrokeredIdentityContext context = super.getFederatedIdentity(response);
String preferredUsername = getPreferredUsername();

if (preferredUsername != null) {
context.setUsername(preferredUsername);
}
if (Boolean.valueOf(model.getConfig().get(USE_SINGLE_REFRESH_TOKEN))) {
if (!usernames.add(context.getUsername())) {
try {
AccessTokenResponse tokenResponse = JsonSerialization.readValue(context.getToken(), AccessTokenResponse.class);
tokenResponse.setRefreshToken(null);
context.setToken(JsonSerialization.writeValueAsString(tokenResponse));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
return context;
}

@Override
protected UriBuilder createAuthorizationUrl(AuthenticationRequest request) {
AuthenticationSessionModel authSession = request.getAuthenticationSession();
String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);

try {
if (isIgnoreMaxAgeParam()) {
authSession.removeClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
}
return super.createAuthorizationUrl(request);
} finally {
authSession.setClientNote(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
}
}

private boolean isIgnoreMaxAgeParam() {
return Boolean.parseBoolean(model.getConfig().getOrDefault(IGNORE_MAX_AGE_PARAM, Boolean.FALSE.toString()));
}

private String getPreferredUsername() {
return model.getConfig().get(PREFERRED_USERNAME);
}
};
}

@Override
public OIDCIdentityProviderConfig createConfig() {
return new OIDCIdentityProviderConfig(super.createConfig()) {
@Override
public String getDisplayIconClasses() {
return "my-custom-idp-icon";
}
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
# limitations under the License.
#

org.keycloak.tests.providers.broker.oidc.OverwrittenMappersTestIdentityProviderFactory
org.keycloak.tests.providers.broker.oidc.OverwrittenMappersTestIdentityProviderFactory
org.keycloak.tests.providers.broker.oidc.TestKeycloakOidcIdentityProviderFactory
Loading
Loading