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
.