0

Please bare with me as I am learning Spring Data REST as I go. Definitely feel free to suggest a safer approach if what I am proposing here is not the safest approach or even possible.

Problem

A user logs into my Spring Data REST API via Google OAuth2. After logging in, I need to get the user's ID, which is just the value of their primary key, from the User table. The reason I need the ID is to restrict access to endpoints such as /users/{id}. If a user's ID is 1, then he is only allowed to view /users/1, unless he is an Administrator.

Current Login Architecture

This portion of the application works as expected:

enter image description here

OAuth2AuthenticationSuccessHandler.java:

@Component("oauth2authSuccessHandler")
public class OAuth2AuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Autowired
    private UserDataRestRepository userRepository;
    @Autowired
    private RandomStringGenerator randomStringGenerator;
    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {

        OAuth2AuthenticationToken authenticationToken = (OAuth2AuthenticationToken) authentication;

        String email = authenticationToken.getPrincipal().getAttributes().get("email").toString();

        if (!userRepository.existsByEmail(email)) {
            // If email not found, create local user account.

            String firstName = CaseUtils.toCamelCase(
                    authenticationToken.getPrincipal().getAttributes().get("given_name").toString().toLowerCase(),
                    true);

            String lastName = CaseUtils.toCamelCase(
                    authenticationToken.getPrincipal().getAttributes().get("family_name").toString().toLowerCase(),
                    true);

            // Generate temporary username.
            BigInteger usernameSuffix = userRepository.getNextValSeqUserUsername();
            String username = "User" + usernameSuffix;

            // Generate temporary password.
            String password = randomStringGenerator.generate(20).toUpperCase();

            // Encode Password.
            String encodedPassword = passwordEncoder.encode(password);

            User newUser = new User();
            newUser.setFirstName(firstName);
            newUser.setLastName(lastName);
            newUser.setUsername(username);
            newUser.setUserSetUsername(false);
            newUser.setEmail(email);
            newUser.setPassword(encodedPassword);
            newUser.setUserSetPassword(false);
            newUser.setCreated(new Timestamp(System.currentTimeMillis()));
            newUser.setDisabled(false);
            newUser.setLocked(false);

            userRepository.save(newUser);
        }

        // Redirect to root page.
        redirectStrategy.sendRedirect(request, response, "/");
    }
}

UserDataRestRepository.java:

@RepositoryRestResource(collectionResourceRel = "users", path = "users")
public interface UserDataRestRepository extends PagingAndSortingRepository<User, Integer>, CrudRepository<User, Integer> {
    
    public boolean existsByEmail(String email);

    @Query(value="SELECT NEXT VALUE FOR Seq_User_Username", nativeQuery=true)
    public BigInteger getNextValSeqUserUsername();
}

OAuth2LoginSecurityConfig.java:

@Configuration
@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    @Autowired
    private AuthenticationSuccessHandler oauth2authSuccessHandler;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests(authorize -> authorize
                .anyRequest()
                .authenticated()
            )
            .oauth2Login()
                .successHandler(oauth2authSuccessHandler);
        return http.build();
    }
}

Database Tables

User:

enter image description here

Role:

enter image description here

UserRole:

enter image description here

Current Post-authentication Problems

Currently, after a user logs in via OAuth2, we do not have access to the user's ID (which is the value of their primary key in the User table) unless we access the Authentication object, get the email address, then get the user's ID from the User table based on the email address. Basically: Select Id from User where Email = user@example.com

Proposed Login Architecture

I am wondering if the UserDetailsService can solve the problem, illustrated in green:

enter image description here

My train of thought is: by getting the user's info from the User table, then loading it into the UserDetailsService, the entire application now has access to the user's ID (primary key) and all of the other info in that row via the UserDetailsService.

Thanks for your help.

fire_water
  • 1,380
  • 1
  • 19
  • 33
  • 1
    "I am wondering if the UserDetailsService can solve the problem, illustrated in green:" -- Yes! This is exactly the way to do it. You can put any info you want from the db in the `UserDetail` object, which will then be available in the `Authentication` object later. – egeorge Feb 16 '23 at 20:39

1 Answers1

1

I would not include a round-trip to the DB for each incoming request on resource-server(s), this is a waste of resources.

Have you considered using an authorization-server in front of Google and either directly bound to your user database, or capable of calling a web-service to fetch additional data? Many propose it, along with "login with Google".

You would have complete control of access and ID tokens claims: you can add about anything as private claim, but the request to the DB (or Web-Service) happens only once per token issuance (and not once per request to a resource-server). And if you define an Authentication of your own (easy, just extend AbstractAuthenticationToken), you can put accessors to cast those private claims from Object to something more relevant to your security / domain model.

I demo something similar (add a private claim to access-token with a value returned by a web-service and then use it for access-control with custom Authentication and security DSL) in this project.

ch4mp
  • 6,622
  • 6
  • 29
  • 49