Remove redirect_uri support from OIDC logout endpoint

Closes #10983

Signed-off-by: Jon Koops <jonkoops@gmail.com>
Signed-off-by: Alexander Schwartz <alexander.schwartz@gmx.net>
Co-authored-by: Alexander Schwartz <alexander.schwartz@gmx.net>
This commit is contained in:
Jon Koops 2024-08-30 14:52:49 +02:00 committed by GitHub
parent e7d71d43c3
commit 2d17024b14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 41 additions and 436 deletions

View file

@ -224,3 +224,14 @@ Update your custom embedded Infinispan cache configuration file with configurati
For more details proceed to the https://www.keycloak.org/server/caching[Configuring distributed caches] guide.
= Support for legacy `redirect_uri` parameter and SPI options has been removed
Previous versions of {project_name} had supported automatic logout of the user and redirecting to the application by opening logout endpoint URL such as
`http(s)://example-host/auth/realms/my-realm-name/protocol/openid-connect/logout?redirect_uri=encodedRedirectUri`. This functionality was deprecated in {project_name} 18 and has been removed in this version in favor of following the OpenID Connect specification.
As part of this change the following related configuration options for the SPI have been removed:
- `--spi-login-protocol-openid-connect-legacy-logout-redirect-uri`
- `--spi-login-protocol-openid-connect-suppress-logout-confirmation-screen`
If you were still making use these options or the `redirect_uri` parameter for logout you should implement the link:https://openid.net/specs/openid-connect-rpinitiated-1_0.html[OpenID Connect RP-Initiated Logout specification] instead.

View file

