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, which is a simulated environment for testing OAuth features without needing real third-party accounts.

By the end of this lesson, you will understand how to configure mock OAuth providers, retrieve mock user profiles, and implement account linking and unlinking features. You will also see how to apply important security practices in these 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 often use mock OAuth configurations. These allow us to simulate the behavior of real OAuth providers, like Google, without needing to set up actual accounts or credentials. In your project, the oauth_config.py file is responsible for storing these mock settings.

Here's what the oauth_config.py file looks like:

from typing import Literal

OAUTH_CONFIG = {
    'google': {
        'mock_user_id': 'mock_google_123',
        'mock_email': 'admin@example.com',
        'mock_name': 'John Doe (Google)',
        'mock_picture': 'https://via.placeholder.com/150?text=Google',
        'provider': 'google'
    },
    'github': {
        'mock_user_id': 'mock_github_456',
        'mock_email': 'jane.github@example.com',
        'mock_name': 'Jane Smith (GitHub)',
        'mock_picture': 'https://via.placeholder.com/150?text=GitHub',
        'provider': 'github'
    }
}

AUTH_CONFIG = {
    'token_expiry': {
        'password': '1d',
        'oauth': '7d'
    },
    'rate_limiting': {
        'max_attempts': 5,
        'window_ms': 15 * 60 * 1000
    },
    'security': {
        'require_confirmation': True,
        'prevent_duplicate_links': True
    }
}

Let's break down what each part does:

The OAUTH_CONFIG dictionary contains mock user details for multiple OAuth providers (Google and GitHub). Each provider has its own set of mock data including a user ID, email, display name, profile picture URL, and a provider identifier. This structure mimics what real OAuth providers would return, allowing you to test OAuth flows with multiple providers.

The AUTH_CONFIG dictionary holds general authentication settings that control security and behavior:

  • token_expiry: Defines different expiration times for different authentication methods. Password-based sessions last 1 day, while OAuth sessions last 7 days, reflecting that OAuth is typically more secure.
  • rate_limiting: Protects against brute-force attacks by limiting login attempts. max_attempts sets the maximum number of tries allowed (5), and window_ms defines the time window in milliseconds (15 minutes).
  • security: Controls the account linking process. require_confirmation ensures users must explicitly confirm before linking accounts, and prevent_duplicate_links blocks multiple users from linking to the same OAuth account.

These settings help you control and secure the authentication process, even in a mock environment.

Understanding The get_mock_oauth_profile Function

The get_mock_oauth_profile function is a helper that returns a mock user profile for a given provider. Here's its implementation:

def get_mock_oauth_profile(provider: Literal['google', 'github']) -> dict:
    config = OAUTH_CONFIG[provider]
    return {
        'id': config['mock_user_id'],
        'email': config['mock_email'],
        'name': config['mock_name'],
        'picture': config['mock_picture'],
        'provider': config['provider']
    }

When you call this function with 'google' or 'github', it pulls the mock user data from OAUTH_CONFIG and returns a dictionary with the user's ID, email, name, profile picture, and provider name. The function uses Python's Literal type hint to indicate that only 'google' or 'github' are expected values. This helps IDEs and static type checkers catch potential errors during development.

For example, calling:

profile = get_mock_oauth_profile('google')
print(profile)

will output:

{
    'id': 'mock_google_123',
    'email': 'john.google@example.com',
    'name': 'John Doe (Google)',
    'picture': 'https://via.placeholder.com/150?text=Google',
    'provider': 'google'
}

This function is especially useful for testing and development. It allows you to simulate the process of retrieving a user's profile from an OAuth provider, so you can build and test your authentication flows without needing real external accounts.

Account Linking Workflow Overview

Account linking is a feature that lets users connect multiple authentication methods to a single account. For example, a user who originally signed up with a password can later link their Google account, so they can log in with either method. This is important in modern applications because it gives users flexibility and a better experience.

The high-level workflow for account linking starts when a logged-in user wants to connect their Google account. The system checks if the user has confirmed this action, verifies that the Google account is not already linked to another user account within the application, and ensures that the email addresses match. If all checks pass, the Google account is linked to the user's profile. The user can then log in with either their password or Google in the future.

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

The /link/google endpoint is responsible for linking a Google account to an existing user. Let's first look at the complete implementation:

