From f874a12ddb7f2079fc680cbfc8c0059d72f69245 Mon Sep 17 00:00:00 2001 From: Joe Grandja Date: Fri, 14 May 2021 10:17:20 -0400 Subject: [PATCH] Document jwt-bearer authorization grant Closes gh-9580 --- .../security/config/spring-security-5.5.rnc | 4 +- .../security/config/spring-security-5.5.xsd | 4 +- .../_includes/servlet/appendix/namespace.adoc | 2 +- .../servlet/oauth2/oauth2-client.adoc | 222 +++++++++++++++++- 4 files changed, 227 insertions(+), 5 deletions(-) diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc index 4d94b28a54..281f3278af 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.rnc @@ -529,8 +529,8 @@ client-registration.attlist &= ## The method used to authenticate the client with the provider. The supported values are client_secret_basic, client_secret_post and none (public clients). attribute client-authentication-method {"client_secret_basic" | "basic" | "client_secret_post" | "post" | "none"}? client-registration.attlist &= - ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials, password and implicit. - attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password" | "implicit"}? + ## The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The supported values are authorization_code, client_credentials, password, implicit, as well as, extension grant type urn:ietf:params:oauth:grant-type:jwt-bearer. + attribute authorization-grant-type {"authorization_code" | "client_credentials" | "password" | "implicit" | "urn:ietf:params:oauth:grant-type:jwt-bearer"}? client-registration.attlist &= ## The client’s registered redirect URI that the Authorization Server redirects the end-user’s user-agent to after the end-user has authenticated and authorized access to the client. attribute redirect-uri {xsd:token}? diff --git a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd index 74bb4fe019..c638082e11 100644 --- a/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd +++ b/config/src/main/resources/org/springframework/security/config/spring-security-5.5.xsd @@ -1673,7 +1673,8 @@ The OAuth 2.0 Authorization Framework defines four Authorization Grant types. The - supported values are authorization_code, client_credentials, password and implicit. + supported values are authorization_code, client_credentials, password, implicit, as well + as, extension grant type urn:ietf:params:oauth:grant-type:jwt-bearer. @@ -1682,6 +1683,7 @@ + diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc index 1ba5f4cd79..c9e840663a 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/appendix/namespace.adoc @@ -1067,7 +1067,7 @@ The supported values are *client_secret_basic*, *client_secret_post* and *none* [[nsa-client-registration-authorization-grant-type]] * **authorization-grant-type** The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. -The supported values are `authorization_code`, `client_credentials` and `password`. +The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. [[nsa-client-registration-redirect-uri]] diff --git a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc index 8b1a61f71d..c6243960fe 100644 --- a/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc +++ b/docs/manual/src/docs/asciidoc/_includes/servlet/oauth2/oauth2-client.adoc @@ -10,6 +10,7 @@ At a high-level, the core features available are: * https://tools.ietf.org/html/rfc6749#section-6[Refresh Token] * https://tools.ietf.org/html/rfc6749#section-1.3.4[Client Credentials] * https://tools.ietf.org/html/rfc6749#section-1.3.3[Resource Owner Password Credentials] +* https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[JWT Bearer] .HTTP Client support * <> (for requesting protected resources) @@ -153,6 +154,7 @@ The following sections will go into more detail on the core components used by O ** <> ** <> ** <> +** <> * <> ** <> * <> @@ -207,7 +209,7 @@ public final class ClientRegistration { <4> `clientAuthenticationMethod`: The method used to authenticate the Client with the Provider. The supported values are *client_secret_basic*, *client_secret_post* and *none* https://tools.ietf.org/html/rfc6749#section-2.1[(public clients)]. <5> `authorizationGrantType`: The OAuth 2.0 Authorization Framework defines four https://tools.ietf.org/html/rfc6749#section-1.3[Authorization Grant] types. - The supported values are `authorization_code`, `client_credentials` and `password`. + The supported values are `authorization_code`, `client_credentials`, `password`, as well as, extension grant type `urn:ietf:params:oauth:grant-type:jwt-bearer`. <6> `redirectUri`: The client's registered redirect URI that the _Authorization Server_ redirects the end-user's user-agent to after the end-user has authenticated and authorized access to the client. <7> `scopes`: The scope(s) requested by the client during the Authorization Request flow, such as openid, email, or profile. @@ -1631,6 +1633,224 @@ class OAuth2ClientController { If not provided, it will default to `ServletRequestAttributes` using `RequestContextHolder.getRequestAttributes()`. +[[oauth2Client-jwt-bearer-grant]] +==== JWT Bearer + +[NOTE] +Please refer to JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants for further details on the https://datatracker.ietf.org/doc/html/rfc7523[JWT Bearer] grant. + + +===== Requesting an Access Token + +[NOTE] +Please refer to the https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[Access Token Request/Response] protocol flow for the JWT Bearer grant. + +The default implementation of `OAuth2AccessTokenResponseClient` for the JWT Bearer grant is `DefaultJwtBearerTokenResponseClient`, which uses a `RestOperations` when requesting an access token at the Authorization Server’s Token Endpoint. + +The `DefaultJwtBearerTokenResponseClient` is quite flexible as it allows you to customize the pre-processing of the Token Request and/or post-handling of the Token Response. + + +===== Customizing the Access Token Request + +If you need to customize the pre-processing of the Token Request, you can provide `DefaultJwtBearerTokenResponseClient.setRequestEntityConverter()` with a custom `Converter>`. +The default implementation `JwtBearerGrantRequestEntityConverter` builds a `RequestEntity` representation of a https://datatracker.ietf.org/doc/html/rfc7523#section-2.1[OAuth 2.0 Access Token Request]. +However, providing a custom `Converter`, would allow you to extend the Token Request and add custom parameter(s). + + +===== Customizing the Access Token Response + +On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultJwtBearerTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. +The default `RestOperations` is configured as follows: + +==== +.Java +[source,java,role="primary"] +---- +RestTemplate restTemplate = new RestTemplate(Arrays.asList( + new FormHttpMessageConverter(), + new OAuth2AccessTokenResponseHttpMessageConverter())); + +restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler()); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +val restTemplate = RestTemplate(listOf( + FormHttpMessageConverter(), + OAuth2AccessTokenResponseHttpMessageConverter())) + +restTemplate.errorHandler = OAuth2ErrorResponseErrorHandler() +---- +==== + +TIP: Spring MVC `FormHttpMessageConverter` is required as it's used when sending the OAuth 2.0 Access Token Request. + +`OAuth2AccessTokenResponseHttpMessageConverter` is a `HttpMessageConverter` for an OAuth 2.0 Access Token Response. +You can provide `OAuth2AccessTokenResponseHttpMessageConverter.setTokenResponseConverter()` with a custom `Converter, OAuth2AccessTokenResponse>` that is used for converting the OAuth 2.0 Access Token Response parameters to an `OAuth2AccessTokenResponse`. + +`OAuth2ErrorResponseErrorHandler` is a `ResponseErrorHandler` that can handle an OAuth 2.0 Error, eg. 400 Bad Request. +It uses an `OAuth2ErrorHttpMessageConverter` for converting the OAuth 2.0 Error parameters to an `OAuth2Error`. + +Whether you customize `DefaultJwtBearerTokenResponseClient` or provide your own implementation of `OAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example: + +==== +.Java +[source,java,role="primary"] +---- +// Customize +OAuth2AccessTokenResponseClient jwtBearerTokenResponseClient = ... + +JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = new JwtBearerOAuth2AuthorizedClientProvider(); +jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient); + +OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build(); + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +// Customize +val jwtBearerTokenResponseClient: OAuth2AccessTokenResponseClient = ... + +val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider() +jwtBearerAuthorizedClientProvider.setAccessTokenResponseClient(jwtBearerTokenResponseClient); + +val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +---- +==== + +===== Using the Access Token + +Given the following Spring Boot 2.x properties for an OAuth 2.0 Client registration: + +[source,yaml] +---- +spring: + security: + oauth2: + client: + registration: + okta: + client-id: okta-client-id + client-secret: okta-client-secret + authorization-grant-type: urn:ietf:params:oauth:grant-type:jwt-bearer + scope: read + provider: + okta: + token-uri: https://dev-1234.oktapreview.com/oauth2/v1/token +---- + +...and the `OAuth2AuthorizedClientManager` `@Bean`: + +==== +.Java +[source,java,role="primary"] +---- +@Bean +public OAuth2AuthorizedClientManager authorizedClientManager( + ClientRegistrationRepository clientRegistrationRepository, + OAuth2AuthorizedClientRepository authorizedClientRepository) { + + JwtBearerOAuth2AuthorizedClientProvider jwtBearerAuthorizedClientProvider = + new JwtBearerOAuth2AuthorizedClientProvider(); + + OAuth2AuthorizedClientProvider authorizedClientProvider = + OAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build(); + + DefaultOAuth2AuthorizedClientManager authorizedClientManager = + new DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository); + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); + + return authorizedClientManager; +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val jwtBearerAuthorizedClientProvider = JwtBearerOAuth2AuthorizedClientProvider() + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .provider(jwtBearerAuthorizedClientProvider) + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + +You may obtain the `OAuth2AccessToken` as follows: + +==== +.Java +[source,java,role="primary"] +---- +@RestController +public class OAuth2ResourceServerController { + + @Autowired + private OAuth2AuthorizedClientManager authorizedClientManager; + + @GetMapping("/resource") + public String resource(JwtAuthenticationToken jwtAuthentication) { + OAuth2AuthorizeRequest authorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(jwtAuthentication) + .build(); + OAuth2AuthorizedClient authorizedClient = this.authorizedClientManager.authorize(authorizeRequest); + OAuth2AccessToken accessToken = authorizedClient.getAccessToken(); + + ... + + } +} +---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +class OAuth2ResourceServerController { + + @Autowired + private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager + + @GetMapping("/resource") + fun resource(jwtAuthentication: JwtAuthenticationToken?): String { + val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(jwtAuthentication) + .build() + val authorizedClient = authorizedClientManager.authorize(authorizeRequest) + val accessToken: OAuth2AccessToken = authorizedClient.accessToken + + ... + + } +} +---- +==== + + [[oauth2Client-additional-features]] === Additional Features