@ -109,21 +109,9 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
public static final String ROLES_SCOPE_CONSENT_TEXT = "${rolesScopeConsentText}";
public static final String ORGANIZATION_SCOPE_CONSENT_TEXT = "${organizationScopeConsentText}";
public static final String CONFIG_LEGACY_LOGOUT_REDIRECT_URI = "legacy-logout-redirect-uri";
public static final String SUPPRESS_LOGOUT_CONFIRMATION_SCREEN = "suppress-logout-confirmation-screen";
private OIDCProviderConfig providerConfig;
@Override
public void init(Config.Scope config) {
initBuiltIns();
this.providerConfig = new OIDCProviderConfig(config);
if (providerConfig.isLegacyLogoutRedirectUri()) {
logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
}
if (providerConfig.suppressLogoutConfirmationScreen()) {
logger.warnf("Deprecated switch '%s' is enabled. Please try to disable it and update your clients to use OpenID Connect compliant way for RP-initiated logout.", SUPPRESS_LOGOUT_CONFIRMATION_SCREEN);
}
}
@Override
@ -444,7 +432,7 @@ public class OIDCLoginProtocolFactory extends AbstractLoginProtocolFactory {
@Override
public Object createProtocolEndpoint(KeycloakSession session, EventBuilder event) {
return new OIDCLoginProtocolService(session, event, providerConfig);
return new OIDCLoginProtocolService(session, event);
}
@Override

View file

@ -64,7 +64,6 @@ public class OIDCLoginProtocolService {
private final RealmModel realm;
private final TokenManager tokenManager;
private final EventBuilder event;
private final OIDCProviderConfig providerConfig;
private final KeycloakSession session;
@ -74,13 +73,12 @@ public class OIDCLoginProtocolService {
private final ClientConnection clientConnection;
public OIDCLoginProtocolService(KeycloakSession session, EventBuilder event, OIDCProviderConfig providerConfig) {
public OIDCLoginProtocolService(KeycloakSession session, EventBuilder event) {
this.session = session;
this.clientConnection = session.getContext().getConnection();
this.realm = session.getContext().getRealm();
this.tokenManager = new TokenManager();
this.event = event;
this.providerConfig = providerConfig;
this.request = session.getContext().getHttpRequest();
this.headers = session.getContext().getRequestHeaders();
}
@ -212,11 +210,9 @@ public class OIDCLoginProtocolService {
return new UserInfoEndpoint(session, tokenManager);
}
/* old deprecated logout endpoint needs to be removed in the future
* https://issues.redhat.com/browse/KEYCLOAK-2940 */
@Path("logout")
public Object logout() {
return new LogoutEndpoint(session, tokenManager, event, providerConfig);
return new LogoutEndpoint(session, tokenManager, event);
}
@Path("revoke")

View file

@ -1,43 +0,0 @@
/*
* 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.protocol.oidc;
import org.keycloak.Config;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class OIDCProviderConfig {
private final boolean legacyLogoutRedirectUri;
private final boolean suppressLogoutConfirmationScreen;
public OIDCProviderConfig(Config.Scope config) {
this.legacyLogoutRedirectUri = config.getBoolean(OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, false);
this.suppressLogoutConfirmationScreen = config.getBoolean(OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, false);
}
public boolean isLegacyLogoutRedirectUri() {
return legacyLogoutRedirectUri;
}
public boolean suppressLogoutConfirmationScreen() {
return suppressLogoutConfirmationScreen;
}
}

View file

@ -48,8 +48,6 @@ import org.keycloak.protocol.oidc.BackchannelLogoutResponse;
import org.keycloak.protocol.oidc.LogoutTokenValidationCode;
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.protocol.oidc.OIDCProviderConfig;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.protocol.oidc.utils.LogoutUtil;
@ -112,17 +110,15 @@ public class LogoutEndpoint {
private final TokenManager tokenManager;
private final RealmModel realm;
private final EventBuilder event;
private final OIDCProviderConfig providerConfig;
private Cors cors;
public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event, OIDCProviderConfig providerConfig) {
public LogoutEndpoint(KeycloakSession session, TokenManager tokenManager, EventBuilder event) {
this.session = session;
this.clientConnection = session.getContext().getConnection();
this.tokenManager = tokenManager;
this.realm = session.getContext().getRealm();
this.event = event;
this.providerConfig = providerConfig;
this.request = session.getContext().getHttpRequest();
this.headers = session.getContext().getRequestHeaders();
}
@ -143,7 +139,6 @@ public class LogoutEndpoint {
*
* All parameters are optional. Some combinations of parameters are invalid as described in the specification
*
* @param deprecatedRedirectUri Parameter "redirect_uri" is not supported by the specification. It is here just for the backwards compatibility
* @param encodedIdToken Parameter "id_token_hint" as described in the specification.
* @param clientId Parameter "client_id" as described in the specification.
* @param postLogoutRedirectUri Parameter "post_logout_redirect_uri" as described in the specification with the URL to redirect after logout.
@ -154,39 +149,23 @@ public class LogoutEndpoint {
*/
@GET
@NoCache
public Response logout(@QueryParam(OIDCLoginProtocol.REDIRECT_URI_PARAM) String deprecatedRedirectUri, // deprecated
@QueryParam(OIDCLoginProtocol.ID_TOKEN_HINT) String encodedIdToken,
public Response logout(@QueryParam(OIDCLoginProtocol.ID_TOKEN_HINT) String encodedIdToken,
@QueryParam(OIDCLoginProtocol.CLIENT_ID_PARAM) String clientId,
@QueryParam(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM) String postLogoutRedirectUri,
@QueryParam(OIDCLoginProtocol.STATE_PARAM) String state,
@QueryParam(OIDCLoginProtocol.UI_LOCALES_PARAM) String uiLocales,
@QueryParam(AuthenticationManager.INITIATING_IDP_PARAM) String initiatingIdp) {
if (!providerConfig.isLegacyLogoutRedirectUri()) {
if (deprecatedRedirectUri != null) {
event.event(EventType.LOGOUT);
String errorMessage = "Parameter 'redirect_uri' no longer supported.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
logger.warnf("%s Please use 'post_logout_redirect_uri' with 'id_token_hint' for this endpoint. Alternatively you can enable backwards compatibility option '%s' of oidc login protocol in the server configuration.",
errorMessage, OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_PARAMETER, OIDCLoginProtocol.REDIRECT_URI_PARAM);
}
if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) {
event.event(EventType.LOGOUT);
String errorMessage = "Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
logger.warnf(errorMessage);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER,
OIDCLoginProtocol.ID_TOKEN_HINT);
}
if (postLogoutRedirectUri != null && encodedIdToken == null && clientId == null) {
event.event(EventType.LOGOUT);
String errorMessage = "Either the parameter 'client_id' or the parameter 'id_token_hint' is required when 'post_logout_redirect_uri' is used.";
event.detail(Details.REASON, errorMessage);
event.error(Errors.INVALID_REQUEST);
logger.warnf(errorMessage);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.MISSING_PARAMETER,
OIDCLoginProtocol.ID_TOKEN_HINT);
}
deprecatedRedirectUri = providerConfig.isLegacyLogoutRedirectUri() ? deprecatedRedirectUri : null;
final String redirectUri = postLogoutRedirectUri != null ? postLogoutRedirectUri : deprecatedRedirectUri;
boolean confirmationNeeded = true;
boolean forcedConfirmation = false;
ClientModel client = clientId == null ? null : realm.getClientByClientId(clientId);
@ -236,21 +215,16 @@ public class LogoutEndpoint {
}
String validatedRedirectUri = null;
if (redirectUri != null) {
if (postLogoutRedirectUri != null) {
if (client != null) {
OIDCAdvancedConfigWrapper wrapper = OIDCAdvancedConfigWrapper.fromClientModel(client);
Set<String> postLogoutRedirectUris = wrapper.getPostLogoutRedirectUris() != null ? new HashSet(wrapper.getPostLogoutRedirectUris()) : new HashSet<>();
validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), redirectUri, postLogoutRedirectUris, true);
} else if (clientId == null && providerConfig.isLegacyLogoutRedirectUri()) {
/*
* Only call verifyRealmRedirectUri against all in the realm, in case when "Legacy" switch is enabled and when we don't have a client - usually due both clientId and client are null
*/
validatedRedirectUri = RedirectUtils.verifyRealmRedirectUri(session, redirectUri);
validatedRedirectUri = RedirectUtils.verifyRedirectUri(session, client.getRootUrl(), postLogoutRedirectUri, postLogoutRedirectUris, true);
}
if (validatedRedirectUri == null) {
event.event(EventType.LOGOUT);
event.detail(Details.REDIRECT_URI, redirectUri);
event.detail(Details.REDIRECT_URI, postLogoutRedirectUri);
event.error(Errors.INVALID_REDIRECT_URI);
return ErrorPage.error(session, null, Response.Status.BAD_REQUEST, Messages.INVALID_REDIRECT_URI);
}
@ -307,7 +281,7 @@ public class LogoutEndpoint {
}
// Logout confirmation screen will be displayed to the user in this case
if ((confirmationNeeded || forcedConfirmation) && !providerConfig.suppressLogoutConfirmationScreen()) {
if (confirmationNeeded || forcedConfirmation) {
return displayLogoutConfirmationScreen(loginForm, logoutSession);
} else {
return doBrowserLogout(logoutSession);
@ -338,13 +312,14 @@ public class LogoutEndpoint {
if (form.containsKey(OAuth2Constants.REFRESH_TOKEN)) {
return logoutToken();
} else {
return logout(form.getFirst(OIDCLoginProtocol.REDIRECT_URI_PARAM),
return logout(
form.getFirst(OIDCLoginProtocol.ID_TOKEN_HINT),
form.getFirst(OIDCLoginProtocol.CLIENT_ID_PARAM),
form.getFirst(OIDCLoginProtocol.POST_LOGOUT_REDIRECT_URI_PARAM),
form.getFirst(OIDCLoginProtocol.STATE_PARAM),
form.getFirst(OIDCLoginProtocol.UI_LOCALES_PARAM),
form.getFirst(AuthenticationManager.INITIATING_IDP_PARAM));
form.getFirst(AuthenticationManager.INITIATING_IDP_PARAM)
);
}
}

View file

@ -24,7 +24,6 @@ import org.keycloak.models.Constants;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakUriInfo;
import org.keycloak.models.RealmModel;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.services.Urls;
import org.keycloak.services.util.ResolveRelative;
@ -33,7 +32,6 @@ import java.util.Collection;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@ -42,17 +40,6 @@ public class RedirectUtils {
private static final Logger logger = Logger.getLogger(RedirectUtils.class);
/**
* This method is deprecated for performance and security reasons and it is available just for the
* backwards compatibility. It is recommended to use some other methods of this class where the client is given as an argument
* to the method, so we know the client, which redirect-uri we are trying to resolve.
*/
@Deprecated
public static String verifyRealmRedirectUri(KeycloakSession session, String redirectUri) {
Set<String> validRedirects = getValidateRedirectUris(session);
return verifyRedirectUri(session, null, redirectUri, validRedirects, true);
}
public static String verifyRedirectUri(KeycloakSession session, String redirectUri, ClientModel client) {
return verifyRedirectUri(session, redirectUri, client, true);
}
@ -77,16 +64,6 @@ public class RedirectUtils {
return resolveValidRedirects;
}
@Deprecated
private static Set<String> getValidateRedirectUris(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
return session.clients().getAllRedirectUrisOfEnabledClients(realm).entrySet().stream()
.filter(me -> me.getKey().isEnabled() && OIDCLoginProtocol.LOGIN_PROTOCOL.equals(me.getKey().getProtocol()) && !me.getKey().isBearerOnly() && (me.getKey().isStandardFlowEnabled() || me.getKey().isImplicitFlowEnabled()))
.map(me -> resolveValidRedirects(session, me.getKey().getRootUrl(), me.getValue()))
.flatMap(Collection::stream)
.collect(Collectors.toSet());
}
public static String verifyRedirectUri(KeycloakSession session, String rootUrl, String redirectUri, Set<String> validRedirects, boolean requireRedirectUri) {
KeycloakUriInfo uriInfo = session.getContext().getUri();
RealmModel realm = session.getContext().getRealm();

View file

@ -232,14 +232,6 @@ public class OAuthClient {
return this;
}
@Deprecated // Use only in backwards compatibility tests
public LogoutUrlBuilder redirectUri(String redirectUri) {
if (redirectUri != null) {
b.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri);
}
return this;
}
public LogoutUrlBuilder state(String state) {
if (state != null) {
b.queryParam(OIDCLoginProtocol.STATE_PARAM, state);

View file

@ -1,5 +1,6 @@
package org.keycloak.testsuite.broker;
import org.keycloak.OAuth2Constants;
import org.keycloak.broker.saml.SAMLIdentityProviderConfig;
import org.keycloak.crypto.Algorithm;
import org.keycloak.dom.saml.v2.protocol.AuthnRequestType;
@ -21,6 +22,7 @@ import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.updaters.IdentityProviderAttributeUpdater;
import org.keycloak.testsuite.updaters.RealmAttributeUpdater;
import org.keycloak.testsuite.util.KeyUtils;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.SamlClient;
import org.keycloak.testsuite.util.SamlClient.Binding;
import org.keycloak.testsuite.util.SamlClientBuilder;
@ -148,8 +150,14 @@ public class KcSamlSignedBrokerTest extends AbstractBrokerTest {
loginUser();
// Logout should fail because logout response is not signed.
final String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
final OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
final String idTokenString = tokenResponse.getIdToken();
final String redirectUri = getAccountUrl(getProviderRoot(), bc.providerRealmName());
final String logoutUri = oauth.realm(bc.providerRealmName()).getLogoutUrl().redirectUri(redirectUri).build();
final String logoutUri = oauth.realm(bc.providerRealmName()).getLogoutUrl()
.idTokenHint(idTokenString)
.postLogoutRedirectUri(redirectUri).build();
driver.navigate().to(logoutUri);
errorPage.assertCurrent();

View file

@ -1,273 +0,0 @@
/*
* 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.oauth;
import java.io.Closeable;
import java.util.Collections;
import jakarta.ws.rs.NotFoundException;
import org.hamcrest.MatcherAssert;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.ClientResource;
import org.keycloak.admin.client.resource.ClientsResource;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.protocol.LoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.protocol.oidc.OIDCLoginProtocolFactory;
import org.keycloak.representations.idm.ClientRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
import org.keycloak.testsuite.Assert;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.admin.ApiUtil;
import org.keycloak.testsuite.pages.AppPage;
import org.keycloak.testsuite.pages.ErrorPage;
import org.keycloak.testsuite.pages.InfoPage;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.pages.LogoutConfirmPage;
import org.keycloak.testsuite.pages.OAuthGrantPage;
import org.keycloak.testsuite.updaters.ClientAttributeUpdater;
import org.keycloak.testsuite.util.InfinispanTestTimeServiceRule;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.ServerURLs;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlDoesntStartWith;
import static org.keycloak.testsuite.util.URLAssert.assertCurrentUrlEquals;
/**
* Test logout endpoint with deprecated "redirect_uri" parameter
*
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class LegacyLogoutTest extends AbstractTestRealmKeycloakTest {
@Rule
public AssertEvents events = new AssertEvents(this);
@Rule
public InfinispanTestTimeServiceRule ispnTestTimeService = new InfinispanTestTimeServiceRule(this);
@Page
protected AppPage appPage;
@Page
protected LoginPage loginPage;
@Page
protected OAuthGrantPage grantPage;
@Page
protected LogoutConfirmPage logoutConfirmPage;
@Page
protected InfoPage infoPage;
@Page
private ErrorPage errorPage;
private String APP_REDIRECT_URI;
@Override
public void configureTestRealm(RealmRepresentation testRealm) {
}
@Before
public void configLegacyRedirectUriEnabled() {
getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "true");
getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc.");
APP_REDIRECT_URI = oauth.APP_AUTH_ROOT;
}
@After
public void revertConfiguration() {
getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.CONFIG_LEGACY_LOGOUT_REDIRECT_URI, "false");
getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, "false");
getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc.");
}
// Test logout with deprecated "redirect_uri" and with "id_token_hint" . Should od automatic redirect
@Test
public void logoutWithLegacyRedirectUriAndIdTokenHint() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String idTokenString = tokenResponse.getIdToken();
String sessionId = tokenResponse.getSessionState();
String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).idTokenHint(idTokenString).build();
driver.navigate().to(logoutUrl);
events.expectLogout(sessionId).detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent();
assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
// Test logout with deprecated "redirect_uri" and without "id_token_hint" . User should confirm logout
@Test
public void logoutWithLegacyRedirectUriAndWithoutIdTokenHint() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
// Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
logoutConfirmPage.assertCurrent();
assertThat(true, is(isSessionActive(sessionId)));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
// Redirected back to the application with expected state
events.expectLogout(sessionId).client("account").removeDetail(Details.REDIRECT_URI).assertEvent();
assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
// Test with "post_logout_redirect_uri" without "id_token_hint": User should confirm logout.
@Test
public void logoutWithPostLogoutUriWithoutIdTokenHint() {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
// Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
logoutConfirmPage.assertCurrent();
assertThat(true, is(isSessionActive(sessionId)));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
// Redirected back to the application with expected state
events.expectLogout(sessionId).client("account").removeDetail(Details.REDIRECT_URI).assertEvent();
assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
// KEYCLOAK-16517 Make sure that just real clients with standardFlow or implicitFlow enabled are considered for redirectUri
@Test
public void logoutRedirectWithStarRedirectUriForDirectGrantClient() {
// Set "*" as redirectUri for some directGrant client
ClientResource clientRes = ApiUtil.findClientByClientId(testRealm(), "direct-grant");
ClientRepresentation clientRepOrig = clientRes.toRepresentation();
ClientRepresentation clientRep = clientRes.toRepresentation();
clientRep.setStandardFlowEnabled(false);
clientRep.setImplicitFlowEnabled(false);
clientRep.setRedirectUris(Collections.singletonList("*"));
clientRes.update(clientRep);
try {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String invalidRedirectUri = ServerURLs.getAuthServerContextRoot() + "/bar";
String idTokenString = tokenResponse.getIdToken();
String logoutUrl = oauth.getLogoutUrl().redirectUri(invalidRedirectUri).build();
driver.navigate().to(logoutUrl);
events.expectLogoutError(Errors.INVALID_REDIRECT_URI).assertEvent();
assertCurrentUrlDoesntStartWith(invalidRedirectUri);
errorPage.assertCurrent();
Assert.assertEquals("Invalid redirect uri", errorPage.getError());
// Session still active
assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
} finally {
// Revert
clientRes.update(clientRepOrig);
}
}
// Test logout with deprecated "redirect_uri" and without "id_token_hint" and client disabled after login
@Test
public void logoutWithLegacyRedirectUriAndWithoutIdTokenHintClientDisabled() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
try (Closeable testAppClient = ClientAttributeUpdater.forClient(adminClient, "test", oauth.getClientId())
.setEnabled(false).update()) {
ClientsResource clients = adminClient.realm(oauth.getRealm()).clients();
ClientRepresentation rep = clients.findByClientId(oauth.getClientId()).get(0);
MatcherAssert.assertThat(false, is(rep.isEnabled()));
String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
// Assert logout confirmation page. Session still exists. Assert default language on logout page (English)
logoutConfirmPage.assertCurrent();
MatcherAssert.assertThat(true, is(isSessionActive(sessionId)));
events.assertEmpty();
logoutConfirmPage.confirmLogout();
// Redirected back to the application with expected state
events.expectLogout(sessionId).client("account").removeDetail(Details.REDIRECT_URI).assertEvent();
MatcherAssert.assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
}
// Test with "post_logout_redirect_uri" without "id_token_hint" and "suppress-logout-confirmation-screen": User should logout non interactive.
@Test
public void logoutWithPostLogoutUriWithoutIdTokenHintAndSuppressedConfirmation() {
getTestingClient().testing().setSystemPropertyOnServer("oidc." + OIDCLoginProtocolFactory.SUPPRESS_LOGOUT_CONFIRMATION_SCREEN, "true");
getTestingClient().testing().reinitializeProviderFactoryWithSystemPropertiesScope(LoginProtocol.class.getName(), OIDCLoginProtocol.LOGIN_PROTOCOL, "oidc.");
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String sessionId = tokenResponse.getSessionState();
String logoutUrl = oauth.getLogoutUrl().postLogoutRedirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
events.expectLogout(sessionId).client("account").detail(Details.REDIRECT_URI, APP_REDIRECT_URI).assertEvent();
assertThat(false, is(isSessionActive(sessionId)));
assertCurrentUrlEquals(APP_REDIRECT_URI);
}
private OAuthClient.AccessTokenResponse loginUser() {
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
events.clear();
return tokenResponse;
}
private boolean isSessionActive(String sessionId) {
try {
testingClient.testing().getClientSessionsCountInUserSession("test", sessionId);
return true;
} catch (NotFoundException nfe) {
return false;
}
}
}

View file

@ -403,29 +403,6 @@ public class RPInitiatedLogoutTest extends AbstractTestRealmKeycloakTest {
}
// Parameter "redirect_uri" is not valid in logoutRequest (See LegacyLogoutTest for the scenario with "redirect_uri" allowed by backwards compatibility switch)
@Test
public void logoutWithRedirectUriParameterShouldFail() throws Exception {
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
String idTokenString = tokenResponse.getIdToken();
// Logout with "redirect_uri" parameter alone should fail
String logoutUrl = oauth.getLogoutUrl().redirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
errorPage.assertCurrent();
events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent();
// Logout with "redirect_uri" parameter and with "id_token_hint" should fail
oauth.getLogoutUrl().idTokenHint(idTokenString).redirectUri(APP_REDIRECT_URI).build();
driver.navigate().to(logoutUrl);
errorPage.assertCurrent();
events.expectLogoutError(OAuthErrorException.INVALID_REQUEST).assertEvent();
// Assert user still authenticated
MatcherAssert.assertThat(true, is(isSessionActive(tokenResponse.getSessionState())));
}
// Test with "post_logout_redirect_uri" without "id_token_hint" should fail
@Test
public void logoutWithPostLogoutUriWithoutIdTokenHintShouldFail() throws Exception {

View file

@ -148,9 +148,6 @@
},
"login-protocol": {
"openid-connect": {
"legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}"
},
"saml": {
"knownProtocols": [
"http=${auth.server.http.port}",