Back to OAuth2 & OpenID Connect
Q2Question 2 of 5

OpenID Connect (OIDC): Authentication Layer on Top of OAuth2

Key Concepts
  • OIDC is an identity layer built on top of OAuth2 that adds standardized authentication: who is the user.
  • It introduces the ID token, a signed JWT containing identity claims like sub, name, email, and picture.
  • The flow is the standard OAuth2 Authorization Code flow with the openid scope added.
  • The access token is for calling resource APIs; the ID token is for the client to learn the user's identity.
  • The ID token must never be sent to resource servers as an API credential.
  • Login with Google, Microsoft, and Okta are all OIDC; the payoff is single sign-on with delegated identity.

Definition

OAuth2 answers what the app may do; OIDC adds a signed ID token that answers who the user is.
  • OpenID Connect (OIDC) – an identity layer built on top of OAuth2 that adds standardized authentication, telling the client who the user is.
  • ID token – a signed JWT containing identity claims such as sub, name, email, and picture, issued by the OpenID Provider.
  • OpenID Provider – the Authorization Server acting as the identity provider, issuing both access tokens and ID tokens.
  • Relying Party – the client application that trusts the ID token to establish the user's identity.
  • UserInfo endpoint – a standardized endpoint that returns additional user claims when called with the access token.

Plain OAuth2 gives a partner app a pass that says what it may do with Rajesh's data, but it never officially says who Rajesh is. OIDC adds an official identity certificate: the ID token, stamped by National Bank of India, that states 'this is Rajesh Kumar, email rajesh@example.com, verified by us.' Now the partner site not only has permission to read data, it also knows, with the bank's signature, exactly which customer just logged in. OIDC is OAuth2 plus a trustworthy name tag.

Understanding the Concepts

What is OpenID Connect?

OpenID Connect (OIDC) is an identity layer built on top of OAuth2. While OAuth2 handles authorization (what the app may do), OIDC adds authentication (who the user is). It does this by introducing the ID token: a signed JWT that the Authorization Server (acting as an OpenID Provider) issues alongside the access token.

The ID token is for the client to establish the user's identity. The access token is still for calling APIs.

TEXT
OAuth2 alone:
  Client receives: access_token (what the app may do)
  Client does NOT know: who the user is

OAuth2 + OIDC:
  Client receives: access_token + id_token
  Client knows: what the app may do AND who the user is

How does OIDC work with the Authorization Code flow?

OIDC reuses the OAuth2 Authorization Code flow but adds the openid scope. The Authorization Server then returns not only an access token but also an id_token in the token response.

TEXT
Step 1: Authorization request with openid scope
  GET /authorize?response_type=code
      &client_id=partner-app
      &redirect_uri=https://partner.com/callback
      &scope=openid profile email
      &state=xyz&nonce=abc

Step 2: User logs in and consents

Step 3: Code returned in redirect
  ?code=AUTH_CODE_456&state=xyz

Step 4: Backend exchanges code for tokens
  POST /token ...

Step 5: Token response includes id_token
  {
    "access_token": "eyJhbGci...",
    "id_token": "eyJhbGci...idtoken...",
    "token_type": "bearer",
    "expires_in": 900
  }

What is the ID token and what claims does it contain?

The ID token is a signed JWT with identity claims. Standard claims include:

  • sub (subject): a stable, unique user identifier
  • iss (issuer): the OpenID Provider URL
  • aud (audience): the client_id of the relying party
  • exp, iat: timing claims
  • nonce: echoes the nonce from the authorization request (prevents replay)
  • Profile claims: name, email, picture, email_verified
JAVA
package com.bank.oidc;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import java.security.PublicKey;

public class IdTokenParser {

