Search code examples
spring-bootspring-securityspring-oauth2spring-authorization-server

Customize OAuth2 Token with Spring OAuth2 Authorization Server 1.1.0 (Spring Boot 3.1.0)


I have created a Spring OAuth2 Authorization Server using Spring boot 3.1.0 and Spring OAuth2 Authorization Server 1.1.0. That works fine and I receive an access token using Authorization Grant Type that looks something like below:

{
"access_token": "eyJraWQiOiJiZGM5MTVlMy01ZjliLTQzYWItOTU5Yi0zYzJmNTE3YTI0NDgiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDEiLCJuYmYiOjE2ODU1NDEwOTYsInNjb3BlIjpbInJlYWQiLCJvcGVuaWQiXSwiaXNzIjoiaHR0cDovL2F1dGgtc2VydmVyOjgwMDAiLCJleHAiOjE2ODU1NDEzOTYsImlhdCI6MTY4NTU0MTA5Nn0.JtQSGR4LlYIZU0NV16ht4-LU0fRDUs9yD33NHFY1nItEc3NUs6vbV8SeSPGbmdMpxUMcr1_Xd1FpSkKrWbPPBZC10hortVrA1k550wGLVrZcknsc7sW10G718dLlJvL7qJGj4sqrqLIP1vVR8Ft3M7CdoT34Z7z6-JcHKRgmnXOP-tyvdWhRtn_OVb1o_29pTumJQ9GPSHU_Z6miOrDvOgUllWUwypw9Cg6aJJyl403P0Cl2wYye4HvP0gfosq6qbNy5OTZ4yiG0HrxrsYvNux9JIvYGbxMUhp9pNF84d3NOzvc24aDxD_VkerBV3zlfrOgLOtSstRdwLxaJ7dc-4Q",
"refresh_token": "VIAvsVUef8ljKBBPvv9gi1-DU48U77h8lZ0OBh0HO57fyGxJTppazUMOlAfnAsCrvMGc5XFVlX1Lii04YltVjF4dk-vrJWHEplhKtIehxxZXEX3HmTqaSL63pYQq9cGr",
"scope": "read openid",
"id_token": "eyJraWQiOiJiZGM5MTVlMy01ZjliLTQzYWItOTU5Yi0zYzJmNTE3YTI0NDgiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJhZG1pbiIsImF1ZCI6ImNsaWVudDEiLCJhenAiOiJjbGllbnQxIiwiYXV0aF90aW1lIjoxNjg1NTQxMDY3LCJpc3MiOiJodHRwOi8vYXV0aC1zZXJ2ZXI6ODAwMCIsImV4cCI6MTY4NTU0Mjg5NiwiaWF0IjoxNjg1NTQxMDk2LCJzaWQiOiJrUllCUjFBNWJPbDZJR0FGYnZTNHJZSDc0dG5ncHhCemh2eWNCV0ZLR1dnIn0.LIjMH6ONDGSBE2pO3sDUsPmDsstJhvQb6NPRrDZO8TAClyNpwMMRkCmPociU2Jv_rjQq8Y-zXrj016WchkGgCeakCyItzCvpTmqUDjM9tHwpG7FWuDC_GBsFstLwHqussVOG23vvy2KyNi6h8EMtbIR_aqFbDfzvknXQkAK-8Hl2ICqPfbzDkcZeomvV9J07ScqCL6iMkWw3g8ISJfvmWtiymuQ3tGa_9qJXA-JcgcZJhYGpSCbd052AxerZTMpJC4tN1afJDCfJy0HrgnChdX1wp_r9QXLKbNb1SEGRd8IUWzOLRHkOiJKqlgFx-AzuQ7sVINYjHHE1A8yHSGqGSQ",
"token_type": "Bearer",
"expires_in": 299

}

Now, As you see that it is a token with very minimum info. For example, user roles, user email, whatsoever. For that purpose I want to customise the creation of the token. To achieve this, I am trying to use OAuth2TokenCustomizer class. But for some reason it is not working.

I have created the following Bean

    @Bean
   public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
      return context -> {
         var userName = context.getPrincipal().getName();
         StringBuilder roles = new StringBuilder();
         var authorities =  context.getPrincipal().getAuthorities();
         if(OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            context.getClaims().claims(claims -> {
               claims.put("userName", userName);
               claims.put("testVal", "This is a test string");
               for (var auth:authorities) {
                  if(roles.isEmpty()){
                     roles.append(auth.getAuthority());
                  } else {
                     roles.append(" ").append(auth.getAuthority());
                  }
               }
               claims.put("roles", roles);
            });
         } };
   }

And this Bean is used in OAuth2TokenGenerator Bean like below

