Implement Single Sign-On with OIDC

Implement single sign-on with OpenID Connect — OAuth 2 · OpenID Connect tutorial, lesson 10.

Focus: implement single sign-on with openid connect

Sponsored

You've mastered OAuth 2.0 for authorization, but now you face a new challenge: you need to let your users log in with their existing Google, GitHub, or corporate accounts — without building a username/password database from scratch. That's where single sign-on (SSO) comes in, and the OAuth 2.0 foundation you've already learned is the perfect launchpad. In this lesson, you'll learn how to implement single sign-on with OpenID Connect (OIDC), the identity layer built on top of OAuth 2.0 that turns authorization into authentication. By the end, you'll have a working SSO flow and a mental model that will serve you in everything from enterprise portals to tiny side projects.

The problem this lesson solves

Imagine you're building a new internal tool. Your users are already logging into your company's central identity provider (IdP) every morning, but your app insists on its own username and password. Now you own password resets, breach notifications, and a login form that users dread. Building your own auth is a security and maintenance burden — and, frankly, a waste of time.

Single sign-on solves this by letting your app delegate authentication to a trusted identity provider. When a user visits your app, you redirect them to the IdP, they log in once, and the IdP hands you an identity token that proves who they are. That's the core promise of OpenID Connect: one login, many apps, no duplicated credentials.

This isn't a niche problem. From enterprise SaaS portals to developer tools and government services, SSO is everywhere. The pain is real: users hate multiple passwords, admins hate managing access, and developers hate building auth from scratch. OIDC gives you a standardized, secure way out — and you already know the OAuth vocabulary to make it click.

Core concept / mental model

Think of OAuth 2.0 as the keycard that grants access to a building (your API), and OpenID Connect as the badge that tells the security guard who you are. OAuth authenticates the client to access resources; OIDC authenticates the user to the client.

Technically, OIDC is a thin layer on top of OAuth 2.0's Authorization Code flow. The magic happens in the ID Token — a signed JWT that contains claims about the user, such as their email, name, and a unique subject identifier (sub). This token is the proof of identity.

Here's the cast of characters you'll meet:

  • End User: The human who wants to log in.
  • Client (your app): The application that needs to know who the user is.
  • Identity Provider (IdP): The authority that authenticates the user and issues tokens. (Examples: Google, Okta, Azure AD, Auth0, Keycloak.)
  • ID Token: A JWT holding identity claims, signed by the IdP.
  • Access Token: (Still present from OAuth) — used to call APIs on behalf of the user.

Why not just use OAuth 2.0 alone? Because OAuth's access tokens are opaque — they're not standardized to contain user info. OIDC defines the ID Token structure and the UserInfo endpoint, giving you a consistent contract across providers.

The key insight: OIDC does not replace OAuth; it builds on it. When you implement SSO with OIDC, you're essentially running an OAuth Authorization Code flow with an added openid scope and a new token type to validate.

How it works step by step

Let's trace the SSO flow for a typical web app using the Authorization Code flow with PKCE (the modern recommended pattern).

  1. User clicks "Sign in with Google" on your site.
  2. Your client redirects the user to the IdP's authorization endpoint with parameters: response_type=code, client_id, redirect_uri, scope=openid profile email, and a state parameter to prevent CSRF.
  3. The user authenticates at the IdP (or is already logged in via an existing session).
  4. The IdP redirects back to your redirect_uri with an authorization code (and the state you sent).
  5. Your client exchanges the code for tokens by calling the token endpoint — sending the code_verifier if using PKCE.
  6. The IdP responds with an id_token, an access_token, and sometimes a refresh token.
  7. Your client validates the ID token: verify the signature, issuer, audience, and expiration.
  8. You extract the user's identity from the ID token claims (or call the UserInfo endpoint if you need additional data).
  9. You create a session in your app (cookie, JWT, etc.) so the user doesn't have to log in again.

The entire dance happens over HTTPS, and the user never sees your client's secrets — only the IdP holds them safe. That's the beauty of the code flow: your app's backend does the token exchange, keeping secrets off the client.

Why PKCE matters: Even if you're building a mobile or single-page app that can't keep a client secret, PKCE (Proof Key for Code Exchange) adds a dynamically generated secret that prevents authorization code interception. Always use it — it's now the recommended practice even for confidential clients.

Hands-on walkthrough