    public void parseIdToken(String idToken, PublicKey providerPublicKey,
                             String expectedClientId, String expectedNonce) {
        Claims claims = Jwts.parser()
            .verifyWith(providerPublicKey)
            .requireIssuer("https://auth.nationalbank.in")
            .requireAudience(expectedClientId)
            .build()
            .parseSignedClaims(idToken)
            .getPayload();

        // Verify nonce to prevent replay
        if (!expectedNonce.equals(claims.get("nonce"))) {
            throw new SecurityException("Nonce mismatch. Possible replay.");
        }

        System.out.println("User: " + claims.getSubject());
        System.out.println("Name: " + claims.get("name"));
        System.out.println("Email: " + claims.get("email"));
        System.out.println("Issuer: " + claims.getIssuer());
    }
}
TEXT
User: rajesh-1234-uuid
Name: Rajesh Kumar
Email: rajesh@nationalbank.in
Issuer: https://auth.nationalbank.in

How do you validate the ID token?

Before trusting the ID token, the relying party must:

  1. Verify the signature with the provider's public key (fetched from the JWKS endpoint).
  2. Check iss equals the expected OpenID Provider.
  3. Check aud equals the client's own client_id.
  4. Check exp is not past.
  5. Check nonce matches the one sent in the authorization request.
JAVA
package com.bank.oidc;

public class IdTokenValidation {

    public boolean validate(String idToken, String expectedAud, String expectedNonce) {
        try {
            Claims claims = Jwts.parser()
                .verifyWith(providerPublicKey)              // 1. signature
                .requireIssuer("https://auth.nationalbank.in") // 2. issuer
                .requireAudience(expectedAud)               // 3. audience
                .build()
                .parseSignedClaims(idToken)
                .getPayload();

            // 4. exp checked automatically by library
            // 5. nonce
            if (!expectedNonce.equals(claims.get("nonce"))) {
                System.out.println("REJECTED: nonce mismatch (possible replay)");
                return false;
            }

            System.out.println("ID token valid for user: " + claims.getSubject());
            return true;
        } catch (Exception e) {
            System.out.println("REJECTED: " + e.getMessage());
            return false;
        }
    }
}
TEXT
Good token: ID token valid for user: rajesh-1234-uuid
Wrong audience: REJECTED: audience does not match
Replay (wrong nonce): REJECTED: nonce mismatch (possible replay)

How is OIDC different from OAuth2?

| Aspect | OAuth2 | OIDC |
|---|---|---|
| Purpose | Authorization (what app may do) | Authentication (who user is) + authorization |
| Tokens issued | Access token (+ refresh token) | Access token + ID token |
| User identity | Not provided | ID token with sub, name, email |
| Scope | Custom scopes (transactions:read) | Must include openid; can add profile, email |
| Standard endpoints | /authorize, /token | + /userinfo, /.well-known/openid-configuration |
| Use case | API access delegation | Single sign-on, identity federation |

JAVA
// OAuth2 alone: client gets access token, no identity
String accessToken = tokenResponse.get("access_token");
// Who is the user? Unknown!

// OIDC: client also gets id_token with identity
String idToken = tokenResponse.get("id_token");
Claims identity = Jwts.parser().verifyWith(key).build()
    .parseSignedClaims(idToken).getPayload();
String userName = identity.get("name", String.class); // "Rajesh Kumar"
String email = identity.get("email", String.class);   // "rajesh@nationalbank.in"

What is the discovery document?

The OpenID Provider publishes a discovery document at /.well-known/openid-configuration. This JSON document lists all endpoints, supported scopes, signing algorithms, and the JWKS URI for public keys.

TEXT
GET https://auth.nationalbank.in/.well-known/openid-configuration

{
  "issuer": "https://auth.nationalbank.in",
  "authorization_endpoint": "https://auth.nationalbank.in/authorize",
  "token_endpoint": "https://auth.nationalbank.in/token",
  "userinfo_endpoint": "https://auth.nationalbank.in/userinfo",
  "jwks_uri": "https://auth.nationalbank.in/.well-known/jwks.json",
  "scopes_supported": ["openid", "profile", "email"],
  "response_types_supported": ["code"],
  "id_token_signing_alg_values_supported": ["RS256"]
}

What is the UserInfo endpoint?

The UserInfo endpoint returns additional user claims when called with the access token. It provides claims that may not be in the ID token, such as address or phone_number.

JAVA
package com.bank.oidc;

import org.springframework.web.client.RestTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpEntity;
import java.util.Map;

