OAuth2 in Spring Boot: Complete Google Login Tutorial
This chapter covers the complete theory, security architecture, and end-to-end implementation of OAuth2 in a Spring Boot application. You will learn how authentication and authorization work in modern web architectures, why OAuth2 replaced older protocols like OAuth 1.0, how to register a project on Google Cloud, and how to build a production-grade custom callback controller to exchange authorization codes for JWT tokens.
1. Why Do We Need OAuth2? (The Core Problem)
Imagine you are using a third-party application (like LeetCode) and you want to grant it access to certain files or resources residing in your Google account.
- The Naive Approach: Giving your username and password directly to the third-party app.
- The Problem: If the third-party app is compromised, your core credentials leak. Furthermore, the app gains unrestricted access to your entire account instead of just the specific resources it actually needs.
Real-Life Analogy: Hotel Room Key Cards vs. Master Keys
Giving your password to a third-party app is like handing the permanent master key of your house to a delivery worker. OAuth2 is like checking into a hotel and receiving a temporary, restricted RFID key card that only opens your specific room and expires after check-out. You never give away your master credentials.
OAuth2 (Open Authorization 2.0) solves this by allowing third-party applications to access specific parts of your user account without ever exposing your credentials, with the ability to revoke that access at any time.
2. Real-World Use Cases of OAuth2
OAuth2 is ubiquitous across modern software systems:
- Social Logins ("Sign in with Google / GitHub / Apple"): Allowing users to register and log in instantly without creating a new password.
- Automated Marketing Tools: Tools that connect to your Instagram or YouTube account to reply to comments or send direct messages (DMs) on your behalf, governed by restricted permissions (scopes).
- Third-Party Integrations: Allowing Spotify to read your public playlist or GitHub to access your repository list.
3. What is OAuth2? (Core Protocol Definition)
- Protocol Definition: OAuth2 is an authorization framework (a set of rules and guidelines) that enables applications to obtain limited access to user accounts on an HTTP service.
- Open Standard: It is completely open and not owned by any single company. Anyone can implement it.
- Why "2"?: OAuth 1.0 existed previously, but it had significant disadvantages (such as requiring cryptographic signatures on every single HTTP request, making it slow and overly complex). OAuth2 fixes this by using bearer tokens over HTTPS.
4. The Complete OAuth2 Authorization Flow (Step-by-Step)
Understanding the step-by-step handshake between the User, Client Application, and Authorization Server is crucial before writing code.
Key Parameters Sent in Step 2:
client_id: Unique identifier issued by Google when you register your app in Google Cloud Console.redirect_uri: The endpoint on your server where Google should send the user back after consent.response_type: Set tocode(indicating we want an authorization code flow).scope: The specific permissions requested (e.g.,email,profile).state: A unique random cryptographic string generated by the client to prevent CSRF (Cross-Site Request Forgery) attacks.
5. Authorization Code Flow vs. Implicit Token Flow
When designing applications, choosing the right response type depends heavily on where your client code executes.
Security Note: In Single Page Applications (SPAs) running purely in a browser, you cannot securely store a
client_secret. Therefore, tokens are sometimes issued directly to the browser, but this exposes tokens in browser history and logs. Routing the flow through a secure backend server keeps theclient_secretsafe.
6. Configuring Google Cloud Console (OAuth Client Setup)
Before writing Spring Boot code, you must register your application with Google to obtain your credentials.
- Go to the Google Cloud Console.
- Create a new project or select an existing one.
- Navigate to APIs & Services > OAuth consent screen:
- Select External user type (so any Google user can log in).
- Fill in the App Name (e.g.,
Journal App) and User Support Email. - Add required scopes:
email,profile, andopenid. - Publish the app (Verification is not required for testing/development).
- Navigate to Credentials > Create Credentials > OAuth client ID:
- Application type: Web application.
- Authorized redirect URIs: Add your callback URL (e.g.,
http://localhost:8080/auth/google/callbackor your OAuth playground redirect URI).
- Copy your generated Client ID and Client Secret.
7. Simulating the Flow Using Google OAuth Playground
Because setting up a full frontend UI just to test the initial redirect can be tedious, we can use the Google OAuth 2.0 Playground (developers.google.com/oauthplayground) to simulate the frontend's role in obtaining the authorization code.
- Open OAuth Playground and click the gear icon in the top right.
- Check "Use your own OAuth credentials".
- Paste your Google Client ID and Client Secret.
- In Step 1 (Select APIs), scroll down to custom scopes and enter:
https://www.googleapis.com/auth/userinfo.email. - Click Authorize APIs, log in with your Google account, and click Continue.
- Copy the resulting Authorization Code provided by the playground.
8. Building the Spring Boot OAuth2 Callback Controller
Now, let's build the backend controller in Spring Boot that receives the authorization code from the frontend, exchanges it with Google for an access token, fetches the user's profile, and issues our application's custom JWT.
Step 8.1: Application Properties Configuration
Add your Google client credentials and redirect URI in application.yml:
spring:
security:
oauth2:
client:
registration:
google:
client-id: "YOUR_GOOGLE_CLIENT_ID"
client-secret: "YOUR_GOOGLE_CLIENT_SECRET"
redirect-uri: "http://localhost:8080/auth/google/callback"
Step 8.2: Writing the Controller Implementation
Create a controller endpoint that accepts the authorization code, packages it into a multi-value map, and posts it to Google's token endpoint.
package net.engineeringdigest.journalapp.controller;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/auth/google")
public class GoogleAuthController {
private static final Logger logger = LoggerFactory.getLogger(GoogleAuthController.class);
@Autowired
private RestTemplate restTemplate;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private UserRepository userRepository;
@Autowired
private JwtUtil jwtUtil;
@GetMapping("/callback")
public ResponseEntity<?> handleGoogleCallback(@RequestParam("code") String code) {
try {
String tokenEndpoint = "https://oauth2.googleapis.com/token";
// 1. Prepare request headers
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
// 2. Prepare request body using LinkedMultiValueMap (Crucial for OAuth form posts)
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("code", code);
params.add("client_id", "YOUR_GOOGLE_CLIENT_ID");
params.add("client_secret", "YOUR_GOOGLE_CLIENT_SECRET");
params.add("redirect_uri", "http://localhost:8080/auth/google/callback");
params.add("grant_type", "authorization_code");
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
// 3. Exchange Authorization Code for Access Token
ResponseEntity<Map> tokenResponse = restTemplate.postForEntity(tokenEndpoint, request, Map.class);
if (!tokenResponse.getStatusCode().is2xxSuccessful() || tokenResponse.getBody() == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Failed to retrieve access token from Google");
}
String accessToken = (String) tokenResponse.getBody().get("access_token");
// 4. Fetch User Info using Access Token
String userInfoUrl = "https://www.googleapis.com/oauth2/v3/userinfo";
HttpHeaders authHeaders = new HttpHeaders();
authHeaders.setBearerAuth(accessToken);
HttpEntity<Void> userInfoRequest = new HttpEntity<>(authHeaders);
ResponseEntity<Map> userInfoResponse = restTemplate.exchange(
userInfoUrl,
HttpMethod.GET,
userInfoRequest,
Map.class
);
if (!userInfoResponse.getStatusCode().is2xxSuccessful() || userInfoResponse.getBody() == null) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Failed to fetch user profile from Google");
}
Map<String, Object> userInfo = userInfoResponse.getBody();
String email = (String) userInfo.get("email");
// 5. Check if user exists in database; if not, auto-register them
User user = userRepository.findByUserName(email);
if (user == null) {
user = new User();
user.setUserName(email);
user.setEmail(email);
// Assign random password since authentication is managed by Google
user.setPassword(passwordEncoder().encode(UUID.randomUUID().toString()));
user.setRoles(Collections.singletonList("USER"));
userRepository.save(user);
}
// 6. Generate application-specific JWT Token for subsequent requests
String jwtToken = jwtUtil.generateToken(email);
Map<String, String> responseBody = new HashMap<>();
responseBody.put("email", email);
responseBody.put("jwt-token", jwtToken);
return ResponseEntity.ok(responseBody);
} catch (Exception e) {
logger.error("Exception occurred while processing Google OAuth callback", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Authentication failed: " + e.getMessage());
}
}
}
9. Common Engineering Pitfalls & Debugging
No HTTP message converter for HashMap and content type application/x-www-form-urlencoded:- Cause: Using a standard
HashMapwhen posting form URL-encoded data withRestTemplate. - Solution: Always use Spring's
LinkedMultiValueMap<String, String>with.add(key, value)because form data can sometimes contain repeated keys.
- Cause: Using a standard
Invalid GrantError:- Cause: Authorization codes expire extremely quickly (typically within 30-60 seconds) and can only be used once. If you test multiple times with the same code or take too long, Google rejects it.
- Solution: Always generate a fresh authorization code via the Playground or frontend redirect before hitting your callback endpoint.
- Redirect URI Mismatch:
- Cause: The
redirect_urisent in the token exchange request must match byte-for-byte with theredirect_urisent in the initial authorization request and the one registered in Google Cloud Console.
- Cause: The
10. Summary and Key Takeaways
- Secure Delegation: OAuth2 allows third-party applications to access user accounts without ever exposing raw user passwords or credentials.
- Open Standard: It is an open authorization protocol consisting of well-defined roles: User, Client App, Authorization Server (Google), and Resource Server.
- Authorization Code Flow: The industry-standard secure flow where the frontend receives an authorization code, passes it to the backend server, and the backend securely exchanges it for access tokens using a hidden
client_secret. - Token Exchange Mechanics: Spring's
RestTemplatecombined withLinkedMultiValueMapallows the backend to programmatically communicate with Google's token endpoint (https://oauth2.googleapis.com/token). - Auto-Registration: When a user logs in via Google for the first time, check your database by email; if they do not exist, provision a new user account with a random password.
- Unified Session Management: Regardless of whether a user logs in via standard username/password or Google OAuth2, the backend should ultimately issue a standardized JWT token to manage subsequent stateless requests.
- OAuth 1.0 Legacy: OAuth2 replaced OAuth 1.0 because 1.0 required complex cryptographic signatures on every HTTP request, making it significantly slower and harder to implement.