1

I have a React application and I have a Spring Boot backend currently using Spring Security to authenticate the users using OAuth2 code grant flow, with both Google and Azure.

This was all working fine when I had just one type of user that required me to ask for a specific set of scopes (email access and calendar access). I was able to login as a Google/Azure user, request permissions for the scopes I needed, get access tokens and access the email and calendars of the users. Lets call this type of user "USER1".

Now I have the requirement for another type of user. For this user I only need access to their profile information and no integration with calendars etc. They still need to be able to login with Google/Azure via the React app (using a different page/button from USER1 of course). Lets call this type of user "USER2".

I cant seem to work out in Spring Security how to configure USER1 to trigger the login process and have Spring ask Google/Azure for one set of scopes and for USER2 to trigger the login process and ask Google/Azure for a different set of scopes.

My current security config looks something like this:

@EnableWebSecurity
public class OAuth2LoginSecurityConfig {

    private final long MAX_AGE = 3600;
    private final CustomOAuth2UserService customOAuth2UserService; //for google
    private final CustomOidcUserService customOidcUserService; //for azure
    
    private final MyUserRepository myUserRepository;
    private final OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler;
    private final OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler;
    private final AppConfig appConfig;

    public OAuth2LoginSecurityConfig(CustomOAuth2UserService customOAuth2UserService, CustomOidcUserService customOidcUserService, MyUserRepository myUserRepository, OAuth2AuthenticationSuccessHandler oAuth2AuthenticationSuccessHandler, OAuth2AuthenticationFailureHandler oAuth2AuthenticationFailureHandler, AppConfig appConfig) {
        this.customOAuth2UserService = customOAuth2UserService;
        this.customOidcUserService = customOidcUserService;
        this.myUserRepository = myUserRepository;
        this.oAuth2AuthenticationSuccessHandler = oAuth2AuthenticationSuccessHandler;
        this.oAuth2AuthenticationFailureHandler = oAuth2AuthenticationFailureHandler;
        this.appConfig = appConfig;
    }

    @Bean
    public HttpCookieOAuth2AuthorizationRequestRepository cookieAuthorizationRequestRepository() {
        return new HttpCookieOAuth2AuthorizationRequestRepository();
    }

    @Bean
    public AuthenticateRequestsFilter tokenAuthenticationFilter() {
        return new AuthenticateRequestsFilter(myUserRepository);
    }

    @Bean
    public OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> accessTokenResponseClient() {
        DefaultAuthorizationCodeTokenResponseClient accessTokenResponseClient =
                new DefaultAuthorizationCodeTokenResponseClient();

        OAuth2AccessTokenResponseHttpMessageConverter tokenResponseHttpMessageConverter =
                new OAuth2AccessTokenResponseHttpMessageConverter();
        tokenResponseHttpMessageConverter.setTokenResponseConverter(new CustomTokenResponseConverter());
        RestTemplate restTemplate = new RestTemplate(Arrays.asList(
                new FormHttpMessageConverter(), tokenResponseHttpMessageConverter));
        restTemplate.setErrorHandler(new OAuth2ErrorResponseErrorHandler());

        accessTokenResponseClient.setRestOperations(restTemplate);
        return accessTokenResponseClient;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .cors()
                .and()
            .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
            .csrf().disable()
            .formLogin().disable()
            .httpBasic().disable()
            .exceptionHandling()
                .authenticationEntryPoint(new RestAuthenticationEntryPoint())
                .and()
            .authorizeHttpRequests()
                .antMatchers("/", "/error", "/data", "/data/*").permitAll()
                .antMatchers(HttpMethod.GET, "/someurl/*/thingx", "/someurl/*/thingy").permitAll()
                .antMatchers("/auth/**", "/oauth2/**", "/user-logout").permitAll()
                .anyRequest().authenticated()
                .and()
            .oauth2Login()
                .authorizationEndpoint()
                .baseUri("/oauth2/authorize")
                .authorizationRequestRepository(cookieAuthorizationRequestRepository())
                .and()
                .redirectionEndpoint()
                .baseUri("/oauth2/callback/*")
                .and()
                .tokenEndpoint()
                .accessTokenResponseClient(accessTokenResponseClient())
                .and()
                .userInfoEndpoint()
                .userService(customOAuth2UserService)
                .oidcUserService(customOidcUserService)
                .and()
                .successHandler(oAuth2AuthenticationSuccessHandler)
                .failureHandler(oAuth2AuthenticationFailureHandler)
                .and()
            .addFilterBefore(tokenAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public CorsFilter corsFilter() {

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.addAllowedOrigin(appConfig.getClientBaseUrl().toString());
        config.addAllowedHeader("*");
        config.addAllowedMethod("GET");
        config.addAllowedMethod("POST");
        config.addAllowedMethod("PUT");
        config.addAllowedMethod("PATCH");
        config.addAllowedMethod("DELETE");
        config.addAllowedMethod("OPTIONS");
        config.setMaxAge(MAX_AGE);
        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }
}

My application.yml looks like this (some things deleted as not relevant):

spring:
  mvc:
    format:
      date: yyyy-MM-dd
      time: HH:mm:ss
  security:
    oauth2:
      client:
        provider:
          azure:
            token-uri: https://login.microsoftonline.com/common/oauth2/v2.0/token
            authorization-uri: https://login.microsoftonline.com/common/oauth2/v2.0/authorize
            user-info-uri: https://graph.microsoft.com/oidc/userinfo
            jwk-set-uri: https://login.microsoftonline.com/common/discovery/v2.0/keys
            user-name-attribute: name
            user-info-authentication-method: header
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline
        registration:
          azure:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            authorization-grant-type: authorization_code
            scope:
              - openid
              - email
              - profile
              - offline_access
              - https://graph.microsoft.com/Calendars.ReadWrite
              - https://graph.microsoft.com/User.Read
          google:
            client-id: FROM_ENV
            client-secret: FROM_ENV
            redirect-uri: "{baseUrl}/oauth2/callback/{registrationId}"
            scope:
              - email
              - profile
              - https://www.googleapis.com/auth/gmail.send
              - https://www.googleapis.com/auth/calendar

app:
  client-base-url: SOME_URL
  server-base-url: SOME_URL

Any help or resources are much appreciated!

dur
  • 15,689
  • 25
  • 79
  • 125
cowley05
  • 123
  • 1
  • 1
  • 10
  • Scopes are for clients not for users, but the user can choose which scopes are granted for a client. The only way I see is to configure two clients for each authorization sever, one client for each type of user. – dur Jan 28 '23 at 11:47
  • 1
    Ahh I think that makes sense. So what you're saying is I could configure another client let call it user2-google and have different scopes defined but point to the same endpoints, then use that when accessing as user1? – cowley05 Jan 30 '23 at 01:00

0 Answers0