Fix ID token not being sent after expiration for OIDC logout

Closes #10164
This commit is contained in:
Ayrat Hudaygulov 2023-01-24 11:01:41 +00:00 committed by Marek Posolda
parent 089492c746
commit f578f91a0b
4 changed files with 159 additions and 57 deletions

View file

@ -127,7 +127,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
public void backchannelLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("") || !getConfig().isBackchannelSupported())
return;
String idToken = getIDTokenForLogout(session, userSession);
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
if (idToken == null) return;
backchannelLogout(userSession, idToken);
}
@ -165,7 +165,7 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
@Override
public Response keycloakInitiatedBrowserLogout(KeycloakSession session, UserSessionModel userSession, UriInfo uriInfo, RealmModel realm) {
if (getConfig().getLogoutUrl() == null || getConfig().getLogoutUrl().trim().equals("")) return null;
String idToken = getIDTokenForLogout(session, userSession);
String idToken = userSession.getNote(FEDERATED_ID_TOKEN);
if (idToken != null && getConfig().isBackchannelSupported()) {
backchannelLogout(userSession, idToken);
return null;
@ -249,25 +249,6 @@ public class OIDCIdentityProvider extends AbstractOAuth2IdentityProvider<OIDCIde
}
}
private String getIDTokenForLogout(KeycloakSession session, UserSessionModel userSession) {
String tokenExpirationString = userSession.getNote(FEDERATED_TOKEN_EXPIRATION);
long exp = tokenExpirationString == null ? 0 : Long.parseLong(tokenExpirationString);
int currentTime = Time.currentTime();
if (exp > 0 && currentTime > exp) {
String response = refreshTokenForLogout(session, userSession);
AccessTokenResponse tokenResponse = null;
try {
tokenResponse = JsonSerialization.readValue(response, AccessTokenResponse.class);
} catch (IOException e) {
throw new RuntimeException(e);
}
return tokenResponse.getIdToken();
} else {
return userSession.getNote(FEDERATED_ID_TOKEN);
}
}
protected void processAccessTokenResponse(BrokeredIdentityContext context, AccessTokenResponse response) {

View file

@ -0,0 +1,41 @@
package org.keycloak.testsuite.broker;
import org.junit.Before;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.UserRepresentation;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
public abstract class AbstractKcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
@Before
public void createUser() {
log.debug("creating user for realm " + bc.providerRealmName());
final UserRepresentation user = new UserRepresentation();
user.setUsername(bc.getUserLogin());
user.setEmail(bc.getUserEmail());
user.setEmailVerified(true);
user.setEnabled(true);
final RealmResource realmResource = adminClient.realm(bc.providerRealmName());
final String userId = createUserWithAdminClient(realmResource, user);
resetUserPassword(realmResource.users().get(userId), bc.getUserPassword(), false);
}
@Before
public void addIdentityProviderToProviderRealm() {
log.debug("adding identity provider to realm " + bc.consumerRealmName());
final RealmResource realm = adminClient.realm(bc.consumerRealmName());
realm.identityProviders().create(bc.setUpIdentityProvider()).close();
}
@Before
public void addClients() {
addClientsToProviderAndConsumer();
}
}

View file

