OpenID Connect (OIDC) — The Identity Layer on Top of OAuth
Thilan Dissanayaka Identity & Access Management May 20, 2020

OpenID Connect (OIDC) — The Identity Layer on Top of OAuth

In the OAuth article, we saw how OAuth 2.0 enables a third-party app to access your Google Photos without needing your password. That's authorization — granting access to resources.

But here's the thing. OAuth 2.0 was never designed to tell an application who you are. It was designed to tell an application what it can access. These are fundamentally different things.

When you click "Sign in with Google," the application needs to know your identity — your name, email, user ID. OAuth alone doesn't provide that in a standardized way. Developers hacked around this by using the access token to call a user profile API, but that was never part of the OAuth spec. Different providers did it differently. It was messy and insecure.

OpenID Connect (OIDC) fixes this. It's a thin identity layer built on top of OAuth 2.0 that standardizes how applications verify who a user is.

One way to think about it:

OAuth tells the app what you CAN DO. OIDC tells the app WHO YOU ARE.

OIDC Terminology

Before we dive into flows, let's get the vocabulary straight. OIDC uses slightly different names than plain OAuth:

OIDC Term OAuth Equivalent Meaning
OpenID Provider (OP) Authorization Server The IdP that authenticates users (Google, Okta, WSO2 IS)
Relying Party (RP) Client The application that relies on the OP for authentication
End-User Resource Owner The human being authenticated
ID Token (no equivalent) A JWT containing identity claims — OIDC's key artifact
UserInfo Endpoint (no equivalent) An API endpoint that returns additional user claims
Claims (no equivalent) Key-value pairs about the user (name, email, sub)

The critical addition that OIDC brings to OAuth is the ID Token. Let's examine it.

The ID Token — OIDC's Key Artifact

The ID Token is a JWT that contains claims about the authentication event and the user. It's issued by the OpenID Provider and consumed by the Relying Party.

Here's a decoded ID Token:

{
    "iss": "https://accounts.google.com",
    "sub": "110169484474386276334",
    "aud": "my-app-client-id.apps.googleusercontent.com",
    "exp": 1716242622,
    "iat": 1716239022,
    "nonce": "abc123random",
    "auth_time": 1716239000,
    "at_hash": "HK6E_P6Dh8Y93mRNtsDB1Q",
    "name": "Thilan Fernando",
    "email": "[email protected]",
    "email_verified": true,
    "picture": "https://lh3.googleusercontent.com/a/photo.jpg"
}

Let's break down each claim:

Claim Required? Purpose
iss Yes Issuer — who issued this token (must match your configured OP)
sub Yes Subject — unique user identifier at the OP (never changes)
aud Yes Audience — your application's client_id (prevents token substitution)
exp Yes Expiration time (Unix timestamp)
iat Yes Issued at time
nonce Conditional Random value you sent in the auth request — prevents replay attacks
auth_time Optional When the user actually authenticated (not when the token was issued)
at_hash Optional Hash of the access token — binds the ID token to a specific access token
acr Optional Authentication Context Reference — how strong the authentication was
amr Optional Authentication Methods References — what methods were used (password, otp, etc.)
name, email, etc. Optional User profile claims — returned based on requested scopes

ID Token vs Access Token — Critical Distinction

This is one of the most common mistakes developers make, so let me be very clear:

ID Token Access Token
Purpose Proves who the user is Authorizes access to APIs
Audience The client application (RP) The resource server (API)
Sent to APIs? NEVER Yes — in the Authorization header
Format Always a JWT JWT or opaque (depends on provider)
Contains User identity claims Scopes and permissions

The ID Token is for the client. The access token is for the API. If you're sending your ID Token to an API as a Bearer token — you're doing it wrong.

OIDC Flows

OIDC inherits its flows from OAuth 2.0 but adds the openid scope and returns an ID Token alongside the access token.

Authorization Code Flow (Recommended for Server-Side Apps)

