Real World Example

Where You See This

  • Swiggy, Zomato, Myntra: "Continue with Google" on the web and Android apps (authorization code + PKCE).
  • Paytm and PhonePe "Login with Paytm" on partner sites: OAuth2 consent screens with scopes like "basic profile".
  • DigiLocker: government partner apps use OAuth2 to fetch your documents with your consent.
  • GitHub apps, Slack apps: third-party apps get scoped tokens instead of your password.

ShopEase: Google Login for a React Frontend

ShopEase uses a Backend-for-Frontend (BFF): the Spring Boot app is the OAuth2 client and keeps tokens on the server. The React app only holds an HttpOnly session cookie, so no token can be stolen by XSS. After login, the user is sent back to the React app:

@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http,
                                        ShopEaseOidcUserService oidcUserService,
                                        @Value("${shopease.frontend-url}") String frontendUrl) throws Exception {
    CookieCsrfTokenRepository csrfRepo = CookieCsrfTokenRepository.withHttpOnlyFalse();  // React reads XSRF-TOKEN

    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers(HttpMethod.GET, "/api/products/**", "/api/categories/**").permitAll()
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            .requestMatchers("/api/**").authenticated()
            .anyRequest().permitAll())
        .oauth2Login(oauth -> oauth
            .userInfoEndpoint(ui -> ui.oidcUserService(oidcUserService))
            .successHandler((request, response, authentication) ->
                response.sendRedirect(frontendUrl + "/account"))
            .failureHandler((request, response, exception) ->
                response.sendRedirect(frontendUrl + "/login?error=" + URLEncoder.encode(exception.getMessage(), StandardCharsets.UTF_8))))
        .exceptionHandling(ex -> ex.defaultAuthenticationEntryPointFor(
            new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), PathPatternRequestMatcher.withDefaults().matcher("/api/**")))
        .csrf(csrf -> csrf.csrfTokenRepository(csrfRepo)
            .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()))
        .logout(logout -> logout.logoutUrl("/api/logout")
            .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT)));
    return http.build();
}
// React: the login button is a plain link to the backend
export function LoginButtons() {
  return (
    <div className="login-buttons">
      <a className="btn" href="/oauth2/authorization/google">Continue with Google</a>
    </div>
  );
}

// Any API call sends the session cookie automatically (same site via the dev proxy / same domain)
const res = await fetch('/api/me', { credentials: 'include' });
if (res.status === 401) navigate('/login');

Log output

Server log on a first-time Google login:

tail -f logs/shopease.log

Example output

2026-03-01T10:02:11.482+05:30  INFO 18244 --- [shopease] [nio-8080-exec-4] i.s.security.ShopEaseOidcUserService : New customer created from google: [email protected] (sub=110248495921238986420)
2026-03-01T10:02:11.497+05:30  INFO 18244 --- [shopease] [nio-8080-exec-4] i.s.security.LoginAuditListener       : LOGIN_SUCCESS [email protected] method=oauth2:google ip=49.36.112.18

Note: PathPatternRequestMatcher is the Spring Security 6.5+ matcher; on older 6.x versions use new AntPathRequestMatcher("/api/**").