4 changed files with 223 additions and 57 deletions
@ -0,0 +1,124 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020-2024 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.server.authorization.oidc.web.authentication; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.nio.charset.StandardCharsets; |
||||||
|
|
||||||
|
import jakarta.servlet.ServletException; |
||||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||||
|
import org.apache.commons.logging.Log; |
||||||
|
import org.apache.commons.logging.LogFactory; |
||||||
|
|
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||||
|
import org.springframework.security.oauth2.core.OAuth2Error; |
||||||
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; |
||||||
|
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; |
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken; |
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.web.OidcLogoutEndpointFilter; |
||||||
|
import org.springframework.security.web.DefaultRedirectStrategy; |
||||||
|
import org.springframework.security.web.RedirectStrategy; |
||||||
|
import org.springframework.security.web.authentication.AuthenticationSuccessHandler; |
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler; |
||||||
|
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler; |
||||||
|
import org.springframework.util.Assert; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
import org.springframework.web.util.UriComponentsBuilder; |
||||||
|
import org.springframework.web.util.UriUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* An implementation of an {@link AuthenticationSuccessHandler} used for handling an |
||||||
|
* {@link OidcLogoutAuthenticationToken} and performing the OpenID Connect 1.0 |
||||||
|
* RP-Initiated Logout. |
||||||
|
* |
||||||
|
* @author Joe Grandja |
||||||
|
* @since 1.4 |
||||||
|
* @see OidcLogoutEndpointFilter#setAuthenticationSuccessHandler(AuthenticationSuccessHandler) |
||||||
|
* @see LogoutHandler |
||||||
|
*/ |
||||||
|
public final class OidcLogoutAuthenticationSuccessHandler implements AuthenticationSuccessHandler { |
||||||
|
|
||||||
|
private final Log logger = LogFactory.getLog(getClass()); |
||||||
|
|
||||||
|
private final RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); |
||||||
|
|
||||||
|
private final SecurityContextLogoutHandler securityContextLogoutHandler = new SecurityContextLogoutHandler(); |
||||||
|
|
||||||
|
private LogoutHandler logoutHandler = this::performLogout; |
||||||
|
|
||||||
|
@Override |
||||||
|
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, |
||||||
|
Authentication authentication) throws IOException, ServletException { |
||||||
|
|
||||||
|
if (!(authentication instanceof OidcLogoutAuthenticationToken)) { |
||||||
|
if (this.logger.isErrorEnabled()) { |
||||||
|
this.logger.error(Authentication.class.getSimpleName() + " must be of type " |
||||||
|
+ OidcLogoutAuthenticationToken.class.getName() + " but was " |
||||||
|
+ authentication.getClass().getName()); |
||||||
|
} |
||||||
|
OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR, |
||||||
|
"Unable to process the OpenID Connect 1.0 RP-Initiated Logout response.", null); |
||||||
|
throw new OAuth2AuthenticationException(error); |
||||||
|
} |
||||||
|
|
||||||
|
this.logoutHandler.logout(request, response, authentication); |
||||||
|
|
||||||
|
sendLogoutRedirect(request, response, authentication); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Sets the {@link LogoutHandler} used for performing logout. |
||||||
|
* @param logoutHandler the {@link LogoutHandler} used for performing logout |
||||||
|
*/ |
||||||
|
public void setLogoutHandler(LogoutHandler logoutHandler) { |
||||||
|
Assert.notNull(logoutHandler, "logoutHandler cannot be null"); |
||||||
|
this.logoutHandler = logoutHandler; |
||||||
|
} |
||||||
|
|
||||||
|
private void performLogout(HttpServletRequest request, HttpServletResponse response, |
||||||
|
Authentication authentication) { |
||||||
|
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication; |
||||||
|
|
||||||
|
// Check for active user session
|
||||||
|
if (oidcLogoutAuthentication.isPrincipalAuthenticated()) { |
||||||
|
this.securityContextLogoutHandler.logout(request, response, |
||||||
|
(Authentication) oidcLogoutAuthentication.getPrincipal()); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void sendLogoutRedirect(HttpServletRequest request, HttpServletResponse response, |
||||||
|
Authentication authentication) throws IOException { |
||||||
|
OidcLogoutAuthenticationToken oidcLogoutAuthentication = (OidcLogoutAuthenticationToken) authentication; |
||||||
|
|
||||||
|
String redirectUri = "/"; |
||||||
|
if (oidcLogoutAuthentication.isAuthenticated() |
||||||
|
&& StringUtils.hasText(oidcLogoutAuthentication.getPostLogoutRedirectUri())) { |
||||||
|
// Use the `post_logout_redirect_uri` parameter
|
||||||
|
UriComponentsBuilder uriBuilder = UriComponentsBuilder |
||||||
|
.fromUriString(oidcLogoutAuthentication.getPostLogoutRedirectUri()); |
||||||
|
if (StringUtils.hasText(oidcLogoutAuthentication.getState())) { |
||||||
|
uriBuilder.queryParam(OAuth2ParameterNames.STATE, |
||||||
|
UriUtils.encode(oidcLogoutAuthentication.getState(), StandardCharsets.UTF_8)); |
||||||
|
} |
||||||
|
// build(true) -> Components are explicitly encoded
|
||||||
|
redirectUri = uriBuilder.build(true).toUriString(); |
||||||
|
} |
||||||
|
this.redirectStrategy.sendRedirect(request, response, redirectUri); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -0,0 +1,94 @@ |
|||||||
|
/* |
||||||
|
* Copyright 2020-2024 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.server.authorization.oidc.web.authentication; |
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest; |
||||||
|
import jakarta.servlet.http.HttpServletResponse; |
||||||
|
import org.junit.jupiter.api.BeforeEach; |
||||||
|
import org.junit.jupiter.api.Test; |
||||||
|
|
||||||
|
import org.springframework.mock.web.MockHttpServletRequest; |
||||||
|
import org.springframework.mock.web.MockHttpServletResponse; |
||||||
|
import org.springframework.mock.web.MockHttpSession; |
||||||
|
import org.springframework.security.authentication.TestingAuthenticationToken; |
||||||
|
import org.springframework.security.core.Authentication; |
||||||
|
import org.springframework.security.oauth2.core.OAuth2AuthenticationException; |
||||||
|
import org.springframework.security.oauth2.core.OAuth2ErrorCodes; |
||||||
|
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcLogoutAuthenticationToken; |
||||||
|
import org.springframework.security.web.authentication.logout.LogoutHandler; |
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy; |
||||||
|
import static org.mockito.ArgumentMatchers.any; |
||||||
|
import static org.mockito.Mockito.mock; |
||||||
|
import static org.mockito.Mockito.verify; |
||||||
|
|
||||||
|
/** |
||||||
|
* Tests for {@link OidcLogoutAuthenticationSuccessHandler}. |
||||||
|
* |
||||||
|
* @author Joe Grandja |
||||||
|
*/ |
||||||
|
public class OidcLogoutAuthenticationSuccessHandlerTests { |
||||||
|
|
||||||
|
private TestingAuthenticationToken principal; |
||||||
|
|
||||||
|
private final OidcLogoutAuthenticationSuccessHandler authenticationSuccessHandler = new OidcLogoutAuthenticationSuccessHandler(); |
||||||
|
|
||||||
|
@BeforeEach |
||||||
|
public void setUp() { |
||||||
|
this.principal = new TestingAuthenticationToken("principal", "credentials"); |
||||||
|
this.principal.setAuthenticated(true); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void setLogoutHandlerWhenNullThenThrowIllegalArgumentException() { |
||||||
|
// @formatter:off
|
||||||
|
assertThatThrownBy(() -> this.authenticationSuccessHandler.setLogoutHandler(null)) |
||||||
|
.isInstanceOf(IllegalArgumentException.class) |
||||||
|
.hasMessage("logoutHandler cannot be null"); |
||||||
|
// @formatter:on
|
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void onAuthenticationSuccessWhenInvalidAuthenticationTypeThenThrowOAuth2AuthenticationException() { |
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||||
|
|
||||||
|
assertThatThrownBy( |
||||||
|
() -> this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, this.principal)) |
||||||
|
.isInstanceOf(OAuth2AuthenticationException.class) |
||||||
|
.extracting((ex) -> ((OAuth2AuthenticationException) ex).getError()) |
||||||
|
.extracting("errorCode") |
||||||
|
.isEqualTo(OAuth2ErrorCodes.SERVER_ERROR); |
||||||
|
} |
||||||
|
|
||||||
|
@Test |
||||||
|
public void onAuthenticationSuccessWhenLogoutHandlerSetThenUsed() throws Exception { |
||||||
|
LogoutHandler logoutHandler = mock(LogoutHandler.class); |
||||||
|
this.authenticationSuccessHandler.setLogoutHandler(logoutHandler); |
||||||
|
|
||||||
|
MockHttpServletRequest request = new MockHttpServletRequest(); |
||||||
|
MockHttpSession session = (MockHttpSession) request.getSession(true); |
||||||
|
MockHttpServletResponse response = new MockHttpServletResponse(); |
||||||
|
|
||||||
|
OidcLogoutAuthenticationToken authentication = new OidcLogoutAuthenticationToken("id-token", this.principal, |
||||||
|
session.getId(), null, null, null); |
||||||
|
this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authentication); |
||||||
|
|
||||||
|
verify(logoutHandler).logout(any(HttpServletRequest.class), any(HttpServletResponse.class), |
||||||
|
any(Authentication.class)); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
Loading…
Reference in new issue