OAuth Integration Essentials

Introduction And Context

Welcome to the first lesson of the "OAuth Advanced Features & Integration" course. In today's world, many applications allow users to sign in using accounts from other services, such as Google or Facebook. This is made possible by OAuth, a secure authorization protocol that lets users grant limited access to their information without sharing their passwords.

In this lesson, you will learn how to work with a mock OAuth setup - a simulated environment for testing OAuth features without needing real third-party accounts.

What You'll Learn:

  • Configure mock OAuth providers
  • Retrieve mock user profiles
  • Implement account linking and unlinking features
  • Apply security best practices in authentication workflows

This knowledge will prepare you to build and test advanced authentication features in your own projects and get you ready for hands-on practice in the CodeSignal IDE.

Overview Of Mock OAuth Configurations

To make development and testing easier, we use mock OAuth configurations. These simulate the behavior of real OAuth providers like Google without needing actual accounts or credentials.

In your project, two files manage these mock settings:

  • OAuthConfig.java - Stores mock user profiles for different OAuth providers
  • AuthConfig.java - Controls authentication behavior and security settings

Understanding OAuthConfig.java

View Full OAuthConfig.java
package com.codesignal.pastebin.config;

import java.util.Map;

public class OAuthConfig {
    
    public enum Provider {
        GOOGLE, GITHUB
    }
    
    public static class MockProfile {
        private final String mockUserId;
        private final String mockEmail;
        private final String mockName;
        private final String mockPicture;
        private final Provider provider;
        
        public MockProfile(String mockUserId, String mockEmail, String mockName, 
                          String mockPicture, Provider provider) {
            this.mockUserId = mockUserId;
            this.mockEmail = mockEmail;
            this.mockName = mockName;
            this.mockPicture = mockPicture;
            this.provider = provider;
        }
        
        public String getMockUserId() { return mockUserId; }
        public String getMockEmail() { return mockEmail; }
        public String getMockName() { return mockName; }
        public String getMockPicture() { return mockPicture; }
        public Provider getProvider() { return provider; }
    }
    
    private static final Map<Provider, MockProfile> OAUTH_CONFIG = Map.of(
        Provider.GOOGLE, new MockProfile(
            "mock_google_123",
            "admin@example.com",
            "John Doe (Google)",
            "https://via.placeholder.com/150?text=Google",
            Provider.GOOGLE
        ),
        Provider.GITHUB, new MockProfile(
            "mock_github_456",
            "jane.github@example.com",
            "Jane Smith (GitHub)",
            "https://via.placeholder.com/150?text=GitHub",
            Provider.GITHUB
        )
    );
    
    public static MockProfile getMockOAuthProfile(Provider provider) {
        return OAUTH_CONFIG.get(provider);
    }
}

Key Components:

Provider Enum: Defines available OAuth providers (Google and GitHub) using Java's enum type for compile-time safety.

MockProfile Class: Encapsulates mock user details including user ID, email, display name, profile picture URL, and provider identifier. This structure mimics what real OAuth providers would return.

OAUTH_CONFIG Map: Associates each provider with its mock data using Java's Map.of(), which creates an immutable map for additional security.

Understanding AuthConfig.java

View Full AuthConfig.java
package com.codesignal.pastebin.config;

public class AuthConfig {
    
    public static class TokenExpiry {
        private final String password = "1d";
        private final String oauth = "7d";
        
        public String getPassword() { return password; }
        public String getOauth() { return oauth; }
    }
    
    public static class RateLimiting {
        private final int maxAttempts = 5;
        private final long windowMs = 15 * 60 * 1000;
        
        public int getMaxAttempts() { return maxAttempts; }
        public long getWindowMs() { return windowMs; }
    }
    
    public static class Security {
        private final boolean requireConfirmation = true;
        private final boolean preventDuplicateLinks = true;
        
        public boolean isRequireConfirmation() { return requireConfirmation; }
        public boolean isPreventDuplicateLinks() { return preventDuplicateLinks; }
    }
    
    public static final TokenExpiry TOKEN_EXPIRY = new TokenExpiry();
    public static final RateLimiting RATE_LIMITING = new RateLimiting();
    public static final Security SECURITY = new Security();
}

Key Components:

TokenExpiry: Defines different expiration times for authentication methods. Password-based sessions last 1 day, while OAuth sessions last 7 days (OAuth is typically more secure).

RateLimiting: Protects against brute-force attacks. Allows maximum 5 attempts within a 15-minute window (900,000 milliseconds).

Security: Controls account linking behavior. Requires explicit user confirmation and prevents multiple users from linking to the same OAuth account.

Understanding The getMockOAuthProfile Method

The getMockOAuthProfile method is a static helper that retrieves mock user profiles:

public static MockProfile getMockOAuthProfile(Provider provider) {
    return OAUTH_CONFIG.get(provider);
}

How It Works:

Call the method with Provider.GOOGLE or Provider.GITHUB, and it returns a MockProfile object from the OAUTH_CONFIG map. The enum ensures only valid provider values can be passed, providing compile-time type safety.

