From 0ebd44c1c9b3b092cc466f4bb0e9f62a7a36b64b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:09:15 +0900 Subject: [PATCH 1/2] test(http): reproduce missing bearer challenge --- .../controller/ArtifactHttpResponseTest.java | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 src/test/java/com/clearfolio/viewer/controller/ArtifactHttpResponseTest.java diff --git a/src/test/java/com/clearfolio/viewer/controller/ArtifactHttpResponseTest.java b/src/test/java/com/clearfolio/viewer/controller/ArtifactHttpResponseTest.java new file mode 100644 index 000000000..4770d47e6 --- /dev/null +++ b/src/test/java/com/clearfolio/viewer/controller/ArtifactHttpResponseTest.java @@ -0,0 +1,38 @@ +package com.clearfolio.viewer.controller; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; + +/** + * Regression tests for security-sensitive artifact token failure response headers. + */ +class ArtifactHttpResponseTest { + + /** + * Requires a bearer challenge while preserving cache and MIME-sniffing defenses on 401. + */ + @Test + void unauthorizedTokenFailureAdvertisesBearerChallenge() { + var response = ArtifactHttpResponse.tokenFailure(HttpStatus.UNAUTHORIZED); + + assertEquals("Bearer", response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)); + assertEquals("no-store", response.getHeaders().getFirst(HttpHeaders.CACHE_CONTROL)); + assertEquals("nosniff", response.getHeaders().getFirst("X-Content-Type-Options")); + } + + /** + * Keeps 403 from restarting authentication while preserving the standard security headers. + */ + @Test + void forbiddenTokenFailureDoesNotRestartAuthentication() { + var response = ArtifactHttpResponse.tokenFailure(HttpStatus.FORBIDDEN); + + assertNull(response.getHeaders().getFirst(HttpHeaders.WWW_AUTHENTICATE)); + assertEquals("no-store", response.getHeaders().getFirst(HttpHeaders.CACHE_CONTROL)); + assertEquals("nosniff", response.getHeaders().getFirst("X-Content-Type-Options")); + } +} From b85034617dc52b47344d8efa13dc57070cd4ed03 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 14:13:01 +0900 Subject: [PATCH 2/2] fix(http): advertise bearer challenge for 401 --- .../viewer/controller/ArtifactHttpResponse.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/clearfolio/viewer/controller/ArtifactHttpResponse.java b/src/main/java/com/clearfolio/viewer/controller/ArtifactHttpResponse.java index af3db1d54..24132efcd 100644 --- a/src/main/java/com/clearfolio/viewer/controller/ArtifactHttpResponse.java +++ b/src/main/java/com/clearfolio/viewer/controller/ArtifactHttpResponse.java @@ -99,10 +99,13 @@ static ResponseEntity unsatisfiable( * @return controlled token failure response */ static ResponseEntity tokenFailure(HttpStatus status) { - return ResponseEntity.status(status) + ResponseEntity.BodyBuilder builder = ResponseEntity.status(status) .header(HttpHeaders.CACHE_CONTROL, "no-store") - .header(CONTENT_TYPE_OPTIONS, "nosniff") - .build(); + .header(CONTENT_TYPE_OPTIONS, "nosniff"); + if (status == HttpStatus.UNAUTHORIZED) { + builder.header(HttpHeaders.WWW_AUTHENTICATE, "Bearer"); + } + return builder.build(); } private static ResponseEntity.BodyBuilder decorateRangeHeaders(