5

I have a spring boot microservice that validates a JWT (issued by a different service) for authentication. It is working nicely, and I can access the JWT details in my controller like so:

// build.gradle
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'

// MyController.java
@RestController
@RequestMapping("/")
public class MyController {
    @GetMapping()
    public String someControllerMethod(@AuthenticationPrincipal Jwt jwt) {
        int userId = Integer.parseInt(jwt.getClaim("userid"));
        ...
    }
}

That works great. I can extract what I need from the JWT and go on to talk to my database with the correct userid etc.

However I find it a bit tedious to have to use the Jwt type to get these values in each controller. Is there a way I can inject a different type as the @AuthenticationPrincipal?

E.g. my own class which has already extracted what is needed from the JWT, and exposes something like .getUserId() that returns an int? That would also let me centralise the logic of parsing the claims or throwing exceptions if they are not as expected etc.

UPDATE

After more google spelunking, it seems I have two options

Option1: @ControllerAdvice and @ModelAttribute

As explained in this answer. I can do something like:

import com.whatever.CustomPrincipal; // a basic "data" class with some properties, getters, setters and constructor
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ModelAttribute;

@ControllerAdvice
public class SecurityControllerAdvice {

    @ModelAttribute
    public CustomPrincipal customPrincipal(Authentication auth) throws Exception {
        CustomPrincipal customPrincipal;
        if (auth != null && auth.getPrincipal() instanceof Jwt) {
            Jwt jwt = (Jwt) auth.getPrincipal();
            String sessionId = jwt.getClaimAsString("sessionid");
            int userId = Integer.parseInt(jwt.getClaimAsString("userid"));
            customPrincipal = new CustomPrincipal(userId, sessionId);
        } else {
            // log an error and throw an exception?
        }
        return customPrincipal;
    }
}

and then

import com.whatever.CustomPrincipal;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RestController;

@RestController
@ControllerAdvice
public class HelloWorldController {
    @GetMapping("/controlleradvice")
    public String index(@ModelAttribute CustomPrincipal cp) {
        log.info(cp.getUserId());
        return "whatever";
    }
}

This seems pretty succinct, and neat and tidy. 1 new class with @ControllerAdvice, and bob's your uncle!

Option2: Using jwtAuthenticationConverter()

This answer shows another way to do it, using a "converter", which seems to convert the default Principal from a JWT to a custom object (that extends AbstractAuthenticationToken) that contains the JWT (.getCredentials()) as well as a custom object like CustomPrincipal (or a User class or something).

@EnableWebSecurity
public class SecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .cors().disable()
            .csrf().disable()
            .authorizeHttpRequests((authorize) -> authorize
                    .anyRequest().authenticated()
            )
            .oauth2ResourceServer().jwt(customizer -> customizer.jwtAuthenticationConverter((new MyPrincipalJwtConvertor())));
        return http.build();
    }
}


import org.springframework.core.convert.converter.Converter;
import org.springframework.security.oauth2.jwt.Jwt;

public class MyPrincipalJwtConvertor implements Converter<Jwt, MyAuthenticationToken> {
    @Override
    public MyAuthenticationToken convert(Jwt jwt) {
        var principal = new MyPrincipal(Integer.parseInt(jwt.getClaimAsString("userid")), jwt.getClaimAsString("sessionid"));
        return new MyAuthenticationToken(jwt, principal);
    }
}


@RestController
public class HelloWorldController {
    @GetMapping("/converter")
    public String converter(@AuthenticationPrincipal MyPrincipal myPrincipal) {
        log.info("/converter triggered");
        log.info("" + myPrincipal.getUserId());
        return "woo";
    }
}


import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class MyPrincipal {
    private int userId;
    private String sessionId;
}

Option 1 is much simpler it seems.

But Option 2 is nice, as, I have Filter's that run to do additional validation (like validate the session id in the JWT). When that filter runs, when it calls SecurityContext.getContext().getAuthentication().getPrincipal(), it will get the MyPrincipal object, and not have to call Jwt.getClaimAsString() and cast it etc.

I guess I am asking, are there pros and cons to these two approaches I have not considered? Is one of them perhaps bastardising/abusing something in a way it is not meant to be?

Or is it much the same and I should select whichever I prefer?

BoomShaka
  • 1,571
  • 7
  • 27
  • 40
  • 1
    Implement your own https://github.com/spring-projects/spring-security/blob/main/web/src/main/java/org/springframework/security/web/method/annotation/AuthenticationPrincipalArgumentResolver.java ? – Matthias Wiedemann Jan 07 '22 at 19:53
  • Thanks @MatthiasWiedemann. Will investigate in that direction if I get stuck. Currently pursuing other options – BoomShaka Jan 08 '22 at 07:07
  • 1
    there is no pro or con, i would go with option 2 as i consider your request as a configuration issue, and thats exactly what a converter was made for, to convert the jwt to the format you want. – Toerktumlare Jan 10 '22 at 08:11
  • @BoomShaka which solution did you finally choose? I'm facing similar issue and cannot find an appropriate solution for this case. – nazkord Feb 01 '23 at 16:40
  • @nazkord I went with Option 2. IMO it's definitely the better option and seems to be designed for exactly this scenario for the reasons stated. Works perfectly for me. – BoomShaka Feb 03 '23 at 08:06

1 Answers1

0

Just wanted to add to this with a similar thing to "option 1". If you are using a username/password format to log in your user before it generates the jwt, you can declare the Authentication Principal as your UserEntity and assign the value from an interceptor.

The controller method would be like this:

//With this method the preAuthorize becomes redundant but I still like including it.

@PreAuthorize("isAuthenticated()")
    @PutMapping("/updatePassword")
    public ResponseEntity<?> updatePassword(@AuthenticationPrincipal UserEntity loggedUser,
            @RequestBody PasswordUpdateRequest passwordRequest) {
        return userEntityService.updatePassword(loggedUser, passwordRequest);
    }

After you claim, for example, the user id from the jwt, you would esentially load the user from a method in UserDetailsService as such:

UserEntity user = (UserEntity) userDetailsService.loadUserById(userId);

Then declare an UsernamePasswordAuthenticationToken that will be used to be set in the context like this:

UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user,
                        user.getRoles(), user.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);

To implement this interceptor make sure to use addFilterBefore so we can implement this interceptor before the UsernamePasswordAuthenticationFilter.

If you are using SecurityFilterChain do:

@Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
       http.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

If using the older configure method then its the same thing but don´t write the "return http.build();" part.

In these 2 you can also use antmatchers to limit which kind of user can consume a method via the methods .hasRole() and .hasAnyRole().

I would also recommend setting a custom AuthenticationEntryPoint for exception handling and enabling cors from a CorsConfigurationSource annotated as bean(with http.cors() declared in the security filter chain, springboot will pick this cors configuration automatically).

The reason why I like doing it like this is because the code only changes very slightly from the microservice that generates the token to the others that will only validate it, it also makes it so people that will work with my rest endpoints can create services with minimal effort if they need the data of the logged user as they just need to declare @AuthenticationPrincipal UserEntity user in the controller.

It´s my first time answering so if I did so in a wrong way or just didn´t explain myself correctly please let me know.