Sequence diagrams for every authentication flow in Birdcage. Each diagram maps directly to source code in the root package.
Rate limiting: Fixed-window middleware gates public auth routes before any business logic runs. Requests exceeding the threshold receive
429 Too Many Requestswith aRetry-Afterheader.
Rendering: GitHub renders Mermaid natively. For local preview, use the Mermaid Live Editor or a VS Code extension.
Owner creates the single account. Requires a registration token generated by birdcage init.
sequenceDiagram
participant U as Browser
participant RL as Rate Limiter
participant H as Handler
participant PS as Password
participant DB as SQLite
U->>RL: POST /auth/register {email, password, registrationToken}
Note over RL: rl:register 5/5min — IP-keyed
alt Rate limit exceeded
RL-->>U: 429 {error: "Too many requests"}
end
RL->>H: handleRegister()
H->>H: subtle.ConstantTimeCompare(token, cfg.RegistrationToken)
alt Invalid token
H-->>U: 403 {code: "INVALID_TOKEN"}
end
H->>H: validEmail() + validPassword()
H->>PS: hashPassword(password)
Note over PS: 1. Generate 16-byte random salt<br/>2. PBKDF2-SHA384 (210K iterations)<br/>3. SHA-384 integrity digest<br/>4. Format: $pbkdf2-sha384$v1$210000$salt$hash$digest
PS-->>H: password_data string
H->>DB: INSERT INTO account ... WHERE (SELECT COUNT(*) FROM account) = 0
alt Account already exists
H-->>U: 201 {success: true}
Note over H: Identical response — prevents enumeration
end
H->>DB: emitEvent("registration.success", ...)
H-->>U: 201 {success: true}
Source: auth.go:11-51 | crypto.go:29-43 | respond.go:18-26
Full authentication flow from credential verification through token issuance. Includes adaptive proof-of-work when brute-force is detected.
sequenceDiagram
participant U as Browser
participant RL as Rate Limiter
participant H as Handler
participant PoW as PoW Engine
participant PS as Password
participant SS as Session
participant DB as SQLite
U->>RL: POST /auth/login {email, password}
Note over RL: rl:login 5/5min — IP-keyed
alt Rate limit exceeded
RL-->>U: 429 {error: "Too many requests"}
end
RL->>H: handleLogin()
H->>PoW: computeChallenge(ip, secret)
PoW->>DB: SELECT COUNT(*) FROM security_event WHERE type IN (...) AND ip = ?
alt 3+ failures in 15 min
PoW-->>H: Challenge {nonce, difficulty: 3-5}
H-->>U: 403 {code: "CHALLENGE_REQUIRED", challenge: {...}}
Note over U: Browser solves SHA-256 PoW
U->>H: POST /auth/login {email, password, challengeNonce, challengeSolution}
H->>PoW: verifySignedNonce() + verifySolution()
Note over PoW: HMAC-signed nonce (5-min TTL, IP-bound)<br/>SHA-256 prefix match at difficulty level
end
H->>DB: SELECT id, password_data FROM account WHERE email = ?
alt User not found
H->>PS: rejectConstantTime(password)
Note over PS: Full PBKDF2 against dummy hash<br/>— equalizes response time
H->>DB: emitEvent("login.failure", ...)
H-->>U: 401 {error: "Invalid email or password"}
end
H->>PS: verifyPassword(password, stored)
Note over PS: 1. PBKDF2 with stored salt<br/>2. hmac.Equal(derived, storedHash)<br/>3. hmac.Equal(derivedDigest, storedDigest)
alt Password incorrect
H->>DB: emitEvent("login.failure", ...)
H-->>U: 401 {error: "Invalid email or password"}
end
H->>SS: createSession(userId, userAgent, ip)
SS->>DB: DELETE expired sessions
SS->>SS: nanoid() — 21 chars, ~126 bits entropy
SS->>DB: INSERT INTO session (..., refresh_gen=0)
SS->>SS: enforceSessionLimit() — max 3 per user
SS-->>H: sessionId
H->>H: signToken(access) + signRefreshToken(refresh, gen=0)
Note over H: Access: {uid, sid, typ:"access", exp:+15min}<br/>Refresh: {uid, sid, typ:"refresh", gen:0, exp:+7d}<br/>Signed with separate secrets (HS256)
H->>U: Set-Cookie: access_token (HttpOnly, Secure, SameSite=Strict)
H->>U: Set-Cookie: refresh_token (HttpOnly, Secure, SameSite=Strict)
H->>DB: emitEvent("login.success", ...)
H-->>U: 200 {success: true}
Source: auth.go:54-102 | events.go:89-161 | session.go:25-71 | crypto.go:46-70
Accessing a protected endpoint with a valid access token.
sequenceDiagram
participant U as Browser
participant MW as Auth Middleware
participant SS as Session
participant DB as SQLite
participant RH as Route Handler
U->>MW: GET /account/me (Cookie: access_token=...)
MW->>MW: tryAccessToken() — read cookie
MW->>MW: verifyToken(token, JWT_ACCESS_SECRET, "access")
Note over MW: Explicit HS256 check<br/>Validates typ == "access"
MW->>SS: getSession(claims.SID)
SS->>DB: SELECT ... FROM session WHERE id = ? AND expires_at > now
SS->>DB: UPDATE session SET expires_at = now + 7d
Note over SS,DB: Sliding expiration — extends on each use
SS-->>MW: session
alt Session revoked or expired
MW-->>U: 403 {code: "SESSION_REVOKED"}
end
MW->>RH: next() with claims in context
RH-->>U: 200 {userId, email, gateway}
Source: middleware.go:42-60 | session.go:73-95
When the access token expires, the middleware transparently refreshes both tokens. A generation counter detects stolen token replay.
sequenceDiagram
participant U as Browser
participant MW as Auth Middleware
participant SS as Session
participant DB as SQLite
U->>MW: GET /account/me [Cookie: access_token(expired) + refresh_token]
MW->>MW: tryAccessToken() — fails (expired)
MW->>MW: tryRefreshAndRotate()
MW->>MW: verifyToken(refreshToken, JWT_REFRESH_SECRET, "refresh")
Note over MW: Validates typ == "refresh"<br/>Extracts Gen from claims
MW->>SS: getSession(claims.SID)
SS->>DB: SELECT ... refresh_gen FROM session WHERE id = ? AND expires_at > now
SS->>DB: UPDATE session SET expires_at = now + 7d
SS-->>MW: session (with RefreshGen)
alt Session revoked or expired
MW-->>U: 403 {code: "SESSION_REVOKED"}
end
alt claims.Gen != session.RefreshGen
MW->>SS: endSession(sid)
MW->>DB: emitEvent("session.refresh_reuse", ...)
Note over MW: Stolen token detected —<br/>entire session revoked
MW-->>U: 403 {code: "SESSION_REVOKED"}
end
MW->>SS: bumpRefreshGen(sid)
SS->>DB: UPDATE session SET refresh_gen = refresh_gen + 1
SS-->>MW: newGen
MW->>MW: signToken(access, gen=0) + signRefreshToken(refresh, gen=newGen)
MW->>U: Set-Cookie: access_token (new)
MW->>U: Set-Cookie: refresh_token (new, gen=newGen)
MW->>MW: Verify new access token, continue to handler
MW-->>U: 200 (original request succeeds)
Source: middleware.go:70-109 | crypto.go:107-117 | session.go:109-118
Ends the server-side session and clears both auth cookies.
sequenceDiagram
participant U as Browser
participant MW as Auth Middleware
participant H as Handler
participant SS as Session
participant DB as SQLite
U->>MW: POST /auth/logout [Cookie: access_token + refresh_token]
MW-->>H: Authenticated (claims in context)
H->>SS: endSession(claims.SID)
SS->>DB: UPDATE session SET expires_at = datetime('now') WHERE id = ?
Note over SS,DB: Session expires immediately<br/>but row kept for audit trail
H->>U: Set-Cookie: access_token="" (delete)
H->>U: Set-Cookie: refresh_token="" (delete)
H->>DB: emitEvent("session.revoke", ...)
H-->>U: 200 {success: true}
Source: auth.go:105-111 | session.go:97-101
Requires re-verification of the current password even though the user is authenticated. All sessions are revoked afterward, forcing re-authentication on every device.
sequenceDiagram
participant U as Browser
participant RL as Rate Limiter
participant MW as Auth Middleware
participant H as Handler
participant PS as Password
participant SS as Session
participant DB as SQLite
U->>RL: POST /account/password {currentPassword, newPassword}
Note over RL: rl:password 3/1hr — user-keyed
RL->>MW: requireAuth validates token + session
MW-->>H: Authenticated (claims in context)
H->>H: validPassword(current) + validPassword(new)
H->>H: Reject if current == new (after normalization)
H->>DB: SELECT password_data FROM account WHERE id = ?
H->>PS: verifyPassword(currentPassword, stored)
Note over PS: PBKDF2 + hmac.Equal (constant-time)
alt Current password incorrect
H-->>U: 401 {error: "Current password is incorrect"}
end
H->>PS: hashPassword(newPassword)
Note over PS: Full PBKDF2 hash with fresh salt
H->>DB: UPDATE account SET password_data = ? WHERE id = ?
H->>SS: endAllSessions(userId)
SS->>DB: UPDATE session SET expires_at = datetime('now') WHERE user_id = ?
Note over SS,DB: All sessions expired atomically —<br/>including the current one
H->>U: Set-Cookie: access_token="" (delete)
H->>U: Set-Cookie: refresh_token="" (delete)
H->>DB: emitEvent("password.change", ...)
H->>DB: emitEvent("session.revoke_all", ...)
H-->>U: 200 {success: true}
Source: auth.go:114-153 | session.go:103-107 | crypto.go:29-43