1

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());
    }

}
nop
  • 4,711
  • 6
  • 32
  • 93
  • Are you unable to get the token by making the call or you have the token and you want to use it ? – Trishul Singh Choudhary Apr 12 '20 at 13:50
  • https://i.imgur.com/pONMoMO.png I'm able to get a token. I'm unable to do that: https://i.imgur.com/ZbMXX4h.png – nop Apr 12 '20 at 14:00
  • try checking https://stackoverflow.com/questions/39773932/spring-oauth2-refresh-token-cannot-convert-access-token-to-json – Trishul Singh Choudhary Apr 12 '20 at 14:17
  • @TrishulSinghChoudhary, it could have been the format but it now says `Invalid refresh token`. However, https://jwt.io/ says it's correct. https://i.imgur.com/YY9sgEr.png – nop Apr 12 '20 at 17:11
  • @TrishulSinghChoudhary, https://github.com/spring-projects/spring-security-oauth/issues/1195. It might not be saving the tokens into the resource server. How can I check that? https://github.com/Hulkstance/mse-forum Full code here, because not sure what's needed. – nop Apr 12 '20 at 22:02
  • Is it possible the refresh token to not be enabled? – nop Apr 13 '20 at 14:10

1 Answers1

0

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.

t3rmian
  • 641
  • 1
  • 7
  • 13
  • I guess that's the reason but how do I fill the ApprovalStore with that list? Any example? I tried `endpoints.userDetailsService(userDetailsService)`, it doesn't really matter if it's there or not. – nop Apr 13 '20 at 14:53
  • @nop I've updated the answer with possible implementation, see if this helps. The username should match your case. – t3rmian Apr 13 '20 at 15:25
  • Still broken :/ I added that ApprovalStore and `endpoints.userDetailsService(userDetailsService)`. As a result: `{ "error": "invalid_grant", "error_description": "Invalid refresh token:....` – nop Apr 13 '20 at 16:28
  • @nop I've cloned your repo at HEAD `ca1090a1034b23e5eedaf2f125f127687faf2c8f`, switched to h2 database, added approvals like in the answer with userDetailsService (otherwise `java.lang.IllegalStateException: UserDetailsService is required` is thrown during token refresh) and it works. – t3rmian Apr 13 '20 at 16:55
  • 1
    Thank you! It worked but I think the most peope made it that way: https://github.com/Hulkstance/mse-forum/blob/master/forum/src/main/java/com/mse/wcp/forum/security/AuthorizationServerConfig.java. Updated the repo. What's your opinion? – nop Apr 13 '20 at 19:28
  • I think that's a perfect solution! By configuring only the `TokenStore` the result should be the same with much less code. – t3rmian Apr 13 '20 at 20:14
  • Indeed, it automatically configures itself. Thanks for your help! – nop Apr 13 '20 at 20:27