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