OAuth Social Login System — Low-Level Design
An OAuth social login system allows users to authenticate via a third-party provider (Google, GitHub, Facebook) without creating a password. It must handle the OAuth flow, account linking, and the case where the same user connects via multiple providers. This design is asked at any company offering sign-in with Google/GitHub.
OAuth 2.0 Authorization Code Flow
Step 1: Redirect user to provider
GET https://accounts.google.com/o/oauth2/auth
?client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/auth/google/callback
&response_type=code
&scope=openid email profile
&state=CSRF_TOKEN ← store in session before redirecting
Step 2: Provider redirects back with authorization code
GET /auth/google/callback?code=AUTH_CODE&state=CSRF_TOKEN
Step 3: Exchange code for tokens (server-side, never expose client secret)
POST https://oauth2.googleapis.com/token
code=AUTH_CODE
client_id=YOUR_CLIENT_ID
client_secret=YOUR_SECRET
redirect_uri=...
grant_type=authorization_code
Response: {access_token, id_token, refresh_token}
Step 4: Fetch user profile using access_token
GET https://openidconnect.googleapis.com/v1/userinfo
Authorization: Bearer ACCESS_TOKEN
Response: {sub: "1234567", email: "user@gmail.com", name: "Alice", picture: "..."}
Core Data Model
User
id BIGSERIAL PK
email TEXT UNIQUE NOT NULL
display_name TEXT
avatar_url TEXT
created_at TIMESTAMPTZ
OAuthConnection
id BIGSERIAL PK
user_id BIGINT FK NOT NULL
provider TEXT NOT NULL -- 'google', 'github', 'facebook'
provider_user_id TEXT NOT NULL -- provider's stable user identifier
access_token TEXT -- encrypted at rest
refresh_token TEXT -- encrypted at rest
token_expires_at TIMESTAMPTZ
created_at TIMESTAMPTZ
UNIQUE (provider, provider_user_id) -- one connection per provider per user
Handling the Callback
def handle_oauth_callback(provider, code, state, session_state):
# 1. Verify CSRF state token
if state != session_state:
raise SecurityError('State mismatch — possible CSRF attack')
# 2. Exchange code for tokens
tokens = exchange_code_for_tokens(provider, code)
# 3. Fetch user profile from provider
profile = fetch_user_profile(provider, tokens['access_token'])
provider_user_id = profile['sub'] # Google uses 'sub'; GitHub uses 'id'
email = profile.get('email')
# 4. Find or create user
user = find_or_create_user(provider, provider_user_id, email, profile, tokens)
# 5. Create app session
session_token = create_session(user.id)
return session_token
def find_or_create_user(provider, provider_user_id, email, profile, tokens):
# Case A: Existing OAuth connection — return the linked user
existing_connection = db.get_by(OAuthConnection,
provider=provider,
provider_user_id=provider_user_id)
if existing_connection:
update_tokens(existing_connection.id, tokens)
return db.get(User, existing_connection.user_id)
# Case B: Email matches an existing user — link the new provider to their account
if email:
existing_user = db.get_by(User, email=email.lower())
if existing_user:
create_oauth_connection(existing_user.id, provider, provider_user_id, tokens)
return existing_user
# Case C: New user — create account and connection
user = db.insert(User, {
'email': email.lower() if email else None,
'display_name': profile.get('name'),
'avatar_url': profile.get('picture'),
})
create_oauth_connection(user.id, provider, provider_user_id, tokens)
return user
Storing Tokens Securely
import cryptography.fernet
ENCRYPTION_KEY = os.environ['TOKEN_ENCRYPTION_KEY'] # 32-byte key
fernet = Fernet(ENCRYPTION_KEY)
def create_oauth_connection(user_id, provider, provider_user_id, tokens):
encrypted_access = fernet.encrypt(tokens['access_token'].encode()).decode()
encrypted_refresh = fernet.encrypt(tokens.get('refresh_token', '').encode()).decode()
db.insert(OAuthConnection, {
'user_id': user_id,
'provider': provider,
'provider_user_id': provider_user_id,
'access_token': encrypted_access,
'refresh_token': encrypted_refresh,
'token_expires_at': now() + timedelta(seconds=tokens.get('expires_in', 3600)),
})
Key Interview Points
- State parameter prevents CSRF: Generate a random state value, store it in the server-side session before redirect, verify it matches on callback. Without this, an attacker can forge the callback URL.
- Exchange code server-side only: The authorization code is exchanged for tokens using your client_secret. This must happen server-side. Never expose client_secret or access tokens to the browser.
- Account linking by email can be dangerous: Linking a new provider to an existing account based on email alone is a security risk if the provider doesn’t verify email ownership. GitHub and some providers let users register unverified emails. Only auto-link on verified email.
- Encrypt tokens at rest: Access and refresh tokens are credentials. If your database is compromised, unencrypted tokens give attackers access to users’ third-party accounts. Encrypt with AES-256 (Fernet) before storing.
OAuth and social login system design is discussed in Google system design interview questions.
OAuth and identity federation design is covered in Meta system design interview preparation.
OAuth and developer authentication design is discussed in Atlassian system design interview guide.