@Bean
   public OAuth2TokenGenerator<OAuth2Token> tokenGenerator() {
      JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
      JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
      OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
      accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
      OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
      return new DelegatingOAuth2TokenGenerator(
         jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
   }

This is not working. I tried few other options like the one given here https://github.com/spring-projects/spring-authorization-server/issues/925 (The answer by jgrandja on Oct 25, 2022), but when I do that, my app is unable to Autowire OAuth2AuthorizationService Bean

I am not sure what am I missing here. If anyone could help, I would really be grateful. My two configuration class files are below:

AuthorizationServerConfig.java

package com.auth.config;

import java.util.UUID;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import com.smarc.auth.config.keys.KeyManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.OAuth2Token;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.token.DelegatingOAuth2TokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.JwtGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2AccessTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2RefreshTokenGenerator;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenClaimsContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenGenerator;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class AuthorizationServerConfig
{
   private final KeyManager keyManager;

   public AuthorizationServerConfig(KeyManager keyManager)
   {
      this.keyManager = keyManager;
   }

   @Bean
   @Order(Ordered.HIGHEST_PRECEDENCE)
   public SecurityFilterChain customSecurityFilterChainOAuth(HttpSecurity http) throws Exception {
      OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);

      http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
      http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).tokenGenerator(tokenGenerator());
      return http.formLogin(Customizer.withDefaults()).build();
   }

   @Bean
   public RegisteredClientRepository registeredClientRepository() {
      RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
         .clientId("client1")
         .clientSecret("{noop}myClientSecretValue")
         .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
         .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
         .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
         .redirectUri("http://127.0.0.1:8080/login/oauth2/code/users-client-oidc")
         .redirectUri("http://127.0.0.1:8080/authorized")
         .scope(OidcScopes.OPENID)
         .scope("read")
         .build();

      return new InMemoryRegisteredClientRepository(registeredClient);
   }

   @Bean
   public ClientSettings clientSettings() {
      return ClientSettings.builder()
         .requireAuthorizationConsent(false)
         .requireProofKey(false)
         .build();
   }

   @Bean
   public AuthorizationServerSettings authorizationServerSettings() {
      return AuthorizationServerSettings.builder()
         .issuer("http://localhost:8080").build();
   }

   @Bean
   public JWKSource<SecurityContext> jwkSource() {
      JWKSet set = new JWKSet(keyManager.rsaKey());
      return (j, jc) -> j.select(set);
   }

   @Bean
   public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
      return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
   }

   @Bean
   public OAuth2TokenGenerator<OAuth2Token> tokenGenerator() {
      JwtEncoder jwtEncoder = new NimbusJwtEncoder(jwkSource());
      JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder);
      OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
      accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer());
      OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
      return new DelegatingOAuth2TokenGenerator(
         jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
   }

   @Bean
   public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
      return context -> {
         var userName = context.getPrincipal().getName();
         StringBuilder roles = new StringBuilder();
         var authorities =  context.getPrincipal().getAuthorities();
         if(OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
            context.getClaims().claims(claims -> {
               claims.put("userName", userName);
               claims.put("testVal", "This is a test string");
               for (var auth:authorities) {
                  if(roles.isEmpty()){
                     roles.append(auth.getAuthority());
                  } else {
                     roles.append(" ").append(auth.getAuthority());
                  }
               }
               claims.put("roles", roles);
            });
         } };
   }

}

WebSecurityConfig.java

package com.auth.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig
{
   @Bean
   public SecurityFilterChain customSecurityFilterChain(HttpSecurity http) throws Exception{
      return http
      .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
      .formLogin(Customizer.withDefaults()).build();
   }

   @Bean
   public UserDetailsService userDetailsService() {
      var externalUser = User.withUsername("external").password(passwordEncoder().encode("12345"))
         .roles("read").build();
      var internalUser = User.withUsername("internal").password(passwordEncoder().encode("12345"))
         .roles("read", "write").build();
      var admin = User.withUsername("admin").password(passwordEncoder().encode("12345"))
         .roles("read").build();
      var userDetailsService = new InMemoryUserDetailsManager();
      userDetailsService.createUser(externalUser);
      userDetailsService.createUser(internalUser);
      userDetailsService.createUser(admin);
      return userDetailsService;
   }

   @Bean
   public PasswordEncoder passwordEncoder() {
      return PasswordEncoderFactories.createDelegatingPasswordEncoder();
   }


}

Please let me know if any more info needed. Thanks in advance.


Solution

  • The bean that you’ve registered (OAuth2TokenCustomizer<OAuth2TokenClaimsContext>) is for customizing opaque tokens (token format = reference) but it looks like you’re using the default token format which is jwt (token format = self contained). If you want to use opaque tokens (which I'm assuming you are not), check the docs for how to customize the token format for each client.

    As stated in the reference, for your case you can simply register a bean of type OAuth2TokenCustomizer<JwtEncodingContext> to customize a JWT.

    Additionally, the docs also note that:

    If the OAuth2TokenGenerator is not provided as a @Bean or is not configured through the OAuth2AuthorizationServerConfigurer, an OAuth2TokenCustomizer<JwtEncodingContext> @Bean will automatically be configured with a JwtGenerator.

    Which means that if you only want to customize the token claims and not the entire process of generating tokens, you can simply publish an OAuth2TokenCustomizer @Bean, and omit the OAuth2TokenGenerator @Bean.

    Therefore, your configuration should simply have:

    @Bean
    public OAuth2TokenCustomizer<JwtEncodingContext> accessTokenCustomizer() {
        return (context) -> {
            ...
        };
    }
    

    I think this point could be made a little clearer in the docs since the example demonstrates customizing both at the same time, but the note specifies that both are not required.