Token Exchange support in Spring Security 6.3.0-M3

Engineering | Steve Riesenberg | March 19, 2024 | ...

I'm excited to share that the there will be support for the OAuth 2.0 Token Exchange Grant (RFC 8693) in Spring Security 6.3, which is available for preview now in the latest milestone (6.3.0-M3). This support provides the ability to use Token Exchange with OAuth2 Client. Similarly, server-side support is also shipping with Spring Authorization Server in 1.3 and is available for preview now in the latest milestone (1.3.0-M3).

OAuth2 Client features of Spring Security allow us to easily make protected resources requests to an API secured with OAuth2 bearer tokens. Similarly, OAuth2 Resource Server features of Spring Security allow us to secure an API with OAuth2. Let's take a look at how we can use the new support to build OAuth2 flows with Token Exchange.

An example

Let's imagine we have a resource server called user-service providing an API to access user information. In order to make requests to user-service, clients must provide an access token. Let's assume tokens must have an audience (aud claim) of user-service. This might look like the following as Spring Boot configuration properties:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://my-auth-server.com
          audiences: user-service

Now let's imagine we want to introduce a new resource server called message-service and call it from user-service. Let's assume then that tokens for this new service must have an audience of message-service. Clearly we can't re-use the token from a request to user-service in a request to message-service. However, we'd like the identity of the user from the original request to be preserved. How would we accomplish this?

In order to obtain the necessary access token for message-service, the resource server user-service must become a client and exchange an existing token for a new one that retains the identity (user) of the original token. This is called "impersonation" and is exactly the kind of scenario OAuth 2.0 Token Exchange is designed for.

Configuring the resource server as a client

To enable Token Exchange, we need to configure user-service to act as both a resource server and a client that can use Token Exchange, as the following Spring Boot configuration properties show:

spring:
  security:
    oauth2:
      resourceserver:
        jwt:
          issuer-uri: https://my-auth-server.com
          audiences: user-service
      client:
        registration:
          my-token-exchange-client:
            provider: my-auth-server
            client-id: token-client
            client-secret: token
            authorization-grant-type: urn:ietf:params:oauth:grant-type:token-exchange
            client-authentication-method: client_secret_basic
            scope:
                - message.read
        provider:
          my-auth-server:
            issuer-uri: https://my-auth-server.com

We also need to enable the use of the new grant type in Spring Security, which we can do by publishing the following bean:

    @Bean
    public OAuth2AuthorizedClientProvider tokenExchange() {
        return new TokenExchangeOAuth2AuthorizedClientProvider();
    }

This is all that is required to begin using Token Exchange. However, if we want to request a specific audience or resource value, we need to configure additional parameters as part of the token request, as the following example shows:

    @Bean
    public OAuth2AuthorizedClientProvider tokenExchange() {
        var requestEntityConverter = new TokenExchangeGrantRequestEntityConverter();
        requestEntityConverter.addParametersConverter((grantRequest) -> {
            var parameters = new LinkedMultiValueMap<String, String>();
            parameters.add(OAuth2ParameterNames.AUDIENCE, "message-service");
            parameters.add(OAuth2ParameterNames.RESOURCE, "https://example.com/messages");

            return parameters;
        });

        var accessTokenResponseClient = new DefaultTokenExchangeTokenResponseClient();
        accessTokenResponseClient.setRequestEntityConverter(requestEntityConverter);

        var authorizedClientProvider = new TokenExchangeOAuth2AuthorizedClientProvider();
        authorizedClientProvider.setAccessTokenResponseClient(accessTokenResponseClient);

        return authorizedClientProvider;
    }

With this configuration in place, we can obtain an access token in one resource server and use it as a Bearer token in a protected resources request to another resource server. The original bearer token passed to the resource server in the Authorization header will be used by default to obtain the new access token.

TIP: See Authorized Client Features in the reference documentation for more information on how to obtain an access token and make a protected resources request with this configuration.

Enabling Token Exchange on the server

To complete the picture, let's build a brand new authorization server application with Spring Authorization Server to support this flow.

Using Spring Initializr with the OAuth2 Authorization Server dependency, we can configure a fully functional authorization server using the following Spring Boot configuration properties:

spring:
  security:
    user:
      name: sally
      password: password
    oauth2:
      authorizationserver:
        client:
          test-client:
            registration:
              client-id: test-client
              client-secret: {noop}secret
              client-authentication-methods:
                - client_secret_basic
              authorization-grant-types:
                - authorization_code
                - refresh_token
              scopes:
                - user.read
          token-client:
            registration:
              client-id: token-client
              client-secret: {noop}token
              client-authentication-methods:
                - client_secret_basic
              authorization-grant-types:
                - urn:ietf:params:oauth:grant-type:token-exchange
              scopes:
                - message.read

As with the client, we may want to support specific request parameters for Token Exchange such as audience or resource. The audience parameter is supported out of the box. To support other parameters such as the resource parameter, we can publish the following bean:

	@Bean
	public OAuth2TokenCustomizer<JwtEncodingContext> accessTokenCustomizer() {
		return (context) -> {
			if (context.getPrincipal() instanceof OAuth2TokenExchangeAuthenticationToken tokenExchangeRequest) {
				var resources = tokenExchangeRequest.getResources();
				// TODO: Validate resource value(s) and map to the
				// appropriate audience value(s) if needed...

				context.getClaims().audience(...);
			}
		};
	}

With this configuration in place, the authorization server supports the Token Exchange grant with the optional resource parameter of the OAuth 2.0 Token Request, and is able to issue tokens allowing a resource server to act as a client and impersonate an end user.

Conclusion

In this blog post, we have discussed the "impersonation" use case for Token Exchange and explored a simple configuration for both a resource server (acting as a client) and an authorization server.

TIP: See Appendix A of RFC 8693 for additional examples including an example of an additional use case called "delegation" which is also supported.

I hope you are as excited as I am about this new support! I encourage you to try out the samples in Spring Authorization Server which includes a working example of this blog post. Please also try the milestones of both Spring Security and Spring Authorization Server in your own project. We would love your feedback!

Get the Spring newsletter

Thank you for your interest. Someone will get back to you shortly.

Get ahead

VMware offers training and certification to turbo-charge your progress.

Learn more

Get support

Tanzu Spring Runtime offers support and binaries for OpenJDK™, Spring, and Apache Tomcat® in one simple subscription.

Learn more

Upcoming events

Check out all the upcoming events in the Spring community.

View all