baseUrl placeholder for OidcLogoutSuccessHandlers

Fixes gh-7842
This commit is contained in:
Josh Cummings 2020-02-05 12:14:36 -07:00
parent 283e451cad
commit 968ebb194b
No known key found for this signature in database
GPG Key ID: 49EF60DD7FF83443
4 changed files with 188 additions and 38 deletions

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; 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.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.core.oidc.user.OidcUser; import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler; import org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
/** /**
@ -41,7 +44,7 @@ import org.springframework.web.util.UriComponentsBuilder;
public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler { public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
private final ClientRegistrationRepository clientRegistrationRepository; private final ClientRegistrationRepository clientRegistrationRepository;
private URI postLogoutRedirectUri; private String postLogoutRedirectUri;
public OidcClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) { public OidcClientInitiatedLogoutSuccessHandler(ClientRegistrationRepository clientRegistrationRepository) {
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null");
@ -54,9 +57,14 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
String targetUrl = null; String targetUrl = null;
URI endSessionEndpoint; URI endSessionEndpoint;
if (authentication instanceof OAuth2AuthenticationToken && authentication.getPrincipal() instanceof OidcUser) { 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) { if (endSessionEndpoint != null) {
targetUrl = endpointUri(endSessionEndpoint, authentication); String idToken = idToken(authentication);
URI postLogoutRedirectUri = postLogoutRedirectUri(request);
targetUrl = endpointUri(endSessionEndpoint, idToken, postLogoutRedirectUri);
} }
} }
if (targetUrl == null) { if (targetUrl == null) {
@ -66,13 +74,11 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
return targetUrl; return targetUrl;
} }
private URI endSessionEndpoint(OAuth2AuthenticationToken token) { private URI endSessionEndpoint(ClientRegistration clientRegistration) {
String registrationId = token.getAuthorizedClientRegistrationId();
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
URI result = null; URI result = null;
if (clientRegistration != 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) { if (endSessionEndpoint != null) {
result = URI.create(endSessionEndpoint.toString()); result = URI.create(endSessionEndpoint.toString());
} }
@ -81,25 +87,62 @@ public final class OidcClientInitiatedLogoutSuccessHandler extends SimpleUrlLogo
return result; 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);
}
return builder.encode(StandardCharsets.UTF_8).build().toUriString();
}
private String idToken(Authentication authentication) { private String idToken(Authentication authentication) {
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue();
} }
private URI postLogoutRedirectUri(HttpServletRequest request) {
if (this.postLogoutRedirectUri == null) {
return null;
}
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 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 * Set the post logout redirect uri to use
* *
* @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user * @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) { 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:
*
* <pre>
* handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
* </pre>
*
* 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"); Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be null");
this.postLogoutRedirectUri = postLogoutRedirectUri; this.postLogoutRedirectUri = postLogoutRedirectUri;
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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.net.URI;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collections;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.client.registration.ClientRegistration; 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.RedirectServerLogoutSuccessHandler;
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler; import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder; import org.springframework.web.util.UriComponentsBuilder;
/** /**
@ -50,7 +53,7 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
= new RedirectServerLogoutSuccessHandler(); = new RedirectServerLogoutSuccessHandler();
private final ReactiveClientRegistrationRepository clientRegistrationRepository; private final ReactiveClientRegistrationRepository clientRegistrationRepository;
private URI postLogoutRedirectUri; private String postLogoutRedirectUri;
/** /**
* Constructs an {@link OidcClientInitiatedServerLogoutSuccessHandler} with the provided parameters * Constructs an {@link OidcClientInitiatedServerLogoutSuccessHandler} with the provided parameters
@ -74,28 +77,40 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
.filter(OAuth2AuthenticationToken.class::isInstance) .filter(OAuth2AuthenticationToken.class::isInstance)
.filter(token -> authentication.getPrincipal() instanceof OidcUser) .filter(token -> authentication.getPrincipal() instanceof OidcUser)
.map(OAuth2AuthenticationToken.class::cast) .map(OAuth2AuthenticationToken.class::cast)
.flatMap(this::endSessionEndpoint) .map(OAuth2AuthenticationToken::getAuthorizedClientRegistrationId)
.map(endSessionEndpoint -> endpointUri(endSessionEndpoint, authentication)) .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 .switchIfEmpty(this.serverLogoutSuccessHandler
.onLogoutSuccess(exchange, authentication).then(Mono.empty())) .onLogoutSuccess(exchange, authentication).then(Mono.empty()))
.flatMap(endpointUri -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri)); .flatMap(endpointUri -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri));
} }
private Mono<URI> endSessionEndpoint(OAuth2AuthenticationToken token) { private URI endSessionEndpoint(ClientRegistration clientRegistration) {
String registrationId = token.getAuthorizedClientRegistrationId(); URI result = null;
return this.clientRegistrationRepository.findByRegistrationId(registrationId) if (clientRegistration != null) {
.map(ClientRegistration::getProviderDetails) Object endSessionEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata()
.map(ClientRegistration.ProviderDetails::getConfigurationMetadata) .get("end_session_endpoint");
.flatMap(configurationMetadata -> Mono.justOrEmpty(configurationMetadata.get("end_session_endpoint"))) if (endSessionEndpoint != null) {
.map(Object::toString) result = URI.create(endSessionEndpoint.toString());
.map(URI::create); }
}
return result;
} }
private URI endpointUri(URI endSessionEndpoint, Authentication authentication) { private URI endpointUri(URI endSessionEndpoint, String idToken, URI postLogoutRedirectUri) {
UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint); UriComponentsBuilder builder = UriComponentsBuilder.fromUri(endSessionEndpoint);
builder.queryParam("id_token_hint", idToken(authentication)); builder.queryParam("id_token_hint", idToken);
if (this.postLogoutRedirectUri != null) { if (postLogoutRedirectUri != null) {
builder.queryParam("post_logout_redirect_uri", this.postLogoutRedirectUri); builder.queryParam("post_logout_redirect_uri", postLogoutRedirectUri);
} }
return builder.encode(StandardCharsets.UTF_8).build().toUri(); return builder.encode(StandardCharsets.UTF_8).build().toUri();
} }
@ -104,13 +119,49 @@ public class OidcClientInitiatedServerLogoutSuccessHandler
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); 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 * Set the post logout redirect uri to use
* *
* @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user * @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) { public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) {
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be empty"); 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:
*
* <pre>
* handler.setPostLogoutRedirectUriTemplate("{baseUrl}");
* </pre>
*
* 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; this.postLogoutRedirectUri = postLogoutRedirectUri;
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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"); "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 @Test
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { 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); .isInstanceOf(IllegalArgumentException.class);
} }
} }

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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; package org.springframework.security.oauth2.client.oidc.web.server.logout;
import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.Collections; import java.util.Collections;
import javax.servlet.ServletException;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
@ -154,9 +156,37 @@ public class OidcClientInitiatedServerLogoutSuccessHandlerTests {
"post_logout_redirect_uri=https://postlogout?encodedparam%3Dvalue"); "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 @Test
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { 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); .isInstanceOf(IllegalArgumentException.class);
} }