2

I'm using Oauth2 resource server JWT for authentication. I'm trying to add authentication for two routes , route "/student/" to give acess to role ROLE_USER and route "/students/" to role ROLE_ADMIN.

Security config

    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception{
        return httpSecurity
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests(a ->
                {
                    a.requestMatchers("/student/").hasAnyRole("ROLE_USER");
                    a.requestMatchers("/student/**").hasAnyRole("ROLE_USER");
                    a.requestMatchers("/students/").hasAnyRole("ROLE_ADMIN");
                    a.requestMatchers("/students/**").hasAnyRole("ROLE_ADMIN");
                    a.requestMatchers("/").permitAll();
                    a.requestMatchers("/token/").permitAll();
                    a.requestMatchers("/token/**").permitAll();
                    a.anyRequest().authenticated();
                })
                .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
                .userDetailsService(userDetailsService)
                .headers(headers -> headers.frameOptions().sameOrigin())
                .httpBasic(Customizer.withDefaults())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();
    }

I tried to change hasRole() to hasAnyRole() , hasAuthority() , treid @PreAuthorize() on controller and checked with using roles without "ROLE_" prefix but nothing has worked.

UserDetailsService

public class jpaUserDetailsService implements UserDetailsService {
    UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return userRepository.findUserByUsername(username)
                .map(SecurityUser::new)
                .orElseThrow(() -> new UsernameNotFoundException("Username not found " + username));
    }
}

UserDetails

public class SecurityUser implements UserDetails {
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getRoles().stream()
                .map(Role::getRole)
                .map(UserRoles::toString)
                .map(SimpleGrantedAuthority::new).toList();
    }
}

I am using an enum for all my Roles

public enum UserRoles {
    ROLE_ADMIN,
    ROLE_USER
}

Service to generate jwt Token

public class TokenService  {
    private final JwtEncoder jwtEncoder;

    public TokenService(JwtEncoder jwtEncoder) {
        this.jwtEncoder = jwtEncoder;
    }

    public String generateToken(Authentication authentication){
        Instant now = Instant.now();

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

        JwtClaimsSet claims =  JwtClaimsSet.builder()
                .issuer("self")
                .issuedAt(now.plus(1 , ChronoUnit.HOURS))
                .subject(authentication.getName())
                .claim("scope" , scope)
                .build();

        return this.jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
    }

User Entity

public class User {
    @Id
    @GeneratedValue
    private long userId;
    @Column(unique = true , nullable = false)
    private String username;
    @Column(nullable = false)
    private String password;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            joinColumns = @JoinColumn(name = "userId"),
            inverseJoinColumns = @JoinColumn(name = "roleId")
    )
    @ToString.Exclude
    private List<Role> roles;

}

Role Entity

public class Role {
    @Id
    @GeneratedValue
    private Long roleId;
    @Column(unique = true , nullable = false)
    @Enumerated(EnumType.STRING)
    private UserRoles role;
}

finally my Contoroller

    @GetMapping("/student/") // route i want to allow with role user;
    public Student getStudent(Principal principal){
     
    }

    @GetMapping("/students/") // route i want to allow only role admin
    public List<Student> getStudents(Principal principal){
       
    }
}

enter image description here

  • Are you sure you want to have the double role in `hasRole("ROLE_USER")`? If you use the `hasRole(..)` method it already checks for authorities starting with `ROLE_`, so `hasRole("USER")` might be enough. – g00glen00b Feb 21 '23 at 21:38
  • @g00glen00b Thanks for replying, for some reason even `hasRole("USER")` is giving me a 403. The JWT token I am receiving on the client seems fine and I am unsure how to debug it. Any tips on how to debug this? – Expert_Introvert Feb 22 '23 at 05:16

1 Answers1

2

The default JwtGrantedAuthoritiesConverter implementation reads the authorities from the scope claim (which you provided) and prefixes them with SCOPE_. This means that your authorities will become SCOPE_ROLE_USER and SCOPE_ROLE_ADMIN.

You can either adapt to it and change the SecurityFilterChain to this:

a.requestMatchers("/student/").hasAnyAuthority("SCOPE_ROLE_USER");

Or you can create a custom JwtGrantedAuthoritiesConverter:

@Bean
public JwtGrantedAuthoritiesConverter authoritiesConverter() {
    JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter();
    converter.setAuthorityPrefix(""); // Cannot be null
}
    
@Bean
public JwtAuthenticationConverter authenticationConverter(JwtGrantedAuthoritiesConverter authoritiesConverter) {
    JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
    converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
    return converter;
}

Also be aware that the difference between authorities and roles is that roles are just authorities that follow a specific naming convention (= being prefixed with ROLE_). This means that you should either use hasAnyAuthority("ROLE_USER") or hasAnyRole("USER"). So if you disable the scope prefix, you should change your SecurityFilterChain to this:

a.requestMatchers("/student/").hasAnyRole("USER");

// or

a.requestMatchers("/student/").hasAnyAuthority("ROLE_USER");

In the comments you also asked how to debug this. To debug whether it's behaving correctly you can:

  • Decode your JWT token to verify that you have a scope claim containing your original authorities.
  • Put a breakpoint in the JwtGrantedAuthoritiesConverter.getAuthorities() method to verify whether the token is properly read.
g00glen00b
  • 41,995
  • 13
  • 95
  • 133