1

I want to use the new Spring Security Authorization Server to implement OAuth2 for my webservice.

At https://www.baeldung.com/spring-security-oauth-auth-server an example is given, separation

  • Authorization Server
  • Resource Server
  • Client

The code can be found at https://github.com/Baeldung/spring-security-oauth/tree/master/oauth-authorization-server

Those three Maven projects function in the given version, when the client in the web browser access http://localhost:8080/articles the output is correctly

["Article 1","Article 2","Article 3"]

I need to restrict the access to paths based on the roles defined on the users, or maybe with authorities would also work.

In this example, in the Resource Server it is implemented

@EnableWebSecurity
public class ResourceServerConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.mvcMatcher("/articles/**")
            .authorizeRequests()
            .mvcMatchers("/articles/**").access("hasAuthority('SCOPE_articles.read')")
            .and()
            .oauth2ResourceServer()
            .jwt();
            
         return http.build();
    } 

I changed it to

http
    .authorizeRequests()
    .antMatchers("/articles").hasRole("ADMIN")

and also defined in the Authorization Server project an user admin / admin123 with .roles("ADMIN")

However, that doesn't function as expected, when the client accesses http://localhost:8080/articles I get in the client project console the output

Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is org.springframework.web.reactive.function.client.WebClientResponseException$Forbidden: 403 Forbidden from GET http://localhost:8090/articles] with root cause

org.springframework.web.reactive.function.client.WebClientResponseException$Forbidden: 403 Forbidden from GET http://localhost:8090/articles
    at org.springframework.web.reactive.function.client.WebClientResponseException.create(WebClientResponseException.java:183) ~[spring-webflux-5.3.4.jar:5.3.4]
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 
Error has been observed at the following site(s):
    |_ checkpoint ⇢ 403 from GET http://localhost:8090/articles [DefaultWebClient]

I found an example project https://blog.jdriven.com/2019/10/spring-security-5-2-oauth-2-exploration-part1/, but unfortunately it uses Keycloak as authorization server, but implements the same idea of restricting the access to paths with user roles.

It defines a KeycloakRealmRoleConverter, but how to define one for the given three Baeldung projects?

The example with Keycloak uses jwt.getClaims().get("realm_access"), which obviously accesses a key realm_access, but this is related to Keycloak.

To print out the JWT token I changed in the resource server REST controller

@GetMapping("/articles")
public String[] getArticles(final @AuthenticationPrincipal Jwt 
    System.out.println("\n\njwt.getTokenValue():\n" + jwt.getTokenValue());
...
}

At jwt.io it shows

HEADER:ALGORITHM & TOKEN TYPE
{
  "kid": "fce0c3e4-a9a6-4e6d-8fbd-c2b774b338f0",
  "typ": "JWT",
  "alg": "RS256"
}

PAYLOAD:DATA
{
  "sub": "admin",
  "aud": "articles-client",
  "nbf": 1628351323,
  "scope": [
    "articles.read"
  ],
  "iss": "http://127.0.0.1:9000",
  "exp": 1628351623,
  "iat": 1628351323,
  "jti": "c4bc5f35-e93f-483f-8952-a694a04f8f32"
}

No roles defined there. I would have expected next to the username admin also the role ADMIN defined for that user.

Not sure how to restrict the access on a path in the resource server with user role, if it is not in the JWT.

neblaz
  • 683
  • 10
  • 32

2 Answers2

1

Let's first look at the relationship between authorities, roles and scopes in Spring Security.

A role is simply an authority that is prefixed with ROLE_.
The configuration .antMatchers("/articles/**").hasRole("ADMIN") is equivalent to .antMatchers("/articles/**").hasAuthority("ROLE_ADMIN").

By default, the resource server populates the authorities based on the "scope" claim, by prefixing each value with SCOPE_.
In the example you provided, the "scope" claim contains "articles.read", which means the only authority is SCOPE_articles.read.
When using the defaults, the resource server has no concept of roles, because it has no authorities prefixed with ROLE_(they are all prefixed with SCOPE_).

The role "ADMIN", that was given to the "admin" user, is available in the authorization server.
It is not part of the token because the token does not represent a user, but the authorization of the client to access specific parts of a user’s data.

If the resource server requires user information, I suggest looking into the OpenID Connect Protocol.

Here is a similar question that explains why scopes should not be used for the purpose.

  • Just spontaneously thinking... could the Authorizaton Server project be modified so that the JWT token includes an additional key / value pair "roles": ["ADMIN", ...]? – neblaz Aug 10 '21 at 11:15
  • It could, but I would still advise you to look into OpenID Connect instead of building something custom. – Eleftheria Stein-Kousathana Aug 10 '21 at 11:35
  • 1
    OK, but this seems to me such a basic requirement/feature, that I think the Spring Security Authorization Server project, now in 0.1.2, shall integrate it. – neblaz Aug 10 '21 at 12:19
0

The following might be a solution, inspired by the Keycloak example https://blog.jdriven.com/2019/10/spring-security-5-2-oauth-2-exploration-part1/

Resource Server project

@EnableWebSecurity
public class ResourceServerConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers("/articles/**").hasRole("ADMIN")
            .and()
            .oauth2ResourceServer()
            .jwt()
            .jwtAuthenticationConverter(jwtAuthenticationConverter())
            ;

        return http.build();
    }
    
    JwtAuthenticationConverter jwtAuthenticationConverter() {
        final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(new MyRoleConverter());
        
        return jwtAuthenticationConverter;
    }    

    public class MyRoleConverter implements Converter<Jwt, Collection<GrantedAuthority>> {
        @Override
        public Collection<GrantedAuthority> convert(final Jwt jwt) {
            Map<String, String> userRoles = Map.of("admin", "ADMIN", "myuser", "MYUSER");
            
            List<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
            
            String subject = jwt.getSubject();
            
            SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_" + userRoles.get(subject));
            simpleGrantedAuthorities.add(simpleGrantedAuthority);   
            
            return new ArrayList<>(simpleGrantedAuthorities);
        }
    }
}

For testing purposes I hard coded my users and their roles in a

Map<String, String> userRoles = Map.of("admin", "ADMIN", "myuser", "MYUSER");

somewhere in the ResourceServerConfig class.

For now I have to store / create the users and roles in two places, in the Authorization Server project and in the Resource Server project, with a database it would be centralized in one place only of course.

And this is a simplyfied example, assuming that a user has only one role.

neblaz
  • 683
  • 10
  • 32