@router.post("/link/google")
async def link_google(request: Request, db: AsyncSession = Depends(get_db)):
    authorization = request.headers.get('authorization')
    user = await verify_token(authorization, db)
    
    data = await request.json()
    confirm = data.get('confirm', False)
    
    # Require explicit user confirmation for account linking
    if AUTH_CONFIG['security']['require_confirmation'] and not confirm:
        return JSONResponse(
            status_code=400,
            content={
                'error': 'Account linking requires explicit confirmation',
                'linkingInfo': {
                    'currentProvider': user.provider,
                    'targetProvider': 'google',
                    'email': user.email
                }
            }
        )
    
    try:
        # Check if Google account already exists
        google_config = OAUTH_CONFIG['google']
        result = await db.execute(
            select(User).where(User.google_id == google_config['mock_user_id'])
        )
        existing_google_user = result.scalar_one_or_none()
        
        if (AUTH_CONFIG['security']['prevent_duplicate_links'] and 
            existing_google_user and 
            existing_google_user.id != user.id):
            return JSONResponse(
                status_code=400,
                content={
                    '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.email != google_config['mock_email']:
            return JSONResponse(
                status_code=400,
                content={
                    'error': 'Email mismatch between accounts',
                    'details': {
                        'currentEmail': user.email,
                        'googleEmail': google_config['mock_email']
                    }
                }
            )
        
        # Link Google account
        user.google_id = google_config['mock_user_id']
        user.email_verified = True
        user.provider = 'mixed' if user.provider == 'local' else user.provider
        user.profile_image = user.profile_image or google_config['mock_picture']
        await db.commit()
        
        # Log account linking event
        print(f"Account linking successful: userId={user.id}, action=google_linked, timestamp={datetime.utcnow().isoformat()}")
        
        return {
            'message': 'Google account linked successfully',
            'linkedAccounts': {
                'google': True,
                'local': bool(user.password)
            }
        }
    except HTTPException:
        raise
    except Exception as e:
        print(f"Google account linking error: {e}")
        raise HTTPException(status_code=500, detail="Failed to link Google account")

Now let's walk through how this endpoint works step by step. When a user sends a request to this endpoint, the first step is to verify their identity using a JWT (JSON Web Token). The verify_token function checks the Authorization header, decodes the token using the python-jose library, and loads the user from the database using SQLAlchemy. If the token is missing or invalid, the request is rejected with a 401 error.

Next, the endpoint checks if the user has explicitly confirmed the linking action. This is controlled by the require_confirmation setting in AUTH_CONFIG. If confirmation is required but not provided, the endpoint returns a 400 error response with information about the linking attempt.

The endpoint then checks if the Google account is already linked to another user by querying the database with select(User).where(User.google_id == google_config['mock_user_id']). If an existing user is found with the same Google ID, and if duplicate links are not allowed, it returns an error to prevent two users from sharing the same Google account. It also compares the email addresses of the current user and the Google account. If they do not match, the linking is blocked to prevent accidental or malicious account merges.

If all checks pass, the endpoint updates the user's profile to include the Google account ID, marks the email as verified, and updates the profile image if needed. The changes are committed to the database using await db.commit(). The system logs the linking event and responds with a success message and the updated linked account status.

For example, a successful response might look like:

{
    'message': 'Google account linked successfully',
    'linkedAccounts': {
        'google': True,
        'local': True
    }
}
Detailed Look At The Unlink Endpoint (/unlink/google)

The /unlink/google endpoint allows a user to disconnect their Google account from their profile. Here's the complete implementation:

@router.delete("/unlink/google")
async def unlink_google(request: Request, db: AsyncSession = Depends(get_db)):
    authorization = request.headers.get('authorization')
    user = await verify_token(authorization, db)
    
    try:
        # Ensure user has password as backup authentication method
        if not user.password:
            return JSONResponse(
                status_code=400,
                content={
                    'error': 'Cannot unlink last authentication method',
                    'suggestion': 'Add a password before unlinking Google account'
                }
            )
        
        # Unlink Google account
        user.google_id = None
        user.provider = 'local'
        await db.commit()
        
        print(f"Account unlinking successful: userId={user.id}, action=google_unlinked, timestamp={datetime.utcnow().isoformat()}")
        
        return {
            'message': 'Google account unlinked successfully',
            'linkedAccounts': {
                'google': False,
                'local': bool(user.password)
            }
        }
    except HTTPException:
        raise
    except Exception as e:
        print(f"Account unlinking error: {e}")
        raise HTTPException(status_code=500, detail="Failed to unlink account")

Before unlinking, the system checks that the user has another way to log in, such as a password. This prevents users from accidentally locking themselves out of their account by removing their only authentication method. The check is performed using if not user.password, which verifies whether a password exists.

If the user has multiple authentication methods, the endpoint removes the Google account ID by setting user.google_id = None and updates the provider field to 'local' since the user will only have password authentication remaining. The changes are committed to the database, the event is logged, and the user receives a confirmation message.

For example, after a successful unlink, the response might be:

{
    'message': 'Google account unlinked successfully',
    'linkedAccounts': {
        'google': False,
        'local': True
    }
}
Security Measures And Best Practices

Security is a key part of any authentication system. In this setup, several measures are in place to protect users and the application. The AUTH_CONFIG dictionary defines rate limiting, which restricts the number of login attempts in a given time window to prevent brute-force attacks. Token expiry settings ensure that authentication tokens are only valid for a limited time, reducing the risk if a token is stolen.

The system also requires explicit confirmation before linking accounts and prevents duplicate links to the same Google account. These rules help prevent accidental or unauthorized account merges. FastAPI's built-in exception handling provides clear and informative error messages to users with helpful suggestions. Logging important events, such as account linking and unlinking using Python's print() function (which would typically be replaced with proper logging in production), helps with auditing and troubleshooting.

By following these practices, you can build authentication features that are both user-friendly and secure.

Summary And Preparation For Hands-On Practice

In this lesson, you learned how to use mock OAuth configurations to simulate real-world authentication flows. You explored the oauth_config.py file, understood how to retrieve mock user profiles with the get_mock_oauth_profile function, and saw how account linking and unlinking work in detail using FastAPI and SQLAlchemy. You also learned about important security settings and best practices for handling authentication.

Now that you have a solid understanding of these concepts, you are ready to move on to hands-on practice in the CodeSignal IDE. In the next exercises, you will apply what you have learned by working directly with the Python code, testing account linking and unlinking, and exploring how security features are enforced. This practical experience will help you build 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