Search code examples
javaspring-bootspring-mvcoauth-2.0

Spring OAuth2 resource server load synchronized user from database


Brief description of the initial situation: Let's assume a Spring Boot based RESTful API acting as a OAuth2 Resource Server. The resource server is cofigured using Spring Security 5 and common approaches. The external authorization server delivers user information (e.g. E-Mail, First Name, Last Name) as JWT claims, received when a client authenticates. The generic user information, hold by the authorization server, are extended by resource server specific user information (e.g. Business Roles, Domain UID). The domain user is composed of information from two data sources:

  • Authorization Server User Data
  • Resource Server (Application) User Data

New users who have never been authenticated at the resource server via a JWT are created in the database of the resource server. The database of the resource server accordingly contains a user entity for each user who uses the API of the resource server, consisting of the information synchronized by the authorization server and the information supplemented by the business logic of the resource server.

public class User {

    // Information synchronized from Authorization Server
    private String subject;
    private String preferredUsername;
    private String firstName;
    private String lastName;
    private String email;

    // Information added by Resource Server's business logic
    private UUID id;
    private String businessRole;
}

The synchronization of the domain users is carried out by listening to the AuthenticationSuccessEvent and creating the user in the database or updating it if necessary. To summarize, for each OAuth2 user, identified by the subject claim, there is a domain profile with domain-specific additional information in database.

This article also clearly describes this synchronization and distribution of user data.

Now to the actual question: While OAuth2 Scopes control which authorizations an application has, it must also be controlled which authorizations the user has. The user authorizations are domain-specific and recorded in the database of the Resource Server. For example, a user should only be able to delete comments that he has created. Such access control cannot be controlled via OAuth2 Scopes. As an aside, I'm talking about the Spring Method Security using @PreAuthorize or @PostAuthorize.

@GetMapping
@PreAuthorize("...")
public void func(@AuthenticationPrincipal JWT jwt) {
}

Such access could be controlled via Attribute based Access control (ABAC), for example. However, this assumes that the current @AuthenticationPrincipal is not a JWT as it is standard for a Spring Resource Server, but an instance of the domain-specific User profile. Is there a way to convert a JWT into a User profile, possibly using a UserDetailsService?

Is there a recommended approach for Spring Security 5 OAuth2 Resource Server to load information from the database based on the JWT, more precisely a User?


Solution

  • What I usually end up with is a JWT converter and creating a custom session object (AbstractAuthenticationToken)

    @Autowired
    private final UserService userService;
    
    @Override
    public void configure(HttpSecurity http) {
        http
          .oauth2ResourceServer().jwt()
          .jwtAuthenticationConverter(new JwtConverter(userService))
          ... other security config ...
    

    JWT Converter:

    public class JwtConverter implements Converter<Jwt, AbstractAuthenticationToken> {
        private final UserService userService;
    
        public JwtConverter(UserService userService) {
            this.userManager = userService;
        }
    
        @Override
        public AbstractAuthenticationToken convert(@NotNull final Jwt jwt) {
            // Here is where I usually lazy-create or sync users instead of AuthenticationSuccessEvent
            User user = userService.getByEmail(jwt.getClaim("email"));
            Collection<? extends GrantedAuthority> authorities = translateAuthorities(jwt);
            return new CustomSession(user, jwt, authorities);
        }
    
        // Translate from your jwt as seen fit (I use a roles claim)
        private static Collection<? extends GrantedAuthority> translateAuthorities(final Jwt jwt) {
            Collection<String> userRoles = jwt.getClaimAsStringList("roles");
            if (userRoles != null)
                return userRoles
                        .stream()
                        .map(role -> new SimpleGrantedAuthority("ROLE_" + role.toUpperCase()))
                        .collect(Collectors.toSet());
            return Collections.emptySet();
        }
    }
    

    Rough example CustomSession:

    public class CustomSession extends AbstractAuthenticationToken {
        final private User user;
        final private Jwt jwt;
    
        public CustomSession(User user, Jwt jwt, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.user = user;
            this.jwt  = jwt;
            this.setAuthenticated(true);
        }
    
        @Override
        public Object getPrincipal() {
            return getUser();
        }
    
        @Override
        public Object getCredentials() {
            return getJwt();
        }
    
        public Jwt getJwt() {
            return jwt;
        }
    
        public User getUser() {
            return user;
        }
        
    
        //*********************************************************************
        //* Static Helpers
        //*********************************************************************
        public static Optional<CustomSession> GetSession() {
            return Optional.ofNullable(SecurityContextHolder.getContext().getAuthentication())
                    .map(s -> s instanceof CustomSession ? (CustomSession) s : null);
        }
    
        public static Optional<Jwt> GetJwt() {
            return GetSession().map(CustomSession::getJwt);
        }
    
        public static Optional<User> GetUser() {
            return GetSession().map(CustomSession::getUser);
        }
    
        public static Optional<String> GetUserId() {
            return GetUser().map(User::getId);
        }
    
        public static Optional<String> GetUserEmail() {
            return GetUser().map(User::getEmail);
        }
    }
    

    In my case, I use JPA behind the userService.getByEmail() so something to note is the User object is 'detached', so you shouldn't use this object for user updates.

    Another option is extending BearerTokenAuthenticationToken and providing a constructor to set the authorities (instead of the CustomSession).

    ====

    Came back from lunch and completely forgot to mention the permission checks. I introduce a custom permission evaluator (a simple example to get you going):

    public class CustomPermissionEvaluator implements PermissionEvaluator {
        private SecurityService securityService;
    
        public CustomPermissionEvaluator(SecurityService securityService) {
            this.securityService = securityService;
        }
    
        @Override
        public boolean hasPermission(Authentication auth, Object resourceId, Object action) {
            if ((auth == null)
                    || !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
                    || !(action instanceof String) || StringUtils.isBlank((String) action)){
                return false;
            }
            try {
                return securityService.hasAccess((String) resourceId, (String) action);
            } catch(Exception e){
                return false;
            }
        }
    
        @Override
        public boolean hasPermission(Authentication auth, Serializable resourceId, String component, Object action) {
            if ((auth == null)
                    || !(resourceId instanceof String) || StringUtils.isBlank((String) resourceId)
                    || StringUtils.isBlank(component)
                    || !(action instanceof String) || StringUtils.isBlank((String) action)) {
                return false;
            }
            try {
                return securityService.hasAccess((String) resourceId, component, (String) action);
            } catch(Exception e){
                return false;
            }
        }
    }
    

    Configured via:

    @Configuration
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
        @Autowired SecurityService securityService;
    
        @Override
        protected MethodSecurityExpressionHandler createExpressionHandler() {
            DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
            expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator(securityService));
            return expressionHandler;
        }
    }
    

    Then utilize @PreAuthorize("hasPersmission(...)"") annotation like:

    // Can I Create {SubDomain} on resourceId
    @PreAuthorize("hasPermission(#resourceId, 'SubDomain', 'CREATE')")
    void example1(int resourceId) {...}    
    
    // Can I Update resource
    @PreAuthorize("hasPermission(#resourceId, 'UPDATE')")
    

    Probably not exactly what you were looking for, but another (clean in my opinion) solution.