Problem with Authorities(Authorization) when using OAuth 2.0 Resource Server JWT

I’m trying to configure access authorization with JWT in a simple application using OAuth 2.0 Resource Server JWT.

The entire Authentication part is working normally, but I’m having problems with Authorization. Even with the correct Authorities being present in the token, all protected endpoints are giving a 403 Forbidden error.

I tried using the default scope(SCOPE_) attribute and changed the configuration to roles(ROLE_), but the problem remains the same.

Does anyone know how to solve it?

Complete source code: https://github.com/GustavoSC1/spring-security-jwt

Example of generated token: eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJzcHJpbmctc2VjdXJpdHktand0Iiwic3ViIjoidXNlcm5hbWUiLCJleHAiOjE3MDU0NDMyOTQsImlhdCI6MTcwNTQwNzI5NCwicm9sZXMiOiJST0xFX0FETUlOIn0.peivwrtHx_7mr6eefqbiD5DplhFFzcVd7sCMmt3f7rk7Sk1i6KeRPQi5ubdvaEfnZSJQ6VKA5NAdltSbqidfzogmoIXjktfhsc5ZrNYyRhikVnWcWb3wRGdd1EZgIHALDFjXWXsyypauNjWdxZNiRKL93e6MG1uAo5pIy9p-9YP8JEr7O31wKDR1COSKzK3gQw42uecIB9H1rRlkx9pdk7Pf9RtfsSfCwc-NtViSMryCrecO9RiaLqFYdpdzeojiMcbqVEyBoqFhN2WoEpgDM8mR5zSdhGdQE1IVsIbfbCJ_0486ZuQiKsXP2kniljHL2b5qnaN07FJPVslK–Ccsg

SecurityConfig:

@Configuration
@EnableWebSecurity
//@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
  @Value("${jwt.public.key}")
  private RSAPublicKey key;
  @Value("${jwt.private.key}")
  private RSAPrivateKey priv;

  @Bean
  SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf(csrf -> csrf.disable())
        .authorizeHttpRequests(
            auth -> auth
                .requestMatchers("/authenticate").permitAll()
                .requestMatchers("/register").permitAll()
                .requestMatchers("/private").hasAnyRole("ADMIN"))
        .httpBasic(Customizer.withDefaults())
        
        // https://docs-spring-io.translate.goog/spring-security/reference/servlet/oauth2/resource-server/jwt.html?_x_tr_sl=en&_x_tr_tl=pt&_x_tr_hl=pt-BR&_x_tr_pto=sc
        .oauth2ResourceServer(
                conf -> conf.jwt(
                    jwt -> jwt.decoder(jwtDecoder())
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())));
                
    return http.build();
  }
  
  @Bean
  public JwtAuthenticationConverter jwtAuthenticationConverter() {
      JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
      grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
      grantedAuthoritiesConverter.setAuthorityPrefix("ROLE_");

      JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
      jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
      return jwtAuthenticationConverter;
  }

  @Bean
  JwtDecoder jwtDecoder() {
    return NimbusJwtDecoder.withPublicKey(this.key).build();
  }

  @Bean
  JwtEncoder jwtEncoder() {
    JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
    JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
    return new NimbusJwtEncoder(jwks);
  }
  
  @Bean
  PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }
  
}

UserDetailsServiceImpl:

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  private final UserRepository userRepository;

  public UserDetailsServiceImpl(UserRepository userRepository) {
    this.userRepository = userRepository;
  }

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    Optional<User> userOptional = userRepository.findByUsername(username);
    
    User user = userOptional.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
    
    return new UserAuthenticated(user.getUsername(), user.getPassword());
  }

}

UserAuthenticated:

public class UserAuthenticated implements UserDetails {
  
  private String username;
  private String password;

  public UserAuthenticated(String username, String password) {
    this.username = username;
    this.password = password;
  }

  @Override
  public String getUsername() {
    return username;
  }

  @Override
  public String getPassword() {
    return password;
  }

  @Override
  public Collection<? extends GrantedAuthority> getAuthorities() {
    return List.of(() -> "ROLE_ADMIN");
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }

}

JwtService:

@Service
public class JwtService {
  private final JwtEncoder encoder;

  public JwtService(JwtEncoder encoder) {
    this.encoder = encoder;
  }

  public String generateToken(Authentication authentication) {
    Instant now = Instant.now();
    long expiry = 36000L;

    String scope = authentication
        .getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors
            .joining(" "));

    JwtClaimsSet claims = JwtClaimsSet.builder()
        .issuer("spring-security-jwt")
        .issuedAt(now)
        .expiresAt(now.plusSeconds(expiry))
        .subject(authentication.getName())
        .claim("roles", scope)
        .build();

    return encoder.encode(
        JwtEncoderParameters.from(claims))
        .getTokenValue();
  }

}

PrivateController:

@RestController
@RequestMapping("private")
public class PrivateController {

  @GetMapping
  //@PreAuthorize("hasAuthority('ROLE_ADMIN')")
  public String getMessage() {
    return "Hello from private API controller";
  }
  
}

The provided token, has already been generated with the roles field set to ROLE_ADMIN. In your jwtAuthenticationConverter(), you’re attempting to use setAuthorityPrefix with ROLE, resulting in ROLE_ROLE_ADMIN.

To rectify this, please modify that line to grantedAuthoritiesConverter.setAuthorityPrefix("");.

After making this adjustment, the issue should be resolved. Please let me know if you encounter any further problems.

NOTE:

If you omit this step, the default prefix will be SCOPE, causing your role to become SCOPE_ROLE_ADMIN.

Leave a Comment