
How do I implement social login for my web app? - Part 2
Part 2 of 3. Start with How do I implement social login for my web app?.
Cross-platform implementation and OAuth callback security
Part 2 covers the implementation layer after the first web sign-in button works: cross-framework setup patterns, native mobile SDKs, Google One Tap, OAuth callback architecture, redirect handling, account linking, and Clerk session architecture.
Cross-Framework Pattern Summary
The pattern is consistent across all frameworks: install the SDK, wrap your app with a provider, optionally add middleware for server-side auth, and drop in the <SignIn /> component. Clerk handles the OAuth redirects, token exchange, session creation, and account linking behind the scenes.
This adapter pattern — the same API surface (<SignIn />, useSignIn(), authenticateWithRedirect()) exposed through framework-specific adapters — keeps authentication logic portable: migrating from React to Vue, for example, requires only changing import paths, not rewriting the auth flow.
Native Mobile SDKs
Clerk's social login support extends beyond web frameworks to native mobile platforms, each using platform-appropriate patterns — native system dialogs where available, browser-based OAuth as the fallback.
Expo (React Native): The @clerk/expo package provides the same useSignIn() hook pattern as the web SDKs. For Apple, the dedicated useSignInWithApple() hook uses Apple's native authorization to provide the OpenID token directly to Clerk — no browser redirect needed.
// Expo - Native Apple Sign-in
import { useSignInWithApple } from '@clerk/expo/apple'
export function AppleSignInButton() {
const { startAppleAuthenticationFlow } = useSignInWithApple()
const onPress = async () => {
try {
const { createdSessionId, setActive } = await startAppleAuthenticationFlow()
if (createdSessionId && setActive) {
await setActive({ session: createdSessionId })
}
} catch (err: any) {
if (err.errors?.[0]?.code === 'ERR_REQUEST_CANCELED') return
console.error(err)
}
}
return <Button title="Sign in with Apple" onPress={onPress} />
}The useSignInWithApple() hook automatically manages the transfer flow between sign-up and sign-in, so a single component handles both scenarios.
iOS (Swift): The Clerk iOS SDK uses ClerkKit and ClerkKitUI via Swift Package Manager. The prebuilt AuthView component handles the complete sign-in/sign-up flow including social providers.
// iOS - Prebuilt AuthView with social login
import SwiftUI
import ClerkKit
import ClerkKitUI
struct ContentView: View {
@Environment(Clerk.self) private var clerk
@State private var showSignIn = false
var body: some View {
if clerk.session != nil {
UserButton()
} else {
Button("Sign in") { showSignIn = true }
.sheet(isPresented: $showSignIn) {
AuthView()
}
}
}
}The AuthView component renders all configured social providers automatically based on your Clerk Dashboard settings — no per-provider code needed.
Android (Kotlin): The Clerk Android SDK provides native Google Sign-in support and prebuilt authentication views.
// Android - Google Sign-in with Clerk
import com.clerk.api.signin.SignIn
import com.clerk.api.sso.OAuthProvider
// Trigger Google OAuth flow
scope.launch {
SignIn.authenticateWithRedirect(
SignIn.AuthenticateWithRedirectParams.OAuth(
provider = OAuthProvider.GOOGLE
)
)
}The Android SDK reached GA in September 2025, built with Kotlin and following modern Android development standards including Jetpack Compose support.
Flutter (Dart): The official Clerk Flutter SDK entered public beta in March 2025 and remains in active beta development (pre-1.0). It includes clerk_flutter for Flutter apps and clerk_auth for Dart backends, with cross-platform support for iOS, Android, and web. Clerk is initialized in your app setup, and social login is configured through the Clerk Dashboard and surfaced via the SDK's authentication methods.
While the Flutter SDK is in beta and its API surface may change, it demonstrates Clerk's commitment to native mobile platforms. For production Flutter apps requiring a stable SDK, Firebase Auth and Supabase Auth offer GA-level Flutter support today.
Google One Tap
Google One Tap displays a non-intrusive prompt to users who are already signed into their Google account, allowing them to sign up or sign in with a single click — no redirect, no popup, no password. Google's One Tap case studies (the Reddit and Pinterest sign-up gains cited in Part 1) report meaningful conversion improvements.
With most authentication providers, adding One Tap means manually loading Google's Identity Services script, initializing the client, handling credential callbacks, managing cryptographic nonces, and wiring everything to your auth backend. Supabase's implementation runs roughly 70 lines of TypeScript including SHA-256 nonce handling. Auth0 and Auth.js lack official One Tap support entirely, requiring community-driven workarounds of 40–80 lines.
Clerk reduces this to a single component. Place <GoogleOneTap /> in your root layout and it appears on every page:
// app/layout.tsx
import { ClerkProvider, GoogleOneTap } from '@clerk/nextjs'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
<ClerkProvider>
<GoogleOneTap />
{children}
</ClerkProvider>
</body>
</html>
)
}The component handles the full lifecycle: it loads the Google Identity Services script, manages the FedCM handshake (Google began migrating One Tap to FedCM in October 2024), smooths over Intelligent Tracking Prevention quirks in Safari, Firefox, and Chrome on iOS, and automatically hides when the user is already authenticated. For additional control, the component accepts configuration props:
<GoogleOneTap cancelOnTapOutside={true} fedCmSupport={true} signUpForceRedirectUrl="/onboarding" />The fedCmSupport prop (default: true) enables the Federated Credential Management (FedCM) API, which Google requires for the One Tap and Automatic Sign-In flows. The itpSupport prop (also default: true) enables the Intelligent Tracking Prevention sign-in UX on browsers that need it — Safari, Firefox, and Chrome on iOS. These are details that competitors leave to the developer to implement manually.
Prerequisites: Enable Google as a social connection in the Clerk Dashboard with custom Google credentials from the Google Cloud Console.
The OAuth Callback: Architecting a Secure Backend
When a user clicks "Sign in with Google," a carefully orchestrated exchange begins between your application, the user's browser, and the identity provider. Understanding this flow is essential for debugging issues and making informed security decisions.
The Authorization Code Flow
- Authorization request — Your app redirects the browser to the provider's
/authorizeendpoint with parameters:client_id,redirect_uri,scope,state,nonce, andcode_challenge(PKCE) - User authentication — The provider authenticates the user and displays a consent screen
- Authorization code — The provider redirects the browser back to your
redirect_uriwith a short-lived authorization code and thestateparameter - Token exchange — Your backend sends the authorization code,
client_secret, andcode_verifier(PKCE) to the provider's/tokenendpoint - Token response — The provider returns an access token, ID token (OIDC), and optionally a refresh token
- Session creation — Your application validates the ID token (verifying its signature and claims), extracts the user's identity, and creates a separate application session — typically an HttpOnly session cookie or a JWT issued by your auth service. The provider's access and refresh tokens are stored server-side only if your app needs ongoing access to the provider's APIs. The application session credential, not the provider tokens, is what keeps the user logged in going forward
Three Layers of Security
The Authorization Code flow uses three complementary mechanisms to prevent different attack vectors:
PKCE (Proof Key for Code Exchange)
PKCE (RFC 7636) was originally designed for mobile and public clients. The OAuth 2.0 Security Best Current Practice (RFC 9700) now requires PKCE for public clients (MUST) and recommends it for confidential (server-side) clients as well (RECOMMENDED), making it a best practice for all OAuth 2.0 deployments.
The mechanism is straightforward:
- Generate a random
code_verifier(43–128 characters) - Compute
code_challenge = BASE64URL(SHA256(code_verifier)) - Send
code_challengewith the authorization request - Send
code_verifierwith the token exchange request - The provider verifies
SHA256(code_verifier) == code_challenge
Even if an attacker intercepts the authorization code, they cannot exchange it without the code_verifier.
Clerk applies state, nonce, and PKCE protections automatically for its built-in social connections. For custom OAuth and OIDC providers, PKCE is available as an opt-in "Use PKCE" toggle, added on November 12, 2025. Per OAuth 2.0 (RFC 6749 Section 4.1.2), authorization codes "MUST expire shortly after" issuance, with a recommended maximum lifetime of 10 minutes.
Redirect Flow vs. Popup Flow
Always use the redirect flow for social login in web applications. The popup flow has fundamental reliability and security problems that make it unsuitable for production use. Auth0's guidance on their Lock component states that "there are almost no reasons not to use redirect mode when writing a regular web application" (Auth0 blog).
The popup approach introduces hard-to-reproduce bugs: it silently fails when a popup blocker intervenes, leaving the user on the sign-in page with no error. On mobile — over half of web traffic — popup-based OAuth is effectively broken.
Clerk, Auth0, WorkOS, and Supabase all default to the redirect flow in their SDKs. Firebase Auth is a notable exception: it offers both signInWithPopup() and signInWithRedirect() (neither is deprecated), but Firebase's signInWithRedirect() relies on a cross-domain iframe that modern third-party-storage blocking (Chrome, Safari, Firefox) breaks by default — so Firebase's own guidance recommends either serving authentication from your own domain (a custom authDomain) or switching to signInWithPopup(). The redirect-first guidance in this section concerns server-side callback flows, where the provider redirects back to your backend — the architecture hosted auth services use.
OAuth Data Flow (TypeScript Reference)
For developers implementing custom OAuth flows (without an auth service handling the details), these TypeScript interfaces illustrate the data exchanged at each step:
// Step 1: Authorization Request
interface AuthorizationRequest {
client_id: string
redirect_uri: string
response_type: 'code'
scope: string // e.g., 'openid email profile'
state: string // CSRF token (cryptographically random)
nonce: string // Replay prevention
code_challenge: string // PKCE: BASE64URL(SHA256(code_verifier))
code_challenge_method: 'S256'
}After the user authenticates, the provider redirects back with the authorization code:
// Step 3: Authorization Response (query parameters on redirect)
interface AuthorizationResponse {
code: string // Short-lived authorization code
state: string // Must match the original state value
}Your backend then exchanges the code for an access token, ID token, and optionally a refresh token:
// Step 4: Token Exchange Request (server-side POST)
interface TokenRequest {
grant_type: 'authorization_code'
code: string // From the authorization response
redirect_uri: string // Must match the original request
client_id: string
client_secret: string // Server-side only — never in browser code
code_verifier: string // PKCE: original random string
}
// Step 5: Token Response
interface TokenResponse {
access_token: string // For calling provider APIs
id_token: string // OIDC: signed JWT with identity claims
token_type: 'Bearer'
expires_in: number // Access token lifetime in seconds
refresh_token?: string // Optional, for long-lived access
scope: string
}When using Clerk, the SDK handles this entire flow; these types are useful for understanding what happens under the hood and for debugging.
In a custom implementation: extract the identity claims you need from the id_token, create or find the user in your database, and issue your own session credential (such as an HttpOnly session cookie). Only persist the access_token and refresh_token server-side if your application needs to call the provider's APIs on behalf of the user (e.g., reading GitHub repositories or Google Calendar events). Discard them otherwise.
Redirect Handling Patterns
During the OAuth redirect, your application must persist certain values across the round trip:
- State and nonce: Store in a server session or a secure, short-lived cookie — not in
localStorage, which is vulnerable to XSS - PKCE code verifier:
sessionStorageis acceptable here because the verifier is a short-lived, single-use cryptographic challenge, not an access token or refresh token. It's cleared when the tab closes and is only used once during token exchange. - Return URL: Preserve the user's intended destination (the page they were on before sign-in) by encoding it in the OAuth
stateparameter or storing it in the server session
Managing User Profiles and Account Linking
Account linking is one of the most security-sensitive aspects of social login. When a user signs in with Google using alice@example.com and later returns with GitHub using the same email, your application must decide: is this the same person, or an attacker?
The Duplicate Account Problem
Without account linking, users who sign in with different providers create separate accounts with fragmented data, duplicated billing, and confused access. With naive automatic linking (merging accounts based solely on email match), you open the door to account takeover attacks.
Three Linking Patterns
- Automatic (email-based) — The system merges accounts when the OAuth email matches an existing account's verified email. No user interaction required. Used by Clerk, Supabase, and WorkOS.
- Link-on-login (prompted) — The system detects a matching email and asks the user to prove ownership of the existing account (enter password, complete MFA) before linking. Used by Ory Kratos and Zitadel; both also support automatic linking for verified-email providers (such as Google and Apple), configurable per identity provider.
- Manual (user-initiated) — Users explicitly link accounts through profile settings while already authenticated. Used by Auth0 and Firebase.
Pre-Hijacking Attacks
Researchers found that at least 35 of 75 popular services were vulnerable to pre-hijacking attacks (Sudhodanan & Paverd, USENIX Security 2022). Andrew Paverd conducted this research at the Microsoft Security Response Center; Avinash Sudhodanan was an independent researcher at the time of publication. In these attacks, an adversary creates an account with a victim's email before the victim registers, then exploits automatic account linking to gain access when the victim eventually signs up via a social provider.
The critical defense — validated by this independent peer-reviewed research — is the email_verified claim in OIDC tokens. Automatic linking should only occur when both the existing account's email and the incoming OAuth email are verified by their respective providers.
How Clerk Handles Account Linking
Clerk implements automatic email-based linking with strict verification requirements:
- Both emails verified: The OAuth account is automatically linked to the existing account. The user is signed in seamlessly.
- OAuth email unverified: Clerk initiates email verification before linking, preventing pre-hijacking.
- Existing account email unverified: Clerk requires enhanced security steps, such as password change or additional validation, before allowing the link.
- Different emails: Users can manually link accounts through the
<UserProfile />component by adding alternative email addresses. See the account linking guide for full details on how Clerk manages linked social accounts.
Clerk audits all supported SSO providers for OIDC compliance and email_verified accuracy. Non-compliant providers that return non-standard claim names (such as verified instead of email_verified) are handled automatically (source: "How We Roll — Chapter 4: Email Verification" blog post).
Handling Email-Absent and Private-Email Providers
Not all providers return an email address by default:
- GitHub: User email may be private. Request the
user:emailscope and call the/user/emailsendpoint. If the user has no public email, you may receivenullfrom the profile endpoint. - Apple: Hide My Email generates unique relay addresses per app (
@privaterelay.appleid.com, with newer addresses issued on@private.icloud.com). These relay addresses won't match existing account emails, preventing automatic linking.
Resolution: Allow users to add and verify their real email address post-signup via profile settings. Store the provider's stable identifier (sub for OIDC providers, id for GitHub) as the primary key rather than relying solely on email.
How Competitors Handle Account Linking
Account Recovery When a Provider Is Unavailable
If a user loses access to their social provider (e.g., a deactivated Google account), they need a way to regain access. Best practices:
- Always offer a fallback authentication method (email/password, email OTP, passkeys, or email magic links)
- Allow users to link multiple social providers to their account so they have alternatives
- Clerk's
<UserProfile />component lets users manage linked accounts and add new authentication methods from their profile settings
Security Best Practices: Token Storage and Session Management
There are two distinct categories of tokens to consider after social login. Provider tokens — the access token, ID token, and optional refresh token issued by Google, GitHub, or other identity providers — are received during the OAuth token exchange and are typically handled entirely by your auth service's backend. You never see or store these directly. Application session credentials — session cookies, JWTs, or other tokens issued by your auth service — represent the user's authenticated session in your application and are what the browser stores and sends on each request. The storage recommendations in this section apply to application session credentials. If your application also needs provider tokens (e.g., to call the Google Calendar API on behalf of the user), those should be stored server-side and never exposed to the browser.
Token Storage Comparison
The following table compares mechanisms for storing application session credentials — the tokens or cookies your auth service issues to maintain the user's logged-in state. (Provider tokens always stay server-side, as noted above.)
The consensus from OWASP and the IETF Browser-Based Apps BCP is clear: HttpOnly cookies are the recommended mechanism for token storage in web applications. The IETF BCP recommends that browser-based applications use a backend component (the BFF pattern) to keep tokens out of the browser entirely, and advises against storing tokens in localStorage or similar browser-accessible storage.
Any JavaScript running on your page can access localStorage, sessionStorage, and IndexedDB. A single XSS vulnerability means an attacker can exfiltrate tokens and use them from any device for their entire lifetime.
The Backend for Frontend (BFF) Pattern
The IETF Browser-Based Apps BCP (currently an Internet-Draft, not yet an RFC) recommends the Backend for Frontend pattern as the preferred architecture for browser-based OAuth applications. Note: RFC 9700 ("OAuth 2.0 Security Best Current Practice") is a separate, finalized document covering general OAuth security; it does not specifically define the BFF pattern. In the BFF model:
- The browser communicates with a backend component using secure, HttpOnly session cookies
- The backend handles all OAuth token exchange as a confidential client
- Provider tokens (access tokens, refresh tokens) are stored server-side — they never reach the browser
- The backend proxies API requests to the provider, attaching the provider's access token before forwarding
Clerk, Auth0, and WorkOS all implement variants of this pattern, keeping provider tokens server-side. In Clerk's variant, the provider's OAuth tokens stay on Clerk's servers and are never sent to the browser, while Clerk issues its own purpose-built session cookies to the browser with short lifetimes and revocation support.
Clerk's Hybrid Token Architecture
Clerk uses a dual-cookie system designed to combine the security of server-side sessions with the performance of stateless JWTs. This architecture is documented in detail in the How Clerk Works guide.
Neither cookie contains the OAuth provider's tokens — those stay on Clerk's servers (per the BFF pattern above). The __client and __session cookies described below are Clerk's own session credentials:
__client cookie — A long-lived, HttpOnly, SameSite=Lax cookie set on the Clerk FAPI (Frontend API) domain. This cookie handles session revocation and is the primary authentication state. Because it's HttpOnly, JavaScript cannot access it. In development mode, Clerk uses __clerk_db_jwt via querystring instead of cookies.
__session cookie — A short-lived JWT (60-second TTL) set on the application's root domain by the client-side SDK (not via Set-Cookie from the FAPI). This cookie carries the JWT claims your application reads for authorization decisions.
Why is __session not HttpOnly? The Clerk client SDK needs to set this cookie via JavaScript on the app domain. The security tradeoff is mitigated by:
- 60-second TTL: Tokens expire before attackers can meaningfully exploit them
- 50-second refresh interval: The SDK refreshes 10 seconds before expiry, ensuring a buffer for network latency
- Stateful revocation: Even if a
__sessiontoken is stolen, revoking the__clientsession invalidates it on the next SDK renewal cycle
From the How Clerk Works documentation: "For an XSS attack to succeed, the developer would need to ship a vulnerability on their site, and the attacker would need to exfiltrate users' tokens and use them to take over accounts in an average of less than 30 seconds."
This architecture combines the best of both models — stateful revocation (via __client) for security, and stateless JWT verification (via __session) for performance. Your backend can verify the session JWT locally using cryptographic signature validation, with no network call to Clerk required on each request — enabling sub-millisecond auth checks at the edge or in serverless functions.
Conclusion
Part 2 shows how social login moves from provider buttons to production architecture: framework portability, mobile SDK behavior, callback security, verified account linking, and secure session credentials. The remaining evaluation work is to compare auth services, cost models, custom OIDC extensibility, and common integration failures.
FAQ
In this series
- How do I implement social login for my web app?
- How do I implement social login for my web app? - Part 2 (you are here)
- How do I implement social login for my web app? - Part 3