I'm trying to have my authorization server generate a JWT access token with some custom claims in it.
Here is what the Bearer token returned by the authorization server /auth/token
endpoint looks like: 51aea31c-6b57-4c80-9d19-a72e15cb2bb7
I find this token a bit short to be a JWT token and to contain my custom claims...
And when using it in subsequent requests to the resource server, it complains with the error: Cannot convert access token to JSON
I'm using the following dependencies:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.2.RELEASE</version>
</dependency>
The authorization server is configured this way:
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.tokenServices(defaultTokenServices())
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST)
.accessTokenConverter(jwtAccessTokenConverter())
.userDetailsService(userDetailsService);
endpoints
.pathMapping("/oauth/token", RESTConstants.SLASH + DomainConstants.AUTH + RESTConstants.SLASH + DomainConstants.TOKEN);
TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), jwtAccessTokenConverter()));
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(tokenEnhancerChain)
.authenticationManager(authenticationManager);
}
@Bean
@Primary
public DefaultTokenServices defaultTokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
return defaultTokenServices;
}
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setKeyPair(new KeyStoreKeyFactory(new ClassPathResource(jwtProperties.getSslKeystoreFilename()), jwtProperties.getSslKeystorePassword().toCharArray()).getKeyPair(jwtProperties.getSslKeyPair()));
return jwtAccessTokenConverter;
}
@Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
And it's using the class:
class CustomTokenEnhancer implements TokenEnhancer {
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
// Add user information to the token
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
info.put("organization", authentication.getName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
return customAccessToken;
}
}
I also have the class:
@Configuration
class CustomOauth2RequestFactory extends DefaultOAuth2RequestFactory {
@Autowired
private TokenStore tokenStore;
@Autowired
private UserDetailsService userDetailsService;
public CustomOauth2RequestFactory(ClientDetailsService clientDetailsService) {
super(clientDetailsService);
}
@Override
public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
if (requestParameters.get("grant_type").equals("refresh_token")) {
OAuth2Authentication authentication = tokenStore
.readAuthenticationForRefreshToken(tokenStore.readRefreshToken(requestParameters.get("refresh_token")));
SecurityContextHolder.getContext()
.setAuthentication(new UsernamePasswordAuthenticationToken(authentication.getName(), null,
userDetailsService.loadUserByUsername(authentication.getName()).getAuthorities()));
}
return super.createTokenRequest(requestParameters, authenticatedClient);
}
}
UPDATE: I also tried the alternative way of specifying the custom claim:
@Component
class CustomAccessTokenConverter extends JwtAccessTokenConverter {
@Autowired
private TokenAuthenticationService tokenAuthenticationService;
@Override
public OAuth2Authentication extractAuthentication(Map<String, ?> claims) {
OAuth2Authentication authentication = super.extractAuthentication(claims);
authentication.setDetails(claims);
return authentication;
}
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
User user = (User) authentication.getPrincipal();
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
info.put(CommonConstants.JWT_CLAIM_USER_EMAIL, user.getEmail().getEmailAddress());
info.put(CommonConstants.JWT_CLAIM_USER_FULLNAME, user.getFirstname() + " " + user.getLastname());
info.put("scopes", authentication.getAuthorities().stream().map(s -> s.toString()).collect(Collectors.toList()));
info.put("organization", authentication.getName());
DefaultOAuth2AccessToken customAccessToken = new DefaultOAuth2AccessToken(accessToken);
customAccessToken.setAdditionalInformation(info);
customAccessToken.setExpiration(tokenAuthenticationService.getExpirationDate());
return super.enhance(customAccessToken, authentication);
}
}
with it being called like:
endpoints
.tokenStore(tokenStore())
.tokenEnhancer(jwtAccessTokenConverter())
.accessTokenConverter(jwtAccessTokenConverter())
but it changed nothing and the error remained identical.
Running with the debugger, none of these two enhancer overrides are called.
To build Spring Boot server with OAuth2, JWT and extra claims we should:
1) Add dependency to the project:
2) Add Web security configuration (to publish
AuthenticationManager
bean - it will be used in the next step), for example:Here is implemented a simple
UserDetailsService
for testing purpose. It works with the following simple 'User' object andRole
enum which implementsGrantedAuthority
interface.AuthUser
has only one additional propertyemail
which will be added to the JWT token as a claim.3) Configure Authorization server and enable Resource server:
A simple
ClientDetailsService
is implemented here. It contains only one client, which has 'client' name, blank password and granted types "password" and "refresh_token". It gives us a possibility to create a new access token and refresh it. (To work with many types of clients or in other scenarios you have to implement more complex, and maybe persistent, variants ofClientDetailsService
.)Authorization endpoints are configured with
TokenEnhancerChain
which containstokenEnhancer
andtokenConverter
. It's important to add them in this sequence. The first one enhances an access token with additional claims (user email in our case). The second one creates a JWT token. Theendpoints
set with a simpleJwtTokenStore
, ourTokenEnhancerChain
andauthenticationManager
.Note to
JwtTokenStore
- I believe that in the real scenarios a persistent variant of the store must be used. More info is here.The last thing here is
authExtractor
which gives us a possibility to extract claims from JWT tokens of incoming requests.Then all things are set up we can request our server to get an access token:
Rsponse:
If we decode this access token on https://jwt.io/ we can see that it contain the
user_email
claim:To extract such a claim (and other data) from a JWT token of incoming requests we can use the following approach:
My working demo: sb-jwt-oauth-demo
Related info:
If you shared a sample project, it would be easier to spot the exact fix for you. In lieu of that, did you set a breakpoint at
.tokenEnhancer(tokenEnhancerChain)
and did it trigger?I've created a super simple sample project, that shows how the
tokenEnhancer
is being invokedIn this sample, there is also a unit test
Feel free to check out the sample project and see if it works for you.,