🎟️ Analogy
Authentication is showing your ID at the door — proving you’re you. Authorization is the wristband that says which areas you can enter — VIP, backstage, or just the lobby. Two different checks: the bouncer verifies the ID once, but security checks the wristband at every restricted door. A venue that checks IDs but never looks at wristbands lets anyone who got in the front door wander into the vault — which is exactly the “broken access control” bug at the top of every vulnerability list.
Two questions, in order
graph LR
REQ["request"] --> AUTHN{"AuthN:<br/>who are you?"}
AUTHN -->|verified| AUTHZ{"AuthZ:<br/>may you do this?"}
AUTHN -->|fails| D1["401 Unauthorized"]
AUTHZ -->|permitted| OK["perform action"]
AUTHZ -->|denied| D2["403 Forbidden"]Authentication establishes identity (password, token, certificate, MFA). Authorization decides if that identity may perform the action. You authenticate once; you authorize every sensitive operation. The classic, devastating mistake is doing the first and skipping the second.
Sessions vs tokens
Two ways to remember a logged-in user:
- Server-side sessions — a random session ID in a cookie maps to state you hold. Easy to revoke (delete it), but needs a session store. Set the cookie
HttpOnly,Secure,SameSite. - Stateless JWTs — a signed token carrying claims the server verifies without a lookup. Scales statelessly (great for microservices), but is valid until it expires — early revocation needs short lifetimes + refresh tokens or a denylist. A JWT is signed, not encrypted: never put secrets in its payload.
See it: secure tokens and a constant-time check
A session token must be unguessable, stored hashed, and compared in constant time. This runs here — output is booleans, so it’s deterministic despite the random token:
package main
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/hex"
"fmt"
)
// newToken returns an unguessable 256-bit token from a secure source.
func newToken() string {
b := make([]byte, 32)
rand.Read(b) // crypto/rand — never math/rand for secrets
return hex.EncodeToString(b)
}
// store only the HASH, so a DB leak doesn't expose live sessions.
func hashToken(t string) [32]byte { return sha256.Sum256([]byte(t)) }
// verify in constant time to avoid timing side-channels.
func verify(presented string, storedHash [32]byte) bool {
h := hashToken(presented)
return subtle.ConstantTimeCompare(h[:], storedHash[:]) == 1
}
func main() {
token := newToken() // issued at login, sent to the client
stored := hashToken(token) // only this is kept server-side
fmt.Println("token length (hex chars):", len(token)) // 64 = 256 bits
fmt.Println("correct token accepted:", verify(token, stored))
fmt.Println("forged token rejected:", verify("deadbeef", stored))
}
That’s the whole secure-token recipe: crypto/rand for unpredictability, hash-at-rest so a leaked database can’t be replayed, and subtle.ConstantTimeCompare so an attacker can’t recover the token byte-by-byte through timing. Password login follows the same shape but verifies against a bcrypt/argon2 hash.
See it: authorization with least privilege
Authentication tells you who; authorization checks may they. A small role→permission map, checked per action, with deny-by-default:
package main
import "fmt"
type perm string
var rolePerms = map[string][]perm{
"viewer": {"read"},
"editor": {"read", "write"},
"admin": {"read", "write", "delete"},
}
// can returns true only if the role explicitly has the permission.
func can(role string, p perm) bool {
for _, allowed := range rolePerms[role] { // unknown role -> no perms -> deny
if allowed == p {
return true
}
}
return false
}
func main() {
checks := []struct {
role string
p perm
}{
{"viewer", "read"}, {"viewer", "delete"},
{"editor", "write"}, {"admin", "delete"}, {"intruder", "read"},
}
for _, c := range checks {
fmt.Printf("%-9s %-7s -> %v\n", c.role, c.p, can(c.role, string(c.p)))
}
}
viewer can’t delete; an unknown intruder role gets nothing — deny by default. Beyond role checks, also verify ownership: a logged-in user requesting /orders/123 must own order 123. Skipping that is IDOR (insecure direct object reference), a top web bug.
🐹 Don't roll your own auth crypto — but do enforce AuthZ yourself
Use vetted libraries for the hard cryptographic parts — password hashing (argon2/bcrypt), JWT signing/verification, OAuth/OIDC flows. But authorization logic is your domain: deny by default, check permission on every sensitive action, verify object ownership (not just login), and centralize the checks (middleware) so you can’t forget one. Most breaches aren’t broken crypto — they’re a missing access-control check on one endpoint.
⚠️ Authenticated is not authorized (and trust the server, not the client)
Two recurring disasters. First, broken access control: verifying a user is logged in but not that they’re allowed to touch this resource — so user A reads user B’s data by changing an ID. Always check ownership/permission, not just authentication. Second, client-side trust: hiding an admin button in the UI while leaving the endpoint open, or trusting a role claim the client can edit. Every authorization decision must happen on the server against state the client can’t forge. For JWTs specifically, pin the expected alg and validate exp/iss/aud — never let the token choose its own verification.
See also
- Hashing & passwords — verifying credentials at login.
- Secrets management — where signing keys and session secrets live.
- Input validation & injection — the layer below access control.
- Web security (web track) — cookies, CSRF, and session handling in net/http.
Next: keeping the keys, tokens, and credentials this all depends on out of your code — secrets management.
Related topics
Storing secrets the right way — hashing vs encryption vs encoding, why fast hashes are wrong for passwords, salts, and the slow KDFs (bcrypt, scrypt, argon2id) that actually protect credentials.
defenseSecrets ManagementKeeping API keys, passwords, and signing keys out of your code, repo, logs, and binary — config from the environment, secret managers, redaction, and rotation.
defenseInput Validation & Injection DefenseThe bug class behind most breaches — why injection happens (mixing data with code), and the structural fixes: parameterized queries, html/template auto-escaping, allowlist validation, and safe path handling.
Check your understanding
Score: 0 / 51. What's the difference between authentication and authorization?
AuthN ('are you who you claim?') verifies identity via a password, token, certificate, or MFA. AuthZ ('may you do this?') checks whether that verified identity has permission for the action. You authenticate once, then authorize every sensitive operation. Confusing the two — e.g. authenticating a user but never checking they own the resource — is a top vulnerability class (broken access control).
2. How should a session token (or API key) be generated and compared?
A token must be unpredictable (crypto/rand, ≥128 bits), stored as a hash so a DB leak doesn't expose live sessions, and compared in constant time (subtle.ConstantTimeCompare) to avoid timing attacks. Sequential IDs are guessable, math/rand is predictable, and == leaks the matching prefix through timing.
3. What is a key trade-off between server-side sessions and stateless JWTs?
A server-side session is a random ID mapping to state you control — instantly revocable by deleting it, but it requires a session store. A JWT carries signed claims the server verifies without a lookup (great for scale and microservices), but it's valid until expiry — revoking early needs a denylist or short lifetimes + refresh tokens. Note: a JWT is signed, not encrypted; don't put secrets in its payload.
4. What is the 'alg: none' JWT vulnerability?
Historically, libraries trusted the token's own 'alg' header. Set it to 'none' and a naive verifier skips signature checking; or switch RS256→HS256 so the verifier uses the public key as an HMAC secret. The fix: the SERVER pins the expected algorithm and never lets the token dictate it. Use a well-maintained library and validate alg, exp, iss, and aud explicitly.
5. What does least-privilege authorization (RBAC) mean in practice?
Authentication alone isn't authorization. RBAC maps roles to permissions and checks them per action; least privilege means granting the minimum. Crucially, also verify object-level ownership: a logged-in user requesting /orders/123 must own order 123 (IDOR — insecure direct object reference — is checking auth but not ownership). Deny by default; allow explicitly.
Comments
Sign in with GitHub to join the discussion.