Skip to content

OpenID Connect (OIDC) — Complete Authentication Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Openid Connect (OIDC). We cover key concepts, practical examples, and best practices to help you master this topic.

OpenID Connect (OIDC) is an identity layer built on top of OAuth 2.0 that adds authentication capabilities, providing ID tokens that contain verified user identity information alongside the standard OAuth 2.0 access tokens.

What You'll Learn

By the end of this lesson, you will understand how OIDC extends OAuth 2.0, implement OIDC authentication flows, decode and validate ID tokens, configure discovery endpoints, and build a complete OIDC client.

Why It Matters

OIDC replaces custom authentication protocols with a standardized, interoperable identity layer. Every major identity provider (Google, Microsoft, Apple, Auth0) supports OIDC. Doda Browser uses OIDC to authenticate users across its sync service, browser extensions, and companion apps with a single identity.

Real-World Use

A user signs into a project management tool using "Sign in with Google." The tool uses OIDC to verify the user's identity, receives their email, name, and profile picture from the ID token, and uses the access token to access their Google Calendar for event integration.

OIDC Flow

sequenceDiagram
    participant App as Application
    participant OP as OIDC Provider
    participant User

    App->>OP: Authentication Request (scope=openid profile)
    OP->>User: Authenticate & Consent
    User->>OP: Approve
    OP->>App: Authorization Code
    App->>OP: Code + Client Secret + redirect_uri
    OP-->>App: ID Token + Access Token + Refresh Token
    App->>App: Validate ID Token (signature, iss, aud, exp)
    App->>OP: Optional: /userinfo endpoint
    OP-->>App: User Claims
    App->>User: Authenticated session

OIDC Client Implementation

import jwt
import requests
from jwt import PyJWKClient

class OIDCClient:
    def __init__(self, client_id, client_secret, issuer, redirect_uri):
        self.client_id = client_id
        self.client_secret = client_secret
        self.issuer = issuer
        self.redirect_uri = redirect_uri
        self.config = self._discover_config()

    def _discover_config(self):
        well_known = f"{self.issuer}/.well-known/openid-configuration"
        response = requests.get(well_known, timeout=10)
        response.raise_for_status()
        config = response.json()
        print(f"[OIDC] Discovery: {config.get('issuer')}")
        print(f"[OIDC] Auth endpoint: {config.get('authorization_endpoint')}")
        print(f"[OIDC] JWKS URI: {config.get('jwks_uri')}")
        return config

    def verify_id_token(self, id_token):
        jwks_client = PyJWKClient(self.config["jwks_uri"])
        signing_key = jwks_client.get_signing_key_from_jwt(id_token)

        claims = jwt.decode(
            id_token,
            signing_key.key,
            algorithms=["RS256"],
            audience=self.client_id,
            issuer=self.issuer,
            options={
                "verify_exp": True,
                "verify_iat": True,
                "require": ["sub", "iss", "aud", "exp", "iat"],
            },
        )
        print(f"[OIDC] ID Token verified for user: {claims.get('sub')}")
        print(f"[OIDC] Email: {claims.get('email', 'N/A')}")
        print(f"[OIDC] Name: {claims.get('name', 'N/A')}")
        return claims

    def get_userinfo(self, access_token):
        response = requests.get(
            self.config["userinfo_endpoint"],
            headers={"Authorization": f"Bearer {access_token}"},
            timeout=10,
        )
        response.raise_for_status()
        return response.json()

# Usage
client = OIDCClient(
    client_id="myapp",
    client_secret="secret",
    issuer="https://accounts.example.com",
    redirect_uri="https://myapp.com/callback",
)

# After receiving id_token from authorization flow:
# claims = client.verify_id_token(id_token)

OIDC with Node.js (using openid-client)

const { Issuer, generators } = require("openid-client");

async function setupOIDC() {
  const issuer = await Issuer.discover("https://accounts.example.com");
  console.log(`Discovered issuer: ${issuer.issuer}`);

  const client = new issuer.Client({
    client_id: process.env.CLIENT_ID,
    client_secret: process.env.CLIENT_SECRET,
    redirect_uris: ["https://myapp.com/callback"],
    response_types: ["code"],
  });

  const code_verifier = generators.codeVerifier();
  const state = generators.state();

  const authUrl = client.authorizationUrl({
    scope: "openid profile email",
    code_challenge: generators.codeChallenge(code_verifier),
    state,
  });

  console.log(`Authorization URL: ${authUrl}`);

  async function handleCallback(callbackParams) {
    const tokenSet = await client.callback(
      "https://myapp.com/callback",
      callbackParams,
      { code_verifier, state }
    );

    console.log(`ID Token: ${tokenSet.id_token}`);
    console.log(`Access Token: ${tokenSet.access_token.substring(0, 20)}...`);

    const userinfo = await client.userinfo(tokenSet.access_token);
    console.log(`User: ${userinfo.name} (${userinfo.email})`);

    return userinfo;
  }

  return { authUrl, handleCallback };
}

