From 968ebb194bed1c58422cbcd49716aaafb84c9631 Mon Sep 17 00:00:00 2001 From: Josh Cummings Date: Wed, 5 Feb 2020 12:14:36 -0700 Subject: [PATCH] baseUrl placeholder for OidcLogoutSuccessHandlers Fixes gh-7842 --- ...dcClientInitiatedLogoutSuccessHandler.java | 77 +++++++++++++---- ...ntInitiatedServerLogoutSuccessHandler.java | 83 +++++++++++++++---- ...entInitiatedLogoutSuccessHandlerTests.java | 30 ++++++- ...tiatedServerLogoutSuccessHandlerTests.java | 34 +++++++- 4 files changed, 187 insertions(+), 37 deletions(-) diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java index 0ea7425a81..54ff6b7737 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ package org.springframework.security.oauth2.client.oidc.web.logout; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Collections; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -27,7 +28,9 @@ import org.springframework.security.oauth2.client.registration.ClientRegistratio import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; +import org.springframework.security.web.util.UrlUtils; import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -41,7 +44,7 @@ import org.springframework.web.util.UriComponentsBuilder; public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { private final ClientRegistrationRepository clientRegistrationRepository; - private URI postLogoutRedirectUri; + private String postLogoutRedirectUri; public OidcClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) { Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); @@ -54,9 +57,14 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo String targetUrl = null; URI endSessionEndpoint; if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof OidcUser) { - endSessionEndpoint = this.endSessionEndpoint((OAuth2AuthenticationToken) authentication); + String registrationId = ((OAuth2AuthenticationToken) authentication).getAuthorizedClientRegistrationId(); + ClientRegistration clientRegistration = this.clientRegistrationRepository + .findByRegistrationId(registrationId); + endSessionEndpoint = this.endSessionEndpoint(clientRegistration); if (endSessionEndpoint != null) { - targetUrl = endpointUri(endSessionEndpoint, authentication); + String idToken = idToken(authentication); + URI postLogoutRedirectUri = postLogoutRedirectUri(request); + targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri); } } if (targetUrl == null) { @@ -66,13 +74,11 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo return targetUrl; } - private URI endSessionEndpoint(OAuth2AuthenticationToken token) { - String registrationId = token.getAuthorizedClientRegistrationId(); - ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId); - + private URI endSessionEndpoint(ClientRegistration clientRegistration) { URI result = null; if (clientRegistration != null) { - Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata().get("end_session_endpoint"); + Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata() + .get("end_session_endpoint"); if (endSessionEndpoint != null) { result = URI.create(endSessionEndpoint.toString()); } @@ -81,25 +87,62 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo return result; } - private String endpointUri(URI endSessionEndpoint, Authentication authentication) { - UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); - builder.queryParam("id_token_hint", idToken(authentication)); - if (this.postLogoutRedirectUri != null) { - builder.queryParam("post_logout_redirect_uri", this.postLogoutRedirectUri); + private String idToken(Authentication authentication) { + return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); + } + + private URI postLogoutRedirectUri(HttpServletRequest request) { + if (this.postLogoutRedirectUri == null) { + return null; } - return builder.encode(StandardCharsets.UTF_8).build().toUriString(); + UriComponents uriComponents = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request)) + .replacePath(request.getContextPath()) + .replaceQuery(null) + .fragment(null) + .build(); + return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri) + .buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString())) + .toUri(); } - private String idToken(Authentication authentication) { - return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); + + private String endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) { + UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); + builder.queryParam("id_token_hint", idToken); + if (postLogoutRedirectUri != null) { + builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri); + } + return builder.encode(StandardCharsets.UTF_8).build().toUriString(); } /** * Set the post logout redirect uri to use * * @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user + * @deprecated {@link #setPostLogoutRedirectUri(String)} */ + @Deprecated public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) { + Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null"); + this.postLogoutRedirectUri = postLogoutRedirectUri.toASCIIString(); + } + + /** + * Set the post logout redirect uri template to use. Supports the {@code "{baseUrl}"} + * placeholder, for example: + * + *
+	 * 	handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
+	 * 
+ * + * will make so that {@code post_logout_redirect_uri} will be set to the base url for the client + * application. + * + * @param postLogoutRedirectUri - A template for creating the {@code post_logout_redirect_uri} + * query parameter + * @since 5.3 + */ + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null"); this.postLogoutRedirectUri = postLogoutRedirectUri; } diff --git a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java index b697bd0eaf..7668fd9e0c 100644 --- a/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java +++ b/oauth2/oauth2-client/src/main/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,9 +18,11 @@ package org.springframework.security.oauth2.client.oidc.web.server.logout; import java.net.URI; import java.nio.charset.StandardCharsets; +import java.util.Collections; import reactor.core.publisher.Mono; +import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.core.Authentication; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.registration.ClientRegistration; @@ -32,6 +34,7 @@ import org.springframework.security.web.server.WebFilterExchange; import org.springframework.security.web.server.authentication.logout.RedirectServerLogoutSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import org.springframework.util.Assert; +import org.springframework.web.util.UriComponents; import org.springframework.web.util.UriComponentsBuilder; /** @@ -50,7 +53,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler = new RedirectServerLogoutSuccessHandler(); private final ReactiveClientRegistrationRepository clientRegistrationRepository; - private URI postLogoutRedirectUri; + private String postLogoutRedirectUri; /** * Constructs an {@link OidcClientInitiatedServerLogoutSuccessHandler} with the provided parameters @@ -74,28 +77,40 @@ public class OidcClientInitiatedServerLogoutSuccessHandler .filter(OAuth2AuthenticationToken.class::isInstance) .filter(token -> authentication.getPrincipal() instanceof OidcUser) .map(OAuth2AuthenticationToken.class::cast) - .flatMap(this::endSessionEndpoint) - .map(endSessionEndpoint -> endpointUri(endSessionEndpoint, authentication)) + .map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId) + .flatMap(this.clientRegistrationRepository::findByRegistrationId) + .flatMap(clientRegistration -> { + URI endSessionEndpoint = endSessionEndpoint(clientRegistration); + if (endSessionEndpoint == null) { + return Mono.empty(); + } + String idToken = idToken(authentication); + URI postLogoutRedirectUri = postLogoutRedirectUri(exchange.getExchange().getRequest()); + return Mono.just(endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri)); + }) .switchIfEmpty(this.serverLogoutSuccessHandler .onLogoutSuccess(exchange, authentication).then(Mono.empty())) .flatMap(endpointUri -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri)); } - private Mono endSessionEndpoint(OAuth2AuthenticationToken token) { - String registrationId = token.getAuthorizedClientRegistrationId(); - return this.clientRegistrationRepository.findByRegistrationId(registrationId) - .map(ClientRegistration::getProviderDetails) - .map(ClientRegistration.ProviderDetails::getConfigurationMetadata) - .flatMap(configurationMetadata -> Mono.justOrEmpty(configurationMetadata.get("end_session_endpoint"))) - .map(Object::toString) - .map(URI::create); + private URI endSessionEndpoint(ClientRegistration clientRegistration) { + URI result = null; + if (clientRegistration != null) { + Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata() + .get("end_session_endpoint"); + if (endSessionEndpoint != null) { + result = URI.create(endSessionEndpoint.toString()); + } + } + + return result; } - private URI endpointUri(URI endSessionEndpoint, Authentication authentication) { + private URI endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) { UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); - builder.queryParam("id_token_hint", idToken(authentication)); - if (this.postLogoutRedirectUri != null) { - builder.queryParam("post_logout_redirect_uri", this.postLogoutRedirectUri); + builder.queryParam("id_token_hint", idToken); + if (postLogoutRedirectUri != null) { + builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri); } return builder.encode(StandardCharsets.UTF_8).build().toUri(); } @@ -104,13 +119,49 @@ public class OidcClientInitiatedServerLogoutSuccessHandler return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); } + private URI postLogoutRedirectUri(ServerHttpRequest request) { + if (this.postLogoutRedirectUri == null) { + return null; + } + UriComponents uriComponents = UriComponentsBuilder.fromUri(request.getURI()) + .replacePath(request.getPath().contextPath().value()) + .replaceQuery(null) + .fragment(null) + .build(); + return UriComponentsBuilder.fromUriString(this.postLogoutRedirectUri) + .buildAndExpand(Collections.singletonMap("baseUrl", uriComponents.toUriString())) + .toUri(); + } + /** * Set the post logout redirect uri to use * * @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user + * @deprecated {@link #setPostLogoutRedirectUri(String)} */ + @Deprecated public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) { Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be empty"); + this.postLogoutRedirectUri = postLogoutRedirectUri.toASCIIString(); + } + + /** + * Set the post logout redirect uri template to use. Supports the {@code "{baseUrl}"} + * placeholder, for example: + * + *
+	 * 	handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
+	 * 
+ * + * will make so that {@code post_logout_redirect_uri} will be set to the base url for the client + * application. + * + * @param postLogoutRedirectUri - A template for creating the {@code post_logout_redirect_uri} + * query parameter + * @since 5.3 + */ + public void setPostLogoutRedirectUri(String postLogoutRedirectUri) { + Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null"); this.postLogoutRedirectUri = postLogoutRedirectUri; } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java index ff667fc04a..59a7c08150 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/logout/OidcClientInitiatedLogoutSuccessHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -145,9 +145,35 @@ public class OidcClientInitiatedLogoutSuccessHandlerTests { "post_logout_redirect_uri=https://postlogout?encodedparam%3Dvalue"); } + @Test + public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirect() + throws IOException, ServletException { + + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( + TestOidcUsers.create(), + AuthorityUtils.NO_AUTHORITIES, + this.registration.getRegistrationId()); + this.handler.setPostLogoutRedirectUri("{baseUrl}"); + this.request.setScheme("https"); + this.request.setServerPort(443); + this.request.setServerName("rp.example.org"); + this.request.setUserPrincipal(token); + this.handler.onLogoutSuccess(this.request, this.response, token); + + assertThat(this.response.getRedirectedUrl()).isEqualTo("https://endpoint?" + + "id_token_hint=id-token&" + + "post_logout_redirect_uri=https://rp.example.org"); + } + @Test public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { - assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri(null)) + assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setPostLogoutRedirectUriTemplateWhenGivenNullThenThrowsException() { + assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((String) null)) .isInstanceOf(IllegalArgumentException.class); } } diff --git a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java index 199eb0c5ab..0dd7ad7209 100644 --- a/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java +++ b/oauth2/oauth2-client/src/test/java/org/springframework/security/oauth2/client/oidc/web/server/logout/OidcClientInitiatedServerLogoutSuccessHandlerTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2019 the original author or authors. + * Copyright 2002-2020 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,10 @@ package org.springframework.security.oauth2.client.oidc.web.server.logout; +import java.io.IOException; import java.net.URI; import java.util.Collections; +import javax.servlet.ServletException; import org.junit.Before; import org.junit.Test; @@ -154,9 +156,37 @@ public class OidcClientInitiatedServerLogoutSuccessHandlerTests { "post_logout_redirect_uri=https://postlogout?encodedparam%3Dvalue"); } + @Test + public void logoutWhenUsingPostLogoutRedirectUriTemplateThenBuildsItForRedirect() + throws IOException, ServletException { + + OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( + TestOidcUsers.create(), + AuthorityUtils.NO_AUTHORITIES, + this.registration.getRegistrationId()); + when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); + MockServerHttpRequest request = MockServerHttpRequest.get("https://rp.example.org/").build(); + when(this.exchange.getRequest()).thenReturn(request); + WebFilterExchange f = new WebFilterExchange(exchange, this.chain); + + this.handler.setPostLogoutRedirectUri("{baseUrl}"); + this.handler.onLogoutSuccess(f, token).block(); + + assertThat(redirectedUrl(this.exchange)) + .isEqualTo("https://endpoint?" + + "id_token_hint=id-token&" + + "post_logout_redirect_uri=https://rp.example.org"); + } + @Test public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { - assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri(null)) + assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((URI) null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void setPostLogoutRedirectUriTemplateWhenGivenNullThenThrowsException() { + assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri((String) null)) .isInstanceOf(IllegalArgumentException.class); }