13.12. Social Login
Table of Contents
Why Social Login Matters
Social login lets users sign in with an existing account from a third‑party provider, such as Google, GitHub, Facebook, or Apple, instead of creating a new username and password.
From a backend perspective, social login is simply a special way to perform authentication using an external identity provider. Your application delegates the job of identifying the user to that provider, then uses the information you get back to create or look up a local user.
Some benefits:
- Fewer passwords for users to remember.
- Higher chance of accurate email addresses.
- Potentially stronger security if the provider uses strong MFA.
- Less responsibility for you to handle sensitive credentials.
Some downsides:
- Dependency on third‑party uptime and APIs.
- Privacy and consent issues.
- Extra complexity in your authentication flow.
Your goal as a backend developer is to integrate these providers safely and predictably, not to fully trust them for everything.
Key idea: Social login is still your own authentication system. The provider only proves the user’s identity. You are responsible for:
- Mapping that identity to a local user.
- Creating and issuing your own session or tokens.
- Enforcing your own authorization rules.
Core Social Login Flow
Most modern social login systems use OAuth 2.0 and often OpenID Connect on top. The specific details belong in the dedicated OAuth 2.0 and OpenID Connect chapters, but the core flow is always similar.
High‑Level Flow
Typical steps:
- User clicks “Continue with X” on your site.
- Your backend redirects the user to the provider’s authorization page.
- User signs in (if needed) and consents.
- Provider redirects back to your backend with a short‑lived authorization code.
- Your backend exchanges that code for:
- An access token (to call the provider’s APIs),
- Often an ID token (OpenID Connect) that describes the user.
- Your backend:
- Validates the tokens.
- Reads the user’s identity (id, email, etc.).
- Creates or finds a corresponding local user.
- Issues your own session or JWT access token to the client.
Example Sequence
Imagine a “Sign in with Google” flow for a web app:
- Frontend calls your backend:
GET /auth/google/login. - Backend builds a Google OAuth URL and replies with a redirect.
- Browser goes to Google, user logs in and consents.
- Google redirects back to
https://your-api.com/auth/google/callback?code=XYZ. - Backend receives
code=XYZ, sends a POST request to Google’s token endpoint: - Includes client_id, client_secret, redirect_uri, and the code.
- Google responds with JSON, for example:
{
"access_token": "ya29.a0Af...",
"id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI...",
"expires_in": 3599,
"token_type": "Bearer"
}- Backend validates
id_token, extracts the user email and provider user id. - Backend looks up or creates a local user.
- Backend issues its own JWT access token and refresh token.
- Frontend now uses those local tokens to call your API.
Provider Identifiers and Local Users
Your database should never fully depend on an external provider structure. The provider is just a way to authenticate.
Local vs External Identity
You usually have:
- A local user table, controlled by you.
- A social account table, mapping provider identity to your local users.
Example tables:
users table
| column | type | description |
|---|---|---|
| id | UUID / int | Primary key, local user id |
| string | User’s main email | |
| name | string | Display name |
| created_at | datetime | Creation timestamp |
| is_active | bool | Active flag |
social_accounts table
| column | type | description |
|---|---|---|
| id | UUID / int | Primary key |
| user_id | FK | References users.id |
| provider | string | Example: "google", "github" |
| provider_user_id | string | Id given by the provider |
| string | Email from provider (optional, may be null) | |
| access_token | string | Optional, if you call provider APIs |
| refresh_token | string | Optional, if you need long‑term access |
| created_at | datetime | When this link was created |
Important rule: The pair (provider, provider_user_id) must be unique and must always refer to one and only one local user.
Creating or Linking Users
When you receive a successful social login response, typical logic is:
- Extract:
provider(for example"google"),provider_user_id,- primary email (if available),
- name or profile info (optional).
- Try to find existing social account:
- If you find one, you already know the
user_id, log the user in. - If not found:
- Option A: Create a new local user and a new social account link.
- Option B: If email matches an existing user, ask user to link this provider to that account.
- Option C: If email missing or unverified, require extra steps (for example ask for an email).
Example pseudocode:
def handle_social_login(provider, provider_user_id, email, name):
social = db.social_accounts.get(provider=provider, provider_user_id=provider_user_id)
if social:
user = db.users.get(id=social.user_id)
return issue_tokens_for_user(user)
if email:
user = db.users.get_by_email(email)
else:
user = None
if user:
# Optional: ask user confirmation before linking
social = db.social_accounts.create(
provider=provider,
provider_user_id=provider_user_id,
user_id=user.id,
email=email,
)
else:
user = db.users.create(email=email, name=name)
social = db.social_accounts.create(
provider=provider,
provider_user_id=provider_user_id,
user_id=user.id,
email=email,
)
return issue_tokens_for_user(user)Common Providers and Data You Receive
Different providers give you different fields. You must read their documentation and adapt.
Google Example (OpenID Connect)
With Google, you often get an ID token that is a JWT. Its payload might look like:
{
"iss": "https://accounts.google.com",
"sub": "110169484474386276334",
"email": "user@example.com",
"email_verified": true,
"name": "John Doe",
"picture": "https://lh3.googleusercontent.com/a-/AOh14Gh...",
"aud": "YOUR_CLIENT_ID.apps.googleusercontent.com",
"exp": 1614077559,
"iat": 1614073959
}Important fields:
subis the provider_user_id.emailandemail_verifiedtell you whether email is trustworthy.audmust match your client id.
GitHub Example
GitHub usually gives you an access token and then you use its API to get user info:
GET https://api.github.com/user
Authorization: Bearer <access_token>Response:
{
"id": 123456,
"login": "octocat",
"email": "octocat@github.com",
"name": "The Octocat"
}You can treat:
idasprovider_user_id,emailas user email, noting that it can benullif private.
Apple Example
Apple can behave differently. You often receive:
- A JWT with a
subfield as user id. - Possibly an email, but sometimes only during first login.
Because of that, you must be prepared that long‑term you can only rely on provider_user_id, not always on email.
Security Considerations
Social login adds external tokens and redirects. You must validate and protect every step.
Validating ID Tokens (JWTs)
For providers that use OpenID Connect, such as Google, you get an ID token that is a JWT. You must validate:
- Signature, using the provider’s public keys (JWKs).
iss(issuer) matches the expected value.aud(audience) contains your client id.expis in the future, token not expired.- Other provider‑specific claims if required.
Always validate external tokens. Never trust any JWT or token from the client unless you verify:
- Who signed it,
- Who it is for,
- That it has not expired.
CSRF and State Parameter
Avoid simple “click and redirect” flows that can be abused by cross‑site request forgery.
Use a state parameter:
- Generate a cryptographically random
statevalue. - Store it in the user’s session or a secure cookie.
- Include it in the authorization URL to the provider.
- When provider redirects back, verify that
statematches what you stored.
Example:
state = generate_random_string()
store_state_in_session(state)
authorization_url = (
f"https://accounts.google.com/o/oauth2/v2/auth"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope=openid%20email%20profile"
f"&state={state}"
)
If incoming state does not match the stored value, reject the request.
HTTPS Required
You must use HTTPS in production:
- Redirect URIs must be
https://.... - Provider configuration usually forbids plain
httpfor production.
This protects tokens and codes while they travel between browser, provider, and backend.
Token Storage
You can store the provider’s access token and refresh token in your database if you plan to call the provider’s APIs later, for example to access user data or contacts.
Keep in mind:
- Encrypt or strongly protect these tokens, they often allow access to the user’s profile.
- Respect provider scopes and privacy rules.
- Do not store more than necessary.
If you do not need long‑term provider access, you can avoid storing these tokens and only keep the identity mapping.
UX and Account Linking
Poorly designed social login can confuse users and produce duplicate accounts.
Avoiding Duplicate Accounts
Common problem:
- User signs up with email + password:
user@example.com. - Later clicks “Sign in with Google” using the same email.
- Your backend creates a second user with the same email.
To avoid that:
- When you see a social login with an email that already exists, decide:
- Automatic linking: If you trust the provider’s email verification, you can automatically link to that user.
- Manual confirmation: Ask the user to confirm they own the existing account, for example by logging in once with password or confirming via email.
Example strategy:
- If
email_verifiedistrueand provider is trusted, link automatically. - If not, ask for confirmation.
Supporting Multiple Providers Per User
Users may want to connect several social accounts with the same local user, for example Google and GitHub.
Your social_accounts table should allow many records per user_id. This allows flows like:
- User logs in with email + password.
- From account settings, user clicks “Connect Google”.
- After social flow, you add a new social account entry for that local user.
Pseudocode:
def link_social_account(current_user, provider, provider_user_id, email):
existing = db.social_accounts.get(
provider=provider,
provider_user_id=provider_user_id,
)
if existing and existing.user_id != current_user.id:
raise ConflictError("This social account is already linked to another user.")
if not existing:
db.social_accounts.create(
user_id=current_user.id,
provider=provider,
provider_user_id=provider_user_id,
email=email,
)Social Login in API‑Only Backends
If your backend only exposes a REST API and your frontend is separate, the flow is slightly more complex, but the principles are the same.
Frontend vs Backend Redirects
Two common patterns:
- Backend handles redirects directly
- Frontend calls your API endpoint to start social login.
- Backend returns a redirect URL.
- Browser goes directly to provider.
- Provider returns to backend callback URL.
- Backend responds with a redirect to your frontend, including an authorization code or a short‑lived token.
- Frontend exchanges that for long‑lived tokens via another API call.
- Frontend handles provider SDKs (for example using Google or Facebook JS SDK)
- Frontend talks directly to the provider, gets an ID token or access token.
- Frontend sends that token to your backend in an API call.
- Backend verifies the token with the provider, then creates or finds the user and issues local tokens.
The second approach is often simpler for SPAs, but you must be careful:
- Always verify tokens server side.
- Do not trust client‑side token validation.
Example Backend‑Only API Flow (Simplified)
GET /auth/google/url
Backend responds:
{
"authorization_url": "https://accounts.google.com/o/oauth2/v2/auth?...&state=abc123"
}- Frontend redirects the user to that URL.
- After Google callback to
/auth/google/callback?code=XYZ&state=abc123, backend: - Validates
state. - Exchanges
codefor tokens. - Identifies or creates the user.
- Issues its own JWT access and refresh tokens.
- Redirects the user to a frontend URL like:
https://app.example.com/auth/callback?access_token=...&refresh_token=...
or better, uses a short code and lets the frontend call an API to exchange it for tokens, so you do not put long‑lived tokens in the URL.
Handling Errors and Edge Cases
Social login can fail at several points. Your backend must produce clear, consistent error responses.
Typical Error Scenarios
- User cancels permission at provider:
- Provider redirects back with
error=access_denied. - Backend should translate it into a friendly error that frontend understands, for example
{"code":"ACCESS_DENIED","message":"Login was cancelled."}. - Code already used or expired:
- Provider token exchange fails with 4xx.
- Backend should return 400 or 401 to frontend.
- Invalid state:
- Possible CSRF or user session mismatch.
- Backend should reject with 400 and not continue the flow.
- Email missing:
- Some providers do not share email by default.
- Backend might require frontend to ask user to input an email and create a flow to confirm it.
Consistent API Responses
Treat social login endpoints as part of your normal authentication API:
- Use JSON responses that match your login / registration endpoints.
- Same token format, same error format, same HTTP status codes.
Example success response:
{
"access_token": "local-jwt-token",
"refresh_token": "local-refresh-token",
"token_type": "Bearer",
"user": {
"id": 42,
"email": "user@example.com",
"name": "John Doe"
}
}Example error response:
{
"error": "SOCIAL_LOGIN_FAILED",
"message": "Google login failed: access denied by user."
}Summary
Social login is an authentication feature where you:
- Redirect users to a third‑party provider to verify their identity.
- Receive tokens or codes that you validate on the backend.
- Map the external identity to a local user, creating or linking accounts as needed.
- Issue your own tokens or sessions, and apply your own authorization rules.
If you keep these principles in mind, social login becomes:
- A clean extension of your existing authentication system.
- A convenience for users, without giving up control of your security or data model.
Views: 11
KAHIBARO