Skip to content

Commit 9d68449

Browse files
committed
Introduce WWWAuthenticateHeaderContext and customizer in BearerTokenAuthenticationEntryPoint
Signed-off-by: Daniel Garnier-Moiroux <git@garnier.wf>
1 parent ac61420 commit 9d68449

3 files changed

Lines changed: 114 additions & 0 deletions

File tree

oauth2/oauth2-resource-server/src/main/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import java.util.LinkedHashMap;
2020
import java.util.Map;
21+
import java.util.function.Consumer;
2122

2223
import jakarta.servlet.http.HttpServletRequest;
2324
import jakarta.servlet.http.HttpServletResponse;
@@ -31,6 +32,7 @@
3132
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
3233
import org.springframework.security.web.AuthenticationEntryPoint;
3334
import org.springframework.security.web.util.UrlUtils;
35+
import org.springframework.util.Assert;
3436
import org.springframework.util.StringUtils;
3537
import org.springframework.web.util.UriComponentsBuilder;
3638

@@ -51,6 +53,9 @@ public final class BearerTokenAuthenticationEntryPoint implements Authentication
5153

5254
private String realmName;
5355

56+
private Consumer<WWWAuthenticateHeaderContext> wwwAuthenticateHeaderContextCustomizer = (ctx) -> {
57+
};
58+
5459
/**
5560
* Collect error details from the provided parameters and format according to RFC
5661
* 6750, specifically {@code error}, {@code error_description}, {@code error_uri}, and
@@ -84,6 +89,9 @@ public void commence(HttpServletRequest request, HttpServletResponse response,
8489
}
8590
}
8691
parameters.put("resource_metadata", getResourceMetadataParameter(request));
92+
WWWAuthenticateHeaderContext wwwAuthenticateHeaderContext = new WWWAuthenticateHeaderContext(parameters,
93+
request, authException);
94+
this.wwwAuthenticateHeaderContextCustomizer.accept(wwwAuthenticateHeaderContext);
8795
String wwwAuthenticate = computeWWWAuthenticateHeaderValue(parameters);
8896
response.addHeader(HttpHeaders.WWW_AUTHENTICATE, wwwAuthenticate);
8997
response.setStatus(status.value());
@@ -97,6 +105,16 @@ public void setRealmName(String realmName) {
97105
this.realmName = realmName;
98106
}
99107

108+
/**
109+
* Sets the customizer for the {@link WWWAuthenticateHeaderContext}.
110+
* @param wwwAuthenticateHeaderContextCustomizer
111+
*/
112+
public void setWwwAuthenticateHeaderContextCustomizer(
113+
Consumer<WWWAuthenticateHeaderContext> wwwAuthenticateHeaderContextCustomizer) {
114+
Assert.notNull(wwwAuthenticateHeaderContextCustomizer, "wwwAuthenticateHeaderContextCustomizer cannot be null");
115+
this.wwwAuthenticateHeaderContextCustomizer = wwwAuthenticateHeaderContextCustomizer;
116+
}
117+
100118
private static String getResourceMetadataParameter(HttpServletRequest request) {
101119
String path = request.getContextPath()
102120
+ OAuth2ProtectedResourceMetadataFilter.DEFAULT_OAUTH2_PROTECTED_RESOURCE_METADATA_ENDPOINT_URI;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2004-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.security.oauth2.server.resource.web;
18+
19+
import java.util.Map;
20+
21+
import jakarta.servlet.http.HttpServletRequest;
22+
23+
import org.springframework.security.core.AuthenticationException;
24+
25+
/**
26+
* A context that holds information to be set in a {@code WWW-Authenticate} header with a
27+
* {@code Bearer} challenge. Only the parameters are mutable.
28+
*
29+
* @author Daniel Garnier-Moiroux
30+
* @since 7.1
31+
*/
32+
public class WWWAuthenticateHeaderContext {
33+
34+
public final Map<String, String> parameters;
35+
36+
public final HttpServletRequest request;
37+
38+
public final AuthenticationException authenticationException;
39+
40+
public WWWAuthenticateHeaderContext(Map<String, String> parameters, HttpServletRequest request,
41+
AuthenticationException authenticationException) {
42+
this.parameters = parameters;
43+
this.request = request;
44+
this.authenticationException = authenticationException;
45+
}
46+
47+
/**
48+
* The parameters to be set in the Bearer challenge of the WWW-Authenticate header.
49+
* @return
50+
*/
51+
public Map<String, String> getParameters() {
52+
return this.parameters;
53+
}
54+
55+
/**
56+
* The request which triggered the creation of the WWW-Authenticate header.
57+
* @return
58+
*/
59+
public HttpServletRequest getRequest() {
60+
return this.request;
61+
}
62+
63+
/**
64+
* The specific exception which triggered the creation of the WWW-Authenticate header.
65+
* @return
66+
*/
67+
public AuthenticationException getAuthenticationException() {
68+
return this.authenticationException;
69+
}
70+
71+
}

oauth2/oauth2-resource-server/src/test/java/org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPointTests.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.security.oauth2.server.resource.BearerTokenErrorCodes;
2929

3030
import static org.assertj.core.api.Assertions.assertThat;
31+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
3132

3233
/**
3334
* Tests for {@link BearerTokenAuthenticationEntryPoint}.
@@ -164,9 +165,33 @@ public void commenceWhenInsufficientScopeAndRealmSetThenStatus403AndHeaderWithEr
164165
+ "error_uri=\"https://example.com\", scope=\"test.read test.write\", resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\"");
165166
}
166167

168+
@Test
169+
void commenceWhenWwwAuthenticateHeaderCustomizerSetThenHeaderWithCustomization() {
170+
MockHttpServletRequest request = new MockHttpServletRequest();
171+
MockHttpServletResponse response = new MockHttpServletResponse();
172+
request.setAttribute("custom-attribute", "request-attribute");
173+
this.authenticationEntryPoint.setWwwAuthenticateHeaderContextCustomizer((context) -> {
174+
context.getParameters().put("req", context.getRequest().getAttribute("custom-attribute").toString());
175+
context.getParameters().put("ex", context.getAuthenticationException().getMessage());
176+
});
177+
178+
this.authenticationEntryPoint.commence(request, response, new BadCredentialsException("exception-message"));
179+
180+
assertThat(response.getStatus()).isEqualTo(401);
181+
assertThat(response.getHeader("WWW-Authenticate"))
182+
.isEqualTo("Bearer resource_metadata=\"http://localhost/.well-known/oauth-protected-resource\", "
183+
+ "req=\"request-attribute\", ex=\"exception-message\"");
184+
}
185+
167186
@Test
168187
public void setRealmNameWhenNullRealmNameThenNoExceptionThrown() {
169188
this.authenticationEntryPoint.setRealmName(null);
170189
}
171190

191+
@Test
192+
void setWwwAuthenticateHeaderCustomizerWhenNullThenThrows() {
193+
assertThatExceptionOfType(IllegalArgumentException.class)
194+
.isThrownBy(() -> this.authenticationEntryPoint.setWwwAuthenticateHeaderContextCustomizer(null));
195+
}
196+
172197
}

0 commit comments

Comments
 (0)