Spring-security-oauth: JWT & Token enhancing, Principal is String when fetching refresh token and User when fetching access token

Created on 17 Apr 2014  路  9Comments  路  Source: spring-projects/spring-security-oauth

Finally were able to upgrade to latest SNAPSHOT, Dont't know whether this is a bug or not, here's my authorizationserver setup now:

@Configuration
public class OAuth2ServerConfig {

    @Configuration
    @EnableAuthorizationServer
    static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

        @Autowired
        private AuthenticationManager authenticationManager;

        @Autowired
        public DataSource dataSource;


        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.inMemory()
                .withClient("xxxx")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(60 * 60 * 24)
                .refreshTokenValiditySeconds(60 * 60 * 24 * 5)
                .scopes("read")
                .and()
                .withClient("yyyy")
                .authorizedGrantTypes("password", "refresh_token")
                .accessTokenValiditySeconds(60 * 60 * 24 * 2)
                .refreshTokenValiditySeconds(60 * 60 * 24 * 10)
                .scopes("read");
        }

        @Bean
        public JwtTokenStore tokenStore() {
            JwtTokenStore store = new JwtTokenStore(tokenEnhancer());
            return store;
        }


        @Bean
        public JwtAccessTokenConverter tokenEnhancer(){
            final JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
            jwtAccessTokenConverter.setSigningKey("xxxx");
            return jwtAccessTokenConverter;
        }

        @Bean
        public TokenEnhancerChain tokenEnhancerChain(){
            final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
            tokenEnhancerChain.setTokenEnhancers(Lists.newArrayList(new MyTokenEnhancer(), tokenEnhancer()));
            return tokenEnhancerChain;
        }

        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenServices(defaultTokenServices()).authenticationManager(authenticationManager);
        }

        @Override
        public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
            oauthServer.allowFormAuthenticationForClients().realm("xx/yy");
        }

        @Autowired
        private ClientDetailsService clientDetailsService;

        @Bean
        public DefaultTokenServices defaultTokenServices(){
            final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
            defaultTokenServices.setTokenStore(tokenStore());
            defaultTokenServices.setClientDetailsService(clientDetailsService);
            defaultTokenServices.setTokenEnhancer(tokenEnhancerChain());
            defaultTokenServices.setSupportRefreshToken(true);
            return defaultTokenServices;
        }

        private static class MyTokenEnhancer implements TokenEnhancer {
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                final User user = (User) authentication.getPrincipal();
                final Map<String, Object> additionalInfo = new HashMap<>();
                additionalInfo.put("userId", user.getUserId());
                additionalInfo.put("companyId", user.getCompanyId());
                additionalInfo.put("roles", user.getAuthorities());
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
                return accessToken;
            }
        }
    }
}

As you can see I have a custom TokenEnchanher that loads additional info from user.
When I do normal token request, the Principal is my custom User class (and can be casted), but when fetching refresh_token, the principal is just the username (String). Is this behaving as expected?

I have a custom UserDetailsService which loads custom User (UserDetails) from database.

stackoverflow

Most helpful comment

I'm not sure if this is what you guys want to accomplish, but I was able to enhance the jwt token by just extending the JwtAccessTokenConverter. and works for all my OAuth2.0 flows (password,refresh_token,authorization_code).

protected static class CustomTokenEnhancer extends JwtAccessTokenConverter {

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

            MyUser user = (MyUser) authentication.getPrincipal();
            Map<String, Object> info = new LinkedHashMap<String, Object>(
                    accessToken.getAdditionalInformation());

            info.put("anyInfoHere", user.getMySpecialInfo());

            DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
            customAccessToken.setAdditionalInformation(info);
            return super.enhance(customAccessToken, authentication);

        }

and then just create the token converter:

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
            Assert.notNull(keystoreConfig.getKeyStorePass(),
                    "You must provide a keystore password for your '"
                            + keystoreConfig.getPath() + "' keystore");

            JwtAccessTokenConverter converter = new CustomTokenEnhancer();
            KeyPair keyPair = new KeyStoreKeyFactory(
                    keystoreConfig.getResource(), keystoreConfig
                            .getKeyStorePass().toCharArray()).getKeyPair(
                    keystoreConfig.getKeyAlias(), keystoreConfig.getKeyPass()
                            .toCharArray());
            converter.setKeyPair(keyPair);

            return converter;
}

