Cannot pass JWT refresh token as an argument

Tags: , , , ,



I’m trying to get a new access token using a refresh token in Spring Boot with OAuth2. It should be done as following: POST: url/oauth/token?grant_type=refresh_token&refresh_token=....

It works fine if I’m using InMemoryTokenStore because the token is tiny and contains only digits/letters but right now I’m using a JWT token and as you probably know it has 3 different parts which probably are breaking the code.

I’m using the official migration guide to 2.4.

When I try to access the URL above, I’m getting the following message:

{
    "error": "invalid_token",
    "error_description": "Cannot convert access token to JSON"
}

How do I pass a JWT token in the params? I tried to set a breakpoint on that message, so I could see what the actual argument was, but it didn’t get to it for some reason.

/**
 * The Authorization Server is responsible for generating tokens specific to a client.
 * Additional information can be found here: https://www.devglan.com/spring-security/spring-boot-security-oauth2-example.
 */
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Value("${user.oauth2.client-id}")
    private String clientId;

    @Value("${user.oauth2.client-secret}")
    private String clientSecret;

    @Value("${user.oauth2.accessTokenValidity}")
    private int accessTokenValidity;

    @Value("${user.oauth2.refreshTokenValidity}")
    private int refreshTokenValidity;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()
                .withClient(clientId)
                .secret(bCryptPasswordEncoder.encode(clientSecret))
                .authorizedGrantTypes("password", "authorization_code", "refresh_token")
                .scopes("read", "write", "trust")
                .resourceIds("api")
                .accessTokenValiditySeconds(accessTokenValidity)
                .refreshTokenValiditySeconds(refreshTokenValidity);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)
                .tokenStore(tokenStore())
                .userApprovalHandler(userApprovalHandler())
                .accessTokenConverter(accessTokenConverter());
    }

    @Bean
    public UserApprovalHandler userApprovalHandler() {
        ApprovalStoreUserApprovalHandler userApprovalHandler = new ApprovalStoreUserApprovalHandler();
        userApprovalHandler.setApprovalStore(approvalStore());
        userApprovalHandler.setClientDetailsService(clientDetailsService);
        userApprovalHandler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
        return userApprovalHandler;
    }

    @Bean
    public TokenStore tokenStore() {
        JwtTokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
        tokenStore.setApprovalStore(approvalStore());
        return tokenStore;
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        final RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());

        JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
            private JsonParser objectMapper = JsonParserFactory.create();

            @Override
            protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                String content;
                try {
                    content = this.objectMapper.formatMap(getAccessTokenConverter().convertAccessToken(accessToken, authentication));
                } catch (Exception ex) {
                    throw new IllegalStateException("Cannot convert access token to JSON", ex);
                }
                Map<String, String> headers = new HashMap<>();
                headers.put("kid", KeyConfig.VERIFIER_KEY_ID);
                return JwtHelper.encode(content, signer, headers).getEncoded();
            }
        };
        converter.setSigner(signer);
        converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
        return converter;
    }

    @Bean
    public ApprovalStore approvalStore() {
        return new InMemoryApprovalStore();
    }

    @Bean
    public JWKSet jwkSet() {
        RSAKey.Builder builder = new RSAKey.Builder(KeyConfig.getVerifierKey())
                .keyUse(KeyUse.SIGNATURE)
                .algorithm(JWSAlgorithm.RS256)
                .keyID(KeyConfig.VERIFIER_KEY_ID);
        return new JWKSet(builder.build());
    }

}

Answer

I assume that the Cannot convert access token to JSON might have been due to incorrectly pasted token.

As for Invalid refresh token, it occurs because when JwtTokenStore reads the refresh token, it validates the scopes and revocation with InMemoryApprovalStore. However, for this implementation, the approvals are registered only during authorization through /oauth/authorize URL (Authorisation Code Grant) by the ApprovalStoreUserApprovalHandler.

Especially for the Authorisation Code Grant (authorization_code), you want to have this validation, so that the refresh token request will not be called with an extended scope without the user knowledge. Moreover, it’s optional to store approvals for future revocation.

The solution is to fill the ApprovalStore with the Approval list for all resource owners either statically or dynamically. Additionally, you might be missing setting the user details service endpoints.userDetailsService(userDetailsService) which is used during the refresh process.

Update:

You can verify this by creating pre-filled InMemoryApprovalStore:

@Bean
public ApprovalStore approvalStore() {
    InMemoryApprovalStore approvalStore = new InMemoryApprovalStore();
    Date expirationDate = Date.from(Instant.now().plusSeconds(3600));
    List<Approval> approvals = Stream.of("read", "write", "trust")
            .map(scope -> new Approval("admin", "trusted", scope, expirationDate,
                    ApprovalStatus.APPROVED))
            .collect(Collectors.toList());
    approvalStore.addApprovals(approvals);
    return approvalStore;
}

I would also take a look at implementing it in the storeRefreshToken()/storeAccessToken() methods of JwtTokenStore, as they have an empty implementation, and the method parameters contain all the necessary data.



Source: stackoverflow