If you’re doing OAuth2/OIDC + JWT with Spring, you might be using the ThreadLocal SecurityContextHandler to get Data about the authorized user. This code extracts the “JWT Subject” which can be used as a unique identifier for users:

SecurityContextHolder.getContext().getAuthentication().getName()

Your User data model might look Something like this:

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(unique = true)
    private String sub;
    ...
}

If we now want to get The User Object of the authenticated User, we would have to do something like this:

String sub = SecurityContextHolder.getContext().getAuthentication().getName();
Optional<User> optionalUser = userRepository.findBySub(sub);
if (optionalUser.isPresent()) {
    doSomething();
} else {
    doSomethingElse();
}

The UserRespository:

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findBySub(String sub);
}

What if we had a ThreadLocal Object (just like the SecurityContextHolder) which holds a User object the JWT Subject belongs to The UserContextHolder is just a few lines of code:

public class UserContextHolder {
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();
    public static User getUser() {
        return userThreadLocal.get();
    }
    public static void setUser(User user) {
        userThreadLocal.set(user);
    }
    public static void clearUser() {
        userThreadLocal.remove();
    }
}

Now we need to set up a “middleware” (or, how it’s called in Spring: interceptor) that sets the User for each incoming request. This code will check if the User already exists in the database. If so, the ThreadLocal User will be set to the existsing User. If not, a new user with the sub will be created The preHandle() method will be called before the Request is passed to the Controller.

@Component
@AllArgsConstructor
public class UserInterceptor implements HandlerInterceptor {
    private final UserRepository userRepository;
    @Override
    public boolean preHandle(
            HttpServletRequest request,
            HttpServletResponse response,
            Object handler) throws Exception {
        String sub = SecurityContextHolder.getContext()
                        .getAuthentication().getName();
        Optional<User> optionalUser = userRepository.findBySub(sub);
        if (optionalUser.isPresent()) {
            UserContextHolder.setUser(optionalUser.get());
        } else {
            User user = userRepository.save(new User(0L, sub));
            UserContextHolder.setUser(user);
        }
        return true;
    }
}

The last thing that must be done is to register our Interceptor/middleware We also need to do some fancy shit to get our userRepository in there. (@Autowiring the constructor, for some random reason) Add or exclude PathPatters, for example on public routes/endpoints:

@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
    private final ApplicationContext applicationContext;
    @Autowired
    public WebConfiguration(ApplicationContext applicationContext) {
        this.applicationContext = applicationContext;
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        UserRepository userRepository = applicationContext.getBean(
            UserRepository.class
        );
        registry.addInterceptor(new UserInterceptor(userRepository))
                .addPathPatterns("/**")
                .excludePathPatterns("/api/public/**");
    }
}