All 9 comments

The enhancer is only called when creating an access token, but the user Authentication -> JWT conversion is not naturally invertible, so it might be a different Authentication object that you see when refreshing an access token (it comes from a UserAuthenticationConverter and the default is pretty dumb). Maybe you need a custom UserAuthenticationConverter (in the JwtAccessTokenConverter)?

It would be good to get some good tests around this, so if you can boil it down to something simple that doesn't use the TokenEnhancerChain that would be very useful. I refactored the token services tests so you have a DefaultTokenServicesWithJwtTests to start from (see commit 61e7720).

Or maybe you don't need a custom converter, but I noticed you _replace_ rather than _enhance_ the additional information in the access token in your token enhancer. Maybe it's losing something vital in that step?

I noticed in the comments to #187 that you stopped overwriting the additional info and it seemed to work. I don't think there's much I can do to stop people from doing that inadvertently. It probably means that you have a "user_name" and a "user_id" field in your JWT. Does that concern you (maybe it should since you carry that information everywhere a token goes)? Again, I'm not sure we can stop you doing it, or easily alert you to the fact that you might be doing it, but suggestions for improvements are welcome.

Are you OK with going to RC1?

Not quite sure what you mean, i.e. why shouldn't I carry user_id and company_id in JWT? Anyways, I'm happy to moving to RC1, cheers!

But still when fetching refresh token, Principal is username, and when fetching the access token with username & pwd, Principal can be casted to User class, I'm talking about this line:

final User user = (User) authentication.getPrincipal();

Meaning that I will get class cast exception with my current implementation.
Just wanted to inform ;)

why shouldn't I carry user_id and company_id in JWT

No reason. But you have "userId" and "user_name" as well (unless you added the UserAuthenticationConverter)

when fetching refresh token, Principal is username

Did you add a UserAuthenticationConverter? If you did, is it converting the "userId", "companyId" and "roles" claims into a User?

I'm not sure if this is what you guys want to accomplish, but I was able to enhance the jwt token by just extending the JwtAccessTokenConverter. and works for all my OAuth2.0 flows (password,refresh_token,authorization_code).

protected static class CustomTokenEnhancer extends JwtAccessTokenConverter {

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

            MyUser user = (MyUser) authentication.getPrincipal();
            Map<String, Object> info = new LinkedHashMap<String, Object>(
                    accessToken.getAdditionalInformation());

            info.put("anyInfoHere", user.getMySpecialInfo());

            DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
            customAccessToken.setAdditionalInformation(info);
            return super.enhance(customAccessToken, authentication);

        }

and then just create the token converter:

@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
            Assert.notNull(keystoreConfig.getKeyStorePass(),
                    "You must provide a keystore password for your '"
                            + keystoreConfig.getPath() + "' keystore");

            JwtAccessTokenConverter converter = new CustomTokenEnhancer();
            KeyPair keyPair = new KeyStoreKeyFactory(
                    keystoreConfig.getResource(), keystoreConfig
                            .getKeyStorePass().toCharArray()).getKeyPair(
                    keystoreConfig.getKeyAlias(), keystoreConfig.getKeyPass()
                            .toCharArray());
            converter.setKeyPair(keyPair);

            return converter;
}

@pagarciaortega Thanks for the clean and easy way to add data in the jwt token !

Can someone describe how to access the value from getAdditionalInformation()? I know that the data being stored in the database table (I am using JDBC) is correct - it is there. I have the same code that @pagarciaortega wrote above, but for whatever reason getAdditionalInformation() returns null. So additionalInfo below is always null...

public Greeting greeting(OAuth2AccessToken accessToken, OAuth2Authentication authentication) { Map<String, Object> additionalInfo = accessToken.getAdditionalInformation(); ...etc...

Was this page helpful?
0 / 5 - 0 ratings