OpenID Connect (OIDC) adds an authentication (who you are) layer on top of OAuth 2.0. It fills the gap OAuth left, delivering the user's identity in a verifiable form via a signed JWT called the ID Token. It is the protocol behind 'Sign in with Google/Apple/Kakao' buttons.
Authorization: Bearer <access_token> + ID Token (JWT) from the token endpointThe flow mirrors OAuth, but when you include `openid` in the requested scope, the token endpoint returns an ID Token (JWT) alongside the access token. Its claims include the issuer (iss), subject/user id (sub), intended client (aud), expiry (exp), authentication time (auth_time), and the nonce you sent, so the client can confirm 'this token was just issued by a trusted IdP for our app.'
The role split matters: the access token is for the resource server (API) while the ID Token is for the client (app). The app must not send the ID Token to the API; it sends the access token there. For more profile data, it calls the standard `/userinfo` endpoint with the access token. IdP metadata is auto-discovered at `/.well-known/openid-configuration`, and signature-verification keys at the JWKS endpoint.
Core principle: when you need login (authentication), verify an OIDC ID Token rather than inferring identity from the presence of an OAuth access token. Logging a user in based on an access token alone exposes you to confused-deputy vulnerabilities, such as a token issued for a different service being reused.
id_token=eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkayJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiIyNDgyODkiLCJhdWQiOiJhYmMxMjMiLCJleHAiOjE2MDk0NTkyMDB9.<sig>GET /authorize?response_type=code&client_id=abc123
&redirect_uri=https://app.example.com/cb
&scope=openid%20profile%20email&state=xyz&nonce=n-0S6_WzA2Mj HTTP/1.1
Host: auth.example.comimport { jwtVerify, createRemoteJWKSet } from 'jose';
const JWKS = createRemoteJWKSet(new URL('https://auth.example.com/.well-known/jwks.json'));
const { payload } = await jwtVerify(idToken, JWKS, {
issuer: 'https://auth.example.com',
audience: 'abc123'
});
if (payload.nonce !== savedNonce) throw new Error('nonce mismatch');