I have 2 separate Spring Boot applications, one serving as an an OAuth 2 authorization server, and the other as resource server. I'm using Spring's RemoteTokenServices
in my resource server to check tokens from the authorization server. Now, I'm trying to define protected controller code in my resource server application, but I'm not sure how to map the UserDetails
class to the authentication principal provided through the OAuth 2 mechanism.
I have set up my authorization server with a custom TokenEnhancer
that adds more details to the token such that /oauth/check_token?token=<token>
returns with custom fields, which I want to map to my resource server controllers.
In a more monolithic setup where the authorization server is also the resource server, I can define controller methods that make use of the authenticated principal this way:
//User implements UserDetails
public Map<String, Object> getResource(@AuthenticationPrincipal User user) {
//code that uses the user object
}
However, this doesn't seem to work as straight forward in a more distributed approach. The mapping fails, and the user
parameter ends up being a null object. I tried using the following approach:
public Map<String, Object> getResource(Authentication authentication) {
//code that uses the authentication object
}
While the code above successfully maps the authentication details, it doesn't provide a way for me to directly access the custom fields I've set through the TokenEnhancer
I mentioned earlier. I can't seem to find anything from the Spring docs regarding this.
To resolve the issue, let me go through a bit of architectural background first. The UserDetails
object automatically mapped through the @AuthenticationPrincipal
comes from the principal
field of the active Authentication
object. A resource server controller has access to an OAuth2Authencation
object, which is a specialized instance of Authentication
for Spring OAuth2 security framework, just by simply declaring it as part of the method parameters.
public void controllerMethod(OAuth2Authentication authentication) {
//controller definition
}
Knowing this, the problem now shifts to how to make sure that the getPrincipal()
method in the Authentication
object is an instance of my custom UserDetails
class. The RemoteTokenServices
I use in the resource server application uses an instance of AccessTokenConverter
to interpret token details sent by the authorization server. By default, it uses DefaultAccessTokenConverter
, which just sets the authentication principal as the username, which is a String
. This converter makes use of UserAuthenticationConverter
to convert the data coming from the authorization server into an instance of Authentication
. This is what I needed to customize:
DefaultAccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
tokenConverter.setUserTokenConverter(new DefaultUserAuthenticationConverter() {
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
Authentication authentication = super.extractAuthentication(map);
// User is my custom UserDetails class
User user = new User();
user.setSpecialKey(map.get("specialKey").toString());
return new UsernamePasswordAuthenticationToken(user,
authentication.getCredentials(), authentication.getAuthorities());
}
});
tokenServices.setAccessTokenConverter(tokenConverter);
With all these set up, the @AuthenticationPrincipal
mechanism now works as expected.
Did you enable the AuthenticationPrincipalArgumentResolver
, like following xml?
<mvc:annotation-driven>
<mvc:argument-resolvers>
<bean class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
</mvc:argument-resolvers>
</mvc:annotation-driven>
And you need to implement the UserDetails
to return your own CustomerUser object
, then you can use the annotation to get principal directly.