Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ public void getWhenUsingDefaultsWithExpiredBearerTokenThenInvalidToken() throws
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -337,7 +337,7 @@ public void getWhenUsingDefaultsWithMalformedBearerTokenThenInvalidToken() throw
// @formatter:off
this.mvc.perform(get("/").with(bearerToken("an\"invalid\"token")))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Bearer token is malformed"));
.andExpect(invalidTokenHeader("Bearer token is malformed", "/"));
// @formatter:on
}

Expand All @@ -349,7 +349,7 @@ public void getWhenUsingDefaultsWithMalformedPayloadThenInvalidToken() throws Ex
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload", "/"));
// @formatter:on
}

Expand All @@ -360,7 +360,7 @@ public void getWhenUsingDefaultsWithUnsignedBearerTokenThenInvalidToken() throws
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Unsupported algorithm of none"));
.andExpect(invalidTokenHeader("Unsupported algorithm of none", "/"));
// @formatter:on
}

Expand All @@ -372,7 +372,7 @@ public void getWhenUsingDefaultsWithBearerTokenBeforeNotBeforeThenInvalidToken()
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -486,7 +486,7 @@ public void getWhenUsingDefaultsAndAuthorizationServerHasNoMatchingKeyThenInvali
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -604,7 +604,7 @@ public void postWhenUsingDefaultsWithExpiredBearerTokenAndNoCsrfThenInvalidToken
// @formatter:off
this.mvc.perform(post("/authenticated").header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE).with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/authenticated"));
// @formatter:on
}

Expand Down Expand Up @@ -955,7 +955,7 @@ public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired() throws
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Jwt expired at"));
.andExpect(invalidTokenHeader("Jwt expired at", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -1006,7 +1006,7 @@ public void requestWhenUsingPublicKeyAndSignatureFailsThenReturnsInvalidToken()
String token = this.token("WrongSignature");
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(invalidTokenHeader("signature"));
.andExpect(invalidTokenHeader("signature", "/"));
// @formatter:on
}

Expand All @@ -1016,7 +1016,7 @@ public void requestWhenUsingPublicKeyAlgorithmDoesNotMatchThenReturnsInvalidToke
String token = this.token("WrongAlgorithm");
// @formatter:off
this.mvc.perform(get("/").with(bearerToken(token)))
.andExpect(invalidTokenHeader("algorithm"));
.andExpect(invalidTokenHeader("algorithm", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -1314,7 +1314,7 @@ public void getWhenMultipleIssuersThenUsesIssuerClaimToDifferentiate() throws Ex
// @formatter:off
this.mvc.perform(get("/authenticated").with(bearerToken(jwtThree)))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Invalid issuer"));
.andExpect(invalidTokenHeader("Invalid issuer", "/authenticated"));
// @formatter:on
}

Expand Down Expand Up @@ -1478,13 +1478,15 @@ private static ResultMatcher invalidRequestHeader(String message) {
", " + "resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\"")));
}

private static ResultMatcher invalidTokenHeader(String message) {
private static ResultMatcher invalidTokenHeader(String message, String endpoint) {
String path = "/".equals(endpoint) ? "" : endpoint;
return header().string(HttpHeaders.WWW_AUTHENTICATE,
AllOf.allOf(new StringStartsWith("Bearer " + "error=\"invalid_token\", " + "error_description=\""),
new StringContains(message),
new StringContains(", " + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""),
new StringEndsWith(
", " + "resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\"")));
", resource_metadata=\"http://localhost/.well-known/oauth-protected-resource%s\""
.formatted(path))));
}

private static ResultMatcher insufficientScopeHeader() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public void getWhenExpiredBearerTokenThenInvalidToken() throws Exception {
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -215,7 +215,7 @@ public void getWhenMalformedBearerTokenThenInvalidToken() throws Exception {
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer an\"invalid\"token"))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Bearer token is malformed"));
.andExpect(invalidTokenHeader("Bearer token is malformed", "/"));
// @formatter:on
}

