Implementing LinkedIn Sign‑In with OIDC: A Server‑Side Authorization‑Code Flow Guide
Implement LinkedIn Sign-In using a server-side authorization-code flow, request only openid profile email scopes, validate the ID token, and map the stable sub claim to your internal account.
13 Oct 2026, 16:49 UTC

Problem & Takeaway
\nYou want to let users log in with LinkedIn without requesting heavy permissions. The safest, most reliable approach is a confidential server‑side OAuth 2.0 authorization‑code flow that returns an OpenID Connect ID token. The sub claim is a stable member identifier you can map to an internal account. This guide walks you through the entire flow, from app configuration to token validation and error handling.
Desired Outcome
\n- \n
- Users authenticate via LinkedIn and receive a local session. \n
- Only the
openid,profile, andemailscopes are requested. \n - The
subclaim is used as the canonical user key. \n - No posting or marketing scopes are requested unless absolutely necessary. \n
Prerequisites
\n- \n
- LinkedIn Developer App\n
- \n
- Product: Sign In with LinkedIn (not the Marketing API). \n
- Redirect URIs: one HTTPS URI per environment (e.g.,
https://auth.example.com/linkedin/callback). \n - Client ID and Client Secret stored securely on the server. \n
- Legal basis for storing the ID token and email (e.g., privacy policy, terms of service). \n
\n - Server Environment\n
- \n
- HTTPS enabled; LinkedIn requires TLS 1.2+. \n
- Language/framework: any that can make HTTPS requests and verify JWTs (e.g., Node.js, Python, Java). \n
- Access to LinkedIn’s JWKS endpoint:
https://www.linkedin.com/oauth/v2/jwks. \n
\n - State & Nonce Generation\n
- \n
- Generate a cryptographically random string for
state(32+ bytes, base64url). \n - Generate a
nonceif you want to protect against replay attacks (recommended). \n - Store both in the user’s session or a signed cookie until the callback. \n
\n - Generate a cryptographically random string for
Procedure
\n1. Redirect to LinkedIn Authorization Endpoint
\nBuild the URL:
\nhttps://www.linkedin.com/oauth/v2/authorization?\n response_type=code\n &client_id={CLIENT_ID}\n &redirect_uri={REDIRECT_URI}\n &state={STATE}\n &scope=openid%20profile%20email\n &nonce={NONCE}\nReplace placeholders with actual values. Ensure the redirect URI matches exactly what is registered.
\n2. Handle the Callback
\nLinkedIn redirects to {REDIRECT_URI} with code, state, and optionally error parameters.
GET /linkedin/callback?code=...&state=...&nonce=...\nSteps:
\n- \n
- Verify
statematches the stored value; if not, fail closed. \n - If
erroris present, map it to a user‑facing message (e.g.,access_denied→ “Login cancelled”). \n - Exchange the
codefor tokens. \n
3. Exchange Code for Tokens (Server‑Side)
\nMake a POST to https://www.linkedin.com/oauth/v2/accessToken with client authentication (basic auth or client_id/client_secret in body). Example with curl:
curl -X POST\n -H 'Content-Type: application/x-www-form-urlencoded'\n -d 'grant_type=authorization_code'\n -d 'code={CODE}'\n -d 'redirect_uri={REDIRECT_URI}'\n -d 'client_id={CLIENT_ID}'\n -d 'client_secret={CLIENT_SECRET}'\n https://www.linkedin.com/oauth/v2/accessToken\nExpected JSON response:
\n{\n "access_token": "",\n "expires_in": 86400,\n "id_token": ""\n}\nDo not reuse the same code again; LinkedIn will return invalid_grant.
4. Validate the ID Token
\nDecode the JWT and perform the following checks:
\n- \n
- Issuer:
https://www.linkedin.comor the exact value returned iniss. \n - Audience: Must equal your
client_id. \n - Expiration (exp) and Issued At (iat): Ensure
exp> now andiatis reasonable. \n - Nonce (if used): Must match the stored value. \n
- Signature: Verify against LinkedIn’s JWKS. Fetch the JWKS once and cache the keys. Use a JWT library that supports JWK verification. \n
If any check fails, treat the authentication attempt as invalid and do not create a session.
\n5. Map sub to Internal Account
\nThe sub claim is a stable, opaque identifier for the LinkedIn member. Store it in your user table as the primary key for LinkedIn‑based users. Example mapping logic:
user = User.find_or_create_by(linkedin_sub: id_token['sub']) do |u|\n u.email = id_token['email']\n u.name = id_token['name']\n # Additional profile fields as needed\nend\ncreate_session_for(user)\nIf the email is unverified or missing, prompt the user to provide a verified email before completing the account.
\nExpected Checks & Recovery
\n- \n
- State mismatch → Show an error page and offer a retry link. \n
- error=access_denied (user cancelled) → Return to login page with a friendly message. \n
- invalid_grant (expired or reused code) → Force a fresh authorization request. \n
- redirect_uri_mismatch or invalid_client → Log the incident, notify dev ops, and abort the login attempt. \n
- insufficient_scope → Only occurs if you accidentally requested a missing scope; in this flow it shouldn’t happen. \n
- Network or 5xx errors during token exchange → Retry with exponential backoff up to 3 attempts; after that show a generic error and a retry button. \n
Limitations & Practical Checks
\n- \n
- LinkedIn may change scope names or token lifetimes. Verify the current docs before deployment. \n
- The
emailclaim can be absent or unverified; enforce a separate verification step if you rely on email for password reset. \n - Rate limits apply to the token endpoint; monitor usage and implement back‑off if you hit 429. \n
- Always store the client secret in a secure vault; never expose it to the client side. \n
- Test the flow in a sandbox or staging environment that mirrors production redirect URIs. \n
Conclusion
\nBy following this server‑side authorization‑code flow and limiting scopes to openid profile email, you provide a secure, privacy‑respecting LinkedIn sign‑in experience. The sub claim gives you a stable identifier that can be safely mapped to your internal user database. Handle errors gracefully, validate tokens rigorously, and keep your LinkedIn app configuration up‑to‑date to avoid surprises.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.