So far the app authenticates users itself. Delegated access is the opposite problem: letting one service act on a user's behalf at another service ("Sign in with Google," or "let this app read your GitHub repos") without ever handing over the user's password to that other service.
Before OAuth, apps literally asked for your Gmail password so they could log in as you and scrape what they needed: total access, no revocation short of changing your password, and a third party storing your credentials. OAuth's design is a reaction to that.
OAuth 2.0
OAuth 2.0 is the framework for this. Its whole point is delegated authorization: granting an app limited access to your data somewhere else. Four roles:
- Resource owner: you, the user. The data is yours; only you can grant access to it.
- Client: the app that wants access. It never sees your password.
- Authorization server: where you log in and consent (Google's, GitHub's). The only party that ever handles your credentials.
- Resource server: the API holding your data. It accepts access tokens, not logins.
Concretely: a scheduling app (client) wants to read your (resource owner) calendar from
the Google Calendar API (resource server), so it sends you to
accounts.google.com (authorization server) to approve it.
What you approve there are scopes : named slices
of access like calendar.readonly or repo. Scopes are least privilege
made visible: the consent screen lists them, the access token is limited to them, and the
resource server rejects anything outside them.
The main flow is the Authorization Code flow:
- Client redirects you to the authorization server (login + consent screen)
- You approve the requested scopes (e.g. "read your repos")
- Authorization server redirects back with a short-lived authorization CODE
- Client exchanges the code (+ its secret, or a PKCE verifier for public clients) for an ACCESS TOKEN , server-to-server
- Client calls the resource server with the access token
The two-step dance (code first, then exchange it for a token on the back channel) exists so the access token never travels through the browser/URL, where it could be logged or leaked. The code is useless on its own; redeeming it requires the client's secret.
The access token is usually short-lived and paired with a refresh token the client redeems on the back channel when it expires. Revocation works because the authorization server keeps state: when you remove an app from Google's "third-party access" page, its refresh token dies there.
OIDC
Here's the catch people trip on: OAuth is authorization, not authentication. An access token proves an app was granted access; it doesn't reliably tell you who the user is. Apps that used raw OAuth as a login mechanism opened subtle security holes by assuming "has a token" meant "is this user."
OpenID Connect (OIDC) is a thin identity layer on top of OAuth 2.0 that adds real authentication. It introduces:
-
An ID token : a JWT containing identity claims (
sub,email,name), signed by the provider, meant specifically to tell the client who logged in. -
A standard
/userinfoendpoint and standard scopes (openid,profile,email).
"Sign in with Google" is OIDC, not bare OAuth, and it's your first taste of
single sign-on (SSO) : authenticate once at a provider
you already trust, and carry that proven identity into any app that trusts the provider. One critical point: the ID token must be
verified, not just decoded: check the signature against the provider's public
keys, and validate the iss, aud, and exp claims.
Accepting an unverified or mis-targeted ID token re-opens the same identity confusion
vulnerabilities that raw OAuth had. The mental split worth keeping:
| OAuth 2.0 | OIDC | |
|---|---|---|
| Answers | What may this app access? | Who is this user? |
| Concern | Authorization (delegated access) | Authentication (identity) |
| Gives you | Access token (for calling APIs) | ID token (a JWT identifying the user) |
| Relationship | The framework | A layer built on top of OAuth |
The tokens split the same way: the access token goes to APIs, the ID token stays with the client as proof of who logged in. Never accept an ID token as an API credential, and never treat possession of an access token as a login.
OIDC covers the consumer and modern-app use case. In enterprise environments, two older standards play the same SSO role. SAML (Security Assertion Markup Language) is the XML-based protocol used by corporate Identity Providers (Okta, AD FS, Azure AD): the user authenticates once at the IdP, which issues a signed XML assertion to the app — no password ever reaches the app. LDAP (Lightweight Directory Access Protocol) is the underlying directory protocol — most commonly Microsoft Active Directory — where user accounts, groups, and attributes live; the IdP typically reads from it to power SSO. The two complement each other: LDAP stores the identities, SAML carries them to web applications.
PKCE
The Authorization Code flow assumes the client can keep a secret. OAuth calls these confidential clients: server-side apps whose code and config live where users can't see them. A public client (a single-page app or a mobile app) can't keep anything: its code ships to the user's device, so any secret baked into it is visible. That breaks step 4's security, because an attacker who intercepts the authorization code could redeem it themselves.
PKCE (Proof Key for Code Exchange, "pixy") closes that gap without a stored secret:
import crypto from "node:crypto";
// Before redirecting, the client makes a one-time secret and sends only its hash
const verifier = crypto.randomBytes(32).toString("base64url");
const challenge = crypto.createHash("sha256").update(verifier).digest("base64url");
// 1. Authorize request includes: code_challenge=<challenge>&code_challenge_method=S256
// 2. Token exchange includes: code_verifier=<verifier>
// The server hashes the verifier and checks it matches the challenge it saw earlier.
An intercepted code is now worthless: redeeming it requires the original verifier,
which never left the legitimate client. Generated fresh for each flow, held in memory, gone
when the exchange completes. PKCE started as a mobile/SPA fix and is now
recommended for all OAuth clients.
Check your understanding
Why use OIDC instead of plain OAuth for login?
A team builds "Log in with Acme" using plain OAuth: if the app receives a valid access token, it treats the session as that user. Why is OIDC the right tool instead?