{} The Go Reference

Defense · Security · Intermediate

OAuth 2.0 & OpenID Connect

Delegated authorization (OAuth2) vs authentication (OIDC) — the Authorization Code + PKCE flow, access vs ID tokens, and the mistakes that turn 'sign in with…' into account takeover.

Defense Intermediate ⏱ 4 min read Complete

🏨 Analogy

A hotel key card is OAuth2: the front desk gives your phone-app a card that opens your room and the gym — delegated, scoped access — without handing over the master key or even telling the gym door who you are. OpenID Connect adds a photo ID to that card: now the door doesn’t just know “this card is allowed,” it knows “this is Ada.” OAuth2 = what you can access; OIDC = who you are. Mixing them up — treating “this card works” as proof of identity — is how the wrong person ends up in your room.

Authorization (OAuth2) vs authentication (OIDC)

OAuth 2.0 is a delegated authorization framework: it lets an app access resources on a user’s behalf (call the Calendar API, read the repo) and yields an access token. It deliberately says nothing about identity. OpenID Connect is a thin layer on top that adds authentication: it returns an ID token — a signed JWT with claims like sub and email proving who logged in. Read the specs at oauth.net and openid.net.

Access token → presented to an API to access something. ID token → consumed by your app to learn who the user is. Never use a bare access token as proof of identity.

The Authorization Code + PKCE flow

This is the recommended flow for SPAs, mobile, and server apps alike:

sequenceDiagram
participant U as User browser
participant C as Your app
participant A as Auth server (IdP)
C->>C: make code_verifier, then challenge = S256(verifier)
C->>A: /authorize?...&code_challenge=challenge&state=xyz
U->>A: log in + consent
A-->>C: redirect ?code=AUTH_CODE&state=xyz
C->>A: /token  code=AUTH_CODE + code_verifier
A-->>C: access_token (+ id_token for OIDC)
Note over C,A: stolen code is useless without the verifier

The browser only ever sees a short-lived authorization code; tokens are fetched at the /token endpoint. PKCE binds the code to the client: a stolen code can’t be redeemed without the original code_verifier.

See it: computing the PKCE S256 challenge

PKCE is small enough to see end-to-end. The client makes a random code_verifier, derives code_challenge = base64url(sha256(verifier)), sends the challenge up front, and proves possession with the verifier at token exchange. This runs here:

pkce.go — editable & runnable
package main

import (
"crypto/sha256"
"encoding/base64"
"fmt"
)

// In real code, codeVerifier is 43-128 random chars from a CSPRNG
// (crypto/rand). Fixed here so the output is deterministic.
const codeVerifier = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"

func challengeS256(verifier string) string {
sum := sha256.Sum256([]byte(verifier))
// base64url WITHOUT padding, per RFC 7636.
return base64.RawURLEncoding.EncodeToString(sum[:])
}

func main() {
challenge := challengeS256(codeVerifier)

// 1) Sent on the /authorize request (public — safe to expose):
fmt.Println("code_challenge:        ", challenge)
fmt.Println("code_challenge_method:  S256")

// 2) Sent on the /token request; server re-hashes & must match (1):
fmt.Println("code_verifier (secret):", codeVerifier)

// The server's check:
fmt.Println("server verifies:       ", challengeS256(codeVerifier) == challenge)
}

Because SHA-256 is one-way, publishing the challenge reveals nothing about the verifier — so intercepting the authorization request gains an attacker nothing.

⚠️ The takeover bugs are all in validation

The dangerous mistakes aren’t in the happy path — they’re in skipped checks. redirect_uri must be matched exactly against a registered allowlist (a loose match or open redirect lets an attacker receive the code/token). The state parameter must be generated, stored, and verified on callback (it’s CSRF protection for the flow). And an ID token must have its signature verified against the provider’s JWKS, plus its iss, aud, and exp claims checked — accepting an unverified JWT means accepting a forged identity. Each omission is a known path to account takeover.

🐹 Use the libraries; delegate identity to a real IdP

Don’t hand-roll this. For the flow, golang.org/x/oauth2 handles the authorize/token dance and token refresh; for OIDC, coreos/go-oidc verifies ID tokens against the provider’s rotating JWKS and checks the standard claims for you. Delegate the hard part — being an identity provider — to a real one (Keycloak, Auth0, Okta, Google, Entra). Your job shrinks to: keep the client secret safe, request least-privilege scopes, validate strictly, and set a sane session policy. The protocol is a minefield; let maintained code walk it.

See also

Next: securing what your service is packaged as — container image security.

Check your understanding

Score: 0 / 5

1. What's the fundamental difference between OAuth 2.0 and OpenID Connect (OIDC)?

OAuth 2.0 answers 'is this app allowed to access that resource on the user's behalf?' — delegated authorization, yielding an access token. It deliberately says nothing about user identity. OpenID Connect is a thin layer on top that adds authentication: it returns an ID token (a signed JWT with claims like sub, email) proving who logged in. Rule of thumb: OAuth2 for access (calling an API), OIDC for login (knowing who the user is). Using a bare OAuth2 access token as proof of identity is a classic, dangerous mistake.

2. Why is the Authorization Code flow with PKCE the recommended flow for almost all clients today?

In the Authorization Code flow the browser only ever receives a short-lived authorization code; the actual tokens are fetched server-to-server (or with PKCE). PKCE (Proof Key for Code Exchange) makes the client send a random 'verifier' hashed into a 'challenge' at the start, and the verifier itself at token exchange — so even if an attacker intercepts the code, they can't redeem it without the verifier. The old Implicit flow (tokens in the URL fragment) is now discouraged; Authorization Code + PKCE is recommended for SPAs, mobile, and confidential clients alike.

3. In PKCE with the S256 method, how is the code_challenge derived from the code_verifier?

The client generates a high-entropy random code_verifier, then computes code_challenge = base64url(sha256(verifier)) with padding stripped, and sends the challenge (plus method=S256) on the authorization request. At token exchange it sends the raw verifier; the server re-hashes it and checks it matches the stored challenge. Because SHA-256 is one-way, the public challenge leaks nothing about the verifier — so an intercepted authorization request can't be replayed.

4. What is the #1 OAuth/OIDC implementation mistake that leads to account takeover?

The high-severity bugs cluster around weak validation: a redirect_uri that isn't strictly matched (open redirect → code/token exfiltration), a missing/unchecked state parameter (CSRF on the callback), and ID tokens accepted without verifying the signature, issuer (iss), audience (aud), and expiry (exp). Each can hand an attacker the victim's session. Strict, exact-match redirect URIs, a verified state, and full JWT validation are non-negotiable.

5. What's the right way to add OAuth2/OIDC to a Go service?

OAuth2/OIDC is a protocol with many sharp edges (redirect validation, state, PKCE, JWKS rotation, clock skew). Use maintained libraries — golang.org/x/oauth2 for the flow and coreos/go-oidc for ID-token verification against the provider's JWKS — and delegate identity to a real IdP (Auth0, Okta, Keycloak, Google, etc.). Hand-rolling it, putting secrets in the browser, or skipping signature/claim validation are how takeovers happen. Let battle-tested code handle the protocol; you handle scopes and session policy.

Comments

Sign in with GitHub to join the discussion.