Example Usage:

MockProfile profile = OAuthConfig.getMockOAuthProfile(Provider.GOOGLE);
System.out.println("User ID: " + profile.getMockUserId());
System.out.println("Email: " + profile.getMockEmail());

Output:

User ID: mock_google_123
Email: admin@example.com

This method is essential for testing authentication flows without real external accounts.

Account Linking Workflow Overview

Account linking lets users connect multiple authentication methods to a single account. For example, a user who signed up with a password can later link their Google account for flexible login options.

High-Level Workflow:

  1. Logged-in user initiates Google account linking
  2. System verifies user has confirmed the action
  3. System checks the Google account isn't already linked to another user
  4. System validates email addresses match
  5. If all checks pass, Google account is linked to user profile
  6. User can now log in with either password or Google

In-Depth Walkthrough Of The Link Endpoint (/api/account/link/google)

The /api/account/link/google endpoint is responsible for linking a Google account to an existing user. Let's examine how it works by breaking down its key components.

View Full Implementation
@PostMapping("/link/google")
public ResponseEntity<?> linkGoogle(
        @RequestHeader(value = "authorization", required = false) String authorization,
        @RequestBody LinkRequest request) {
    
    var outcome = verifyToken(authorization);
    if (outcome.error() != null) return outcome.error();
    
    User user = outcome.user();
    boolean confirm = request.confirm() != null ? request.confirm() : false;
    
    // Require explicit user confirmation for account linking
    if (AuthConfig.SECURITY.isRequireConfirmation() && !confirm) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
            "error", "Account linking requires explicit confirmation",
            "linkingInfo", Map.of(
                "currentProvider", user.getProvider(),
                "targetProvider", "google",
                "email", user.getEmail() != null ? user.getEmail() : ""
            )
        ));
    }
    
    try {
        OAuthConfig.MockProfile googleConfig = OAuthConfig.getMockOAuthProfile(OAuthConfig.Provider.GOOGLE);
        
        // Check if Google account already exists
        Optional<User> existingGoogleUser = users.findByGoogleId(googleConfig.getMockUserId());
        
        if (AuthConfig.SECURITY.isPreventDuplicateLinks() && 
            existingGoogleUser.isPresent() && 
            !existingGoogleUser.get().getId().equals(user.getId())) {
            
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
                "error", "This Google account is already linked to another user",
                "suggestion", "Log out and use Google OAuth directly, or use a different Google account"
            ));
        }
        
        // Validate email matches
        if (!user.getEmail().equals(googleConfig.getMockEmail())) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
                "error", "Email mismatch between accounts",
                "details", Map.of(
                    "currentEmail", user.getEmail(),
                    "googleEmail", googleConfig.getMockEmail()
                )
            ));
        }
        
        // Link Google account
        user.setGoogleId(googleConfig.getMockUserId());
        user.setEmailVerified(true);
        user.setProvider("local".equals(user.getProvider()) ? "mixed" : user.getProvider());
        if (user.getProfileImage() == null) {
            user.setProfileImage(googleConfig.getMockPicture());
        }
        users.save(user);
        
        // Log account linking event
        System.out.println(String.format(
            "Account linking successful: userId=%d, action=google_linked, timestamp=%s",
            user.getId(), LocalDateTime.now()
        ));
        
        return ResponseEntity.ok(Map.of(
            "message", "Google account linked successfully",
            "linkedAccounts", Map.of(
                "google", true,
                "github", user.getGithubId() != null,
                "local", user.getPassword() != null
            )
        ));
    } catch (Exception e) {
        System.out.println("Google account linking error: " + e.getMessage());
        return error(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to link Google account");
    }
}

Key Checks

When a user sends a linking request, the endpoint performs several security checks:

Token Verification: The verifyToken method checks the Authorization header, decodes the JWT token, and loads the user from the database. Invalid tokens receive a 401 error.

Confirmation Check: Verifies explicit user confirmation if required by AuthConfig.SECURITY.requireConfirmation. Missing confirmation returns a 400 error with linking details.

Duplicate Account Check: Queries the database to check if the Google account is already linked to a different user. If found and preventDuplicateLinks is enabled, the request is blocked to prevent security issues.

Email Validation: Compares email addresses between the current user and Google account. Mismatches are blocked to prevent unauthorized account merges.

Data Changes

If all checks pass, the endpoint updates the user's profile:

user.setGoogleId(googleConfig.getMockUserId());
user.setEmailVerified(true);
user.setProvider("local".equals(user.getProvider()) ? "mixed" : user.getProvider());
if (user.getProfileImage() == null) {
    user.setProfileImage(googleConfig.getMockPicture());
}
users.save(user);

Updates Made:

  • Sets the Google account ID
  • Marks email as verified
  • Updates provider field (to 'mixed' if user previously had only local account)
  • Optionally sets profile image

Changes are persisted using users.save(user), which Spring Data JPA translates into an SQL UPDATE statement. The event is logged using System.out.println (typically replaced with SLF4J in production).

Response Structure

