Skip to content

OAuth 2.0 / OpenID Connect — a from-scratch primer

If you're new to authentication and authorization, the standards stack looks like an alphabet soup: OAuth, OIDC, JWT, OP, RP, RS, PAR, JAR, JARM, DPoP, mTLS, PKCE, FAPI. The good news: there are only three roles, and almost every standard is a refinement of the same flow between them.

Specs referenced on this page

The three roles

The actual login steps (redirect to /authorize, code exchange, token retrieval) are spelled out in the authorization code + PKCE flow below. The diagram above is the static "who's responsible for what" view.

Same actor, different hat

A single piece of software can wear two hats. Your "backend for frontend" might be both the RP (it logs users in) and the RS (it has APIs 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." OAuth 2.0 by itself does not tell the app who Alice is; it only hands out an opaque access token.

OpenID Connect (OIDC) is OAuth 2.0 plus identity — the OP additionally issues an ID Token (a signed JWT) that says "this token was issued for user sub=alice123, audience client_id=myapp, at this time, and the following claims about her are true." OIDC adds a userinfo endpoint, a discovery document, RP-Initiated Logout, and a back-channel logout notification.

JWT — what's that?

A JWT (JSON Web Token, RFC 7519) is a string of three base64url chunks 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.

In OIDC, ID Tokens are always JWTs, and access tokens issued by go-oidc-provider are JWTs too. If you can read JSON and check a signature, you can read a JWT — no proprietary binary format involved.

Opaque vs JWT — quick refresher
  • Opaque token — a random string that means nothing to whoever holds it. To know what it grants, the receiver calls the issuer's introspection endpoint (RFC 7662), which looks up a row.
  • JWT — self-describing: the contents are encoded inside the token, and a signature lets the receiver verify it offline.

The trade-off is "every request hits the OP" vs "OP loses fine-grained revocation visibility." 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

TokenLifetimeWhat it isWhere it goes
Authorization codeSecondsSingle-use opaque string.Server-to-server: RP → OP /token.
Access tokenMinutes (default 5 min)The thing you put on Authorization: Bearer … to call APIs. JWT or opaque.RP → RS.
Refresh tokenDays–weeks (30d default)Long-lived; lets the RP get a new access token without re-authenticating.RP → OP /token.
ID TokenMinutes (matches access)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: sending the ID Token to your API. Don't — ID Tokens are for the RP to read; access tokens are for the RS. Your API should validate the access token, optionally with Authorization Server-side introspection (RFC 7662) or as a self-contained JWT (RFC 9068).

The flow you'll see most often: Authorization Code + PKCE

The "+ PKCE" piece (steps highlighted via code_challenge/code_verifier) is what stops a malicious app from intercepting the authorization code. Detailed walk-through.

Concepts you'll see in this site's docs

TermMeaning
ScopeSpace-separated list of permissions, e.g. openid profile email. The user consents to these.
ClaimA field inside a token, e.g. sub, email, email_verified.
ConsentThe "this app wants to read your email" screen. The OP records it; subsequent logins skip it 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 matches their expectation.
JWKSJSON Web Key Set — the OP's public keys, fetched from /jwks. RPs use this to verify ID Tokens.
Discovery document/.well-known/openid-configuration — a JSON catalog of every endpoint, supported scope, supported algorithm, etc.

What FAPI 2.0 adds

If you're building a banking-grade or healthcare-grade OP, you need FAPI 2.0 on top of OIDC — sender-constrained tokens (DPoP / mTLS), PAR (the authorize request goes server-to-server first), JAR (the request is a signed JWT), and a tighter algorithm allow-list. The library makes this one option:

go
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