Designing a Minimal Secure Facebook Login Integration for a Web Application
Learn how to implement a minimal, secure Facebook Login flow using OAuth 2.0, keeping the app secret server‑side, exchanging codes for long‑lived tokens, and storing them in an HttpOnly cookie.
28 Apr 2026, 15:37 UTC

Problem
You need to let users sign in with their Facebook account without exposing secrets or creating unnecessary attack surface. A mis‑configured OAuth flow can leak the app secret, accept tokens from the wrong audience, or leave sessions vulnerable to theft.
Takeaway
By keeping the app secret on the server, exchanging the short‑lived code for a long‑lived token only in a backend endpoint, and storing that token in an HttpOnly, Secure, SameSite‑Strict cookie, you obtain a login flow that satisfies Facebook’s requirements and common security baselines.
Requirements
- HTTPS everywhere (TLS 1.2+).
- A Facebook App with a valid OAuth redirect URI that matches the one used in the login request.
- Secure storage of the app secret (e.g., environment variable or secret manager) that is never sent to the browser.
- A backend endpoint capable of receiving the authorization code, calling Facebook’s token exchange API, and setting a session cookie.
Smallest Suitable Design
The flow consists of three parties: the user’s browser, your backend, and Facebook’s OAuth server.
- The browser loads your login page and clicks “Login with Facebook”. The Facebook JavaScript SDK (or a simple redirect) sends the user to
https://www.facebook.com/v19.0/dialog/oauth?client_id={APP_ID}&redirect_uri={YOUR_REDIRECT_URI}&scope=email,public_profile&response_type=code&state={RANDOM}. - After the user grants permission, Facebook redirects the browser to
{YOUR_REDIRECT_URI}?code={AUTH_CODE}&state={RANDOM}. Your backend verifies thestatematches the value stored in the user’s session (or a signed cookie) to prevent CSRF. - The backend makes a server‑to‑server POST to
https://graph.facebook.com/v19.0/oauth/access_tokenwith parametersclient_id={APP_ID},redirect_uri={YOUR_REDIRECT_URI},client_secret={APP_SECRET}, andcode={AUTH_CODE}. Facebook replies with a JSON object containingaccess_token(short‑lived, ~1 hour) andexpires_in. - The backend immediately exchanges that short‑lived token for a long‑lived token by calling the same endpoint with
grant_type=fb_exchange_token,client_secret={APP_SECRET}, andfb_exchange_token={SHORT_LIVED_TOKEN}. The response includes a long‑lived token valid for ~60 days. - The backend sets an HttpOnly cookie named, for example,
fb_sessionwith the long‑lived token as its value, and flagsSecure,SameSite=Strict, and a reasonableMax-Age(e.g., 5184000 seconds for 60 days). No token is ever sent to the browser in the response body. - Subsequent authenticated requests include the cookie; the backend validates the token’s signature and audience (
audclaim) by introspecting it viahttps://graph.facebook.com/debug_token?input_token={TOKEN}&access_token={APP_ID}|{APP_SECRET}or by using the token directly in Graph API calls (Facebook validates the token internally). - To fetch user data, the backend calls
https://graph.facebook.com/v19.0/me?fields=id,name,email&access_token={LONG_LIVED_TOKEN}and returns the needed fields to the frontend.
Trust & Data Boundaries
- Browser → Backend: Only the authorization code and state are transmitted; the app secret never crosses this boundary.
- Backend → Facebook: The app secret is used solely in server‑to‑server calls over TLS; Facebook never sees the secret in plaintext beyond its own verification.
- Cookie: Because it is HttpOnly, JavaScript cannot read it, mitigating XSS theft. Secure and SameSite=Strict ensure it is never sent over HTTP and is not included in cross‑site requests.
Operational Checks
- Verify HTTPS enforcement (e.g., HSTS header).
- Confirm the redirect URI registered in the Facebook App Dashboard exactly matches the one used in the login request (including protocol, host, port, and path).
- After token exchange, inspect the
audclaim in the debug_token response; it must equal your Facebook App ID. - Check that the cookie’s attributes include
Secure,HttpOnly, andSameSite=Strict(browser dev tools → Application → Cookies). - Log failures of the token exchange step (non‑200 HTTP response, invalid JSON, missing
access_token) with redacted secrets. - Monitor rate‑limit headers (
X‑Business‑Use‑Case‑Quota,X‑App‑Usage) and implement exponential back‑off when limits approach.
Failure Modes
- Expired or revoked token: Graph API calls return HTTP 400 with error code 190 or 102. The backend should clear the cookie and prompt re‑login.
- Network failure during token exchange: Retry with a limited number of attempts; persistent failure indicates a configuration issue (wrong secret or redirect URI).
- Mismatched redirect URI: Facebook returns error invalid_request with message “Redirect URI does not match”. Verify the URI in the App Dashboard and the one used in the login URL.
- Invalid client secret: Results in error 100 (“Invalid OAuth access token”). Ensure the secret is stored correctly and not accidentally logged.
- Missing permissions: Calls to
/memay succeed but lack requested fields; handle gracefully by requesting additional scopes on next login.
Conditions That Would Change the Design
- If you need to support native mobile apps, you would switch to the Facebook Login SDK for iOS/Android, which handles token exchange internally and returns a token to the app rather than using a web redirect.
- If regulatory requirements demand token rotation more frequent than 60 days, you would implement a refresh endpoint that exchanges the long‑lived token for a new one before expiration, storing the new token in the cookie.
- Should you decide to store additional user data (e.g., friends list) requiring extended permissions, you must adjust the
scopeparameter and handle the corresponding review process with Facebook. - If your architecture moves to a serverless function model, the backend endpoint becomes a stateless function; you must ensure the app secret is injected securely via the platform’s secret manager and that the function’s execution environment enforces the same HTTPS and cookie settings.
Practical Verification Steps
- In a staging environment, initiate the login flow and capture the redirect to your backend. Confirm the backend receives a non‑empty
codeparameter. - Run a curl test to the token exchange endpoint (replace placeholders):
curl -X POST "https://graph.facebook.com/v19.0/oauth/access_token" \ -d "client_id=YOUR_APP_ID" \ -d "redirect_uri=https://staging.example.com/auth/facebook/callback" \ -d "client_secret=YOUR_APP_SECRET" \ -d "code=AUTH_CODE_FROM_REDIRECT"
Expect a JSON response containingaccess_tokenandexpires_in. - Exchange that token for a long‑lived token:
curl -X POST "https://graph.facebook.com/v19.0/oauth/access_token" \ -d "grant_type=fb_exchange_token" \ -d "client_id=YOUR_APP_ID" \ -d "client_secret=YOUR_APP_SECRET" \ -d "fb_exchange_token=SHORT_LIVED_TOKEN"
Verify the response includes a long‑lived token with a far‑future expiration. - Set the cookie in your backend response and inspect it in the browser: ensure
Secure,HttpOnly, andSameSite=Strictflags are present. - Call the Graph API
/meendpoint with the long‑lived token and confirm you receive the expected fields (id, name, email). - Simulate token revocation: in the Facebook App Dashboard, go to Settings → Advanced → Security → “Remove” the token or use the Debug Token tool to invalidate it. Subsequent API calls should return error 190, prompting your backend to clear the cookie and redirect to login.
Limitations
- The long‑lived token expires after roughly 60 days; without a refresh mechanism users will be logged out and must re‑authenticate.
- Facebook enforces per‑user rate limits (e.g., 200 Graph API calls per hour). Applications that need higher throughput must implement caching, batch requests, or request a higher‑tier allocation.
- Compliance with GDPR/CCPA requires you to provide a way for users to delete their data; you must delete any stored Facebook‑derived data upon logout or user request.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.