KEYCLOAK-18499 Add max_age support to oauth2 brokered logins

Revise KcOidcBrokerPassMaxAgeTest to use setTimeOffset(...)
This commit is contained in:
Thomas Darimont 2021-06-19 15:59:46 +02:00 committed by Pedro Igor
parent a3f339a1c4
commit 43623ea9d0
7 changed files with 125 additions and 3 deletions

View file

@ -30,6 +30,7 @@ public class IdentityProviderModel implements Serializable {
public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew"; public static final String ALLOWED_CLOCK_SKEW = "allowedClockSkew";
public static final String LOGIN_HINT = "loginHint"; public static final String LOGIN_HINT = "loginHint";
public static final String PASS_MAX_AGE = "passMaxAge";
public static final String SYNC_MODE = "syncMode"; public static final String SYNC_MODE = "syncMode";
@ -230,6 +231,14 @@ public class IdentityProviderModel implements Serializable {
getConfig().put(LOGIN_HINT, String.valueOf(loginHint)); getConfig().put(LOGIN_HINT, String.valueOf(loginHint));
} }
public boolean isPassMaxAge() {
return Boolean.valueOf(getConfig().get(PASS_MAX_AGE));
}
public void setPassMaxAge(boolean passMaxAge) {
getConfig().put(PASS_MAX_AGE, String.valueOf(passMaxAge));
}
public boolean isHideOnLogin() { public boolean isHideOnLogin() {
return Boolean.valueOf(getConfig().get(HIDE_ON_LOGIN)); return Boolean.valueOf(getConfig().get(HIDE_ON_LOGIN));

View file

@ -331,6 +331,11 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint); uriBuilder.queryParam(OIDCLoginProtocol.LOGIN_HINT_PARAM, loginHint);
} }
String maxAge = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
if (getConfig().isPassMaxAge() && maxAge != null) {
uriBuilder.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
}
if (getConfig().isUiLocales()) { if (getConfig().isUiLocales()) {
uriBuilder.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, session.getContext().resolveLocale(null).toLanguageTag()); uriBuilder.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, session.getContext().resolveLocale(null).toLanguageTag());
} }

View file

@ -121,16 +121,17 @@ public class AbstractOAuth2IdentityProviderTest {
} }
private TestProvider getTested() { private TestProvider getTested() {
return new TestProvider(getConfig(null, null, null, Boolean.FALSE)); return new TestProvider(getConfig(null, null, null, Boolean.FALSE, Boolean.FALSE));
} }
private OAuth2IdentityProviderConfig getConfig(final String autorizationUrl, final String defaultScope, final String clientId, final Boolean isLoginHint) { private OAuth2IdentityProviderConfig getConfig(final String autorizationUrl, final String defaultScope, final String clientId, final Boolean isLoginHint, final Boolean passMaxAge) {
IdentityProviderModel model = new IdentityProviderModel(); IdentityProviderModel model = new IdentityProviderModel();
OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(model); OAuth2IdentityProviderConfig config = new OAuth2IdentityProviderConfig(model);
config.setAuthorizationUrl(autorizationUrl); config.setAuthorizationUrl(autorizationUrl);
config.setDefaultScope(defaultScope); config.setDefaultScope(defaultScope);
config.setClientId(clientId); config.setClientId(clientId);
config.setLoginHint(isLoginHint); config.setLoginHint(isLoginHint);
config.setPassMaxAge(passMaxAge);
return config; return config;
} }

View file

