parent
a0079b516b
commit
3518362002
6 changed files with 164 additions and 10 deletions
|
@ -331,11 +331,6 @@ public abstract class AbstractOAuth2IdentityProvider<C extends OAuth2IdentityPro
|
|||
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()) {
|
||||
uriBuilder.queryParam(OIDCLoginProtocol.UI_LOCALES_PARAM, session.getContext().resolveLocale(null).toLanguageTag());
|
||||
}
|
||||
|
|
|
@ -387,6 +387,20 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
|
||||
JsonWebToken idToken = validateToken(encodedIdToken);
|
||||
|
||||
if (getConfig().isPassMaxAge()) {
|
||||
AuthenticationSessionModel authSession = session.getContext().getAuthenticationSession();
|
||||
|
||||
if (isAuthTimeExpired(idToken, authSession)) {
|
||||
throw new IdentityBrokerException("User not re-authenticated by the target OpenID Provider");
|
||||
}
|
||||
|
||||
Object authTime = idToken.getOtherClaims().get(IDToken.AUTH_TIME);
|
||||
|
||||
if (authTime != null) {
|
||||
authSession.setClientNote(AuthenticationManager.AUTH_TIME_BROKER, authTime.toString());
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
BrokeredIdentityContext identity = extractIdentity(tokenResponse, accessToken, idToken);
|
||||
|
||||
|
@ -411,6 +425,25 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
}
|
||||
}
|
||||
|
||||
protected boolean isAuthTimeExpired(JsonWebToken idToken, AuthenticationSessionModel authSession) {
|
||||
String maxAge = authSession.getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
|
||||
if (maxAge == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String authTime = idToken.getOtherClaims().getOrDefault(IDToken.AUTH_TIME, "0").toString();
|
||||
int authTimeInt = authTime == null ? 0 : Integer.parseInt(authTime);
|
||||
int maxAgeInt = Integer.parseInt(maxAge);
|
||||
|
||||
if (authTimeInt + maxAgeInt < Time.currentTime()) {
|
||||
logger.debugf("Invalid auth_time claim. User not re-authenticated by the target OP.");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static final MediaType APPLICATION_JWT_TYPE = MediaType.valueOf("application/jwt");
|
||||
|
||||
protected BrokeredIdentityContext extractIdentity(AccessTokenResponse tokenResponse, String accessToken, JsonWebToken idToken) throws IOException {
|
||||
|
@ -814,6 +847,12 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
|
|||
authenticationSession.setClientNote(BROKER_NONCE_PARAM, nonce);
|
||||
uriBuilder.queryParam(OIDCLoginProtocol.NONCE_PARAM, nonce);
|
||||
|
||||
String maxAge = request.getAuthenticationSession().getClientNote(OIDCLoginProtocol.MAX_AGE_PARAM);
|
||||
|
||||
if (getConfig().isPassMaxAge() && maxAge != null) {
|
||||
uriBuilder.queryParam(OIDCLoginProtocol.MAX_AGE_PARAM, maxAge);
|
||||
}
|
||||
|
||||
return uriBuilder;
|
||||
}
|
||||
|
||||
|
|
|
@ -138,6 +138,10 @@ public class AuthenticationManager {
|
|||
|
||||
// userSession note with authTime (time when authentication flow including requiredActions was finished)
|
||||
public static final String AUTH_TIME = "AUTH_TIME";
|
||||
|
||||
// authSession client note set during brokering indicating the time when the authentication happened at the IdP
|
||||
public static final String AUTH_TIME_BROKER = "AUTH_TIME_BROKER";
|
||||
|
||||
// clientSession note with flag that clientSession was authenticated through SSO cookie
|
||||
public static final String SSO_AUTH = "SSO_AUTH";
|
||||
|
||||
|
@ -956,7 +960,7 @@ public class AuthenticationManager {
|
|||
clientSession.setNote(SSO_AUTH, "true");
|
||||
authSession.removeAuthNote(SSO_AUTH);
|
||||
} else {
|
||||
int authTime = Time.currentTime();
|
||||
int authTime = Optional.ofNullable(authSession.getClientNote(AUTH_TIME_BROKER)).map(Integer::parseInt).orElse(Time.currentTime());
|
||||
userSession.setNote(AUTH_TIME, String.valueOf(authTime));
|
||||
clientSession.removeNote(SSO_AUTH);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* Copyright 2022 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.oidc;
|
||||
|
||||
import javax.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.models.IdentityProviderModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||
|
||||
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 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)) {
|
||||
@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()));
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
|
@ -15,4 +15,5 @@
|
|||
# limitations under the License.
|
||||
#
|
||||
|
||||
org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory
|
||||
org.keycloak.testsuite.broker.oidc.LegacyIdIdentityProviderFactory
|
||||
org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory
|
|
@ -2,15 +2,17 @@ package org.keycloak.testsuite.broker;
|
|||
|
||||
import org.junit.Ignore;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.OAuth2Constants;
|
||||
import org.keycloak.admin.client.resource.IdentityProviderResource;
|
||||
import org.keycloak.models.IdentityProviderModel;
|
||||
import org.keycloak.models.IdentityProviderSyncMode;
|
||||
import org.keycloak.representations.idm.IdentityProviderRepresentation;
|
||||
import org.keycloak.testsuite.Assert;
|
||||
import org.keycloak.testsuite.broker.oidc.TestKeycloakOidcIdentityProviderFactory;
|
||||
|
||||
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;
|
||||
|
@ -31,13 +33,13 @@ public class KcOidcBrokerPassMaxAgeTest extends AbstractBrokerTest {
|
|||
|
||||
@Override
|
||||
public IdentityProviderRepresentation setUpIdentityProvider(IdentityProviderSyncMode syncMode) {
|
||||
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, IDP_OIDC_PROVIDER_ID);
|
||||
IdentityProviderRepresentation idp = createIdentityProvider(IDP_OIDC_ALIAS, TestKeycloakOidcIdentityProviderFactory.ID);
|
||||
|
||||
Map<String, String> config = idp.getConfig();
|
||||
applyDefaultConfiguration(config, syncMode);
|
||||
config.put(IdentityProviderModel.LOGIN_HINT, "false");
|
||||
|
||||
config.put(IdentityProviderModel.PASS_MAX_AGE, "true");
|
||||
config.remove(OAuth2Constants.PROMPT);
|
||||
|
||||
return idp;
|
||||
}
|
||||
|
@ -92,4 +94,49 @@ public class KcOidcBrokerPassMaxAgeTest extends AbstractBrokerTest {
|
|||
|
||||
testSingleLogout();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEnforceReAuthenticationWhenMaxAgeIsSet() {
|
||||
// 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();
|
||||
|
||||
IdentityProviderResource idpResource = realmsResouce().realm(bc.consumerRealmName()).identityProviders()
|
||||
.get(bc.getIDPAlias());
|
||||
IdentityProviderRepresentation idpRep = idpResource.toRepresentation();
|
||||
|
||||
TestKeycloakOidcIdentityProviderFactory.setIgnoreMaxAgeParam(idpRep);
|
||||
|
||||
idpResource.update(idpRep);
|
||||
|
||||
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);
|
||||
loginPage.getError();
|
||||
Assert.assertEquals("Unexpected error when authenticating with identity provider",
|
||||
loginPage.getInstruction());
|
||||
|
||||
testSingleLogout();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue