Reputation: 5973
Trying to get a UserDetailsService working for an oauth2 resource server I set up. I'm able to successfully authenticate the jwt, but nothing I do seems to get it to call the loadUserByUsername method. This originally was using SAML and it was working, but now I've cut over to Oauth2 and I can't get it working.
@Service
public class OauthUsersDetailsServiceImpl implements UserDetailsService{
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//some user loading junk here - this is never called
}
}
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception
{
//test key for now
SecretKeySpec key = new SecretKeySpec("private key0000000000000000000000000000000".getBytes(), "HMACSHA256");
http
.authorizeRequests()
.antMatchers(/*some endpoints im excluding from auth - this all works*/)
.permitAll().and()
.authorizeRequests()
.anyRequest().authenticated().and()
.oauth2ResourceServer().jwt().decoder(NimbusJwtDecoder.withSecretKey(key).build());
}
}
I found with google that I could just register the class as a bean with @service and spring would just pick it up, but it's not working. I also tried adding it through the AuthenticationManagerBuilder, but that didn't work either. My guess is that the jwt side of this has its own UserDetailsService that its implemented and is taking priority over mine. That said, what is the proper way to get mine to call, or is it better to somehow call my user loading logic manually after authentication is complete and overwrite the Principal object? I need this to happen before an endpoint is called so PreAuthorize can check for the roles that were loaded by the UserDetailsService.
Upvotes: 7
Views: 3682
Reputation: 3
A previous post is correct that the UserDetailsService will not be called with oauth2ResourceServer.jwt().
One thing you can do is take advantage of the JwtAuthenticationToken
returned by Spring Security with the @AuthenticationPrincipal
annotation. You can extend this annotation to automatically load your User entity with the information contained.
First create a method to do this like:
public User getUserFromJwt(JwtAuthenticationToken principal) {
try {
User user = ValidateGoogleAuthToken.verifyGoogleAuthToken(principal.getToken().getTokenValue())
.orElseThrow(() -> new RuntimeException("Failed to validate JWT."));
return userRepository.findByUsername(user.getUsername())
.orElseThrow(() -> new UsernameNotFoundException("Unable to get User from JWT"));
} catch (Exception e) {
LOGGER.info("Exception in getUserFromJwt", e);
}
return null;
}
Then create a custom annotation like this:
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = "@userServiceImpl.getUserFromJwt(#this)")
public @interface CurrentUser {
}
Then you can add that @CurrentUser
annotation to your controller to load a user automatically like:
public ResponseEntity<String> doSomethingWithAuthenticatedUser(@CurrentUser User user) {
System.out.println("current user: " + user);
return null;
}
Upvotes: 0
Reputation: 47
The problem is that JwtAuthenticationProvider
does not invoke UserDetailService
- it assumes that the JWT has all the relevant authentication information - so there is no need to go the UserDetailService
to fetch Authorities, etc..
So, what you have to do is build a JWT/Token Converter that pulls the username from the jwt and authenticates with the DaoAuthenticationProvider
(which will invoke your UserDetailsService
). Additionally, since the password will be null, you have to override the DaoAuthenticationProvider
with a version that has a noop additionalAuthenticationChecks
method.
Here is the code that works for me:
@Configuration
class OAuthSecurityConfiguration() :
WebSecurityConfigurerAdapter() {
/*
Override the default DaoAuthenticationProvider to prevent password validity checks since they will not be set
*/
@Bean
fun daoAuthenticationProvider(userDetailsService: UserDetailsService): DaoAuthenticationProvider {
val daoAuthenticationProvider = object : DaoAuthenticationProvider() {
override fun additionalAuthenticationChecks(
userDetails: UserDetails,
authentication: UsernamePasswordAuthenticationToken
) {
// Do nothing as the password will be set to null
}
}
daoAuthenticationProvider.setUserDetailsService(userDetailsService)
return daoAuthenticationProvider
}
override fun configure(http: HttpSecurity) {
http
.authorizeRequests()
.regexMatchers(
"/customers.*",
"/accounts.*",
"/administrators.*"
)
.authenticated()
.and()
.oauth2ResourceServer()
.jwt()
.jwtAuthenticationConverter { jwt ->
convertJwtToUsernamePasswordToken(jwt)
}
}
private fun convertJwtToUsernamePasswordToken(
jwt: Jwt
): AbstractAuthenticationToken {
val username = jwt.getClaimAsString("username") // whichever claim you use to transmit the lookup key in the token
val userPasswordToken = UsernamePasswordAuthenticationToken(username, null)
return authenticationManager().authenticate(userPasswordToken) as AbstractAuthenticationToken
}
}
Upvotes: 2
Reputation: 5973
Figured it out. Hopefully this will help anyone that comes across the same problem. I had to add a custom filter into the chain to call my user details service and overwrite the context:
public class Oauth2AuthorizationFilter extends GenericFilterBean {
@Autowired
private OauthUsersDetailsServiceImpl oauthUsersDetailsServiceImpl;
public Oauth2AuthorizationFilter (OauthUsersDetailsServiceImpl userDetailsService) {
this.oauthUsersDetailsServiceImpl = userDetailsService;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
SecurityContext context = SecurityContextHolder.getContext();
if(context.getAuthentication() != null && !(context.getAuthentication().getPrincipal() instanceof Users)) {
UserDetails user = oauthUsersDetailsServiceImpl.loadUserByUsername(((Jwt)context.getAuthentication().getPrincipal()).getClaimAsString("user_name"));
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
context.setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
@Override
protected void configure(HttpSecurity http) throws Exception
{
//test key for now
SecretKeySpec key = new SecretKeySpec("private key0000000000000000000000000000000".getBytes(), "HMACSHA256");
http.authorizeRequests().antMatchers(/*bunch of junk...*/).permitAll().and().authorizeRequests().anyRequest().authenticated().and()
.oauth2ResourceServer().jwt().decoder(NimbusJwtDecoder.withSecretKey(key).build());
http.addFilterAfter(jwtAuthTokenFilterBean(), SwitchUserFilter.class);
}
That finally did what I needed
Upvotes: 4
Reputation: 13973
You need to register the UserDetailsService implementation which is then used by the DaoAuthenticationProvider
// userDetailsService bean
@Autowired
private OauthUsersDetailsServiceImpl oauthUsersDetailsServiceImpl;
//
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(oauthUsersDetailsServiceImpl);
}
Upvotes: 0