@ -292,7 +292,7 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
/** /**
* Get the login page for an existing client in provided realm * Get the login page for an existing client in provided realm
* *
* @param contextRoot * @param contextRoot server base url without /auth
* @param realmName Name of the realm * @param realmName Name of the realm
* @param clientId ClientId of a client. Client has to exists in the realm. * @param clientId ClientId of a client. Client has to exists in the realm.
* @return Login URL * @return Login URL
@ -303,6 +303,9 @@ public abstract class AbstractBaseBrokerTest extends AbstractKeycloakTest {
assertThat(clients, Matchers.is(Matchers.not(Matchers.empty()))); assertThat(clients, Matchers.is(Matchers.not(Matchers.empty())));
String redirectURI = clients.get(0).getBaseUrl(); String redirectURI = clients.get(0).getBaseUrl();
if (redirectURI.startsWith("/")) {
redirectURI = contextRoot + "/auth" + redirectURI;
}
return contextRoot + "/auth/realms/" + realmName + "/protocol/openid-connect/auth?client_id=" + return contextRoot + "/auth/realms/" + realmName + "/protocol/openid-connect/auth?client_id=" +
clientId + "&redirect_uri=" + redirectURI + "&response_type=code&scope=openid"; clientId + "&redirect_uri=" + redirectURI + "&response_type=code&scope=openid";

View file

@ -0,0 +1,95 @@
package org.keycloak.testsuite.broker;
import org.junit.Ignore;
import org.junit.Test;
import org.keycloak.models.IdentityProviderModel;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testsuite.Assert;
import java.util.Map;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_ALIAS;
import static org.keycloak.testsuite.broker.BrokerTestConstants.IDP_OIDC_PROVIDER_ID;
import static org.keycloak.testsuite.broker.BrokerTestTools.createIdentityProvider;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
/**
* Tests the propagation of the max_age parameter for brokered logins.
*
* see https://issues.redhat.com/browse/KEYCLOAK-18499
*/
public class KcOidcBrokerPassMaxAgeTest extends AbstractBrokerTest {
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationWithPassMaxAge();
}
private static class KcOidcBrokerConfigurationWithPassMaxAge extends KcOidcBrokerConfiguration {
@Override
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
Map<String, String> config = idp.getConfig();
applyDefaultConfiguration(config, syncMode);
config.put(IdentityProviderModel.LOGIN_HINT, "false");
config.put(IdentityProviderModel.PASS_MAX_AGE, "true");
return idp;
}
}
@Override
@Test
@Ignore
public void testLogInAsUserInIDP() {
// super.testLogInAsUserInIDP();
}
@Test
@Override
public void loginWithExistingUser() {
// login as brokered user user, perform profile update on first broker login and logout user
loginUser();
testSingleLogout();
driver.navigate().to(getAccountUrl(getConsumerRoot(), bc.consumerRealmName()));
loginPage.clickSocial(bc.getIDPAlias());
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
loginPage.login(bc.getUserLogin(), bc.getUserPassword());
accountUpdateProfilePage.assertCurrent();
setTimeOffset(2);
// trigger re-auth with max_age while we are still authenticated
String loginUrlWithMaxAge = getLoginUrl(getConsumerRoot(), bc.consumerRealmName(), "account") + "&max_age=1";
driver.navigate().to(loginUrlWithMaxAge);
// we should now see the login page of the consumer
waitForPage(driver, "sign in to", true);
loginPage.assertCurrent(bc.consumerRealmName());
Assert.assertTrue("Driver should be on the consumer realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.consumerRealmName() + "/protocol/openid-connect/auth"));
loginPage.clickSocial(bc.getIDPAlias());
// we should see the login page of the provider, since the max_age was propagated
waitForPage(driver, "sign in to", true);
Assert.assertTrue("Driver should be on the provider realm page right now",
driver.getCurrentUrl().contains("/auth/realms/" + bc.providerRealmName() + "/"));
loginPage.assertCurrent(bc.providerRealmName());
// reauthenticate with password
loginPage.login(bc.getUserPassword());
waitForPage(driver, "account management", true);
testSingleLogout();
}
}

View file

@ -718,6 +718,8 @@ token-url=Token URL
token-url.tooltip=The Token URL. token-url.tooltip=The Token URL.
loginHint=Pass login_hint loginHint=Pass login_hint
loginHint.tooltip=Pass login_hint to identity provider. loginHint.tooltip=Pass login_hint to identity provider.
passMaxAge=Pass max_age
passMaxAge.tooltip=Pass max_age to identity provider.
uiLocales=Pass current locale uiLocales=Pass current locale
uiLocales.tooltip=Pass the current locale to the identity provider as a ui_locales parameter. uiLocales.tooltip=Pass the current locale to the identity provider as a ui_locales parameter.
logout-url=Logout URL logout-url=Logout URL

View file

@ -139,6 +139,13 @@
</div> </div>
<kc-tooltip>{{:: 'loginHint.tooltip' | translate}}</kc-tooltip> <kc-tooltip>{{:: 'loginHint.tooltip' | translate}}</kc-tooltip>
</div> </div>
<div class="form-group">
<label class="col-sm-2 control-label" for="passMaxAge">{{:: 'passMaxAge' | translate}}</label>
<div class="col-sm-4">
<input ng-model="identityProvider.config.passMaxAge" id="passMaxAge" onoffswitchvalue on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}" />
</div>
<kc-tooltip>{{:: 'passMaxAge.tooltip' | translate}}</kc-tooltip>
</div>
<div class="form-group"> <div class="form-group">
<label class="col-sm-2 control-label" for="uiLocales">{{:: 'uiLocales' | translate}}</label> <label class="col-sm-2 control-label" for="uiLocales">{{:: 'uiLocales' | translate}}</label>
<div class="col-sm-4"> <div class="col-sm-4">