public class UserInfoClient {

    public Map<String, Object> getUserInfo(String accessToken) {
        RestTemplate rest = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setBearerAuth(accessToken);
        HttpEntity<?> request = new HttpEntity<>(headers);

        return rest.postForObject(
            "https://auth.nationalbank.in/userinfo",
            request, Map.class);
    }
}

What are common mistakes?

The most critical mistake is using the ID token as an API credential. The ID token is for the client to learn the user's identity. It must never be sent to resource servers as an Authorization header. The access token is for API calls.

JAVA
// WRONG: sending ID token to resource server
webClient.get()
    .uri("/api/v1/accounts")
    .headers(h -> h.setBearerAuth(idToken)) // ID token is NOT for APIs!
    .retrieve();

// CORRECT: use access token for APIs
webClient.get()
    .uri("/api/v1/accounts")
    .headers(h -> h.setBearerAuth(accessToken)) // access token for APIs
    .retrieve();

// ID token: client reads claims locally to know who logged in

What should a senior engineer know?

Senior engineers understand that the ID token is consumed only by the client (relying party) and must never leave the client. The access token is consumed by resource servers. This distinction prevents confused-deputy attacks where a resource server trusts an ID token that was meant for a different audience.

Senior engineers also know that Login with Google, Microsoft, and Okta are all OIDC, and the big payoff for a bank is single sign-on with delegated identity: the bank authenticates the user once and many relying applications trust the signed ID token.

Visual Overview

DIAGRAM
  OIDC = OAuth2 + Identity Layer:
  ┌────────────┐                       ┌──────────────────┐
  │  Partner   │  scope=openid         │  OpenID Provider │
  │  App       │  profile email        │  (Bank Auth)     │
  │            ├──────────────────────►│                  │
  │            │                       │  User logs in    │
  │            │◄──────────────────────┤  and consents    │
  │            │  code=ABC             │                  │
  │            │                       │                  │
  │  Backend   │  POST /token          │                  │
  │            ├──────────────────────►│                  │
  │            │                       │  Returns:        │
  │            │◄──────────────────────┤  access_token    │
  │            │  access_token         │  + id_token      │
  │            │  + id_token           │                  │
  └────────────┘                       └──────────────────┘

  Token Usage:
  ┌────────────┐
  │  Partner   │
  │  App       │
  │            │── id_token ──► Read claims locally:
  │            │                name=Rajesh, email=r@bank.in
  │            │                (NEVER send to Resource Server)
  │            │
  │            │── access_token ──► Call Resource Server APIs
  └────────────┘

Key Takeaway

OpenID Connect is an identity layer built on top of OAuth2 that adds standardized authentication, telling the client who the user is, which plain OAuth2 does not do. OIDC introduces the ID token, a signed JWT containing identity claims such as sub, name, email, and picture, issued by the Authorization Server acting as an OpenID Provider. The client still receives an OAuth2 access token for calling APIs, but now it also gets a trustworthy, signed statement of the user's identity.

In the National Bank of India project, a partner wealth-management site lets customers sign in with their bank identity and shows their name and email on the dashboard. The openid scope is added to the Authorization Code flow, and the provider returns an id_token alongside the access token. The site decodes the id_token to learn who logged in, so it never has to build or store passwords itself.

The validation is where rigor shows. Before trusting the ID token, the relying party verifies the signature with the provider's public key from the JWKS endpoint, checks the issuer, checks that the audience equals its own client id, checks expiry, and checks that the nonce matches the one it sent to prevent replay. A good token logs the user in, a token addressed to another audience is rejected, and a replay with a mismatched nonce is rejected.

The distinction to stress is that the access token is for calling resource APIs while the ID token is only for the client to learn the user's identity. The ID token must never be sent to resource servers as an API credential. Login with Google, Microsoft, and Okta are all OIDC, and the big payoff for a bank is single sign-on with delegated identity.

Remember one sentence: OIDC adds a signed ID token to OAuth2 so the client knows who the user is, not just what the app may do.

---

Share
Use ← → arrow keys to navigate