How to decode and verify customized JWT from Spring Authorization server in Spring Resource Server?

632 views Asked by At

I am using Spring authorization Server 1.0.0. In this I have customized the JWT as per my requirement as follows. Let's say there is a user "vinay". And his role is also "vinay".

I am adding extra field "authority" : [{"role":"ROLE_vinay"}] in JWT. Below is the payload of JWT.

{ "sub": "vinay", "aud": "messaging-client", "nbf": 1671430162, "authority": [ { "role": "ROLE_vinay" } ], "scope": [ "openid" ], "iss": "http://localhost:8000", "exp": 1671430462, "iat": 1671430162 }

For Role "vinay", there are multiple permissions/restrictions. This data is present in MySQL. After user authentication(login), I am adding this data in Redis in KEY:VALUE pair where key is "vinay", value is [Restriction 1, Permission 1, Permission 2].

"vinay" : [Permission 1, Permission 2, Restriction 1]

After succesful login from user(vinay), Client gets the custom token, and sends the request to spring resource server with custom JWT in the header with Bearer prefix. For each request How to decode incoming JWT and get "role" and verify it from Redis? If there is any restriction in Redis, it should not allow it. It should throw "401 unauthorized". If there is permission, then It shall proceed with the request.

1

There are 1 answers

0
Steve Riesenberg On BEST ANSWER

In your latest comment you described using only MySQL, and I think that makes sense. I don't see Redis fitting in anywhere in this scenario. So the flow is:

  1. In authorization server, you use OAuth2TokenCustomizer<JwtEncodingContext> to add the authority field to the JWT (e.g. ROLE_xyz).
  2. In a resource server, you use JwtGrantedAuthoritiesConverter to map the authority field of the JWT to authentication.getAuthorities() in Spring Security.

The challenge is that in your DB, you're mapping roles directly to a set of allowed URLs (e.g. ROLE_xyz => POST /customer, ...). Perhaps you're dealing with an existing database schema and can't design the data or relationships from scratch. What I would expect is a mapping of roles to a set of permissions (e.g. ROLE_xyz => customer.write, ...).

If you have a permissions scheme like this, you could do the lookup in the JwtGrantedAuthoritiesConverter above and map those permissions as authorities instead.


However, if the data in MySQL maps roles to URL patterns, your best bet is to use the AuthorizationManager API to integrate authorization with your MySQL table (see Configure RequestMatcherDelegatingAuthorizationManager under Authorize HttpServletRequests with AuthorizationFilter for an example).

A custom AuthorizationManager can be implemented to look up ROLE_xyz and dynamically build a list of authorization rules with RequestMatcherDelegatingAuthorizationManager.builder().

For example:

@Configuration
@EnableWebMvc
@EnableWebSecurity
public class SecurityConfiguration {

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http, AuthorizationManager<RequestAuthorizationContext> access)
            throws Exception {
        // @formatter:off
        http
            .authorizeHttpRequests((authorize) -> authorize
                .anyRequest().access(access)
            )
            .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
        // @formatter:on

        return http.build();
    }

    @Bean
    AuthorizationManager<RequestAuthorizationContext> authorizationManager(
            HandlerMappingIntrospector introspector) {
        MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector);
        RequestMatcher anyRequest = AnyRequestMatcher.INSTANCE;

        AuthorizationManager<RequestAuthorizationContext> authenticated =
            AuthenticatedAuthorizationManager.authenticated();
        AuthorizationManager<RequestAuthorizationContext> denyAll =
            (a, c) -> new AuthorizationDecision(false);

        return (supplier, context) -> {
            Authentication authentication = supplier.get();
            HttpServletRequest request = context.getRequest();

            // TODO: Look up mappings in MySQL using authentication.getAuthorities()...

            RequestMatcherDelegatingAuthorizationManager.Builder builder =
                RequestMatcherDelegatingAuthorizationManager.builder();

            // TODO: Add custom mappings from MySQL...
            //builder.add(mvcMatcherBuilder.pattern("/customer/**"), authenticated);

            RequestMatcherDelegatingAuthorizationManager delegate =
                builder.add(anyRequest, denyAll).build();

            return delegate.check(() -> authentication, request);
        };
    }

}

This may seem complex, but your authorization scheme (using OAuth2 + JWTs AND roles/authorities AND MySQL to manage roles) is a somewhat complex setup. The AuthorizationManager API actually makes it easier than it has been in earlier versions of Spring Security.

Also note that the above example assumes that role mappings in the database can change at runtime, and so must be looked up every time. If this is not the case, the example could be changed to load data from MySQL only on startup. MySQL lookup performance could also be improved with caching if needed.