OAuth 2.0 / OpenID Connect — a from-scratch primer
To someone new to authentication and authorization, the standards look like alphabet soup: OAuth, OIDC, JWT, OP, RP, RS, PAR, JAR, JARM, DPoP, mTLS, PKCE, FAPI. There are only three roles, though, and almost every standard refines the same flow between them.
Acronym cheat sheet (open me first)
The three roles
- OP (OpenID Provider) — the server that authenticates users and issues tokens.
go-oidc-provideris this. Also called AS (Authorization Server) in pure-OAuth contexts. - RP (Relying Party) — the client app that uses the OP to log users in. Also called Client.
- RS (Resource Server) — the API that accepts the access token and returns data.
Tokens and crypto
- JWT (JSON Web Token, RFC 7519) —
header.payload.signatureJSON, base64url-encoded. Self-describing, signature-verifiable. - JWS (JSON Web Signature, RFC 7515) — the signing scheme JWTs use.
- JWE (JSON Web Encryption, RFC 7516) — encrypted variant; outer envelope wraps an inner JWS.
- JWK / JWKS (RFC 7517) — JSON Web Key / Key Set. The OP's public keys, fetched from
/jwks. See JOSE basics. - PKCE (RFC 7636) — proof of possession on the authorization code; stops code-interception attacks. Pronounced "pixie."
Profile / hardening acronyms
- PAR (RFC 9126) — Pushed Authorization Request. The RP POSTs the authorize request to the OP first; the browser only carries a
request_urireference. - JAR (RFC 9101) — JWT-Secured Authorization Request. The authorize request is itself a signed JWT.
- JARM (OpenID FAPI) — JWT-Secured Authorization Response Mode. The authorize response is a signed JWT.
- DPoP (RFC 9449) — Demonstrating Proof of Possession. Binds a token to a key the client holds, on every request.
- mTLS (RFC 8705) — mutual TLS. Same idea as DPoP, but the binding is the client's TLS certificate.
- FAPI (Financial-grade API) — the OpenID profile that pins all of the above into one set.
- CIBA — Client-Initiated Backchannel Authentication. Push-to-phone flow, no browser on the device.
Identity claims you'll see early
sub— Subject. The user's opaque identifier on this OP.aud— Audience. Who the token is for.iss— Issuer. The OP that signed the token.scope— space-separated permission list (openid profile email).acr(Authentication Context Class Reference) — assurance level the auth method provided. Used by step-up.amr(Authentication Methods References) — RFC 8176 codes for the factors actually used (pwd,otp,mfa,hwk,face,fpt).cnf— confirmation. The key the token is bound to (DPoPjktthumbprint or mTLSx5t#S256).
Specs referenced on this page
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 6750 — Bearer Token Usage
- RFC 7519 — JSON Web Token (JWT)
- RFC 7636 — PKCE
- RFC 7662 — Token Introspection
- RFC 9068 — JWT Profile for OAuth 2.0 Access Tokens
- OpenID Connect Core 1.0
- OpenID Connect RP-Initiated Logout 1.0
- FAPI 2.0 Security Profile
The three roles
The diagram above shows who is responsible for what. The login steps themselves (redirect to /auth, code exchange, token retrieval) are in the authorization code + PKCE flow below.
One program, two roles
A backend-for-frontend can be both the RP (it logs users in) and the RS (it serves APIs that the SPA calls with the access token).
OAuth 2.0 vs OpenID Connect
OAuth 2.0 is delegated authorization: "Alice's app gets permission to read Alice's data on Service X." It does not tell the app who Alice is. An access token only represents the granted authorization, and the authorization server decides whether it is opaque or self-contained.
OpenID Connect (OIDC) is OAuth 2.0 plus identity. Alongside the access token, the OP issues an ID Token, a signed JWT saying "this token was issued for user sub=alice123, audience client_id=myapp, at this time, and these claims about her are true." OIDC also adds the /userinfo endpoint, a discovery document, RP-Initiated Logout, and Back-Channel Logout notifications.
JWT — what's that?
A signed compact JWT (JSON Web Token, RFC 7519) is a string of three base64url-encoded parts joined by dots: header.payload.signature. The header and payload are JSON; the signature is what lets a receiver verify the issuer cryptographically using a public key. An encrypted JWT uses compact JWE serialization with five parts and can carry a signed JWT inside it.
In OIDC, ID Tokens are always JWTs. Access tokens issued by go-oidc-provider are JWTs by default and become opaque tokens if you opt in. Anyone who can read JSON and check a signature can read the default format.
Opaque vs JWT — quick refresher
- Opaque token: a random string that means nothing to whoever holds it. To learn what it grants, the receiver calls the issuer's introspection endpoint (RFC 7662), which looks the token up.
- JWT: the contents are encoded in the token and a signature lets the receiver verify it offline.
The trade-off: an opaque token costs a round trip to the OP on every check but can be revoked at once. A JWT is checked locally but stays valid until it expires, unless the receiver also checks for revocation. See tokens for how this library splits the difference.
So when do I use which?
- Pure OAuth 2.0: an API that just needs to say "this token is allowed to call
POST /things." Common for service-to-service. - OIDC: anything where a human logs in and the app needs to say "hello, Alice." Almost every web/mobile app login is OIDC.
go-oidc-provider defaults to OIDC (the openid scope is required) but flips to pure OAuth 2.0 with op.WithOpenIDScopeOptional().
The four token types you'll meet
| Token | Lifetime | What it is | Where it goes |
|---|---|---|---|
| Authorization code | Seconds (default 60s) | Single-use opaque string. | Server-to-server: RP → OP /token. |
| Access token | Minutes (default 5 min) | The thing you put on Authorization: Bearer … to call APIs. JWT or opaque. | RP → RS. |
| Refresh token | Days–weeks (30d default) | Long-lived; lets the RP get a new access token without re-authenticating. | RP → OP /token. |
| ID Token | Minutes (default 10 min) | Signed JWT proving who the user is. Never sent to APIs. | OP → RP, consumed inside the RP. |
Don't put ID Tokens on Bearer
A common beginner mistake is sending the ID Token to your API. An ID Token's audience is the RP, not the RS, and passing it as a Bearer token exposes RP-oriented claims (such as the email address) to every API the user touches. The API should validate the access token, either through introspection (RFC 7662) or as a self-contained JWT (RFC 9068).
# Wrong: the ID Token the OP issued to the RP is sent to the API
GET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ...id_token...
# Right: the access token whose audience is the API
GET /orders HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJ...access_token...The flow you'll see most often: Authorization Code + PKCE
/token travels through the browser, which is why state and code_challenge are sent that early: they are the only things tying the redirect that comes back to the request that went out. The token call skips the browser lane entirely — a direct back-channel request the browser never sees.The PKCE part (code_challenge on the redirect to /auth, code_verifier on POST /token) stops a malicious app from using an authorization code it intercepted. An OP built without op.WithProfile requires it on every authorization request (profile.OAuth21); profile.OIDCCore makes it optional for confidential clients. See the detailed walk-through.
Concepts you'll see in this site's docs
| Term | Meaning |
|---|---|
| Scope | Space-separated list of permissions, e.g. openid profile email. The user consents to these. |
| Claim | A field inside a token, e.g. sub, email, email_verified. |
| Consent | The "this app wants to read your email" screen. The OP records the answer and skips the screen on later logins for the same scopes. |
Audience (aud) | Who the token is for. ID Tokens have aud = client_id; access tokens have aud = resource server. |
Issuer (iss) | The OP that signed the token. RP and RS both check it against the value they expect. |
| JWKS | JSON Web Key Set, the OP's public keys, fetched from /jwks. RPs use it to verify ID Tokens. |
| Discovery document | /.well-known/openid-configuration, a JSON catalog of the OP's endpoints, scopes, algorithms and features. |
acr and amr in one paragraph
acr says how strong the authentication was (an assurance-level label like aal2); amr says which factors were used (["pwd","otp"]). An RP that needs stronger assurance for a sensitive operation asks for a higher acr through acr_values, and the OP runs step-up authentication and issues a new ID Token. RFC 8176 lists the standard amr codes; RFC 9470 standardises step-up through WWW-Authenticate: error="insufficient_user_authentication". See MFA / step-up for the setup.
What FAPI 2.0 adds
A banking-grade or healthcare-grade OP needs the FAPI 2.0 Security Profile on top of OIDC: sender-constrained tokens (DPoP / mTLS), PAR (the authorization request is pushed server-to-server first), and a tighter algorithm allow-list. The profile does not mandate JAR (a signed authorization request), although this library's FAPI2Baseline configuration turns it on. One option enables the configuration:
op.WithProfile(profile.FAPI2Baseline)A primer with all the acronyms expanded lives at FAPI 2.0 primer. For mechanics, see sender constraint; for the full wiring, see Use case: FAPI 2.0 Baseline.
Read these next
- Authorization Code + PKCE flow — the flow above as a full sequence diagram, with a parameter glossary.
- Client Credentials — service-to-service, no end user.
- Refresh tokens — rotation, reuse detection, grace period.
- ID Token vs access token vs userinfo — they look the same, they're not.
- Sender constraint (DPoP / mTLS) — what FAPI 2.0 actually adds.
- FAPI 2.0 primer — what the FAPI profile is, what each acronym (PAR, JAR, JARM, …) does, and why a profile beats "OIDC + best practices."