35

I have seen in some oauth2 implementations additional information on the response returned by the authorization server when it issues access tokens. I'm wondering if there is a way to accomplish this using spring-security-oauth2. I would love to be able to include some user authorities on the access token response so that my consuming applications don't need to manage the user authorities but can still set the user on their own security contexts and apply any of their own spring-security checks.

  1. How would I get that information on the access token response?
  2. How would I intercept that information on the oauth2 client side and set it on the security context?

I suppose another option would be to use JWT tokens and share the appropriate information with the client applications so that they can parse the user / authorities out of the token and set it on the context. This makes me more uncomfortable since I'd prefer to be in control of which client applications could have access to this information (trusted apps only) and AFAIK only the authorization server and resource server should know how to parse the JWT tokens.

RutledgePaulV
  • 2,568
  • 3
  • 24
  • 47
  • FWIW my concerns at the time around JWT and which applications have the ability to parse the information were poorly founded. In some cases, this might be totally okay! In more restrictive cases you can use JWE and be judicious about who you share the key with. – RutledgePaulV Nov 10 '17 at 12:25

6 Answers6

67

You will need to implement a custom TokenEnhancer like so:

public class CustomTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        User user = (User) authentication.getPrincipal();
        final Map<String, Object> additionalInfo = new HashMap<>();

        additionalInfo.put("customInfo", "some_stuff_here");
        additionalInfo.put("authorities", user.getAuthorities());

        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

        return accessToken;
    }

}

and add it to your AuthorizationServerConfigurerAdapter as a bean with the corresponding setters

@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

    // Some autowired stuff here

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // @formatter:off
        endpoints
            // ...
            .tokenEnhancer(tokenEnhancer());
        // @formatter:on
    }

    @Bean
    @Primary
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        // ...
        tokenServices.setTokenEnhancer(tokenEnhancer());
        return tokenServices;
    }

    // Some @Bean here like tokenStore

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return new CustomTokenEnhancer();
    }

}

then in a controller (for example)

@RestController
public class MyController {

    @Autowired
    private AuthorizationServerTokenServices tokenServices;

    @RequestMapping(value = "/getSomething", method = RequestMethod.GET)
    public String getSection(OAuth2Authentication authentication) {
        Map<String, Object> additionalInfo = tokenServices.getAccessToken(authentication).getAdditionalInformation();

        String customInfo = (String) additionalInfo.get("customInfo");
        Collection<? extends GrantedAuthority> authorities = (Collection<? extends GrantedAuthority>) additionalInfo.get("authorities");

        // Play with authorities

        return customInfo;
    }

}

I'm personally using a JDBC TokenStore so my "Some autowired stuff here" are corresponding to some @Autowired Datasource, PasswordEncoder and what not.

Hope this helped!

Pang
  • 9,564
  • 146
  • 81
  • 122
Philippe
  • 1,356
  • 14
  • 20
  • I implemented what you suggested, I see that the token enhancer is called when the token is being generated, but when I see token in the response I got when I call /oauth/token, I don't see the additional information I added in the enhancer. Any idea? – Pedro Madrid Nov 26 '15 at 01:33
  • @PedroMadrid not sure why it's not showing in the result... Did you add a `logger.info("Here")` (for example) just to make sure it gets into your CustomTokenEnhancer? By calling the setAdditionalInformation() on the token and adding the info, everything should be good from there. – Philippe Dec 06 '15 at 23:10
  • 1
    thanks for the answer! For poeple using JDBC Token the bean `tokenServices()` has to have `.setTokenStore(tokenStore)` line added above enhancer – kiedysktos Aug 09 '17 at 12:51
  • 4
    Just a mention: this does NOT work for JwtTokenStore. in Spring Boot 1.4.x, the `public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication)` method is hardcoded to return null. – demaniak Sep 18 '17 at 21:45
  • 1
    @demaniak For JWT tokens @ Autowired private AuthorizationServerTokenServices tokenServices; @ Autowired private TokenStore tokenStore; OAuth2AuthenticationDetails auth2AuthenticationDetails = (OAuth2AuthenticationDetails) authentication.getDetails(); Map details = tokenStore.readAccessToken(auth2AuthenticationDetails.getTokenValue()).getAdditionalInformation(); String department = (String) details.get("department"); – Udara S.S Liyanage May 15 '18 at 03:20
  • 1
    Can i use this even thought i'm not using the Auth2 in my configuration ? I'm using only the JWT token , my classe securityConfig is extending WebSecurityConfiguerAdaptater . – dEs12ZER May 19 '18 at 00:52
  • If this approach does not seem to be working and you are using a `JdbcTokenStore`, make sure you are not retrieving a cached token from the db. If the token in the db was created before adding the enhancement, the un-enhanced token will be returned. You need to make sure a new token is created. There goes 30min I'll never get back... – Glenn Mar 10 '19 at 01:41
  • Is it possible to add information from a repository in TokenEnhancer? I want to add custom roless that will come from an external query database. – Miletos May 30 '21 at 04:27
22

If you are using Spring's JwtAccessTokenConverter or DefaultAccessTokenConverter you can add your custom CustomTokenEnhancer (see first response) and apply it using a TokenEnhancerChain like this:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(), accessTokenConverter()));

    endpoints.tokenStore(tokenStore())
            .tokenEnhancer(enhancerChain)
            .authenticationManager(authenticationManager);
}

