3 changed files with 385 additions and 7 deletions
@ -0,0 +1,126 @@
@@ -0,0 +1,126 @@
|
||||
/* |
||||
* Copyright 2002-2019 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.security.oauth2.client.web.server.oidc.logout; |
||||
|
||||
import java.net.URI; |
||||
import java.nio.charset.StandardCharsets; |
||||
|
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration; |
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.core.oidc.user.OidcUser; |
||||
import org.springframework.security.web.server.DefaultServerRedirectStrategy; |
||||
import org.springframework.security.web.server.ServerRedirectStrategy; |
||||
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.UriComponentsBuilder; |
||||
|
||||
/** |
||||
* A reactive logout success handler for initiating OIDC logout through the user agent. |
||||
* |
||||
* @author Josh Cummings |
||||
* @since 5.2 |
||||
* @see <a href="http://openid.net/specs/openid-connect-session-1_0.html#RPLogout">RP-Initiated Logout</a> |
||||
* @see org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler |
||||
*/ |
||||
public class OidcClientInitiatedServerLogoutSuccessHandler |
||||
implements ServerLogoutSuccessHandler { |
||||
|
||||
private final ServerRedirectStrategy redirectStrategy = new DefaultServerRedirectStrategy(); |
||||
private final RedirectServerLogoutSuccessHandler serverLogoutSuccessHandler |
||||
= new RedirectServerLogoutSuccessHandler(); |
||||
private final ReactiveClientRegistrationRepository clientRegistrationRepository; |
||||
|
||||
private URI postLogoutRedirectUri; |
||||
|
||||
/** |
||||
* Constructs an {@link OidcClientInitiatedServerLogoutSuccessHandler} with the provided parameters |
||||
* |
||||
* @param clientRegistrationRepository The {@link ReactiveClientRegistrationRepository} to use to derive |
||||
* the end_session_endpoint value |
||||
*/ |
||||
public OidcClientInitiatedServerLogoutSuccessHandler |
||||
(ReactiveClientRegistrationRepository clientRegistrationRepository) { |
||||
|
||||
Assert.notNull(clientRegistrationRepository, "clientRegistrationRepository cannot be null"); |
||||
this.clientRegistrationRepository = clientRegistrationRepository; |
||||
} |
||||
|
||||
/** |
||||
* {@inheritDoc} |
||||
*/ |
||||
@Override |
||||
public Mono<Void> onLogoutSuccess(WebFilterExchange exchange, Authentication authentication) { |
||||
return Mono.just(authentication) |
||||
.filter(OAuth2AuthenticationToken.class::isInstance) |
||||
.filter(token -> authentication.getPrincipal() instanceof OidcUser) |
||||
.map(OAuth2AuthenticationToken.class::cast) |
||||
.flatMap(this::endSessionEndpoint) |
||||
.map(endSessionEndpoint -> endpointUri(endSessionEndpoint, authentication)) |
||||
.switchIfEmpty(this.serverLogoutSuccessHandler |
||||
.onLogoutSuccess(exchange, authentication).then(Mono.empty())) |
||||
.flatMap(endpointUri -> this.redirectStrategy.sendRedirect(exchange.getExchange(), endpointUri)); |
||||
} |
||||
|
||||
private Mono<URI> 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 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().toUri(); |
||||
} |
||||
|
||||
private String idToken(Authentication authentication) { |
||||
return ((OidcUser) authentication.getPrincipal()).getIdToken().getTokenValue(); |
||||
} |
||||
|
||||
/** |
||||
* Set the post logout redirect uri to use |
||||
* |
||||
* @param postLogoutRedirectUri - A valid URL to which the OP should redirect after logging out the user |
||||
*/ |
||||
public void setPostLogoutRedirectUri(URI postLogoutRedirectUri) { |
||||
Assert.notNull(postLogoutRedirectUri, "postLogoutRedirectUri cannot be empty"); |
||||
this.postLogoutRedirectUri = postLogoutRedirectUri; |
||||
} |
||||
|
||||
/** |
||||
* The URL to redirect to after successfully logging out when not originally an OIDC login |
||||
* |
||||
* @param logoutSuccessUrl the url to redirect to. Default is "/login?logout". |
||||
*/ |
||||
public void setLogoutSuccessUrl(URI logoutSuccessUrl) { |
||||
Assert.notNull(logoutSuccessUrl, "logoutSuccessUrl cannot be null"); |
||||
this.serverLogoutSuccessHandler.setLogoutSuccessUrl(logoutSuccessUrl); |
||||
} |
||||
} |
||||
@ -0,0 +1,163 @@
@@ -0,0 +1,163 @@
|
||||
/* |
||||
* Copyright 2002-2019 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. |
||||
* You may obtain a copy of the License at |
||||
* |
||||
* https://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.springframework.security.oauth2.client.web.server.oidc.logout; |
||||
|
||||
import java.net.URI; |
||||
import java.util.Collections; |
||||
|
||||
import org.junit.Before; |
||||
import org.junit.Test; |
||||
import reactor.core.publisher.Mono; |
||||
|
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpResponse; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.authority.AuthorityUtils; |
||||
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; |
||||
import org.springframework.security.oauth2.client.registration.ClientRegistration; |
||||
import org.springframework.security.oauth2.client.registration.InMemoryReactiveClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository; |
||||
import org.springframework.security.oauth2.client.registration.TestClientRegistrations; |
||||
import org.springframework.security.oauth2.core.oidc.user.TestOidcUsers; |
||||
import org.springframework.security.oauth2.core.user.TestOAuth2Users; |
||||
import org.springframework.security.web.server.WebFilterExchange; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
import org.springframework.web.server.WebFilterChain; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||
import static org.mockito.Mockito.mock; |
||||
import static org.mockito.Mockito.when; |
||||
|
||||
public class OidcClientInitiatedServerLogoutSuccessHandlerTests { |
||||
ClientRegistration registration = TestClientRegistrations |
||||
.clientRegistration() |
||||
.providerConfigurationMetadata( |
||||
Collections.singletonMap("end_session_endpoint", "http://endpoint")) |
||||
.build(); |
||||
ReactiveClientRegistrationRepository repository = new InMemoryReactiveClientRegistrationRepository(registration); |
||||
|
||||
ServerWebExchange exchange; |
||||
WebFilterChain chain; |
||||
|
||||
OidcClientInitiatedServerLogoutSuccessHandler handler; |
||||
|
||||
@Before |
||||
public void setup() { |
||||
this.exchange = mock(ServerWebExchange.class); |
||||
when(this.exchange.getResponse()).thenReturn(new MockServerHttpResponse()); |
||||
when(this.exchange.getRequest()).thenReturn(MockServerHttpRequest.get("/").build()); |
||||
this.chain = mock(WebFilterChain.class); |
||||
this.handler = new OidcClientInitiatedServerLogoutSuccessHandler(this.repository); |
||||
} |
||||
|
||||
@Test |
||||
public void logoutWhenOidcRedirectUrlConfiguredThenRedirects() { |
||||
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( |
||||
TestOidcUsers.create(), |
||||
AuthorityUtils.NO_AUTHORITIES, |
||||
this.registration.getRegistrationId()); |
||||
|
||||
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); |
||||
WebFilterExchange f = new WebFilterExchange(exchange, this.chain); |
||||
this.handler.onLogoutSuccess(f, token).block(); |
||||
|
||||
assertThat(redirectedUrl(this.exchange)).isEqualTo("http://endpoint?id_token_hint=id-token"); |
||||
} |
||||
|
||||
@Test |
||||
public void logoutWhenNotOAuth2AuthenticationThenDefaults() { |
||||
Authentication token = mock(Authentication.class); |
||||
|
||||
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); |
||||
WebFilterExchange f = new WebFilterExchange(exchange, this.chain); |
||||
|
||||
this.handler.setLogoutSuccessUrl(URI.create("http://default")); |
||||
this.handler.onLogoutSuccess(f, token).block(); |
||||
|
||||
assertThat(redirectedUrl(this.exchange)).isEqualTo("http://default"); |
||||
} |
||||
|
||||
@Test |
||||
public void logoutWhenNotOidcUserThenDefaults() { |
||||
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( |
||||
TestOAuth2Users.create(), |
||||
AuthorityUtils.NO_AUTHORITIES, |
||||
this.registration.getRegistrationId()); |
||||
|
||||
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); |
||||
WebFilterExchange f = new WebFilterExchange(exchange, this.chain); |
||||
|
||||
this.handler.setLogoutSuccessUrl(URI.create("http://default")); |
||||
this.handler.onLogoutSuccess(f, token).block(); |
||||
|
||||
assertThat(redirectedUrl(this.exchange)).isEqualTo("http://default"); |
||||
} |
||||
|
||||
@Test |
||||
public void logoutWhenClientRegistrationHasNoEndSessionEndpointThenDefaults() { |
||||
|
||||
ClientRegistration registration = TestClientRegistrations.clientRegistration().build(); |
||||
ReactiveClientRegistrationRepository repository = |
||||
new InMemoryReactiveClientRegistrationRepository(registration); |
||||
OidcClientInitiatedServerLogoutSuccessHandler handler = |
||||
new OidcClientInitiatedServerLogoutSuccessHandler(repository); |
||||
|
||||
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( |
||||
TestOidcUsers.create(), |
||||
AuthorityUtils.NO_AUTHORITIES, |
||||
registration.getRegistrationId()); |
||||
|
||||
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); |
||||
WebFilterExchange f = new WebFilterExchange(exchange, this.chain); |
||||
|
||||
handler.setLogoutSuccessUrl(URI.create("http://default")); |
||||
handler.onLogoutSuccess(f, token).block(); |
||||
|
||||
assertThat(redirectedUrl(this.exchange)).isEqualTo("http://default"); |
||||
} |
||||
|
||||
@Test |
||||
public void logoutWhenUsingPostLogoutRedirectUriThenIncludesItInRedirect() { |
||||
|
||||
OAuth2AuthenticationToken token = new OAuth2AuthenticationToken( |
||||
TestOidcUsers.create(), |
||||
AuthorityUtils.NO_AUTHORITIES, |
||||
this.registration.getRegistrationId()); |
||||
|
||||
when(this.exchange.getPrincipal()).thenReturn(Mono.just(token)); |
||||
WebFilterExchange f = new WebFilterExchange(exchange, this.chain); |
||||
|
||||
this.handler.setPostLogoutRedirectUri(URI.create("http://postlogout?encodedparam=value")); |
||||
this.handler.onLogoutSuccess(f, token).block(); |
||||
|
||||
assertThat(redirectedUrl(this.exchange)) |
||||
.isEqualTo("http://endpoint?" + |
||||
"id_token_hint=id-token&" + |
||||
"post_logout_redirect_uri=http://postlogout?encodedparam%3Dvalue"); |
||||
} |
||||
|
||||
@Test |
||||
public void setPostLogoutRedirectUriWhenGivenNullThenThrowsException() { |
||||
assertThatThrownBy(() -> this.handler.setPostLogoutRedirectUri(null)) |
||||
.isInstanceOf(IllegalArgumentException.class); |
||||
} |
||||
|
||||
private String redirectedUrl(ServerWebExchange exchange) { |
||||
return exchange.getResponse().getHeaders().getFirst("Location"); |
||||
} |
||||
} |
||||
Loading…
Reference in new issue