@ -0,0 +1,76 @@
package org.keycloak.testsuite.broker;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.models.IdentityProviderSyncMode;
import org.keycloak.representations.IDToken;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.OAuthClient;
import java.util.Map;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
public class KcOidcBrokerLogoutFrontChannelTest extends AbstractKcOidcBrokerLogoutTest {
@Rule public AssertEvents events = new AssertEvents(this);
@Override
protected BrokerConfiguration getBrokerConfiguration() {
return new KcOidcBrokerConfigurationIdpLogoutFrontChannel();
}
private static class KcOidcBrokerConfigurationIdpLogoutFrontChannel
extends KcOidcBrokerConfiguration {
@Override
protected void applyDefaultConfiguration(
Map<String, String> config, IdentityProviderSyncMode syncMode) {
super.applyDefaultConfiguration(config, syncMode);
config.put("backchannelSupported", "false");
}
}
@Test
public void logoutAfterIdpTokenExpired() throws VerificationException {
driver.navigate().to(getLoginUrl(getConsumerRoot(), bc.consumerRealmName(), "broker-app"));
logInWithBroker(bc);
updateAccountInformation();
// Exchange code from "broker-app" client of "consumer" realm for the tokens
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response =
oauth
.realm(bc.consumerRealmName())
.clientId("broker-app")
.redirectUri(getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app")
.doAccessTokenRequest(code, "broker-app-secret");
assertEquals(200, response.getStatusCode());
String idTokenString = response.getIdToken();
IDToken idToken = TokenVerifier.create(idTokenString, IDToken.class).getToken();
int expiresInMs = (int) (idToken.getExp() - idToken.getIat());
// simulate token expiration
setTimeOffset(expiresInMs * 2);
logoutFromRealm(
getConsumerRoot(),
bc.consumerRealmName(),
"something-else",
idTokenString,
"broker-app",
getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app");
// user should be logged out successfully from the IDP even though the id_token_hint is expired
driver.navigate().to(getAccountUrl(getProviderRoot(), REALM_PROV_NAME));
waitForPage(driver, "sign in to provider", true);
}
}

View file

@ -1,26 +1,24 @@
package org.keycloak.testsuite.broker;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.TokenVerifier;
import org.keycloak.common.VerificationException;
import org.keycloak.representations.IDToken;
import org.keycloak.services.managers.AuthenticationManager;
import org.keycloak.services.util.CookieHelper;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.util.OAuthClient;
import static org.junit.Assert.assertEquals;
import static org.keycloak.testsuite.admin.ApiUtil.createUserWithAdminClient;
import static org.keycloak.testsuite.admin.ApiUtil.resetUserPassword;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_CONS_NAME;
import static org.keycloak.testsuite.broker.BrokerTestConstants.REALM_PROV_NAME;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
import static org.keycloak.testsuite.broker.BrokerTestTools.getConsumerRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.getProviderRoot;
import static org.keycloak.testsuite.broker.BrokerTestTools.waitForPage;
public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
public class KcOidcBrokerLogoutTest extends AbstractKcOidcBrokerLogoutTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@ -30,35 +28,6 @@ public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
return KcOidcBrokerConfiguration.INSTANCE;
}
@Before
public void createUser() {
log.debug("creating user for realm " + bc.providerRealmName());
final UserRepresentation user = new UserRepresentation();
user.setUsername(bc.getUserLogin());
user.setEmail(bc.getUserEmail());
user.setEmailVerified(true);
user.setEnabled(true);
final RealmResource realmResource = adminClient.realm(bc.providerRealmName());
final String userId = createUserWithAdminClient(realmResource, user);
resetUserPassword(realmResource.users().get(userId), bc.getUserPassword(), false);
}
@Before
public void addIdentityProviderToProviderRealm() {
log.debug("adding identity provider to realm " + bc.consumerRealmName());
final RealmResource realm = adminClient.realm(bc.consumerRealmName());
realm.identityProviders().create(bc.setUpIdentityProvider()).close();
}
@Before
public void addClients() {
addClientsToProviderAndConsumer();
}
@Test
public void logoutWithoutInitiatingIdpLogsOutOfIdp() {
logInAsUserInIDPForFirstTime();
@ -117,4 +86,39 @@ public class KcOidcBrokerLogoutTest extends AbstractBaseBrokerTest {
waitForPage(driver, "sign in to provider", true);
}
@Test
public void logoutAfterIdpTokenExpired() throws VerificationException {
driver.navigate().to(getLoginUrl(getConsumerRoot(), bc.consumerRealmName(), "broker-app"));
logInWithBroker(bc);
updateAccountInformation();
// Exchange code from "broker-app" client of "consumer" realm for the tokens
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.realm(bc.consumerRealmName())
.clientId("broker-app")
.redirectUri(getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app")
.doAccessTokenRequest(code, "broker-app-secret");
assertEquals(200, response.getStatusCode());
String idTokenString = response.getIdToken();
IDToken idToken = TokenVerifier.create(idTokenString, IDToken.class).getToken();
int expiresInMs = (int) (idToken.getExp() - idToken.getIat());
// simulate token expiration
setTimeOffset(expiresInMs * 2);
logoutFromRealm(
getConsumerRoot(),
bc.consumerRealmName(),
"something-else",
idTokenString,
"broker-app",
getConsumerRoot() + "/auth/realms/" + REALM_CONS_NAME + "/app"
);
// user should be logged out successfully from the IDP even though the id_token_hint is expired
driver.navigate().to(getAccountUrl(getProviderRoot(), REALM_PROV_NAME));
waitForPage(driver, "sign in to provider", true);
}
}