This is the most secure flow and works for any application with a backend.

    Browser              Your App (RP)           Google (OP)
       |                      |                       |
  1.   | --- Click Login ----> |                       |
       |                      |                       |
  2.   |                      | --- Auth Request ----> |
       |                      |  (with scope=openid)   |
       |                      |                       |
  3.   | <--- Redirect to Google Login Page ---------- |
       |                      |                       |
  4.   | --- User logs in with Google --------------> |
       |                      |                       |
  5.   | <--- Redirect back with auth code ----------- |
       |  (code=xyz123)       |                       |
       |                      |                       |
  6.   | --- Forward code --> |                       |
       |                      | --- Exchange code ---> |
       |                      |  (back-channel POST)   |
       |                      |                       |
  7.   |                      | <--- Tokens ---------- |
       |                      |  (id_token + access_token + refresh_token)
       |                      |                       |
  8.   |                      | Validate ID Token     |
       |                      | Create session         |
       |                      |                       |
  9.   | <--- Logged in! ---- |                       |

Step 2 — The Authorization Request:

GET https://accounts.google.com/o/oauth2/v2/auth?
    response_type=code
    &client_id=my-app-client-id.apps.googleusercontent.com
    &redirect_uri=https://myapp.com/callback
    &scope=openid profile email
    &state=random-csrf-token
    &nonce=random-replay-prevention

The key additions for OIDC:

  • scope=openid — this is what makes it an OIDC request (vs plain OAuth)
  • nonce — a random value that will be embedded in the ID Token to prevent replay attacks
  • state — CSRF protection (also used in plain OAuth)

Step 6-7 — Token Exchange (Back-Channel):

POST https://oauth2.googleapis.com/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=xyz123
&client_id=my-app-client-id.apps.googleusercontent.com
&client_secret=your-client-secret
&redirect_uri=https://myapp.com/callback

Response:

{
    "access_token": "ya29.a0ARrdaM...",
    "token_type": "Bearer",
    "expires_in": 3600,
    "refresh_token": "1//06vXYZ...",
    "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ijg..."
}

There it is — the id_token. That's what OIDC adds to the OAuth flow.

Authorization Code Flow with PKCE (For SPAs and Mobile Apps)

Public clients (SPAs, mobile apps) can't securely store a client_secret. PKCE (Proof Key for Code Exchange, pronounced "pixy") solves this.

The idea: the client generates a random secret for each request and proves it knows it during the code exchange — without ever exposing a long-lived secret.

1. Client generates a random code_verifier (43-128 characters)
   code_verifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

2. Client computes code_challenge = SHA256(code_verifier), base64url-encoded
   code_challenge = "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

3. Auth request includes code_challenge:
   GET /authorize?
       response_type=code
       &client_id=...
       &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
       &code_challenge_method=S256
       &scope=openid profile email
       ...

4. Token exchange includes code_verifier:
   POST /token
       grant_type=authorization_code
       &code=xyz123
       &code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk

5. Server computes SHA256(code_verifier) and compares with stored code_challenge
   If they match → issue tokens

Even if an attacker intercepts the authorization code, they can't exchange it because they don't know the code_verifier. This is now the recommended flow for all public clients.

Implicit Flow (Deprecated)

