Skip to content

Commit 66eaebb

Browse files
committed
fix: Move SPRING_REDIRECT_URI cookie invalidation to correct place
Invalidating SPRING_REDIRECT_URI cookie right after the cookie is read. JIRA: LX-1022 risk: high
1 parent 9f60b95 commit 66eaebb

File tree

3 files changed

+14
-78
lines changed

3 files changed

+14
-78
lines changed

Diff for: gooddata-server-oauth2-autoconfigure/src/main/kotlin/CookieServerRequestCache.kt

+3-20
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,12 @@
1818
*/
1919
package com.gooddata.oauth2.server
2020

21+
import java.net.URI
2122
import org.springframework.http.HttpMethod
22-
import org.springframework.http.HttpStatus
23-
import org.springframework.http.server.reactive.ServerHttpRequest
2423
import org.springframework.security.web.server.savedrequest.ServerRequestCache
2524
import org.springframework.security.web.server.util.matcher.ServerWebExchangeMatchers
2625
import org.springframework.web.server.ServerWebExchange
2726
import reactor.core.publisher.Mono
28-
import java.net.URI
2927

3028
/**
3129
* An implementation of [ServerRequestCache] that saves the requested URI in a cookie.
@@ -51,22 +49,7 @@ class CookieServerRequestCache(private val cookieService: ReactiveCookieService)
5149
Mono.just(exchange)
5250
.flatMap { cookieService.decodeCookie(it, SPRING_REDIRECT_URI) }
5351
.map { URI.create(it) }
52+
.doOnNext { cookieService.invalidateCookie(exchange, SPRING_REDIRECT_URI) }
5453

55-
override fun removeMatchingRequest(exchange: ServerWebExchange): Mono<ServerHttpRequest> {
56-
return Mono.just(exchange)
57-
.flatMap { webExchange ->
58-
if (webExchange.response.statusCode == HttpStatus.UNAUTHORIZED) {
59-
// For 401 responses, always preserve the redirect URI
60-
// If there's an existing one, keep it
61-
// If not, the saveRequest() will set it later
62-
Mono.just(webExchange.request)
63-
} else {
64-
// For non-401 responses (like successful authentication),
65-
// we can safely invalidate the cookie as we don't need it anymore
66-
Mono.just(webExchange)
67-
.doOnNext { cookieService.invalidateCookie(it, SPRING_REDIRECT_URI) }
68-
.thenReturn(webExchange.request)
69-
}
70-
}
71-
}
54+
override fun removeMatchingRequest(exchange: ServerWebExchange) = Mono.just(exchange.request)
7255
}

Diff for: gooddata-server-oauth2-autoconfigure/src/test/kotlin/CookieServerRequestCacheTest.kt

+9-50
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,12 @@ import io.mockk.slot
2323
import io.mockk.spyk
2424
import io.mockk.verify
2525
import io.netty.handler.codec.http.cookie.CookieHeaderNames
26+
import java.net.URI
27+
import java.time.Duration
28+
import java.time.Instant
2629
import org.intellij.lang.annotations.Language
2730
import org.junit.jupiter.api.Test
2831
import org.springframework.http.HttpCookie
29-
import org.springframework.http.HttpStatus
3032
import org.springframework.mock.http.server.reactive.MockServerHttpRequest
3133
import org.springframework.mock.web.server.MockServerWebExchange
3234
import org.springframework.util.CollectionUtils
@@ -35,10 +37,6 @@ import reactor.core.publisher.Mono
3537
import strikt.api.expectThat
3638
import strikt.assertions.isEqualTo
3739
import strikt.assertions.isTrue
38-
import java.net.URI
39-
import java.time.Duration
40-
import java.time.Instant
41-
import strikt.assertions.isNotNull
4240

4341
internal class CookieServerRequestCacheTest {
4442

@@ -68,7 +66,9 @@ internal class CookieServerRequestCacheTest {
6866
"""
6967

7068
private val client: AuthenticationStoreClient = mockk {
71-
mockCookieSecurityProperties(this, ORG_ID,
69+
mockCookieSecurityProperties(
70+
this,
71+
ORG_ID,
7272
CookieSecurityProperties(
7373
keySet = CleartextKeysetHandle.read(JsonKeysetReader.withBytes(keyset.toByteArray())),
7474
lastRotation = Instant.now(),
@@ -100,22 +100,6 @@ internal class CookieServerRequestCacheTest {
100100
verify(exactly = 1) { cookieService.createCookie(exchange, SPRING_REDIRECT_URI, any()) }
101101
}
102102

103-
@Test
104-
fun `should remove redirect URI from cookies`() {
105-
val request = MockServerHttpRequest.get("http://localhost/requestURI").queryParam("query", "true").build()
106-
val exchange = MockServerWebExchange.from(request)
107-
every { cookieService.invalidateCookie(any(), any()) } returns Unit
108-
109-
val matchingRequest = cache.removeMatchingRequest(exchange)
110-
111-
expectThat(matchingRequest.blockOptional()) {
112-
get { isPresent }.isTrue()
113-
get { get() }.isEqualTo(request)
114-
}
115-
116-
verify(exactly = 1) { cookieService.invalidateCookie(exchange, SPRING_REDIRECT_URI) }
117-
}
118-
119103
@Test
120104
fun `should not load redirect URI when nothing is stored in cookies`() {
121105
val exchange = MockServerWebExchange.from(
@@ -169,11 +153,10 @@ internal class CookieServerRequestCacheTest {
169153
}
170154

171155
@Test
172-
fun `should preserve redirect URI during 401 response`() {
156+
fun `should invalidate cookie after reading redirect URI`() {
173157
val webExchange = mockk<ServerWebExchange> {
174158
every { request.uri.host } returns "localhost"
175159
every { attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns Organization(ORG_ID)
176-
every { response.statusCode } returns HttpStatus.UNAUTHORIZED
177160
}
178161

179162
val redirect = "/requestURI?query=true"
@@ -182,36 +165,12 @@ internal class CookieServerRequestCacheTest {
182165
HttpCookie(SPRING_REDIRECT_URI, cookieSerializer.encodeCookieBlocking(webExchange, redirect))
183166
)
184167
)
185-
exchange.response.statusCode = HttpStatus.UNAUTHORIZED
186168

187-
val request = cache.removeMatchingRequest(exchange).block()
188-
189-
// Verify the cookie was not invalidated
190-
verify(exactly = 0) { cookieService.invalidateCookie(exchange, SPRING_REDIRECT_URI) }
191-
expectThat(request).isNotNull()
192-
}
193-
194-
@Test
195-
fun `should clear redirect URI for non-401 response`() {
196-
val webExchange = mockk<ServerWebExchange> {
197-
every { request.uri.host } returns "localhost"
198-
every { attributes[OrganizationWebFilter.ORGANIZATION_CACHE_KEY] } returns Organization(ORG_ID)
199-
every { response.statusCode } returns HttpStatus.FOUND
200-
}
201-
202-
val redirect = "/requestURI?query=true"
203-
val exchange = MockServerWebExchange.from(
204-
MockServerHttpRequest.get("http://localhost/").cookie(
205-
HttpCookie(SPRING_REDIRECT_URI, cookieSerializer.encodeCookieBlocking(webExchange, redirect))
206-
)
207-
)
208-
exchange.response.statusCode = HttpStatus.FOUND
169+
val uri = cache.getRedirectUri(exchange).block()
209170

210-
val request = cache.removeMatchingRequest(exchange).block()
171+
expectThat(uri).isEqualTo(URI.create(redirect))
211172

212-
// Verify the cookie was invalidated
213173
verify(exactly = 1) { cookieService.invalidateCookie(exchange, SPRING_REDIRECT_URI) }
214-
expectThat(request).isNotNull()
215174
}
216175

217176
companion object {

Diff for: gooddata-server-oauth2-autoconfigure/src/test/kotlin/UserContextWebFluxTest.kt

+2-8
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import com.ninjasquad.springmockk.SpykBean
2323
import io.mockk.every
2424
import io.mockk.mockk
2525
import io.mockk.verify
26+
import java.time.Duration
27+
import java.time.Instant
2628
import net.javacrumbs.jsonunit.core.util.ResourceUtils
2729
import org.intellij.lang.annotations.Language
2830
import org.junit.jupiter.api.Test
@@ -49,8 +51,6 @@ import org.springframework.web.server.ResponseStatusException
4951
import org.springframework.web.server.ServerWebExchange
5052
import reactor.core.publisher.Mono
5153
import reactor.util.context.Context
52-
import java.time.Duration
53-
import java.time.Instant
5454

5555
@WebFluxTest(properties = ["spring.security.oauth2.client.applogin.allow-redirect=https://localhost:8443"])
5656
@Import(ServerOAuth2AutoConfiguration::class, UserContextWebFluxTest.Config::class)
@@ -512,7 +512,6 @@ class UserContextWebFluxTest(
512512
.exchange()
513513
.expectStatus().isFound
514514
.expectHeader().location("/")
515-
.expectCookie().exists("SPRING_REDIRECT_URI")
516515
}
517516

518517
@Test
@@ -543,7 +542,6 @@ class UserContextWebFluxTest(
543542
.exchange()
544543
.expectStatus().isFound
545544
.expectHeader().location("/userReturnTo")
546-
.expectCookie().exists("SPRING_REDIRECT_URI")
547545
}
548546

549547
@Test
@@ -582,7 +580,6 @@ class UserContextWebFluxTest(
582580
.exchange()
583581
.expectStatus().isFound
584582
.expectHeader().location("/")
585-
.expectCookie().exists("SPRING_REDIRECT_URI")
586583
}
587584

588585
@Test
@@ -621,7 +618,6 @@ class UserContextWebFluxTest(
621618
.exchange()
622619
.expectStatus().isFound
623620
.expectHeader().location("/userReturnTo")
624-
.expectCookie().exists("SPRING_REDIRECT_URI")
625621
}
626622

627623
@Test
@@ -634,7 +630,6 @@ class UserContextWebFluxTest(
634630
webClient.post().uri("http://localhost/logout")
635631
.exchange()
636632
.expectStatus().isEqualTo(HttpStatus.METHOD_NOT_ALLOWED)
637-
.expectCookie().exists("SPRING_REDIRECT_URI")
638633
}
639634

640635
@Test
@@ -647,7 +642,6 @@ class UserContextWebFluxTest(
647642
webClient.post().uri("http://localhost/logout/all")
648643
.exchange()
649644
.expectStatus().isEqualTo(HttpStatus.METHOD_NOT_ALLOWED)
650-
.expectCookie().exists("SPRING_REDIRECT_URI")
651645
}
652646

653647
private fun encodeCookieBlocking(authenticationToken: String) =

0 commit comments

Comments
 (0)