{} The Go Reference

Defense · Security · Intermediate

Authentication & Authorization

Proving who you are and deciding what you may do — sessions vs tokens, secure token generation and constant-time checks, password verification, and least-privilege authorization (RBAC).

Defense Intermediate ⏱ 5 min read Complete

🎟️ 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:

token.go — editable & runnable
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:

rbac.go — editable & runnable
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

Next: keeping the keys, tokens, and credentials this all depends on out of your code — secrets management.

Check your understanding

Score: 0 / 5

1. 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.