Fix ID token not being sent after expiration for OIDC logout
Closes #10164
This commit is contained in:
parent
089492c746
commit
f578f91a0b
4 changed files with 159 additions and 57 deletions
|
@ -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) {
|
||||
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue