Skip to content

Commit

Permalink
feat: allow to define logout URL in query parameter
Browse files Browse the repository at this point in the history
JIRA:STL-458
risk:low
  • Loading branch information
sadam21 committed Jun 25, 2024
1 parent 55a2caa commit 4e79cd4
Show file tree
Hide file tree
Showing 6 changed files with 131 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,17 @@ class JwtAuthenticationLogoutHandler(
.filter { authentication is JwtAuthenticationToken }
.flatMap {
// TODO where should we redirect??
redirectStrategy.sendRedirect(exchange.exchange, URI.create("/"))
redirectStrategy.sendRedirect(
exchange.exchange,
// workaround for STL-458: use URL from 'returnTo' query parameter if provided,
// otherwise use default URL
URI.create(exchange.returnToQueryParam() ?: DEFAULT_REDIRECT_URL)
)
}

companion object {
private const val DEFAULT_REDIRECT_URL = "/"
}
}

internal class JwtAuthenticationLogoutException(originalError: Throwable) : ResponseStatusException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2024 GoodData Corporation
*
* 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 com.gooddata.oauth2.server

import org.springframework.security.core.Authentication
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.security.web.server.authentication.logout.ServerLogoutSuccessHandler
import reactor.core.publisher.Mono
import java.net.URI

/**
* Extends functionality of [OidcClientInitiatedServerLogoutSuccessHandler]. Allows to set the post logout redirect URI
* based on the `returnTo` query parameter from the request.
*
* @param [clientRegistrationRepository] the repository for client registrations
* @param [postLogoutRedirectUri] the default post logout redirect URI,
* see [OidcClientInitiatedServerLogoutSuccessHandler.setPostLogoutRedirectUri]
* @param [defaultLogoutSuccessUrl] the default logout success URL,
* see [OidcClientInitiatedServerLogoutSuccessHandler.setLogoutSuccessUrl]
*
* @see [OidcClientInitiatedServerLogoutSuccessHandler]
*/
class QueryParamOidcClientInitiatedServerLogoutSuccessHandler(
private val clientRegistrationRepository: ReactiveClientRegistrationRepository,
private val postLogoutRedirectUri: String,
private val defaultLogoutSuccessUrl: String,
) : ServerLogoutSuccessHandler {

override fun onLogoutSuccess(exchange: WebFilterExchange, authentication: Authentication): Mono<Void> =
OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository)
.apply {
setPostLogoutRedirectUri(postLogoutRedirectUri)
setLogoutSuccessUrl(
// workaround for STL-458: use URL from 'returnTo' query parameter if provided,
// otherwise use default URL
URI.create(exchange.returnToQueryParam() ?: defaultLogoutSuccessUrl)
)
}
.onLogoutSuccess(exchange, authentication)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
*/
package com.gooddata.oauth2.server

import java.net.URI
import java.util.Base64
import org.springframework.beans.factory.ObjectProvider
import org.springframework.beans.factory.annotation.Value
Expand All @@ -42,7 +41,6 @@ import org.springframework.security.oauth2.client.endpoint.OAuth2RefreshTokenGra
import org.springframework.security.oauth2.client.endpoint.ReactiveOAuth2AccessTokenResponseClient
import org.springframework.security.oauth2.client.oidc.authentication.OidcAuthorizationCodeReactiveAuthenticationManager
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest
import org.springframework.security.oauth2.client.oidc.web.server.logout.OidcClientInitiatedServerLogoutSuccessHandler
import org.springframework.security.oauth2.client.registration.ClientRegistration
import org.springframework.security.oauth2.client.registration.ReactiveClientRegistrationRepository
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest
Expand Down Expand Up @@ -248,11 +246,7 @@ class ServerOAuth2AutoConfiguration {

val logoutSuccessHandler = DelegatingServerLogoutSuccessHandler(
// Order of handlers is important!
OidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository).apply {
setPostLogoutRedirectUri("{baseUrl}")
setLogoutSuccessUrl(URI.create("/"))
},
// Keep Auth0 handler as last one in OIDC handlers
QueryParamOidcClientInitiatedServerLogoutSuccessHandler(clientRegistrationRepository, "{baseUrl}", "/"),
Auth0LogoutHandler(clientRegistrationRepository, auth0CustomDomain),
JwtAuthenticationLogoutHandler(authenticationStoreClient.`object`),
client = authenticationStoreClient.`object`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,18 @@
*/
package com.gooddata.oauth2.server

import org.springframework.security.web.server.WebFilterExchange
import org.springframework.web.util.UriComponentsBuilder
import java.net.URI

const val RETURN_TO_QUERY_PARAM = "returnTo"

/**
* Get "returnTo" query parameter from exchange request
*/
fun WebFilterExchange.returnToQueryParam(): String? =
exchange.request.queryParams.getFirst(RETURN_TO_QUERY_PARAM)

/**
* Build URI from string
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
*/
package com.gooddata.oauth2.server

import io.mockk.mockk
import org.junit.jupiter.api.Test
import org.springframework.mock.http.server.reactive.MockServerHttpRequest
import org.springframework.mock.web.server.MockServerWebExchange
import org.springframework.security.web.server.WebFilterExchange
import org.springframework.web.server.WebFilterChain
import strikt.api.expectThat
import strikt.assertions.isEqualTo
import strikt.assertions.isFalse
Expand Down Expand Up @@ -35,4 +40,14 @@ class UriExtensionsTest {
val uri = "https://auth0.example.org".toUri()
expectThat(uri.isAuth0()).isFalse()
}

@Test
fun `returnToQueryParam should return correct query parameter`() {
val request = MockServerHttpRequest.get("http://localhost?returnTo=urlToReturnTo")
val exchange = MockServerWebExchange.from(request)
val chain = mockk<WebFilterChain>()
val webFilterExchange = WebFilterExchange(exchange, chain)

expectThat(webFilterExchange.returnToQueryParam()).isEqualTo("urlToReturnTo")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,27 @@ class UserContextWebFluxTest(
.expectCookie().exists("SPRING_REDIRECT_URI")
}

@Test
fun `filter redirects logout from returnTo query param without cookies`() {
val organization = Organization(
id = ORG_ID,
oauthClientId = "clientId",
oauthClientSecret = "clientSecret",
allowedOrigins = listOf("https://localhost:8443"),
)
mockOrganization(authenticationStoreClient, LOCALHOST, organization)
every { exchange.attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns organization
every { serverSecurityContextRepository.load(any()) } returns Mono.empty()
every { serverSecurityContextRepository.save(any(), null) } returns Mono.empty()
every { clientRegistrationRepository.findByRegistrationId(any()) } returns Mono.empty()

webClient.get().uri("http://localhost/logout?returnTo=/userReturnTo")
.exchange()
.expectStatus().isFound
.expectHeader().location("/userReturnTo")
.expectCookie().exists("SPRING_REDIRECT_URI")
}

@Test
fun `filter redirects logout with cookies`() {
every { exchange.attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns ORGANIZATION
Expand All @@ -465,6 +486,26 @@ class UserContextWebFluxTest(
.expectCookie().exists("SPRING_REDIRECT_URI")
}

@Test
fun `filter redirects logout from returnTo query param with cookies`() {
every { exchange.attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns ORGANIZATION
everyValidSecurityContext()
every { serverSecurityContextRepository.save(any(), null) } returns Mono.empty()
every { clientRegistrationRepository.findByRegistrationId(any()) } returns Mono.empty()
everyValidOrganization()
mockUserByAuthId(authenticationStoreClient, ORG_ID, SUB_CLAIM_VALUE, User(USER_ID))
val authenticationToken = ResourceUtils.resource("oauth2_authentication_token.json").readText()
val authorizedClient = ResourceUtils.resource("simplified_oauth2_authorized_client.json").readText()

webClient.get().uri("http://localhost/logout?returnTo=/userReturnTo")
.cookie(SPRING_SEC_SECURITY_CONTEXT, encodeCookieBlocking(authenticationToken))
.cookie(SPRING_SEC_OAUTH2_AUTHZ_CLIENT, encodeCookieBlocking(authorizedClient))
.exchange()
.expectStatus().isFound
.expectHeader().location("/userReturnTo")
.expectCookie().exists("SPRING_REDIRECT_URI")
}

@Test
fun `POST logout ends with 405`() {
every { exchange.attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns ORGANIZATION
Expand Down

0 comments on commit 4e79cd4

Please sign in to comment.