Chapter 4: OAuth2 and Social Login
Introduction
Instead of managing passwords yourself, why not let Google, GitHub, or Facebook handle it? OAuth2 allows your users to log in with their existing accounts, thus improving UX and security.
🎯 Chapter Objectives
By the end of this chapter, you will know how to:
- ✅ Understand OAuth2 and its concepts
- ✅ Distinguish between different OAuth2 flows
- ✅ Configure Spring Security OAuth2 Client
- ✅ Implement Google login
- ✅ Retrieve user information
1. OAuth2 Concepts
The 4 Roles
| Role | Description | Example |
|---|---|---|
| Resource Owner | The user who owns the data | John who wants to log in |
| Client | Your application | your-app.com |
| Authorization Server | Server that authenticates | accounts.google.com |
| Resource Server | Server that has the data | googleapis.com |
Authorization Code Flow
The most secure flow for web applications:
2. Spring Boot Configuration
Dependencies
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Google Configuration
Step 1: Create a Google Cloud Project
- Go to https://console.cloud.google.com
- Create a new project
- Go to "APIs & Services" → "Credentials"
- Create "OAuth 2.0 Client IDs"
- Type: "Web application"
- Authorized redirect URIs:
http://localhost:8080/login/oauth2/code/google
Step 2: application.properties
# Google OAuth2
spring.security.oauth2.client.registration.google.client-id=YOUR_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=YOUR_CLIENT_SECRET
spring.security.oauth2.client.registration.google.scope=openid,profile,email
GitHub Configuration
Step 1: GitHub Developer Settings
- Go to https://github.com/settings/developers
- "OAuth Apps" → "New OAuth App"
- Homepage URL:
http://localhost:8080 - Authorization callback URL:
http://localhost:8080/login/oauth2/code/github
Step 2: application.properties
# GitHub OAuth2
spring.security.oauth2.client.registration.github.client-id=YOUR_CLIENT_ID
spring.security.oauth2.client.registration.github.client-secret=YOUR_CLIENT_SECRET
spring.security.oauth2.client.registration.github.scope=user:email
3. SecurityConfig for OAuth2
Basic Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout
.logoutSuccessUrl("/")
)
.build();
}
}
Login Page with OAuth2 Buttons
<!-- templates/login.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
<style>
.oauth-buttons { margin-top: 20px; }
.oauth-btn {
display: inline-block;
padding: 10px 20px;
margin: 5px;
border-radius: 5px;
text-decoration: none;
color: white;
}
.google { background: #db4437; }
.github { background: #333; }
</style>
</head>
<body>
<div class="login-container">
<h1>Login</h1>
<!-- Classic Login -->
<form th:action="@{/login}" method="post">
<input type="text" name="username" placeholder="Username">
<input type="password" name="password" placeholder="Password">
<button type="submit">Log in</button>
</form>
<hr>
<!-- OAuth2 Login -->
<div class="oauth-buttons">
<p>Or log in with:</p>
<a href="/oauth2/authorization/google" class="oauth-btn google">
🔵 Google
</a>
<a href="/oauth2/authorization/github" class="oauth-btn github">
⚫ GitHub
</a>
</div>
</div>
</body>
</html>
4. Retrieve User Information
In a Controller
@Controller
public class ProfileController {
@GetMapping("/dashboard")
public String dashboard(
@AuthenticationPrincipal OAuth2User oauth2User,
Model model) {
if (oauth2User != null) {
model.addAttribute("name", oauth2User.getAttribute("name"));
model.addAttribute("email", oauth2User.getAttribute("email"));
model.addAttribute("picture", oauth2User.getAttribute("picture"));
model.addAttribute("attributes", oauth2User.getAttributes());
}
return "dashboard";
}
}
Attributes by Provider
Google
oauth2User.getAttribute("sub"); // Unique ID
oauth2User.getAttribute("name"); // Full name
oauth2User.getAttribute("email"); // Email
oauth2User.getAttribute("picture"); // Photo URL
GitHub
oauth2User.getAttribute("id"); // Unique ID
oauth2User.getAttribute("login"); // Username
oauth2User.getAttribute("name"); // Full name
oauth2User.getAttribute("email"); // Email (can be null)
oauth2User.getAttribute("avatar_url"); // Avatar URL
5. Linking OAuth2 to Your Users
Strategy
OAuth2Connection Entity
@Entity
@Table(name = "oauth2_connections")
@Data
@NoArgsConstructor
public class OAuth2Connection {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String provider; // google, github
@Column(nullable = false)
private String providerUserId; // ID at the provider
@Column(nullable = false)
private LocalDateTime connectedAt = LocalDateTime.now();
}
CustomOAuth2UserService
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final UserRepository userRepository;
private final OAuth2ConnectionRepository connectionRepository;
@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oauth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();
String providerUserId = getProviderUserId(oauth2User, provider);
String email = oauth2User.getAttribute("email");
String name = oauth2User.getAttribute("name");
// Look for an existing connection
Optional<OAuth2Connection> existingConnection = connectionRepository
.findByProviderAndProviderUserId(provider, providerUserId);
User user;
if (existingConnection.isPresent()) {
user = existingConnection.get().getUser();
} else if (email != null && userRepository.existsByEmail(email)) {
// Link to an existing account by email
user = userRepository.findByEmail(email).orElseThrow();
createConnection(user, provider, providerUserId);
} else {
// Create a new account
user = createNewUser(email, name);
createConnection(user, provider, providerUserId);
}
return new CustomOAuth2User(oauth2User, user);
}
private String getProviderUserId(OAuth2User oauth2User, String provider) {
return switch (provider) {
case "google" -> oauth2User.getAttribute("sub");
case "github" -> String.valueOf(oauth2User.getAttribute("id"));
default -> throw new OAuth2AuthenticationException("Unsupported provider");
};
}
private User createNewUser(String email, String name) {
User user = new User();
user.setEmail(email);
user.setUsername(email); // Or generate a username
user.setPassword(""); // No password for OAuth2
user.setEnabled(true);
// Add default role
return userRepository.save(user);
}
private void createConnection(User user, String provider, String providerUserId) {
OAuth2Connection connection = new OAuth2Connection();
connection.setUser(user);
connection.setProvider(provider);
connection.setProviderUserId(providerUserId);
connectionRepository.save(connection);
}
}
CustomOAuth2User
public class CustomOAuth2User implements OAuth2User {
private final OAuth2User oauth2User;
private final User user;
public CustomOAuth2User(OAuth2User oauth2User, User user) {
this.oauth2User = oauth2User;
this.user = user;
}
@Override
public Map<String, Object> getAttributes() {
return oauth2User.getAttributes();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
@Override
public String getName() {
return user.getUsername();
}
public User getUser() {
return user;
}
}
Configuration with Custom Service
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CustomOAuth2UserService customOAuth2UserService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.defaultSuccessUrl("/dashboard")
)
.build();
}
}
6. Combine OAuth2 + Form Login
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register", "/error").permitAll()
.anyRequest().authenticated()
)
// Form login
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
)
// OAuth2 login
.oauth2Login(oauth -> oauth
.loginPage("/login")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService)
)
.defaultSuccessUrl("/dashboard")
)
.logout(logout -> logout.permitAll())
.build();
}
7. OAuth2 for REST API (JWT + OAuth2)
Hybrid Flow
Token Exchange Endpoint
@RestController
@RequestMapping("/api/auth")
@RequiredArgsConstructor
public class OAuth2TokenController {
private final GoogleIdTokenVerifier verifier;
private final UserService userService;
private final JwtService jwtService;
@PostMapping("/google")
public ResponseEntity<AuthResponse> googleAuth(@RequestBody GoogleAuthRequest request) {
try {
// Validate Google token
GoogleIdToken idToken = verifier.verify(request.getIdToken());
if (idToken == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail();
String name = (String) payload.get("name");
String pictureUrl = (String) payload.get("picture");
// Create or find the user
User user = userService.findOrCreateGoogleUser(email, name, pictureUrl);
// Generate JWT
UserDetails userDetails = new CustomUserDetails(user);
String accessToken = jwtService.generateToken(userDetails);
String refreshToken = jwtService.generateRefreshToken(userDetails);
return ResponseEntity.ok(AuthResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.username(user.getUsername())
.build());
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
}
}
}
8. Summary
Key Concepts
| Term | Description |
|---|---|
| OAuth2 | Authorization protocol |
| Authorization Code | Flow for web apps |
| Access Token | Access to resources |
| ID Token | User identity (OpenID Connect) |
Minimal Configuration
spring.security.oauth2.client.registration.google.client-id=XXX
spring.security.oauth2.client.registration.google.client-secret=YYY
Login Links
/oauth2/authorization/google → Google Login
/oauth2/authorization/github → GitHub Login
🎯 Next Step
Now that you have mastered authentication, let's move on to advanced security features (CSRF, CORS, @PreAuthorize).
➡️ Chapter 5: Advanced Security