Skip to content

Commit 4b0e74a

Browse files
committed
Separate Resource Server Servlet Docs
Issue gh-10367
1 parent d40e8f6 commit 4b0e74a

File tree

9 files changed

+3072
-3063
lines changed

9 files changed

+3072
-3063
lines changed

docs/modules/ROOT/nav.adoc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@
5757
** xref:servlet/oauth2/index.adoc[OAuth2]
5858
*** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In]
5959
*** xref:servlet/oauth2/oauth2-client.adoc[OAuth2 Client]
60-
*** xref:servlet/oauth2/oauth2-resourceserver.adoc[OAuth2 Resource Server]
60+
*** xref:servlet/oauth2/resource-server/index.adoc[OAuth2 Resource Server]
61+
**** xref:servlet/oauth2/resource-server/jwt.adoc[JWT]
62+
**** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token]
63+
**** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
64+
**** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
6165
** xref:servlet/saml2/index.adoc[SAML2]
6266
** xref:servlet/exploits/index.adoc[Protection Against Exploits]
6367
*** xref:servlet/exploits/csrf.adoc[]

docs/modules/ROOT/pages/reactive/oauth2/resource-server.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -897,7 +897,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {
897897

898898
[[webflux-oauth2resourceserver-opaque-minimaldependencies]]
899899
=== Minimal Dependencies for Introspection
900-
As described in xref:servlet/oauth2/oauth2-resourceserver.adoc#oauth2resourceserver-jwt-minimaldependencies[Minimal Dependencies for JWT] most of Resource Server support is collected in `spring-security-oauth2-resource-server`.
900+
As described in xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-minimaldependencies[Minimal Dependencies for JWT] most of Resource Server support is collected in `spring-security-oauth2-resource-server`.
901901
However unless a custom <<webflux-oauth2resourceserver-opaque-introspector-bean,`ReactiveOpaqueTokenIntrospector`>> is provided, the Resource Server will fallback to ReactiveOpaqueTokenIntrospector.
902902
Meaning that both `spring-security-oauth2-resource-server` and `oauth2-oidc-sdk` are necessary in order to have a working minimal Resource Server that supports opaque Bearer Tokens.
903903
Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`.
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
= OAuth2
2+
:page-section-summary-toc: 1
23

34
Spring Security provides comprehensive OAuth 2 support.
45
This section discusses how to integrate OAuth 2 into your servlet based application.
56

6-
* xref:servlet/oauth2/oauth2-login.adoc[]
7-
* xref:servlet/oauth2/oauth2-client.adoc[]
8-
* xref:servlet/oauth2/oauth2-resourceserver.adoc[]
7+

docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc

Lines changed: 0 additions & 3058 deletions
This file was deleted.
Lines changed: 290 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,290 @@
1+
= OAuth 2.0 Bearer Tokens
2+
3+
[[oauth2resourceserver-bearertoken-resolver]]
4+
== Bearer Token Resolution
5+
6+
By default, Resource Server looks for a bearer token in the `Authorization` header.
7+
This, however, can be customized in a handful of ways.
8+
9+
=== Reading the Bearer Token from a Custom Header
10+
11+
For example, you may have a need to read the bearer token from a custom header.
12+
To achieve this, you can expose a `DefaultBearerTokenResolver` as a bean, or wire an instance into the DSL, as you can see in the following example:
13+
14+
.Custom Bearer Token Header
15+
====
16+
.Java
17+
[source,java,role="primary"]
18+
----
19+
@Bean
20+
BearerTokenResolver bearerTokenResolver() {
21+
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
22+
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION);
23+
return bearerTokenResolver;
24+
}
25+
----
26+
27+
.Kotlin
28+
[source,kotlin,role="secondary"]
29+
----
30+
@Bean
31+
fun bearerTokenResolver(): BearerTokenResolver {
32+
val bearerTokenResolver = DefaultBearerTokenResolver()
33+
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION)
34+
return bearerTokenResolver
35+
}
36+
----
37+
38+
.Xml
39+
[source,xml,role="secondary"]
40+
----
41+
<http>
42+
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
43+
</http>
44+
45+
<bean id="bearerTokenResolver"
46+
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
47+
<property name="bearerTokenHeaderName" value="Proxy-Authorization"/>
48+
</bean>
49+
----
50+
====
51+
52+
Or, in circumstances where a provider is using both a custom header and value, you can use `HeaderBearerTokenResolver` instead.
53+
54+
=== Reading the Bearer Token from a Form Parameter
55+
56+
Or, you may wish to read the token from a form parameter, which you can do by configuring the `DefaultBearerTokenResolver`, as you can see below:
57+
58+
.Form Parameter Bearer Token
59+
====
60+
.Java
61+
[source,java,role="primary"]
62+
----
63+
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
64+
resolver.setAllowFormEncodedBodyParameter(true);
65+
http
66+
.oauth2ResourceServer(oauth2 -> oauth2
67+
.bearerTokenResolver(resolver)
68+
);
69+
----
70+
71+
.Kotlin
72+
[source,kotlin,role="secondary"]
73+
----
74+
val resolver = DefaultBearerTokenResolver()
75+
resolver.setAllowFormEncodedBodyParameter(true)
76+
http {
77+
oauth2ResourceServer {
78+
bearerTokenResolver = resolver
79+
}
80+
}
81+
----
82+
83+
.Xml
84+
[source,xml,role="secondary"]
85+
----
86+
<http>
87+
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
88+
</http>
89+
90+
<bean id="bearerTokenResolver"
91+
class="org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver">
92+
<property name="allowFormEncodedBodyParameter" value="true"/>
93+
</bean>
94+
----
95+
====
96+
97+
== Bearer Token Propagation
98+
99+
Now that you're resource server has validated the token, it might be handy to pass it to downstream services.
100+
This is quite simple with `{security-api-url}org/springframework/security/oauth2/server/resource/web/reactive/function/client/ServletBearerExchangeFilterFunction.html[ServletBearerExchangeFilterFunction]`, which you can see in the following example:
101+
102+
====
103+
.Java
104+
[source,java,role="primary"]
105+
----
106+
@Bean
107+
public WebClient rest() {
108+
return WebClient.builder()
109+
.filter(new ServletBearerExchangeFilterFunction())
110+
.build();
111+
}
112+
----
113+
114+
.Kotlin
115+
[source,kotlin,role="secondary"]
116+
----
117+
@Bean
118+
fun rest(): WebClient {
119+
return WebClient.builder()
120+
.filter(ServletBearerExchangeFilterFunction())
121+
.build()
122+
}
123+
----
124+
====
125+
126+
When the above `WebClient` is used to perform requests, Spring Security will look up the current `Authentication` and extract any `{security-api-url}org/springframework/security/oauth2/core/AbstractOAuth2Token.html[AbstractOAuth2Token]` credential.
127+
Then, it will propagate that token in the `Authorization` header.
128+
129+
For example:
130+
131+
====
132+
.Java
133+
[source,java,role="primary"]
134+
----
135+
this.rest.get()
136+
.uri("https://other-service.example.com/endpoint")
137+
.retrieve()
138+
.bodyToMono(String.class)
139+
.block()
140+
----
141+
142+
.Kotlin
143+
[source,kotlin,role="secondary"]
144+
----
145+
this.rest.get()
146+
.uri("https://other-service.example.com/endpoint")
147+
.retrieve()
148+
.bodyToMono<String>()
149+
.block()
150+
----
151+
====
152+
153+
Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you.
154+
155+
In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so:
156+
157+
====
158+
.Java
159+
[source,java,role="primary"]
160+
----
161+
this.rest.get()
162+
.uri("https://other-service.example.com/endpoint")
163+
.headers(headers -> headers.setBearerAuth(overridingToken))
164+
.retrieve()
165+
.bodyToMono(String.class)
166+
.block()
167+
----
168+
169+
.Kotlin
170+
[source,kotlin,role="secondary"]
171+
----
172+
this.rest.get()
173+
.uri("https://other-service.example.com/endpoint")
174+
.headers{ headers -> headers.setBearerAuth(overridingToken)}
175+
.retrieve()
176+
.bodyToMono<String>()
177+
.block()
178+
----
179+
====
180+
181+
In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.
182+
183+
[NOTE]
184+
Unlike the {security-api-url}org/springframework/security/oauth2/client/web/reactive/function/client/ServletOAuth2AuthorizedClientExchangeFilterFunction.html[OAuth 2.0 Client filter function], this filter function makes no attempt to renew the token, should it be expired.
185+
To obtain this level of support, please use the OAuth 2.0 Client filter.
186+
187+
=== `RestTemplate` support
188+
189+
There is no `RestTemplate` equivalent for `ServletBearerExchangeFilterFunction` at the moment, but you can propagate the request's bearer token quite simply with your own interceptor:
190+
191+
====
192+
.Java
193+
[source,java,role="primary"]
194+
----
195+
@Bean
196+
RestTemplate rest() {
197+
RestTemplate rest = new RestTemplate();
198+
rest.getInterceptors().add((request, body, execution) -> {
199+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
200+
if (authentication == null) {
201+
return execution.execute(request, body);
202+
}
203+
204+
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
205+
return execution.execute(request, body);
206+
}
207+
208+
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
209+
request.getHeaders().setBearerAuth(token.getTokenValue());
210+
return execution.execute(request, body);
211+
});
212+
return rest;
213+
}
214+
----
215+
216+
.Kotlin
217+
[source,kotlin,role="secondary"]
218+
----
219+
@Bean
220+
fun rest(): RestTemplate {
221+
val rest = RestTemplate()
222+
rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution ->
223+
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
224+
if (authentication != null) {
225+
execution.execute(request, body)
226+
}
227+
228+
if (authentication!!.credentials !is AbstractOAuth2Token) {
229+
execution.execute(request, body)
230+
}
231+
232+
val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token
233+
request.headers.setBearerAuth(token.tokenValue)
234+
execution.execute(request, body)
235+
})
236+
return rest
237+
}
238+
----
239+
====
240+
241+
242+
[NOTE]
243+
Unlike the {security-api-url}org/springframework/security/oauth2/client/OAuth2AuthorizedClientManager.html[OAuth 2.0 Authorized Client Manager], this filter interceptor makes no attempt to renew the token, should it be expired.
244+
To obtain this level of support, please create an interceptor using the xref:servlet/oauth2/oauth2-client.adoc#oauth2client[OAuth 2.0 Authorized Client Manager].
245+
246+
[[oauth2resourceserver-bearertoken-failure]]
247+
== Bearer Token Failure
248+
249+
A bearer token may be invalid for a number of reasons. For example, the token may no longer be active.
250+
251+
In these circumstances, Resource Server throws an `InvalidBearerTokenException`.
252+
Like other exceptions, this results in an OAuth 2.0 Bearer Token error response:
253+
254+
[source,http request]
255+
----
256+
HTTP/1.1 401 Unauthorized
257+
WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
258+
----
259+
260+
Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`, which you can xref:servlet/authentication/events.adoc#servlet-events[listen for in your application] like so:
261+
262+
====
263+
.Java
264+
[source,java,role="primary"]
265+
----
266+
@Component
267+
public class FailureEvents {
268+
@EventListener
269+
public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) {
270+
if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
271+
// ... handle
272+
}
273+
}
274+
}
275+
----
276+
277+
.Kotlin
278+
[source,kotlin,role="secondary"]
279+
----
280+
@Component
281+
class FailureEvents {
282+
@EventListener
283+
fun onFailure(badCredentials: AuthenticationFailureBadCredentialsEvent) {
284+
if (badCredentials.authentication is BearerTokenAuthenticationToken) {
285+
// ... handle
286+
}
287+
}
288+
}
289+
----
290+
====
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
[[oauth2resourceserver]]
2+
= OAuth 2.0 Resource Server
3+
:figures: servlet/oauth2
4+
5+
Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]:
6+
7+
* https://tools.ietf.org/html/rfc7519[JWT]
8+
* Opaque Tokens
9+
10+
This is handy in circumstances where an application has delegated its authority management to an https://tools.ietf.org/html/rfc6749[authorization server] (for example, Okta or Ping Identity).
11+
This authorization server can be consulted by resource servers to authorize requests.
12+
13+
This section provides details on how Spring Security provides support for OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens].
14+
15+
[NOTE]
16+
====
17+
Working samples for both {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/jwe[JWTs] and {gh-samples-url}/servlet/spring-boot/java/oauth2/resource-server/opaque[Opaque Tokens] are available in the {gh-samples-url}[Spring Security Samples repository].
18+
====
19+
20+
Let's take a look at how Bearer Token Authentication works within Spring Security.
21+
First, we see that, like xref:servlet/authentication/passwords/basic.adoc#servlet-authentication-basic[Basic Authentication], the https://tools.ietf.org/html/rfc7235#section-4.1[WWW-Authenticate] header is sent back to an unauthenticated client.
22+
23+
.Sending WWW-Authenticate Header
24+
image::{figures}/bearerauthenticationentrypoint.png[]
25+
26+
The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
27+
28+
image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized.
29+
30+
image:{icondir}/number_2.png[] Spring Security's xref:servlet/authorization/authorize-requests.adoc#servlet-authorization-filtersecurityinterceptor[`FilterSecurityInterceptor`] indicates that the unauthenticated request is __Denied__ by throwing an `AccessDeniedException`.
31+
32+
image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__.
33+
The configured xref:servlet/authentication/architecture.adoc#servlet-authentication-authenticationentrypoint[`AuthenticationEntryPoint`] is an instance of {security-api-url}org/springframework/security/oauth2/server/resource/web/BearerTokenAuthenticationEntryPoint.html[`BearerTokenAuthenticationEntryPoint`] which sends a WWW-Authenticate header.
34+
The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested.
35+
36+
When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token.
37+
Below is the flow for the bearer token being processed.
38+
39+
[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
40+
.Authenticating Bearer Token
41+
image::{figures}/bearertokenauthenticationfilter.png[]
42+
43+
The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.
44+
45+
image:{icondir}/number_1.png[] When the user submits their bearer token, the `BearerTokenAuthenticationFilter` creates a `BearerTokenAuthenticationToken` which is a type of xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[`Authentication`] by extracting the token from the `HttpServletRequest`.
46+
47+
image:{icondir}/number_2.png[] Next, the `HttpServletRequest` is passed to the `AuthenticationManagerResolver`, which selects the `AuthenticationManager`. The `BearerTokenAuthenticationToken` is passed into the `AuthenticationManager` to be authenticated.
48+
The details of what `AuthenticationManager` looks like depends on whether you're configured for xref:servlet/oauth2/resource-server/jwt.adoc#oauth2resourceserver-jwt-minimalconfiguration[JWT] or xref:servlet/oauth2/resource-server/opaque-token.adoc#oauth2resourceserver-opaque-minimalconfiguration[opaque token].
49+
50+
image:{icondir}/number_3.png[] If authentication fails, then __Failure__
51+
52+
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out.
53+
* The `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate header to be sent again.
54+
55+
image:{icondir}/number_4.png[] If authentication is successful, then __Success__.
56+
57+
* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
58+
* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic.

0 commit comments

Comments
 (0)