Skip to content

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-provider is 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.signature JSON, 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_uri reference.
  • 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 (DPoP jkt thumbprint or mTLS x5t#S256).
Specs referenced on this page

The three roles ​

Login is delegated; the API only checks a token
The three OAuth and OIDC roles. The Relying Party delegates login to the OpenID Provider and receives tokens back; it then calls the Resource Server with a Bearer access token, which the Resource Server validates against the provider's keys.delegate the loginid_token · access_tokenRelying Partyyour app: starts the loginholds tokens, calls the APIOpenID Providergo-oidc-providerauthenticates, signs tokensResource Serveryour API: checks the tokenreturns the datacheck the signature/jwks · /introspectAuthorization: Bearer <access_token>
The Resource Server never sees a credential — only a token, and only for as long as that token lives. That is the whole point of delegating: your API trusts the provider's signature instead of holding the user's password.

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 ​

TokenLifetimeWhat it isWhere it goes
Authorization codeSeconds (default 60s)Single-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 (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).

http
# 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 ​

The code travels by browser, the tokens do not
The Authorization Code flow with PKCE in short: the browser carries the user between the Relying Party and the OpenID Provider for login and consent, the Relying Party swaps the returned code for tokens directly at the token endpoint, and then calls the Resource Server with the access token.Relying Partyyour appUserbrowserOPgo-oidc-provideropen the app302 → /authredirect_uri · code_challengeGET /authlogin and consentthe user signs in, approves302 → redirect_uri?code=xyz&state=…GET /callback?code=xyzPOST /token · client authcode · code_verifier{ access_token · id_token · refresh_token }call the Resource ServerBearer <access_token>
Everything before /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 ​

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 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.
JWKSJSON 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:

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 ​