MGZero
MGZero

Reputation: 5973

Spring Boot Oauth2 Resource Server UserDetailsService

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

Answers (4)

Peter Linnehan
Peter Linnehan

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

Andrew Hall
Andrew Hall

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

MGZero
MGZero

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

Beppe C
Beppe C

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

Related Questions