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:
parent
e7d71d43c3
commit
2d17024b14
11 changed files with 41 additions and 436 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -148,9 +148,6 @@
|
|||
},
|
||||
|
||||
"login-protocol": {
|
||||
"openid-connect": {
|
||||
"legacy-logout-redirect-uri": "${keycloak.oidc.legacyLogoutRedirectUri:false}"
|
||||
},
|
||||
"saml": {
|
||||
"knownProtocols": [
|
||||
"http=${auth.server.http.port}",
|
||||
|
|
Loading…
Reference in a new issue