ID Token Claims

| Claim | Description | Example | |-------|-------------|---------| | sub | Subject identifier (unique user ID) | "google-oauth2|123456" | | iss | Issuer identifier | "https://accounts.google.com" | | aud | Audience (client ID) | "myapp-client-id" | | exp | Expiration time (UNIX timestamp) | 1719561600 | | iat | Issued at time | 1719558000 | | nonce | Replay attack prevention | "random-value" | | email | User's email address | "alice@example.com" | | email_verified | Whether email is verified | true | | name | User's full name | "Alice Smith" |

Common Mistakes

  1. Not validating the ID token signature exposes the app to token forgery attacks.
  2. Failing to verify the aud claim allows tokens issued for other applications to be used.
  3. Not checking the iss claim allows tokens from unexpected providers to authenticate users.
  4. Using the access token as proof of authentication (access tokens are for API access, not identity).
  5. Not handling the nonce claim properly exposes the flow to replay attacks.
  6. Relying solely on the ID token for user information without validating it first.

Practice Questions

  1. What is the difference between an ID token and an access token in OIDC?

An ID token is a JWT that contains verified identity claims about the user (authentication). An access token is an opaque token that grants access to resources (authorization). ID tokens are for the client; access tokens are for the API.

  1. How does OIDC discovery work?

The OIDC provider publishes an OpenID Configuration document at /.well-known/openid-configuration. It contains all endpoint URLs and supported features, allowing clients to auto-configure.

  1. Why must ID tokens always be validated?

Without validation, an attacker can forge or modify ID tokens to impersonate users. Validation includes signature verification, audience check, issuer check, expiration check, and nonce verification.

  1. Challenge: Build an OIDC client that supports multiple providers (Google, Microsoft, GitHub), stores user sessions, handles token refresh, and provides a unified user profile endpoint.

FAQ

What scopes are required for OIDC?

The openid scope is mandatory. Without it, the provider does not return an ID token. Optional scopes include profile, email, address, and phone.

Can I use OIDC without OAuth 2.0?

No. OIDC is built on top of OAuth 2.0. The authentication flow uses OAuth 2.0 grants, and the ID token is returned alongside access and refresh tokens.

What is the userinfo endpoint?

The userinfo endpoint returns identity claims about the authenticated user. It requires a valid access token. Use it when the ID token does not contain all needed claims.

How do I handle OIDC logout?

OIDC provides RP-Initiated Logout (redirect the user to the provider's logout endpoint) and Session Management (iframe-based session monitoring). After local session destruction, redirect to the provider's end_session_endpoint.

Mini Project: OIDC Token Validator

Build a CLI tool that takes an ID token, fetches the provider's JWKS, validates the signature, decodes the payload, and displays all claims with validation results.

import jwt
import requests
import sys
from jwt import PyJWKClient

def validate_oidc_token(id_token, issuer, client_id):
    try:
        config_url = f"{issuer}/.well-known/openid-configuration"
        config = requests.get(config_url, timeout=10).json()
        jwks_uri = config["jwks_uri"]
        jwks_client = PyJWKClient(jwks_uri)
        signing_key = jwks_client.get_signing_key_from_jwt(id_token)

        claims = jwt.decode(
            id_token, signing_key.key, algorithms=["RS256"],
            audience=client_id, issuer=issuer,
            options={"verify_exp": True, "require": ["sub", "iss", "aud", "exp"]},
        )
        print("=== ID Token Validation PASSED ===")
        print(f"Subject: {claims.get('sub')}")
        print(f"Issuer: {claims.get('iss')}")
        print(f"Email: {claims.get('email', 'N/A')}")
        print(f"Name: {claims.get('name', 'N/A')}")
        print(f"Expires: {'OK' if claims.get('exp', 0) > __import__('time').time() else 'EXPIRED'}")
    except Exception as e:
        print(f"Validation FAILED: {e}")

if __name__ == "__main__":
    token = sys.argv[1]
    validate_oidc_token(token, "https://accounts.example.com", "myapp")

What's Next

Learn about SAML authentication for enterprise SSO, then explore API key authentication for service-to-service communication.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro