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 cdb65f3a5b..0905c913fc 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 @@ -93,7 +93,9 @@ The `OAuth2AuthorizedClientManager` is responsible for managing the authorizatio The following code shows an example of how to register an `OAuth2AuthorizedClientManager` `@Bean` and associate it with an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -117,6 +119,27 @@ public OAuth2AuthorizedClientManager authorizedClientManager( } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider: OAuth2AuthorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + The following sections will go into more detail on the core components used by OAuth 2.0 Client and the configuration options available: * <> @@ -206,12 +229,21 @@ A `ClientRegistration` can be initially configured using discovery of an OpenID `ClientRegistrations` provides convenience methods for configuring a `ClientRegistration` in this way, as can be seen in the following example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- ClientRegistration clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build(); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val clientRegistration = ClientRegistrations.fromIssuerLocation("https://idp.example.com/issuer").build() +---- +==== + The above code will query in series `https://idp.example.com/issuer/.well-known/openid-configuration`, and then `https://idp.example.com/.well-known/openid-configuration/issuer`, and finally `https://idp.example.com/.well-known/oauth-authorization-server/issuer`, stopping at the first to return a 200 response. As an alternative, you can use `ClientRegistrations.fromOidcIssuerLocation()` to only query the OpenID Connect Provider's Configuration endpoint. @@ -234,7 +266,9 @@ The auto-configuration also registers the `ClientRegistrationRepository` as a `@ The following listing shows an example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Controller public class OAuth2ClientController { @@ -254,6 +288,27 @@ public class OAuth2ClientController { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var clientRegistrationRepository: ClientRegistrationRepository + + @GetMapping("/") + fun index(): String { + val oktaRegistration = + this.clientRegistrationRepository.findByRegistrationId("okta") + + //... + + return "index"; + } +} +---- +==== [[oauth2Client-authorized-client]] ==== OAuth2AuthorizedClient @@ -274,7 +329,9 @@ From a developer perspective, the `OAuth2AuthorizedClientRepository` or `OAuth2A The following listing shows an example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Controller public class OAuth2ClientController { @@ -296,6 +353,29 @@ public class OAuth2ClientController { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientService: OAuth2AuthorizedClientService + + @GetMapping("/") + fun index(authentication: Authentication): String { + val authorizedClient: OAuth2AuthorizedClient = + this.authorizedClientService.loadAuthorizedClient("okta", authentication.getName()); + val accessToken = authorizedClient.accessToken + + ... + + return "index"; + } +} +---- +==== + [NOTE] Spring Boot 2.x auto-configuration registers an `OAuth2AuthorizedClientRepository` and/or `OAuth2AuthorizedClientService` `@Bean` in the `ApplicationContext`. However, the application may choose to override and register a custom `OAuth2AuthorizedClientRepository` or `OAuth2AuthorizedClientService` `@Bean`. @@ -328,7 +408,9 @@ The `OAuth2AuthorizedClientProviderBuilder` may be used to configure and build t The following code shows an example of how to configure and build an `OAuth2AuthorizedClientProvider` composite that provides support for the `authorization_code`, `refresh_token`, `client_credentials` and `password` authorization grant types: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -352,6 +434,27 @@ public OAuth2AuthorizedClientManager authorizedClientManager( } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken() + .clientCredentials() + .password() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + When an authorization attempt succeeds, the `DefaultOAuth2AuthorizedClientManager` will delegate to the `OAuth2AuthorizationSuccessHandler`, which (by default) will save the `OAuth2AuthorizedClient` via the `OAuth2AuthorizedClientRepository`. In the case of a re-authorization failure, eg. a refresh token is no longer valid, the previously saved `OAuth2AuthorizedClient` will be removed from the `OAuth2AuthorizedClientRepository` via the `RemoveAuthorizedClientOAuth2AuthorizationFailureHandler`. The default behaviour may be customized via `setAuthorizationSuccessHandler(OAuth2AuthorizationSuccessHandler)` and `setAuthorizationFailureHandler(OAuth2AuthorizationFailureHandler)`. @@ -361,7 +464,9 @@ This can be useful when you need to supply an `OAuth2AuthorizedClientProvider` w The following code shows an example of the `contextAttributesMapper`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -404,6 +509,46 @@ private Function> contextAttributesM } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) + val username: String = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) + val password: String = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password + } + contextAttributes + } +} +---- +==== + The `DefaultOAuth2AuthorizedClientManager` is designed to be used *_within_* the context of a `HttpServletRequest`. When operating *_outside_* of a `HttpServletRequest` context, use `AuthorizedClientServiceOAuth2AuthorizedClientManager` instead. @@ -413,7 +558,9 @@ An OAuth 2.0 Client configured with the `client_credentials` grant type can be c The following code shows an example of how to configure an `AuthorizedClientServiceOAuth2AuthorizedClientManager` that provides support for the `client_credentials` grant type: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -434,6 +581,24 @@ public OAuth2AuthorizedClientManager authorizedClientManager( } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientService: OAuth2AuthorizedClientService): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = AuthorizedClientServiceOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientService) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + [[oauth2Client-auth-grant-support]] === Authorization Grant Support @@ -545,7 +710,9 @@ OPTIONAL. Space delimited, case sensitive list of ASCII string values that speci The following example shows how to configure the `DefaultOAuth2AuthorizationRequestResolver` with a `Consumer` that customizes the Authorization Request for `oauth2Login()`, by including the request parameter `prompt=consent`. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @EnableWebSecurity public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { @@ -587,6 +754,47 @@ public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@EnableWebSecurity +class SecurityConfig : WebSecurityConfigurerAdapter() { + + @Autowired + private lateinit var customClientRegistrationRepository: ClientRegistrationRepository + + override fun configure(http: HttpSecurity) { + http { + authorizeRequests { + authorize(anyRequest, authenticated) + } + oauth2Login { + authorizationEndpoint { + authorizationRequestResolver = authorizationRequestResolver(customClientRegistrationRepository) + } + } + } + } + + private fun authorizationRequestResolver( + clientRegistrationRepository: ClientRegistrationRepository?): OAuth2AuthorizationRequestResolver? { + val authorizationRequestResolver = DefaultOAuth2AuthorizationRequestResolver( + clientRegistrationRepository, "/oauth2/authorization") + authorizationRequestResolver.setAuthorizationRequestCustomizer( + authorizationRequestCustomizer()) + return authorizationRequestResolver + } + + private fun authorizationRequestCustomizer(): Consumer { + return Consumer { customizer -> + customizer + .additionalParameters { params -> params["prompt"] = "consent" } + } + } +} +---- +==== + For the simple use case, where the additional request parameter is always the same for a specific provider, it may be added directly in the `authorization-uri` property. For example, if the value for the request parameter `prompt` is always `consent` for the provider `okta`, than simply configure as follows: @@ -610,7 +818,9 @@ Alternatively, if your requirements are more advanced, you can take full control The following example shows a variation of `authorizationRequestCustomizer()` from the preceding example, and instead overrides the `OAuth2AuthorizationRequest.authorizationRequestUri` property. -[source,java] +==== +.Java +[source,java,role="primary"] ---- private Consumer authorizationRequestCustomizer() { return customizer -> customizer @@ -619,6 +829,21 @@ private Consumer authorizationRequestCustomi } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +private fun authorizationRequestCustomizer(): Consumer { + return Consumer { customizer: OAuth2AuthorizationRequest.Builder -> + customizer + .authorizationRequestUri { uriBuilder: UriBuilder -> + uriBuilder + .queryParam("prompt", "consent").build() + } + } +} +---- +==== + ===== Storing the Authorization Request @@ -705,7 +930,9 @@ IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representa On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultAuthorizationCodeTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), @@ -714,6 +941,17 @@ RestTemplate restTemplate = new RestTemplate(Arrays.asList( 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. @@ -806,7 +1044,9 @@ IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representa On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultRefreshTokenTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), @@ -815,6 +1055,17 @@ RestTemplate restTemplate = new RestTemplate(Arrays.asList( 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. @@ -825,7 +1076,9 @@ It uses an `OAuth2ErrorHttpMessageConverter` for converting the OAuth 2.0 Error Whether you customize `DefaultRefreshTokenTokenResponseClient` or provide your own implementation of `OAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- // Customize OAuth2AccessTokenResponseClient refreshTokenTokenResponseClient = ... @@ -841,6 +1094,23 @@ OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Customize +val refreshTokenTokenResponseClient: OAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .authorizationCode() + .refreshToken { it.accessTokenResponseClient(refreshTokenTokenResponseClient) } + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +---- +==== + [NOTE] `OAuth2AuthorizedClientProviderBuilder.builder().refreshToken()` configures a `RefreshTokenOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Refresh Token grant. @@ -880,7 +1150,9 @@ IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representa On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultClientCredentialsTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), @@ -889,6 +1161,17 @@ RestTemplate restTemplate = new RestTemplate(Arrays.asList( 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. @@ -899,7 +1182,9 @@ It uses an `OAuth2ErrorHttpMessageConverter` for converting the OAuth 2.0 Error Whether you customize `DefaultClientCredentialsTokenResponseClient` or provide your own implementation of `OAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- // Customize OAuth2AccessTokenResponseClient clientCredentialsTokenResponseClient = ... @@ -914,6 +1199,22 @@ OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +// Customize +val clientCredentialsTokenResponseClient: OAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials { it.accessTokenResponseClient(clientCredentialsTokenResponseClient) } + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +---- +==== + [NOTE] `OAuth2AuthorizedClientProviderBuilder.builder().clientCredentials()` configures a `ClientCredentialsOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Client Credentials grant. @@ -941,7 +1242,9 @@ spring: ...and the `OAuth2AuthorizedClientManager` `@Bean`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -962,9 +1265,29 @@ public OAuth2AuthorizedClientManager authorizedClientManager( } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .clientCredentials() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + return authorizedClientManager +} +---- +==== + You may obtain the `OAuth2AccessToken` as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Controller public class OAuth2ClientController { @@ -995,6 +1318,36 @@ public class OAuth2ClientController { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +class OAuth2ClientController { + + @Autowired + private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager + + @GetMapping("/") + fun index(authentication: Authentication?, + servletRequest: HttpServletRequest, + servletResponse: HttpServletResponse): String { + val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attributes(Consumer { attrs: MutableMap -> + attrs[HttpServletRequest::class.java.name] = servletRequest + attrs[HttpServletResponse::class.java.name] = servletResponse + }) + .build() + val authorizedClient = authorizedClientManager.authorize(authorizeRequest) + val accessToken: OAuth2AccessToken = authorizedClient.accessToken + + ... + + return "index" + } +} +---- +==== + [NOTE] `HttpServletRequest` and `HttpServletResponse` are both OPTIONAL attributes. If not provided, it will default to `ServletRequestAttributes` using `RequestContextHolder.getRequestAttributes()`. @@ -1031,7 +1384,9 @@ IMPORTANT: The custom `Converter` must return a valid `RequestEntity` representa On the other end, if you need to customize the post-handling of the Token Response, you will need to provide `DefaultPasswordTokenResponseClient.setRestOperations()` with a custom configured `RestOperations`. The default `RestOperations` is configured as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- RestTemplate restTemplate = new RestTemplate(Arrays.asList( new FormHttpMessageConverter(), @@ -1040,6 +1395,17 @@ RestTemplate restTemplate = new RestTemplate(Arrays.asList( 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. @@ -1050,7 +1416,9 @@ It uses an `OAuth2ErrorHttpMessageConverter` for converting the OAuth 2.0 Error Whether you customize `DefaultPasswordTokenResponseClient` or provide your own implementation of `OAuth2AccessTokenResponseClient`, you'll need to configure it as shown in the following example: -[source,java] +==== +.Java +[source,java,role="primary"] ---- // Customize OAuth2AccessTokenResponseClient passwordTokenResponseClient = ... @@ -1066,6 +1434,22 @@ OAuth2AuthorizedClientProvider authorizedClientProvider = authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider); ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +val passwordTokenResponseClient: OAuth2AccessTokenResponseClient = ... + +val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .password { it.accessTokenResponseClient(passwordTokenResponseClient) } + .refreshToken() + .build() + +... + +authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) +---- +==== + [NOTE] `OAuth2AuthorizedClientProviderBuilder.builder().password()` configures a `PasswordOAuth2AuthorizedClientProvider`, which is an implementation of an `OAuth2AuthorizedClientProvider` for the Resource Owner Password Credentials grant. @@ -1093,7 +1477,9 @@ spring: ...and the `OAuth2AuthorizedClientManager` `@Bean`: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean public OAuth2AuthorizedClientManager authorizedClientManager( @@ -1135,10 +1521,51 @@ private Function> contextAttributesM }; } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun authorizedClientManager( + clientRegistrationRepository: ClientRegistrationRepository, + authorizedClientRepository: OAuth2AuthorizedClientRepository): OAuth2AuthorizedClientManager { + val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder.builder() + .password() + .refreshToken() + .build() + val authorizedClientManager = DefaultOAuth2AuthorizedClientManager( + clientRegistrationRepository, authorizedClientRepository) + authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider) + + // Assuming the `username` and `password` are supplied as `HttpServletRequest` parameters, + // map the `HttpServletRequest` parameters to `OAuth2AuthorizationContext.getAttributes()` + authorizedClientManager.setContextAttributesMapper(contextAttributesMapper()) + return authorizedClientManager +} + +private fun contextAttributesMapper(): Function> { + return Function { authorizeRequest -> + var contextAttributes: MutableMap = mutableMapOf() + val servletRequest: HttpServletRequest = authorizeRequest.getAttribute(HttpServletRequest::class.java.name) + val username = servletRequest.getParameter(OAuth2ParameterNames.USERNAME) + val password = servletRequest.getParameter(OAuth2ParameterNames.PASSWORD) + if (StringUtils.hasText(username) && StringUtils.hasText(password)) { + contextAttributes = hashMapOf() + + // `PasswordOAuth2AuthorizedClientProvider` requires both attributes + contextAttributes[OAuth2AuthorizationContext.USERNAME_ATTRIBUTE_NAME] = username + contextAttributes[OAuth2AuthorizationContext.PASSWORD_ATTRIBUTE_NAME] = password + } + contextAttributes + } +} +---- +==== You may obtain the `OAuth2AccessToken` as follows: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Controller public class OAuth2ClientController { @@ -1169,6 +1596,36 @@ public class OAuth2ClientController { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @Autowired + private lateinit var authorizedClientManager: OAuth2AuthorizedClientManager + + @GetMapping("/") + fun index(authentication: Authentication?, + servletRequest: HttpServletRequest, + servletResponse: HttpServletResponse): String { + val authorizeRequest: OAuth2AuthorizeRequest = OAuth2AuthorizeRequest.withClientRegistrationId("okta") + .principal(authentication) + .attributes(Consumer { + it[HttpServletRequest::class.java.name] = servletRequest + it[HttpServletResponse::class.java.name] = servletResponse + }) + .build() + val authorizedClient = authorizedClientManager.authorize(authorizeRequest) + val accessToken: OAuth2AccessToken = authorizedClient.accessToken + + ... + + return "index" + } +} +---- +==== + [NOTE] `HttpServletRequest` and `HttpServletResponse` are both OPTIONAL attributes. If not provided, it will default to `ServletRequestAttributes` using `RequestContextHolder.getRequestAttributes()`. @@ -1184,7 +1641,9 @@ If not provided, it will default to `ServletRequestAttributes` using `RequestCon The `@RegisteredOAuth2AuthorizedClient` annotation provides the capability of resolving a method parameter to an argument value of type `OAuth2AuthorizedClient`. This is a convenient alternative compared to accessing the `OAuth2AuthorizedClient` using the `OAuth2AuthorizedClientManager` or `OAuth2AuthorizedClientService`. -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Controller public class OAuth2ClientController { @@ -1200,6 +1659,23 @@ public class OAuth2ClientController { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Controller +class OAuth2ClientController { + @GetMapping("/") + fun index(@RegisteredOAuth2AuthorizedClient("okta") authorizedClient: OAuth2AuthorizedClient): String { + val accessToken = authorizedClient.accessToken + + ... + + return "index" + } +} +---- +==== + The `@RegisteredOAuth2AuthorizedClient` annotation is handled by `OAuth2AuthorizedClientArgumentResolver`, which directly uses an <> and therefore inherits it's capabilities. @@ -1219,7 +1695,9 @@ It directly uses an < + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== + <1> `oauth2AuthorizedClient()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. The following code shows how to set the `ClientRegistration.getRegistrationId()` as a request attribute: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @GetMapping("/") public String index() { @@ -1280,6 +1796,28 @@ public String index() { return "index"; } ---- + +.Kotlin +[source,kotlin,role="secondary"] +---- +@GetMapping("/") +fun index(): String { + val resourceUri: String = ... + + val body: String = webClient + .get() + .uri(resourceUri) + .attributes(clientRegistrationId("okta")) <1> + .retrieve() + .bodyToMono() + .block() + + ... + + return "index" +} +---- +==== <1> `clientRegistrationId()` is a `static` method in `ServletOAuth2AuthorizedClientExchangeFilterFunction`. @@ -1291,7 +1829,9 @@ If `setDefaultOAuth2AuthorizedClient(true)` is configured and the user has authe The following code shows the specific configuration: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { @@ -1304,6 +1844,20 @@ WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultOAuth2AuthorizedClient(true) + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + [WARNING] It is recommended to be cautious with this feature since all HTTP requests will receive the access token. @@ -1311,7 +1865,9 @@ Alternatively, if `setDefaultClientRegistrationId("okta")` is configured with a The following code shows the specific configuration: -[source,java] +==== +.Java +[source,java,role="primary"] ---- @Bean WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { @@ -1324,5 +1880,19 @@ WebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) { } ---- +.Kotlin +[source,kotlin,role="secondary"] +---- +@Bean +fun webClient(authorizedClientManager: OAuth2AuthorizedClientManager?): WebClient { + val oauth2Client = ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager) + oauth2Client.setDefaultClientRegistrationId("okta") + return WebClient.builder() + .apply(oauth2Client.oauth2Configuration()) + .build() +} +---- +==== + [WARNING] It is recommended to be cautious with this feature since all HTTP requests will receive the access token.