OpenID Connect (OIDC) — Complete Authentication Guide
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
- Not validating the ID token signature exposes the app to token forgery attacks.
- Failing to verify the
audclaim allows tokens issued for other applications to be used. - Not checking the
issclaim allows tokens from unexpected providers to authenticate users. - Using the access token as proof of authentication (access tokens are for API access, not identity).
- Not handling the
nonceclaim properly exposes the flow to replay attacks. - Relying solely on the ID token for user information without validating it first.
Practice Questions
- 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.
- 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.
- 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.
- 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
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