The Implicit Flow returned tokens directly in the URL fragment (#id_token=...). This was designed for SPAs before PKCE existed.

It's deprecated because:

  • Tokens are exposed in the browser URL and history
  • No refresh tokens (the spec forbids it for implicit)
  • Vulnerable to token leakage through referrer headers

Use Authorization Code + PKCE instead. Always.

Flow Comparison

Flow Client Type Returns Secure? Use Today?
Authorization Code Server-side web apps Code → exchange for tokens Yes Yes
Authorization Code + PKCE SPAs, mobile apps, any public client Code → exchange for tokens (with PKCE proof) Yes Yes
Implicit SPAs (legacy) Tokens directly in URL No Deprecated
Hybrid Advanced use cases ID Token + code from auth endpoint Yes (if needed) Rarely

OIDC Discovery

One of OIDC's most useful features is automatic configuration. Every OIDC provider publishes a discovery document at a well-known URL:

GET https://accounts.google.com/.well-known/openid-configuration

Response (abbreviated):

{
    "issuer": "https://accounts.google.com",
    "authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
    "token_endpoint": "https://oauth2.googleapis.com/token",
    "userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
    "jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
    "scopes_supported": ["openid", "profile", "email", "address", "phone"],
    "response_types_supported": ["code", "token", "id_token", "code token", "code id_token"],
    "id_token_signing_alg_values_supported": ["RS256"],
    "subject_types_supported": ["public"],
    "claims_supported": ["sub", "name", "given_name", "family_name", "picture", "email", "email_verified", "locale"]
}

This document tells your application everything it needs to know:

  • Where to send users for authentication (authorization_endpoint)
  • Where to exchange codes for tokens (token_endpoint)
  • Where to get public keys for signature verification (jwks_uri)
  • What scopes and claims are available

Most OIDC client libraries use this endpoint for automatic configuration — you just provide the issuer URL, and the library discovers everything else.

Scopes and Claims

OIDC defines standard scopes that control what user information is returned:

Scope Claims Returned
openid sub (required — makes it an OIDC request)
profile name, family_name, given_name, middle_name, nickname, picture, updated_at
email email, email_verified
address address (structured object with street, city, etc.)
phone phone_number, phone_number_verified

When your application requests scope=openid profile email, you'll get claims from all three scopes in the ID Token or from the UserInfo endpoint.

GET /authorize?
    scope=openid profile email
    &...

The claims can appear in the ID Token itself, or be available via the UserInfo endpoint. Where they appear depends on the provider and the response type.

UserInfo Endpoint

The UserInfo endpoint provides additional claims about the authenticated user:

GET https://openidconnect.googleapis.com/v1/userinfo
Authorization: Bearer ya29.a0ARrdaM...

Response:

{
    "sub": "110169484474386276334",
    "name": "Thilan Fernando",
    "given_name": "Thilan",
    "family_name": "Fernando",
    "picture": "https://lh3.googleusercontent.com/a/photo.jpg",
    "email": "[email protected]",
    "email_verified": true
}

When to use UserInfo vs the ID Token:

  • If the claims you need are already in the ID Token — just read them from there (no extra network call)
  • If you need claims not included in the ID Token — call the UserInfo endpoint
  • If you need the most up-to-date user information — call UserInfo (the ID Token is a snapshot from authentication time)

Implementing "Sign in with Google"

Let's put it all together with a concrete example.

1. Register Your App

Go to the Google Cloud Console, create an OAuth 2.0 client, and configure:

  • Authorized redirect URI: https://myapp.com/callback

You'll get a client_id and client_secret.

2. Redirect the User

<a href="https://accounts.google.com/o/oauth2/v2/auth?
    response_type=code
    &client_id=YOUR_CLIENT_ID
    &redirect_uri=https://myapp.com/callback
    &scope=openid profile email
    &state=random-csrf-token
    &nonce=random-nonce-value">
    Sign in with Google
</a>

3. Handle the Callback

Google redirects back to your callback with an authorization code:

GET https://myapp.com/callback?code=4/0AY0e-g...&state=random-csrf-token

Verify that state matches what you sent (CSRF protection).

4. Exchange the Code

import requests

response = requests.post('https://oauth2.googleapis.com/token', data={
    'grant_type': 'authorization_code',
    'code': '4/0AY0e-g...',
    'client_id': 'YOUR_CLIENT_ID',
    'client_secret': 'YOUR_CLIENT_SECRET',
    'redirect_uri': 'https://myapp.com/callback'
})

tokens = response.json()
id_token = tokens['id_token']
access_token = tokens['access_token']

5. Validate the ID Token

This is the critical step. You must validate:

import jwt
import requests

# Fetch Google's public keys
jwks = requests.get('https://www.googleapis.com/oauth2/v3/certs').json()
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(jwks['keys'][0])

# Verify the ID Token
claims = jwt.decode(
    id_token,
    public_key,
    algorithms=['RS256'],
    audience='YOUR_CLIENT_ID',
    issuer='https://accounts.google.com'
)

# Verify nonce matches
assert claims['nonce'] == 'random-nonce-value'

# User is authenticated
print(f"Welcome, {claims['name']}!")
print(f"Email: {claims['email']}")
print(f"User ID: {claims['sub']}")

Validation checklist:

  1. Verify the signature using the OP's public keys (from JWKS)
  2. Check iss matches the expected issuer
  3. Check aud matches your client_id
  4. Check exp hasn't passed
  5. Check nonce matches what you sent in the auth request
  6. Check iat isn't too far in the past

OIDC Security Considerations

Nonce — Preventing Replay Attacks

The nonce is a random value generated by the client for each authentication request. It's included in the auth request and embedded in the resulting ID Token. When the client receives the ID Token, it checks that the nonce matches. If an attacker replays an old ID Token, the nonce won't match, and the token is rejected.

Audience Validation — Preventing Token Substitution

If you don't check aud, an attacker could take an ID Token issued for a different application and use it with yours. Always verify that aud matches your client_id.

Don't Use Access Tokens for Identity

Access tokens are for APIs. They may not contain user identity claims, they may be opaque, and they're not bound to your application. If you need to know who the user is — use the ID Token.

Open Redirect via redirect_uri

If the OP doesn't strictly validate the redirect_uri, an attacker could manipulate it to steal the authorization code:

GET /authorize?
    redirect_uri=https://attacker.com/steal
    &...

Defense: The OP must enforce exact match on redirect URIs. No wildcards, no open patterns. Register every valid redirect URI explicitly.

PKCE is Mandatory for Public Clients

If you're building a SPA or mobile app and not using PKCE — stop and add it. Without PKCE, the authorization code can be intercepted and exchanged by an attacker.

OIDC vs SAML — When to Use What

Aspect OIDC SAML 2.0
Token format JSON (JWT) XML
Transport HTTP redirects + back-channel HTTP redirects + POST
Complexity Simpler More complex (XML signatures, schemas)
Mobile support Excellent Poor (not designed for mobile)
Token size Small (~1KB) Large (~5-20KB)
Adoption Modern apps, APIs, SPAs Enterprise legacy, corporate SSO
Best for New applications, consumer-facing Existing enterprise integrations

If you're building something new — use OIDC. If you're integrating with an enterprise that requires SAML — you'll need SAML. Many organizations use both — OIDC for modern apps and SAML for legacy systems.

We'll cover SAML in depth in the next article.

Final Thoughts

OIDC is elegant in its simplicity — it takes OAuth 2.0, adds the openid scope, the ID Token, the UserInfo endpoint, and the discovery document, and suddenly you have a complete identity protocol. No more hacking OAuth for login. No more proprietary user profile APIs. Just a standardized way to answer the question: who is this user?

The key takeaways:

  • OIDC is OAuth 2.0 + identity. It's not a separate protocol — it's a layer on top.
  • The ID Token is for the client application. The access token is for APIs. Don't mix them up.
  • Always validate ID Tokens: signature, issuer, audience, expiration, nonce.
  • Use Authorization Code + PKCE for all new applications. Implicit flow is dead.
  • The discovery document (.well-known/openid-configuration) makes integration painless.

Next up — SAML 2.0, the enterprise SSO standard that OIDC is gradually replacing but hasn't killed yet.

Stay secure!

ALSO READ
 OWASP Top 10 explained - 2021
Feb 11 Application Security

The Open Worldwide Application Security Project (OWASP) is a nonprofit foundation focused on improving the security of software. It provides free, vendor neutral tools, resources, and standards that...

Bypassing DEP with Return-to-libc
Apr 05 Exploit development

DEP makes the stack non-executable — our shellcode can't run. The simplest bypass? Don't inject code at all. Instead, call functions that already exist in libc. In this post, we exploit a stack overflow to call system('/bin/sh') without writing a single byte of shellcode.

How I built a web based CPU Simulator
May 07 Pet Projects

As someone passionate about computer engineering, reverse engineering, and system internals, I've always been fascinated by what happens "under the hood" of a computer. This curiosity led me to...

SQL Injection Login Bypass
Feb 10 Application Security

SQL Injection (SQLi) is one of the oldest and most fundamental web application vulnerabilities. While modern frameworks have made it harder to introduce, understanding SQL injection is essential for anyone learning web security. In this post, we'll break it down from the ground up using a classic login bypass.

Exploiting a format string vulnerebility on Linux
Apr 12 Exploit development

A misused printf can leak stack contents, read arbitrary memory, and write to arbitrary addresses. Format string vulnerabilities are one of the most powerful bug classes in C and they're the key to defeating ASLR. In this post, we exploit printf from leak to shell.

Access Control Models
Apr 08 Identity & Access Management

Access control is one of the most fundamental concepts in security. Every time you set file permissions, assign user roles, or restrict access to a resource, you're implementing some form of access control. But not all access control is created equal...