Skip to content

Commit

Permalink
Separate Resource Server Servlet Docs
Browse files Browse the repository at this point in the history
Issue gh-10367
  • Loading branch information
jzheaux committed Oct 27, 2021
1 parent d40e8f6 commit 4b0e74a
Show file tree
Hide file tree
Showing 9 changed files with 3,072 additions and 3,063 deletions.
6 changes: 5 additions & 1 deletion docs/modules/ROOT/nav.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,11 @@
** xref:servlet/oauth2/index.adoc[OAuth2]
*** xref:servlet/oauth2/oauth2-login.adoc[OAuth2 Log In]
*** xref:servlet/oauth2/oauth2-client.adoc[OAuth2 Client]
*** xref:servlet/oauth2/oauth2-resourceserver.adoc[OAuth2 Resource Server]
*** xref:servlet/oauth2/resource-server/index.adoc[OAuth2 Resource Server]
**** xref:servlet/oauth2/resource-server/jwt.adoc[JWT]
**** xref:servlet/oauth2/resource-server/opaque-token.adoc[Opaque Token]
**** xref:servlet/oauth2/resource-server/multitenancy.adoc[Multitenancy]
**** xref:servlet/oauth2/resource-server/bearer-tokens.adoc[Bearer Tokens]
** xref:servlet/saml2/index.adoc[SAML2]
** xref:servlet/exploits/index.adoc[Protection Against Exploits]
*** xref:servlet/exploits/csrf.adoc[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ fun jwtDecoder(): ReactiveJwtDecoder {

[[webflux-oauth2resourceserver-opaque-minimaldependencies]]
=== Minimal Dependencies for Introspection
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`.
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`.
However unless a custom <<webflux-oauth2resourceserver-opaque-introspector-bean,`ReactiveOpaqueTokenIntrospector`>> is provided, the Resource Server will fallback to ReactiveOpaqueTokenIntrospector.
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.
Please refer to `spring-security-oauth2-resource-server` in order to determin the correct version for `oauth2-oidc-sdk`.
Expand Down
5 changes: 2 additions & 3 deletions docs/modules/ROOT/pages/servlet/oauth2/index.adoc
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
= OAuth2
:page-section-summary-toc: 1

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

* xref:servlet/oauth2/oauth2-login.adoc[]
* xref:servlet/oauth2/oauth2-client.adoc[]
* xref:servlet/oauth2/oauth2-resourceserver.adoc[]

3,058 changes: 0 additions & 3,058 deletions docs/modules/ROOT/pages/servlet/oauth2/oauth2-resourceserver.adoc

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
= OAuth 2.0 Bearer Tokens

[[oauth2resourceserver-bearertoken-resolver]]
== Bearer Token Resolution

By default, Resource Server looks for a bearer token in the `Authorization` header.
This, however, can be customized in a handful of ways.

=== Reading the Bearer Token from a Custom Header

For example, you may have a need to read the bearer token from a custom header.
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:

.Custom Bearer Token Header
====
.Java
[source,java,role="primary"]
----
@Bean
BearerTokenResolver bearerTokenResolver() {
DefaultBearerTokenResolver bearerTokenResolver = new DefaultBearerTokenResolver();
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION);
return bearerTokenResolver;
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun bearerTokenResolver(): BearerTokenResolver {
val bearerTokenResolver = DefaultBearerTokenResolver()
bearerTokenResolver.setBearerTokenHeaderName(HttpHeaders.PROXY_AUTHORIZATION)
return bearerTokenResolver
}
----
.Xml
[source,xml,role="secondary"]
----
<http>
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>
<bean id="bearerTokenResolver"
class="org.springframework.security.oauth2.server.resource.web.DefaultBearerTokenResolver">
<property name="bearerTokenHeaderName" value="Proxy-Authorization"/>
</bean>
----
====

Or, in circumstances where a provider is using both a custom header and value, you can use `HeaderBearerTokenResolver` instead.

=== Reading the Bearer Token from a Form Parameter

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:

.Form Parameter Bearer Token
====
.Java
[source,java,role="primary"]
----
DefaultBearerTokenResolver resolver = new DefaultBearerTokenResolver();
resolver.setAllowFormEncodedBodyParameter(true);
http
.oauth2ResourceServer(oauth2 -> oauth2
.bearerTokenResolver(resolver)
);
----
.Kotlin
[source,kotlin,role="secondary"]
----
val resolver = DefaultBearerTokenResolver()
resolver.setAllowFormEncodedBodyParameter(true)
http {
oauth2ResourceServer {
bearerTokenResolver = resolver
}
}
----
.Xml
[source,xml,role="secondary"]
----
<http>
<oauth2-resource-server bearer-token-resolver-ref="bearerTokenResolver"/>
</http>
<bean id="bearerTokenResolver"
class="org.springframework.security.oauth2.server.resource.web.HeaderBearerTokenResolver">
<property name="allowFormEncodedBodyParameter" value="true"/>
</bean>
----
====

== Bearer Token Propagation

Now that you're resource server has validated the token, it might be handy to pass it to downstream services.
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:

====
.Java
[source,java,role="primary"]
----
@Bean
public WebClient rest() {
return WebClient.builder()
.filter(new ServletBearerExchangeFilterFunction())
.build();
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun rest(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
----
====

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.
Then, it will propagate that token in the `Authorization` header.

For example:

====
.Java
[source,java,role="primary"]
----
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono(String.class)
.block()
----
.Kotlin
[source,kotlin,role="secondary"]
----
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.retrieve()
.bodyToMono<String>()
.block()
----
====

Will invoke the `https://other-service.example.com/endpoint`, adding the bearer token `Authorization` header for you.

In places where you need to override this behavior, it's a simple matter of supplying the header yourself, like so:

====
.Java
[source,java,role="primary"]
----
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers(headers -> headers.setBearerAuth(overridingToken))
.retrieve()
.bodyToMono(String.class)
.block()
----
.Kotlin
[source,kotlin,role="secondary"]
----
this.rest.get()
.uri("https://other-service.example.com/endpoint")
.headers{ headers -> headers.setBearerAuth(overridingToken)}
.retrieve()
.bodyToMono<String>()
.block()
----
====

In this case, the filter will fall back and simply forward the request onto the rest of the web filter chain.

[NOTE]
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.
To obtain this level of support, please use the OAuth 2.0 Client filter.

=== `RestTemplate` support

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:

====
.Java
[source,java,role="primary"]
----
@Bean
RestTemplate rest() {
RestTemplate rest = new RestTemplate();
rest.getInterceptors().add((request, body, execution) -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return execution.execute(request, body);
}
if (!(authentication.getCredentials() instanceof AbstractOAuth2Token)) {
return execution.execute(request, body);
}
AbstractOAuth2Token token = (AbstractOAuth2Token) authentication.getCredentials();
request.getHeaders().setBearerAuth(token.getTokenValue());
return execution.execute(request, body);
});
return rest;
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Bean
fun rest(): RestTemplate {
val rest = RestTemplate()
rest.interceptors.add(ClientHttpRequestInterceptor { request, body, execution ->
val authentication: Authentication? = SecurityContextHolder.getContext().authentication
if (authentication != null) {
execution.execute(request, body)
}
if (authentication!!.credentials !is AbstractOAuth2Token) {
execution.execute(request, body)
}
val token: AbstractOAuth2Token = authentication.credentials as AbstractOAuth2Token
request.headers.setBearerAuth(token.tokenValue)
execution.execute(request, body)
})
return rest
}
----
====


[NOTE]
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.
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].

[[oauth2resourceserver-bearertoken-failure]]
== Bearer Token Failure

A bearer token may be invalid for a number of reasons. For example, the token may no longer be active.

In these circumstances, Resource Server throws an `InvalidBearerTokenException`.
Like other exceptions, this results in an OAuth 2.0 Bearer Token error response:

[source,http request]
----
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer error_code="invalid_token", error_description="Unsupported algorithm of none", error_uri="https://tools.ietf.org/html/rfc6750#section-3.1"
----

Additionally, it is published as an `AuthenticationFailureBadCredentialsEvent`, which you can xref:servlet/authentication/events.adoc#servlet-events[listen for in your application] like so:

====
.Java
[source,java,role="primary"]
----
@Component
public class FailureEvents {
@EventListener
public void onFailure(AuthenticationFailureBadCredentialsEvent badCredentials) {
if (badCredentials.getAuthentication() instanceof BearerTokenAuthenticationToken) {
// ... handle
}
}
}
----
.Kotlin
[source,kotlin,role="secondary"]
----
@Component
class FailureEvents {
@EventListener
fun onFailure(badCredentials: AuthenticationFailureBadCredentialsEvent) {
if (badCredentials.authentication is BearerTokenAuthenticationToken) {
// ... handle
}
}
}
----
====
58 changes: 58 additions & 0 deletions docs/modules/ROOT/pages/servlet/oauth2/resource-server/index.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[[oauth2resourceserver]]
= OAuth 2.0 Resource Server
:figures: servlet/oauth2

Spring Security supports protecting endpoints using two forms of OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens]:

* https://tools.ietf.org/html/rfc7519[JWT]
* Opaque Tokens

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).
This authorization server can be consulted by resource servers to authorize requests.

This section provides details on how Spring Security provides support for OAuth 2.0 https://tools.ietf.org/html/rfc6750.html[Bearer Tokens].

[NOTE]
====
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].
====

Let's take a look at how Bearer Token Authentication works within Spring Security.
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.

.Sending WWW-Authenticate Header
image::{figures}/bearerauthenticationentrypoint.png[]

The figure above builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.

image:{icondir}/number_1.png[] First, a user makes an unauthenticated request to the resource `/private` for which it is not authorized.

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`.

image:{icondir}/number_3.png[] Since the user is not authenticated, xref:servlet/architecture.adoc#servlet-exceptiontranslationfilter[`ExceptionTranslationFilter`] initiates __Start Authentication__.
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.
The `RequestCache` is typically a `NullRequestCache` that does not save the request since the client is capable of replaying the requests it originally requested.

When a client receives the `WWW-Authenticate: Bearer` header, it knows it should retry with a bearer token.
Below is the flow for the bearer token being processed.

[[oauth2resourceserver-authentication-bearertokenauthenticationfilter]]
.Authenticating Bearer Token
image::{figures}/bearertokenauthenticationfilter.png[]

The figure builds off our xref:servlet/architecture.adoc#servlet-securityfilterchain[`SecurityFilterChain`] diagram.

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`.

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.
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].

image:{icondir}/number_3.png[] If authentication fails, then __Failure__

* The xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder] is cleared out.
* The `AuthenticationEntryPoint` is invoked to trigger the WWW-Authenticate header to be sent again.

image:{icondir}/number_4.png[] If authentication is successful, then __Success__.

* The xref:servlet/authentication/architecture.adoc#servlet-authentication-authentication[Authentication] is set on the xref:servlet/authentication/architecture.adoc#servlet-authentication-securitycontextholder[SecurityContextHolder].
* The `BearerTokenAuthenticationFilter` invokes `FilterChain.doFilter(request,response)` to continue with the rest of the application logic.
Loading

0 comments on commit 4b0e74a

Please sign in to comment.