A successful linking returns confirmation and updated account status:

{
    "message": "Google account linked successfully",
    "linkedAccounts": {
        "google": true,
        "github": false,
        "local": true
    }
}

Detailed Look At The Unlink Endpoint (/api/account/unlink/google)

The /api/account/unlink/google endpoint allows users to disconnect their Google account. Let's examine its components.

View Full Implementation
@DeleteMapping("/unlink/google")
public ResponseEntity<?> unlinkGoogle(@RequestHeader(value = "authorization", required = false) String authorization) {
    var outcome = verifyToken(authorization);
    if (outcome.error() != null) return outcome.error();
    
    User user = outcome.user();
    
    try {
        // Ensure user has at least one authentication method remaining
        int authMethods = 0;
        if (user.getPassword() != null) authMethods++;
        if (user.getGoogleId() != null) authMethods++;
        if (user.getGithubId() != null) authMethods++;
        
        if (authMethods <= 1) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
                "error", "Cannot unlink last authentication method",
                "suggestion", "Add another authentication method before unlinking this one"
            ));
        }
        
        user.setGoogleId(null);
        
        // Update provider field based on remaining methods
        int remaining = 0;
        boolean hasPassword = user.getPassword() != null;
        boolean hasGithub = user.getGithubId() != null;
        if (hasPassword) remaining++;
        if (hasGithub) remaining++;
        
        if (remaining == 1) {
            if (hasPassword) {
                user.setProvider("local");
            } else if (hasGithub) {
                user.setProvider("github");
            }
        } else if (remaining > 1) {
            user.setProvider("mixed");
        }
        
        users.save(user);
        
        System.out.println(String.format(
            "Account unlinking successful: userId=%d, action=google_unlinked, timestamp=%s",
            user.getId(), LocalDateTime.now()
        ));
        
        return ResponseEntity.ok(Map.of(
            "message": "Google account unlinked successfully",
            "linkedAccounts", Map.of(
                "google", false,
                "github", user.getGithubId() != null,
                "local", user.getPassword() != null
            )
        ));
    } catch (Exception e) {
        System.out.println("Account unlinking error: " + e.getMessage());
        return error(HttpStatus.INTERNAL_SERVER_ERROR, "Failed to unlink account");
    }
}

Key Checks For Unlinking

The endpoint performs two critical checks:

Token Verification: Verifies user identity using the JWT token in the Authorization header.

Remaining Authentication Methods: Counts available authentication methods by checking if user.getPassword(), user.getGoogleId(), and user.getGithubId() are non-null. This prevents users from locking themselves out by removing their only authentication method.

Data Changes For Unlinking

When unlinking is allowed, the endpoint updates the user profile:

user.setGoogleId(null);

// Update provider field based on remaining methods
if (remaining == 1) {
    if (hasPassword) {
        user.setProvider("local");
    } else if (hasGithub) {
        user.setProvider("github");
    }
} else if (remaining > 1) {
    user.setProvider("mixed");
}

users.save(user);

Updates Made:

  • Removes Google account ID
  • Updates provider field based on remaining methods
    • One method remaining: Sets to 'local' or 'github'
    • Multiple methods remaining: Sets to 'mixed'

Changes are persisted using users.save(user), and the event is logged.

Response Structure For Unlinking

A successful unlink returns confirmation and updated account status:

{
    "message": "Google account unlinked successfully",
    "linkedAccounts": {
        "google": false,
        "github": false,
        "local": true
    }
}

Security Measures And Best Practices

Security is critical in authentication systems. This setup implements several protective measures:

Rate Limiting: The AuthConfig.RATE_LIMITING settings restrict login attempts to prevent brute-force attacks. Maximum 5 attempts are allowed within a 15-minute window.

Token Expiry: Authentication tokens have limited validity periods, reducing risk if a token is stolen. Password sessions last 1 day; OAuth sessions last 7 days.

Account Protection:

  • Requires explicit confirmation before linking accounts
  • Prevents duplicate links to the same OAuth account
  • Blocks removal of the last authentication method

Clear Error Messages: Spring Boot's ResponseEntity builder pattern provides informative error messages with helpful suggestions for users.

Event Logging: Important actions like account linking and unlinking are logged using System.out.println (typically replaced with SLF4J or another logging framework in production) for auditing and troubleshooting.

These practices help build authentication features that are both user-friendly and secure.

Summary And Preparation For Hands-On Practice

What You've Learned:

  • How to use mock OAuth configurations to simulate authentication flows
  • The structure and purpose of OAuthConfig.java and AuthConfig.java
  • How to retrieve mock user profiles with the getMockOAuthProfile method
  • How account linking and unlinking work using Spring Boot and Spring Data JPA
  • Important security settings and best practices for authentication

Next Steps:

You're now ready for hands-on practice in the CodeSignal IDE. In the upcoming exercises, you'll:

  • Work directly with Java code
  • Test account linking and unlinking
  • Explore how security features are enforced

This practical experience will build your confidence and prepare you for more advanced OAuth integrations in the future.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal