Session 01 ยท ๐Ÿ”‘ Identity

Identity & Cryptography

Prove who the user is โ€” without ever storing their password.

โœ“
1aAuthN vs AuthZ
โœ“
1bbcrypt & cost
โœ“
1cconstant-time compare

1a

Two questions, two status codes

Every protected request passes two separate checks, always in this order.

AUTHENTICATION

Who are you?

password ยท token ยท session
fails โ†’ 401 Unauthorized
โ†’
AUTHORIZATION

What may you do?

role ยท ownership ยท permission
fails โ†’ 403 Forbidden
โ†’
HANDLER

Do the work

200 OK
โ†’AuthN runs first โ€” you cannot check permissions for someone you haven't identified.
โ†’401 means "prove who you are again". 403 means "I know who you are โ€” still no".
โ†’Sessions 01โ€“04 build the first check; sessions 05โ€“07 build the second.

1b

Store a hash that is slow on purpose

bcrypt turns a password into a salted, deliberately expensive hash. You store only the hash.

internal/auth/password.go
import "golang.org/x/crypto/bcrypt"

const cost = 12 // ~270 ms on this machine

func HashPassword(pw string) (string, error) {
    h, err := bcrypt.GenerateFromPassword(
        []byte(pw), cost)
    return string(h), err
}

func CheckPassword(hash, pw string) bool {
    err := bcrypt.CompareHashAndPassword(
        []byte(hash), []byte(pw))
    return err == nil
}
$2a$12$EcX1op6oy.w0pO8mONBPr.6ZSuiU7NbxZOJ77Mqqvt/Ef5l12frhy
โ–  algorithmโ–  costโ–  random salt (22)โ–  hash (31)
go run ./demos/01
hash 1: $2a$12$EcX1op6oy.w0pO8mONBPr.6ZSuiU7NbxZOJ77Mqqvt/Ef5l12frhy
hash 2: $2a$12$q4/EfxVA3zOi64hBhpAxXeURCRWl3l3b9ZdDIQpBV01ZUv4mcOd7K
same?  false
right password: <nil>
wrong password: crypto/bcrypt: hashedPassword is not the hash of the given password
73-byte password: bcrypt: password length exceeds 72 bytes
REAL TIMINGS ยท one GenerateFromPassword call
cost 4
1 ms
cost 8
17 ms
cost 10
67 ms
cost 12
268 ms
cost 14
1.07 s
โ†’Same password, different hash โ€” the salt is random and lives inside the hash.
โ†’+1 cost = 2ร— the work. Pick the cost that takes ~250 ms on your server.
โ†’The cost is stored in the hash, so you can raise it later and re-hash on the next login.
โ†’bcrypt accepts at most 72 bytes โ€” cap password length in validation.
Mistake

Hashing passwords with SHA-256. It is built to be fast โ€” a GPU tries billions per second. For passwords, slow is the feature.

1c

Compare secrets without leaking time

A normal == stops at the first wrong byte. How long it takes tells an attacker how much they got right.

internal/auth/apikey.go
import "crypto/subtle"

func validKey(got, want string) bool {
    return subtle.ConstantTimeCompare(
        []byte(got), []byte(want)) == 1
}
go run ./demos/01b ยท 256-byte secret, avg of 2M runs
wrong at byte 0    naive    1ns   constant  101ns
wrong at byte 128  naive   52ns   constant  101ns
wrong at byte 255  naive   85ns   constant  101ns
# naive time grows with the correct prefix; constant never moves
โ†’Use subtle.ConstantTimeCompare for anything secret you compare yourself: API keys, reset tokens, signatures.
โœ“bcrypt.CompareHashAndPassword is already constant-time.
โ†’Same timing for unknown emails too: if the user doesn't exist, still run bcrypt against a dummy hash โ€” otherwise ~0 ms vs ~270 ms reveals which emails are registered.
โ†’Same message for both failures: invalid email or password.
TASK

Register & login with bcrypt

  • POST /register โ€” hash with cost 12, store only the hash.
  • POST /login โ€” compare; one error message for every failure.
  • Time a login with an unknown email and a known one โ€” make them match.

Recap

Session 01 in four lines

1AuthN (who, 401) always runs before AuthZ (what, 403).
2Store bcrypt hashes, never passwords โ€” salt and cost are inside the hash.
3Tune cost to ~250 ms; each step doubles the attacker's work too.
4Compare secrets in constant time and keep failure responses identical.