Expand All @@ -227,7 +227,7 @@ public void getWhenMalformedPayloadThenInvalidToken() throws Exception {
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt: Malformed payload", "/"));
// @formatter:on
}

Expand All @@ -238,7 +238,7 @@ public void getWhenUnsignedBearerTokenThenInvalidToken() throws Exception {
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Unsupported algorithm of none"));
.andExpect(invalidTokenHeader("Unsupported algorithm of none", "/"));
// @formatter:on
}

Expand All @@ -250,7 +250,7 @@ public void getWhenBearerTokenBeforeNotBeforeThenInvalidToken() throws Exception
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -342,7 +342,7 @@ public void getWhenAuthorizationServerHasNoMatchingKeyThenInvalidToken() throws
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -398,7 +398,7 @@ public void postWhenExpiredBearerTokenAndNoCsrfThenInvalidToken() throws Excepti
// @formatter:off
this.mvc.perform(post("/authenticated").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt"));
.andExpect(invalidTokenHeader("An error occurred while attempting to decode the Jwt", "/authenticated"));
// @formatter:on
}

Expand Down Expand Up @@ -621,7 +621,7 @@ public void requestWhenClockSkewSetButJwtStillTooLateThenReportsExpired() throws
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Jwt expired at"));
.andExpect(invalidTokenHeader("Jwt expired at", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -661,7 +661,7 @@ public void requestWhenUsingPublicKeyAndSignatureFailsThenReturnsInvalidToken()
String token = this.token("WrongSignature");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(invalidTokenHeader("signature"));
.andExpect(invalidTokenHeader("signature", "/"));
// @formatter:on
}

Expand All @@ -671,7 +671,7 @@ public void requestWhenUsingPublicKeyAlgorithmDoesNotMatchThenReturnsInvalidToke
String token = this.token("WrongAlgorithm");
// @formatter:off
this.mvc.perform(get("/").header("Authorization", "Bearer " + token))
.andExpect(invalidTokenHeader("algorithm"));
.andExpect(invalidTokenHeader("algorithm", "/"));
// @formatter:on
}

Expand Down Expand Up @@ -779,7 +779,7 @@ public void getWhenMultipleIssuersThenUsesIssuerClaimToDifferentiate() throws Ex
// @formatter:off
this.mvc.perform(get("/authenticated").header("Authorization", "Bearer " + jwtThree))
.andExpect(status().isUnauthorized())
.andExpect(invalidTokenHeader("Invalid issuer"));
.andExpect(invalidTokenHeader("Invalid issuer", "/authenticated"));
// @formatter:on
}

Expand Down Expand Up @@ -934,13 +934,15 @@ private static ResultMatcher invalidRequestHeader(String message) {
", " + "resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\"")));
}

private static ResultMatcher invalidTokenHeader(String message) {
private static ResultMatcher invalidTokenHeader(String message, String endpoint) {
String path = "/".equals(endpoint) ? "" : endpoint;
return header().string(HttpHeaders.WWW_AUTHENTICATE,
AllOf.allOf(new StringStartsWith("Bearer " + "error=\"invalid_token\", " + "error_description=\""),
new StringContains(message),
new StringContains(", " + "error_uri=\"https://tools.ietf.org/html/rfc6750#section-3.1\""),
new StringEndsWith(
", " + "resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\"")));
", resource_metadata=\"http://localhost/.well-known/oauth-protected-resource%s\""
.formatted(path))));
}

private static ResultMatcher insufficientScopeHeader() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,16 +115,13 @@ public void setResourceMetadataParameterResolver(
}

private static String getResourceMetadataParameter(HttpServletRequest request) {
String path = request.getContextPath()
+ OAuth2ProtectedResourceMetadataFilter.DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI;
// @formatter:off
return UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request))
.replacePath(path)
.replaceQuery(null)
.fragment(null)
.build()
.toUriString();
// @formatter:on

UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(UrlUtils.buildFullRequestUrl(request));
String[] pathSegments = builder.build().getPathSegments().toArray(String[]::new);
return builder.replacePath(request.getContextPath())
.path(OAuth2ProtectedResourceMetadataFilter.DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI)
.pathSegment(pathSegments)
.toUriString();
}

private static String computeWWWAuthenticateHeaderValue(Map<String, String> parameters) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import org.springframework.http.HttpStatus;
import org.springframework.mock.web.MockHttpServletRequest;
Expand Down Expand Up @@ -90,6 +92,35 @@ public void commenceWhenNoBearerTokenErrorAndResourceMetadataResolverSetThenStat
.isEqualTo("Bearer resource_metadata=\"https://example.com/resource-from-request\"");
}

// gh-19639
@ParameterizedTest
@CsvSource(
textBlock = """
, , https://example.com/.well-known/oauth-protected-resource,
'', '', https://example.com/.well-known/oauth-protected-resource,
/, /, https://example.com/.well-known/oauth-protected-resource,
requestUri, contextPath, https://example.com/contextPath/.well-known/oauth-protected-resource/requestUri,
/requestUri, /contextPath, https://example.com/contextPath/.well-known/oauth-protected-resource/requestUri,
requestUri/, contextPath/, https://example.com/contextPath/.well-known/oauth-protected-resource/requestUri,
/requestUri/, /contextPath/, https://example.com/contextPath/.well-known/oauth-protected-resource/requestUri,
//requestUri//, //contextPath//, https://example.com/contextPath/.well-known/oauth-protected-resource/requestUri
""")
public void commenceShouldIncludeContextPathAndRequestUriInResourceMetadata(String requestUri, String contextPath,
String expected) {

MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("https");
request.setServerName("example.com");
request.setServerPort(443);
request.setContextPath(contextPath);
MockHttpServletResponse response = new MockHttpServletResponse();

this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("test"));
assertThat(response.getStatus()).isEqualTo(401);
assertThat(response.getHeader("WWW-Authenticate"))
.isEqualTo("Bearer resource_metadata=\"%s\"".formatted(expected));
}

@Test
public void commenceWhenInvalidRequestErrorThenStatus400AndHeaderWithError() throws Exception {
MockHttpServletRequest request = new MockHttpServletRequest();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
import jakarta.servlet.http.HttpServletRequest;
import org.jspecify.annotations.Nullable;

import org.springframework.util.StringUtils;

/**
* Provides static methods for composing URLs.
* <p>
Expand Down Expand Up @@ -67,7 +69,12 @@ else if ("https".equals(scheme)) {
}
// Use the requestURI as it is encoded (RFC 3986) and hence suitable for
// redirects.
url.append(requestURI);
if (StringUtils.hasText(requestURI)) {
if (requestURI.charAt(0) != '/') {
url.append("/");
}
url.append(requestURI);
}
if (queryString != null) {
url.append("?").append(queryString);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,40 @@
package org.springframework.security.web.util;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

import org.springframework.mock.web.MockHttpServletRequest;

import static org.assertj.core.api.Assertions.assertThat;

/**
* Test for {@link UrlUtils}
*
* @author Luke Taylor
*/
public class UrlUtilsTests {

@ParameterizedTest
@CsvSource(textBlock = """
, https://example.com,
'', https://example.com,
/, https://example.com/,
requestUri, https://example.com/requestUri,
/requestUri, https://example.com/requestUri,
requestUri/, https://example.com/requestUri/,
/requestUri/, https://example.com/requestUri/,
//requestUri//, https://example.com//requestUri//
""")
public void buildFullRequestUrl(String requestUri, String expected) {

MockHttpServletRequest request = new MockHttpServletRequest("GET", requestUri);
request.setScheme("https");
request.setServerName("example.com");
request.setServerPort(443);
assertThat(UrlUtils.buildFullRequestUrl(request)).isEqualTo(expected);
}

@Test
public void absoluteUrlsAreMatchedAsAbsolute() {
assertThat(UrlUtils.isAbsoluteUrl("https://something/")).isTrue();
Expand Down
Loading