Let's get our hands dirty. We'll implement a minimal SSO client in Python using the requests library and a well-known public OIDC provider (we'll use Google, but the pattern is identical for any provider).

Prerequisites:

  • A registered OAuth 2.0 client with Google Cloud Console (redirect URI http://localhost:8000/callback).
  • Python 3.10+ with the requests and jwt (PyJWT) libraries.
  • pip install requests PyJWT cryptography

Step 1: Discover the provider configuration.

Most IdPs publish an OpenID Provider Configuration document at a well-known URL. It tells you the endpoints, signing keys, and supported scopes.

import requests

GOOGLE_DISCOVERY_URL = "https://accounts.google.com/.well-known/openid-configuration"

def get_provider_config():
    resp = requests.get(GOOGLE_DISCOVERY_URL)
    resp.raise_for_status()
    return resp.json()

config = get_provider_config()
print("Authorization endpoint:", config["authorization_endpoint"])
print("Token endpoint:", config["token_endpoint"])
print("JWKS endpoint:", config["jwks_uri"])

Step 2: Build the authorization URL and redirect the user.

import secrets
import urllib.parse

CLIENT_ID = "your-client-id.apps.googleusercontent.com"
REDIRECT_URI = "http://localhost:8000/callback"

# Generate state and PKCE code verifier
state = secrets.token_urlsafe(32)
code_verifier = secrets.token_urlsafe(64)
code_challenge = (  # For simplicity, we skip S256 hashing here — use pkce library in practice
    code_verifier
)

params = {
    "response_type": "code",
    "client_id": CLIENT_ID,
    "redirect_uri": REDIRECT_URI,
    "scope": "openid profile email",
    "state": state,
    "code_challenge": code_challenge,
    "code_challenge_method": "plain",  # Use S256 in production!
}

auth_url = config["authorization_endpoint"] + "?" + urllib.parse.urlencode(params)
print("Redirect the user to:")
print(auth_url)

Step 3: Handle the callback and exchange the code.

After the user logs in and approves, Google redirects to your callback with a code and the state. Now you exchange the code for tokens.

# Inside your /callback route, you receive the code and state
auth_code = "authorization-code-from-query"

# Verify state matches what you stored in the user's session here!

token_params = {
    "code": auth_code,
    "client_id": CLIENT_ID,
    "client_secret": "your-client-secret",  # Confidential client only
    "redirect_uri": REDIRECT_URI,
    "grant_type": "authorization_code",
    "code_verifier": code_verifier,
}

token_resp = requests.post(config["token_endpoint"], data=token_params)

tokens = token_resp.json()
print("ID Token:", tokens["id_token"])
print("Access Token:", tokens.get("access_token"))

Step 4: Validate and decode the ID token.

Neuer trust the claims without verifying the signature and audience. In production, fetch the JWKS (JSON Web Key Set) from the discovery document and validate the signature.

import jwt

jwks = requests.get(config["jwks_uri"]).json()

# This simple example uses a public key from a test issuer —
# in practice you need to match the 'kid' header and pick the right key.
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(jwks["keys"][0])

id_token = tokens["id_token"]
decoded = jwt.decode(
    id_token,
    public_key,
    algorithms=["RS256"],
    audience=CLIENT_ID,
    issuer=config["issuer"],
)

print("User subject (sub):", decoded["sub"])
print("Email:", decoded.get("email"))
print("Name:", decoded.get("name"))

Expected output (simplified):

Redirect the user to:
https://accounts.google.com/o/oauth2/v2/auth?...
ID Token: eyJhbGciOiJSUzI1NiIsImtpZCI6...
User subject (sub): 1234567890
Email: jane.doe@example.com
Name: Jane Doe

That's the core of SSO! You now have a verified identity. In a real app, you'd store the sub in a session or your user database, and you're done.

Pro tip: Never log the raw ID token — it contains PII. Only log a hash or the sub.

Compare options / when to choose what

You've seen the OIDC Authorization Code flow. But there are other OIDC flows, each with its own trade-offs. Which one should you use?

Flow Use When Security Notes
Authorization Code + PKCE Web apps (server-side), mobile apps, SPAs — the universal choice Best security; code never exposed to browser directly; PKCE prevents interception
Authorization Code (no PKCE) Legacy server apps that can't implement PKCE Less secure; client secret required; avoid in new code
Implicit Flow (deprecated) Never — killed in security best practices Tokens in URL fragment; vulnerable to leakage; don't use it
Hybrid Flow Complex multi-party scenarios Mixes code and tokens; rarely needed for standard SSO
Device Flow CLI tools, IoT devices with no browser User enters a code on another device; good for headless environments

For 99% of web SSO, Authorization Code + PKCE is the answer. It's what Auth0, Google, and most IdPs recommend. If you're building a mobile app, the same flow applies — just keep the redirect URI custom-schemed and make sure no secret is stored in the app.

What about just using the ID token as an API credential? Don't. ID tokens are for the client, not for APIs. Access tokens are for protected resources. Mixing them up is a classic mistake we'll touch on next.

Troubleshooting & edge cases

Even with the right flow, things go wrong. Here are the most common pitfalls and how to fix them.

Error: invalid_grant during token exchange. - Cause: The authorization code is expired (usually 10 minutes) or already used. Codes are single-use. - Fix: Ensure your redirect URI exactly matches the one registered, and don't re-use a code. If using PKCE, the code_verifier must match the code_challenge exactly.

Error: redirect_uri mismatch. - Cause: The IdP compares the exact string. http://localhost:8000/callback is different from http://localhost:8000/callback/ or https://localhost:8000/callback. - Fix: Copy the registered URI verbatim. Avoid trailing slashes unless you added them in the console.

ID token validation fails. - Cause: You didn't verify the aud or iss. Or you used the wrong signing key. - Fix: Always validate aud equals your client_id. Keep the key ID (kid) in mind — rotate keys periodically. Use a library like pyjwt or python-jose to handle JWKS properly.

User logs in but gets a blank profile. - Cause: You didn't request the right scopes. openid is mandatory, but profile and email are optional and may not be granted if absent. - Fix: Add profile email to your scope request. Also check Privacy settings on the IdP — some claims (like email) may be null if not allowed.

SSO works in dev but not in production. - Cause: Production callback URI is different, or you're sending a client secret in a non-HTTPS context. - Fix: Add all allowed redirect URIs to the client configuration. Enforce HTTPS — OIDC requires TLS for token endpoints.

Edge case alert: Some IdPs don't include the name claim — rely on sub as the stable identifier, not the email. Emails can change; sub never does.

What you learned & what's next

You've now implemented single sign-on with OpenID Connect — a critical skill in modern application development. Let's recap what you've mastered:

  • Why OIDC: It's the identity layer on OAuth 2.0, giving you a standardized ID token with user claims.
  • The full SSO flow: From redirect to token exchange to validation, you now understand every step.
  • The right flow to choose: Authorization Code + PKCE is your go-to for web and mobile.
  • Troubleshooting: You can diagnose common failures like invalid_grant and redirect mismatches.

Now that you can authenticate a user once, you can build on this foundation. Your next lesson likely covers managing user sessions and logout — how to end an SSO session correctly, which is surprisingly tricky. Or you might dive into fetching user info with the UserInfo endpoint for fine-grained claims. Both build directly on what you've learned here.

Keep this momentum — go build an SSO-enabled app, and remember: delegate authentication, never store passwords you don't need.

Practice recap

Time to build! Set up a small Python server (Flask or FastAPI) and implement the full OIDC flow with Google or GitHub as your IdP. Use PKCE, validate every ID token, and add a logout button — then try breaking it: use a wrong redirect URI or ignore PKCE, and observe the errors. This hands-on practice will solidify the concepts for your next lesson.

Common mistakes

  • Using the ID token as an access token to call APIs. ID tokens are for the client, not for resource servers.
  • Not validating the ID token's signature, issuer, and audience — accepting any JWT as truth.
  • Forgetting to use PKCE even for server-side apps, leaving the code exchange vulnerable to interception.
  • Hardcoding the provider endpoints instead of fetching the discovery document — breaking when providers change URLs.
  • Storing the code_verifier in a client-side cookie or localStorage, defeating PKCE's purpose.

Variations

  1. Use a dedicated OIDC library like authlib or python-oidc to handle flows and token validation for you, reducing boilerplate.
  2. Implement SSO with a hosted IdP like Okta, Auth0, or Azure AD — they often provide SDKs and pre-built login widgets.
  3. Choose the Device Authorization flow for CLI tools or IoT devices where a browser isn't available for the full redirect dance.

Real-world use cases

  • Enterprise internal tool: employees log in with their corporate Active Directory account via Azure AD, getting seamless access across HR, finance, and IT apps.
  • Developer platform: GitHub users authenticate with their existing GitHub account via OIDC, letting them leave comments, star repos, and sync their profile.
  • SaaS app onboarding: users sign up with their Google Workspace account, skipping the password creation and enabling team membership via OIDC groups claim.

Key takeaways

  • OpenID Connect is OAuth 2.0 + an identity token (ID Token) that standardizes user authentication.
  • Always use the Authorization Code flow with PKCE for web and mobile SSO — it's the safest modern approach.
  • Validate ID tokens rigorously: check signature, issuer, audience, and expiration — never trust an unverified token.
  • Use the openid scope and request additional scopes like profile and email to get identity claims.
  • Treat the sub claim as the stable user identifier; do not rely on email or name for ID.
  • Fetch the provider's discovery document and JWKS to stay resilient to endpoint changes and key rotation.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.