@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey("my_signing_key");
    return converter;
}

@Bean public TokenEnhancer customTokenEnhancer() {
    return new CustomTokenEnhancer();
}

Another solution is to create a custom TokenConverter that extends Spring's JwtAccessTokenConverter and override the enhance() method with your custom claims.

public class CustomTokenConverter extends JwtAccessTokenConverter {

@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

    final Map<String, Object> additionalInfo = new HashMap<>();
    additionalInfo.put("customized", "true");
    User user = (User) authentication.getPrincipal();
    additionalInfo.put("isAdmin", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()).contains("BASF_ADMIN"));
    ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

    return super.enhance(accessToken, authentication);
    }
} 

And then:

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

    endpoints.tokenStore(tokenStore())
            .tokenEnhancer(customTokenEnhancer())
            .authenticationManager(authenticationManager);
}

@Bean public CustomTokenConverter customTokenEnhancer() {
    return new CustomTokenConverter();
}
jchrbrt
  • 1,006
  • 1
  • 11
  • 12
  • Your solution is working very well for me using configurations described by you! Now I am able to provide information making a token request! – RazvanParautiu Nov 10 '17 at 14:06
4

package com.security;

import java.util.HashMap;
import java.util.Map;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;

@Component
public class CustomTokenEnhancer implements TokenEnhancer {

 @Override
 public OAuth2AccessToken enhance(OAuth2AccessToken accessToken,
   OAuth2Authentication authentication) {
  // TODO Auto-generated method stub
  User user = (User) authentication.getPrincipal();
        final Map<String, Object> additionalInfo = new HashMap<>();

        additionalInfo.put("customInfo", "some_stuff_here");
        additionalInfo.put("authorities", user.getAuthorities());

        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

        return accessToken;
 }

}

Following is the xml configuration:

<bean id="tokenEnhancer" class="com.security.CustomTokenEnhancer" />

<!-- Used to create token and and every thing about them except for their persistence that is reposibility of TokenStore (Given here is a default implementation) -->
<bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
  <property name="tokenStore" ref="tokenStore" />
  <property name="accessTokenValiditySeconds" value="30000000"></property>
  <property name="refreshTokenValiditySeconds" value="300000000"></property>
  <property name="supportRefreshToken" value="true"></property>
  <property name="clientDetailsService" ref="clientDetails"></property>
  <property name="tokenEnhancer" ref="tokenEnhancer" />
</bean>

That's how I was able to add extra information to the Token.

Wojciech Wirzbicki
  • 3,887
  • 6
  • 36
  • 59
harshlal028
  • 1,539
  • 1
  • 16
  • 25
4

Together with:

@Bean
public TokenEnhancer tokenEnhancer() {
   return new CustomTokenEnhancer();
}

You have to include

@Bean
public DefaultAccessTokenConverter accessTokenConverter() {
    return new DefaultAccessTokenConverter();
}

and add everything to endpoints config:

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {

        endpoints
                .tokenStore(tokenStore)
                .tokenEnhancer(tokenEnhancer())
                .accessTokenConverter(accessTokenConverter())
                .authorizationCodeServices(codeServices)
                .authenticationManager(authenticationManager)
        ;
    }

Without it, your CustomTokenEnhancer will not work.

Yaroslav
  • 446
  • 4
  • 15
  • 2
    Thanks for clean answer. In fact accessTokenConverter is not necessary in config if you don't use it. Minimum set is `endpoints.tokenStore(tokenStore).tokenEnhancer(tokenEnhancer()).authenticationManager(authenticationManager);`. – kiedysktos Aug 09 '17 at 12:31
1
  1. create a class file CustomTokenEnhancer
@Component
public class CustomTokenConverter extends JwtAccessTokenConverter {


    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {

        final Map<String, Object> additionalInfo = new HashMap<>();
        additionalInfo.put("customized", "true");
        User user = (User) authentication.getPrincipal();
        additionalInfo.put("role", user.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toList()));
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);

        return super.enhance(accessToken, authentication);
    }
}
  1. paste below written code in AuthorizationServerConfig
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    enhancerChain.setTokenEnhancers(Arrays.asList(customTokenEnhancer(),accessTokenConverter()));

    endpoints
        .tokenStore(tokenStore())
        .tokenEnhancer(customTokenEnhancer())
        .authenticationManager(authenticationManager);
}

@Bean
protected JwtAccessTokenConverter jwtTokenEnhancer() {
    JwtAccessTokenConverter converter=  new JwtAccessTokenConverter();
    converter.setSigningKey("my_signing_key");

    return converter;
}

@Bean
public CustomTokenConverter customTokenEnhancer() {
    return new CustomTokenConverter();
}

@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource);
}
  1. import appropriate libraries after paste the above codes

output response of Custom Token Enhancer..click here

emptak
  • 429
  • 6
  • 11
Anjali K A
  • 11
  • 3
0

I solve this problem when excluded UserDetailsServiceAutoConfiguration. Like this. Maybe wiil be helpful in OAuth2 resource servers.

@SpringBootApplication(exclude = [UserDetailsServiceAutoConfiguration::class])
class Application

fun main(args: Array<String>) {
    runApplication<Application>(*args)
}
rinorium
  • 11
  • 4