Definition
- 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, andpicture, 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.
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.
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 identifieriss(issuer): the OpenID Provider URLaud(audience): the client_id of the relying partyexp,iat: timing claimsnonce: echoes the nonce from the authorization request (prevents replay)- Profile claims:
name,email,picture,email_verified
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());
}
}
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:
- Verify the signature with the provider's public key (fetched from the JWKS endpoint).
- Check
issequals the expected OpenID Provider. - Check
audequals the client's ownclient_id. - Check
expis not past. - Check
noncematches the one sent in the authorization request.
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;
}
}
}
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 |
// 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.
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.
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.
// 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
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.
---