Social Login Security

Cross-cutting security concerns when integrating OAuth 2.0 / OpenID Connect: CSRF (state), replay (nonce), token validation, and the most common implementation mistakes.

CSRF Protection — the state Parameter

Threat: an attacker initiates an OAuth flow themselves, then tricks a victim into completing it, causing the victim’s account to be linked to the attacker’s identity at your app — or vice versa.

Mitigation:

  1. On the authorize request, generate a high-entropy random state value.
  2. Bind it to the user’s session (server-side store keyed by session, or signed cookie).
  3. On callback, verify the returned state matches what was stored. If not, abort.
  4. Rotate after use.
import secrets
state = secrets.token_urlsafe(32)
session["oauth_state"] = state
# ... build authorize URL with state=...
 
# in /callback:
if request.args["state"] != session.pop("oauth_state", None):
    abort(400, "state mismatch")

state should also encode the post-login redirect target if you have one — sign it (HMAC) or store it server-side keyed by the random value.

Replay Protection — the nonce Parameter

Threat: an attacker captures a valid ID Token (e.g., from an old log) and replays it to your app to impersonate the user.

Mitigation (OIDC-specific):

  1. Generate a random nonce and include in the authorize request.
  2. Bind it to the session like state.
  3. The provider includes nonce in the issued ID Token.
  4. On token receipt, verify id_token.nonce == sent_nonce. Mismatch → reject.

state and nonce solve overlapping but distinct problems. Use both.

Token Validation Checklist

Every time you receive an ID Token (full algorithm in ID Token):

  • Signature verified with the provider’s public key (JWKS, by kid).
  • alg pinned to a specific allow-list (RS256 / ES256), never none.
  • iss matches the discovery issuer.
  • aud includes your client_id (and azp == client_id if multiple aud).
  • exp is in the future, with small clock-skew tolerance (~5 min).
  • iat is sane (not far future).
  • nonce matches the value you sent.

For access tokens:

  • If opaque, hit the introspection endpoint or treat as a bearer the resource server validates.
  • If JWT, validate like an ID Token (different aud — the resource server, not your client).

Replay Window for Codes

Authorization codes are single-use. A spec-compliant server invalidates a code on first redemption. If the same code is presented twice, the second request fails and the originally-issued tokens are revoked. Combined with PKCE, stolen codes are useless without the verifier.

Identifier Hygiene

  • Federated user identity = (iss, sub), never email.
  • Two providers can issue the same sub; always include iss.
  • email_verified == false means unverified. Don’t link accounts by email unless verified, and even then do an additional confirmation step.

Common Mistakes (Top 10)

  1. Using email as user ID. Emails change, get reassigned. Use sub.
  2. Skipping state validation. Account-linking CSRF.
  3. Skipping ID Token signature verification. Accepting forged tokens.
  4. Not checking aud. Accepting tokens issued to a different app.
  5. Accepting alg: none. Old library defaults; pin algorithms.
  6. Storing tokens in localStorage in browsers. XSS reads them. Prefer httpOnly cookies (with SameSite) for session tokens.
  7. Using the implicit flow. Tokens leak via referrer / history.
  8. Treating an OAuth access token as identity (no openid scope, no ID Token).
  9. Doing the token exchange from the browser with a confidential client secret.
  10. Mishandling Apple’s first-login user data. See Sign in with Apple.

Refresh Token Hygiene

  • Rotate on every use; the auth server should issue a new refresh token and invalidate the old.
  • Detect reuse: if a previously-rotated refresh token is presented, treat as compromise — revoke the entire token family.
  • Store securely: server-side encrypted at rest, never exposed to the browser for confidential clients.

Logout Considerations

  • OAuth has no standard logout. OIDC defines RP-Initiated Logout and Back-Channel Logout.
  • “Logging the user out of your app” doesn’t log them out of the IdP. They click “Sign in with Google” again → they’re back in immediately. This is usually desired UX; document it.

See Also