OAuth2 & Social Login
The OAuth2 login flow with a provider like Google, using spring-boot-starter-oauth2-client and oauth2Login().
What "Login with Google" actually does
OAuth2 login delegates authentication to a third party (Google, GitHub, and so on) instead of your application collecting and checking a password itself. The end result: your application never sees the user's Google password at all — it receives a token proving the user successfully authenticated with Google, along with some basic profile information Google is willing to share.
1. User clicks "Log in with Google" on your app
|
v
2. Your app redirects to Google's login/consent screen
|
v
3. User logs into Google (or is already logged in) and approves the requested access
|
v
4. Google redirects back to your app with an authorization code
|
v
5. Your app exchanges that code (server-to-server) for an access token + ID token
|
v
6. Your app calls Google's userinfo endpoint (or reads the ID token) to get the user's
email/name, and considers them logged in
This is the OAuth2 authorization code flow, and it's the same flow underlying "Sign in with Google/GitHub/Microsoft" buttons across the web — Spring Security implements the entire exchange for you once it's configured.
Dependency and provider configuration
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
That's the entire configuration needed for Google specifically — Spring Boot recognizes google as one of a handful of well-known providers (also github, facebook, okta) and auto-configures the authorization URL, token URL, and user-info URL for it. A provider Spring doesn't recognize out of the box needs those endpoints declared explicitly under a provider: block.
Wiring it into the SecurityFilterChain
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login") // an optional custom login page listing provider buttons
.defaultSuccessUrl("/dashboard", true)
);
return http.build();
}
}
.oauth2Login(...) enables the entire flow shown above — Spring Security auto-registers the redirect endpoints (/oauth2/authorization/google), handles the callback, exchanges the code for tokens, and populates the SecurityContext with an authenticated principal, all without you writing any of the token-exchange logic by hand.
Reading the authenticated user
The resulting principal is an OAuth2User (or OidcUser for OpenID Connect providers like Google), carrying whatever attributes the provider returned:
@RestController
public class ProfileController {
@GetMapping("/api/me")
public Map<String, Object> me(@AuthenticationPrincipal OAuth2User principal) {
return Map.of(
"email", principal.getAttribute("email"),
"name", principal.getAttribute("name")
);
}
}
Most real applications still want their own local User entity (to store application-specific data — roles, preferences, an internal ID) rather than relying on the OAuth2 principal directly everywhere. The standard pattern links the two on first login:
@Service
public class OAuth2UserLinkingService {
private final UserRepository userRepository;
public OAuth2UserLinkingService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User findOrCreateFromOAuth2(OAuth2User oauth2User) {
String email = oauth2User.getAttribute("email");
return userRepository.findByEmail(email)
.orElseGet(() -> userRepository.save(new User(email, oauth2User.getAttribute("name"))));
}
}
A custom OAuth2UserService bean is the typical hook point for running this linking logic automatically as part of the login flow itself, rather than in every controller that needs the local user.
Common mistakes
- Storing the OAuth2 client secret in source control instead of an environment variable/secrets manager — functionally the same mistake as hardcoding a database password.
- Assuming
OAuth2User/OidcUserattributes are a stable, guaranteed contract — different providers return different attribute keys (emailisn't always present, some providers use different casing/nesting), so defensive handling matters when supporting more than one provider. - Skipping the local-user-linking step and threading
OAuth2Userdirectly through business logic everywhere — couples application logic to a specific identity provider's attribute shape instead of your own stable domain model. - Forgetting that
oauth2Login()and traditionalformLogin()can coexist in the sameSecurityFilterChain— an application isn't forced to choose only one authentication method.