Documentation
¶
Overview ¶
Package fiberauth provides the shared auth building blocks used by creativeyann17's Fiber apps: JWT sessions with TokenVersion revocation, optional rotating refresh tokens, login throttling, TOTP 2FA, trusted devices, single-use tickets, 6-digit email codes, and a Resend mail client. Storage-agnostic: every piece that touches a user record takes closures or plain data.
Index ¶
- Constants
- func AdminOnly(c *fiber.Ctx) error
- func AuthMiddleware(secret string, bootTime time.Time, onReject func(reason string, c *fiber.Ctx)) fiber.Handler
- func CheckBackupCode(hashed []string, input string) (matchIndex int, ok bool)
- func ClientIP(c *fiber.Ctx) string
- func CookieDeviceID(cookieValue string) string
- func DeviceCookie(name, id, plain string, ttl time.Duration) *fiber.Cookie
- func DummyVerify(pw string)
- func ExpiredDeviceCookie(name string) *fiber.Cookie
- func ExpiredRefreshCookie(name, path string) *fiber.Cookie
- func GenerateBackupCodes(n int) (plaintext []string, hashed []string, err error)
- func GenerateCode() (string, error)
- func GenerateTOTPSecret(cfg TOTPConfig, accountName string) (secret, uri string, err error)
- func HasRole(c *fiber.Ctx, r string) bool
- func HashPassword(pw string) (string, error)
- func IsNewIP(ip string, history []LoginRecord) bool
- func Issue(secret, uid string, roles []string, ver int64, ttl time.Duration) (string, error)
- func IssuePurpose(secret, uid, purpose string, ver int64, ttl time.Duration) (string, error)
- func NewTrustedDeviceToken() (id, plain, hashed string, err error)
- func RefreshCookie(name, path, id, plain string, ttl time.Duration) *fiber.Cookie
- func RefreshSessionsToEvict(sessions []RefreshSession, max int) []string
- func Roles(c *fiber.Ctx) []string
- func SplitRefreshCookie(v string) (id, plain string, ok bool)
- func TokenVersionMiddleware(lookup func(uid string) (currentVersion int64, found bool, err error)) fiber.Handler
- func UID(c *fiber.Ctx) string
- func ValidateDeviceCookie(cookieValue string, devices []TrustedDevice) (deviceID string, ok bool)
- func ValidateTOTPCode(secret, code string) bool
- func Ver(c *fiber.Ctx) int64
- func VerifyPassword(hash, pw string) bool
- type Claims
- type CodeCheck
- type CodeState
- type LoginRecord
- type LoginThrottle
- type LoginThrottleConfig
- type Mailer
- type RefreshResult
- type RefreshSession
- type TOTPConfig
- type Throttle
- type TicketStore
- type TrustedDevice
Constants ¶
const ( CtxUID ctxKey = "uid" CtxRoles ctxKey = "roles" CtxVer ctxKey = "ver" )
const RoleAdmin = "admin"
RoleAdmin is the shared role-name convention across apps.
Variables ¶
This section is empty.
Functions ¶
func AuthMiddleware ¶
func AuthMiddleware(secret string, bootTime time.Time, onReject func(reason string, c *fiber.Ctx)) fiber.Handler
AuthMiddleware validates the bearer token and stashes its claims in request locals. Header-only: tokens never ride in a URL, keeping them out of access logs (use TicketStore for browser-navigation downloads).
onReject, nil-safe, receives a short reason string for security-warn logging on suspicious rejections (bad signature, alg confusion, malformed, purpose token). Expired tokens are normal, no callback.
Tokens issued before bootTime are rejected, forcing re-login after a restart/redeployment.
func CheckBackupCode ¶
CheckBackupCode tests input against the stored bcrypt-hashed codes. On a match the caller must remove hashed[matchIndex] from their slice and persist immediately, single-use, crash-safe against replay.
func ClientIP ¶
ClientIP returns the best available client IP: X-Forwarded-For when behind a trusted reverse proxy (see fiber.Config.ProxyHeader), falling back to the raw TCP remote address in local dev. Returns "unknown" when nothing is resolvable, the sentinel LoginThrottle skips.
func CookieDeviceID ¶
CookieDeviceID returns the device id embedded in a cookie value, or "" if absent/malformed.
func DeviceCookie ¶
DeviceCookie builds the trusted-device cookie for a freshly minted token. HTTPOnly + SameSite=Strict + Secure: the token never reaches JS, never rides a cross-site request and never goes over plain http.
func DummyVerify ¶
func DummyVerify(pw string)
DummyVerify spends a bcrypt comparison against a throwaway hash. Call it on the unknown-login path so the response latency matches the wrong-password path, denying an attacker a timing oracle for username/email enumeration. The result is intentionally discarded.
func ExpiredDeviceCookie ¶
ExpiredDeviceCookie tells the browser to drop the trusted-device cookie immediately. Attributes must match DeviceCookie's so the browser targets the same cookie.
func ExpiredRefreshCookie ¶ added in v0.2.0
ExpiredRefreshCookie tells the browser to drop the refresh cookie. name and path must match RefreshCookie's.
func GenerateBackupCodes ¶
GenerateBackupCodes returns n one-time recovery codes (10 hex chars each) in plaintext, shown to the user exactly once, plus their bcrypt hashes for storage.
func GenerateCode ¶
GenerateCode returns a cryptographically random 6-digit string used for email verification and password-reset codes (leading zeros allowed: argue about entropy, not display width).
func GenerateTOTPSecret ¶
func GenerateTOTPSecret(cfg TOTPConfig, accountName string) (secret, uri string, err error)
GenerateTOTPSecret mints a fresh TOTP secret and its otpauth:// URI (QR-code source) for accountName. Nothing is persisted: the app stores the secret only after the user proves enrollment via ValidateTOTPCode.
func HashPassword ¶
func IsNewIP ¶
func IsNewIP(ip string, history []LoginRecord) bool
IsNewIP reports whether ip has never appeared in history. False on empty history: a first login isn't an alert.
func IssuePurpose ¶
IssuePurpose mints a single-purpose token (e.g. a password-reset link) bound to ver, the caller's current TokenVersion at issuance, so bumping TokenVersion after use invalidates any replay of this exact token without needing separate token storage. Never accepted by AuthMiddleware as a session token (see Purpose check there).
func NewTrustedDeviceToken ¶
NewTrustedDeviceToken mints a device id plus its secret token. plain goes in the cookie (via DeviceCookie), hashed in the stored record.
func RefreshCookie ¶ added in v0.2.0
RefreshCookie builds the refresh cookie. path should be the refresh endpoint so the cookie never rides normal API calls. HTTPOnly keeps it from JS, SameSite=Strict blocks cross-site (CSRF) refreshes, Secure keeps it off plain http (browsers still allow it on http://localhost).
func RefreshSessionsToEvict ¶ added in v0.2.1
func RefreshSessionsToEvict(sessions []RefreshSession, max int) []string
RefreshSessionsToEvict returns the ids to delete before creating a new session so a user keeps at most max sessions including the new one: every expired session, then the least recently used live ones. max <= 0 means no cap.
func SplitRefreshCookie ¶ added in v0.2.0
SplitRefreshCookie parses an "id:plainToken" cookie value.
func TokenVersionMiddleware ¶
func TokenVersionMiddleware(lookup func(uid string) (currentVersion int64, found bool, err error)) fiber.Handler
TokenVersionMiddleware rejects tokens older than the user's current TokenVersion (bumped on logout-all/password reset/ban). lookup fetches the stored version for uid; found=false or an error rejects the request, treating a missing user as a revoked session.
func ValidateDeviceCookie ¶
func ValidateDeviceCookie(cookieValue string, devices []TrustedDevice) (deviceID string, ok bool)
ValidateDeviceCookie checks a cookie value against the stored devices: constant-time token compare plus expiry check.
func ValidateTOTPCode ¶
ValidateTOTPCode checks a 6-digit authenticator code against secret.
func VerifyPassword ¶
Types ¶
type Claims ¶
type Claims struct {
UID string `json:"uid"`
Roles []string `json:"roles"`
Ver int64 `json:"ver,omitempty"`
// Purpose marks single-purpose tokens (e.g. "pwreset") that must never
// be accepted as a normal session bearer token. Empty = regular session.
Purpose string `json:"purp,omitempty"`
jwt.RegisteredClaims
}
type CodeCheck ¶
type CodeCheck int
const ( // CodeOK: input matches. Caller clears the three fields and persists. CodeOK CodeCheck = iota // CodeExpired: no code pending or past expiry. Caller prompts a resend. CodeExpired // CodeLocked: attempt budget exhausted. Only issuing a fresh code // (which resets Attempts) unlocks. CodeLocked // CodeWrong: mismatch. Caller increments Attempts and persists. CodeWrong )
func CheckCode ¶
CheckCode runs the shared validation decision tree for 6-digit email codes. Pure: the caller mutates its record per the result (increment Attempts on CodeWrong, clear fields on CodeOK) and persists. remaining is only meaningful for CodeWrong: attempts left after this failure, floored at 0.
type CodeState ¶
CodeState mirrors the three fields apps persist per code flow on their user record (code, expiry, wrong-attempt counter). Plain data in, the caller owns storage.
type LoginRecord ¶
type LoginRecord struct {
IP string `json:"ip"`
UserAgent string `json:"userAgent,omitempty"`
At time.Time `json:"at"`
}
LoginRecord is one successful login, kept newest-first on the app's user record for the "recent activity" view and new-IP detection.
func AppendLoginRecord ¶
func AppendLoginRecord(history []LoginRecord, ip, ua string, max int) []LoginRecord
AppendLoginRecord prepends a record and caps the history at max entries. The user agent is truncated to 120 runes so a hostile UA header can't bloat the stored record.
type LoginThrottle ¶
type LoginThrottle struct {
// contains filtered or unexported fields
}
LoginThrottle wraps Throttle with two independent dimensions, each its own namespaced key:
- per-login ("login:<name>"): blunts a targeted brute-force against one account. Includes unknown logins, so hammering a guessed username is throttled too.
- per-IP ("ip:<addr>"): blunts a password-spray that walks many usernames from one source. Higher threshold since several real users may legitimately share an IP (NAT/office).
A login is locked if *either* dimension is tripped. Both wrong-password and wrong-TOTP attempts count as a failure.
func NewLoginThrottle ¶
func NewLoginThrottle(cfg LoginThrottleConfig) *LoginThrottle
func (*LoginThrottle) Fail ¶
func (l *LoginThrottle) Fail(login, ip string) int
Fail records a failure against both dimensions and returns the running per-login count (used to pace failed-login admin email alerts).
func (*LoginThrottle) Locked ¶
func (l *LoginThrottle) Locked(login, ip string) (bool, time.Duration)
Locked reports whether either the login or the source IP is currently locked out, and the longest retry-after of the two.
func (*LoginThrottle) Succeeded ¶
func (l *LoginThrottle) Succeeded(login string)
Succeeded clears the per-login counter on a successful auth. The per-IP counter is intentionally left to age out via TTL so one valid credential among a spray can't immediately reset the IP's budget.
type LoginThrottleConfig ¶
type Mailer ¶
type Mailer struct {
APIKey string
From string
HTTPClient *http.Client
Endpoint string // overridden in tests, defaults to Resend's API
}
Mailer sends transactional email via Resend's HTTP API (https://resend.com/docs/api-reference/emails/send-email). A plain HTTP call rather than the official SDK: one endpoint, one request shape, not worth a new dependency.
APIKey-gated: an empty APIKey makes Send a no-op returning nil, so email flows degrade gracefully in dev/CI instead of erroring. Apps check Enabled() to log the code instead of sending.
type RefreshResult ¶ added in v0.2.0
type RefreshResult int
const ( RefreshOK RefreshResult = iota // rotate, persist, set cookie, issue access token RefreshGrace // previous token within grace: issue access token only RefreshExpired // delete record, clear cookie RefreshRevoked // TokenVersion bumped since issuance: delete record, clear cookie RefreshReuse // token matches nothing: likely stolen, delete record and security-warn )
func CheckRefresh ¶ added in v0.2.0
func CheckRefresh(s RefreshSession, plain string, currentVer int64, grace time.Duration) RefreshResult
CheckRefresh validates plain against the stored session. currentVer is the user's stored TokenVersion; grace is how long the previous token stays usable after a rotation (a few seconds is plenty).
type RefreshSession ¶ added in v0.2.0
type RefreshSession struct {
ID string `json:"id"`
UID string `json:"uid"`
Ver int64 `json:"ver"`
HashedToken string `json:"hashedToken"`
// PrevHashedToken is the token replaced by the last rotation, still
// accepted within the grace window (concurrent tabs refreshing).
PrevHashedToken string `json:"prevHashedToken,omitempty"`
RotatedAt time.Time `json:"rotatedAt"`
Label string `json:"label"`
CreatedAt time.Time `json:"createdAt"`
// ExpiresAt is absolute, rotation never extends it.
ExpiresAt time.Time `json:"expiresAt"`
}
RefreshSession is an optional long-lived session that mints short access tokens (see Issue). The token rotates on every use; only its SHA-256 is stored. ID stays stable across rotations so the app looks the record up by it. The cookie value is "id:plainToken".
func NewRefreshSession ¶ added in v0.2.0
func NewRefreshSession(uid string, ver int64, label string, ttl time.Duration) (RefreshSession, string, error)
NewRefreshSession mints a session bound to uid and ver, the user's current TokenVersion, so bumping it revokes the session. plain goes in the cookie (see RefreshCookie), the returned record is persisted.
func RotateRefreshSession ¶ added in v0.2.0
func RotateRefreshSession(s RefreshSession) (RefreshSession, string, error)
RotateRefreshSession swaps in a fresh token, keeping the old hash for the grace window. Persist with a compare-and-swap on the old HashedToken so two concurrent rotations can't both win.
type TOTPConfig ¶
type TOTPConfig struct {
Issuer string // shown in authenticator apps, e.g. "Mist Drive"
}
type Throttle ¶
type Throttle struct {
// contains filtered or unexported fields
}
Throttle is an in-memory keyed lockout. Generic core: callers pick the key namespace ("login:<name>", "ip:<addr>", "uid:<id>", ...) and the per-key threshold. No database: the map lives for the process lifetime. Stale entries are swept lazily so the map stays bounded even under a spray of random keys.
func NewThrottle ¶
func (*Throttle) Fail ¶
Fail records a failed attempt for key and returns the running count. Once the count reaches maxFails the key is locked for lockFor.
type TicketStore ¶
type TicketStore struct {
// contains filtered or unexported fields
}
TicketStore mints short-lived, single-use tickets that authorize one browser-navigation action (e.g. a streaming zip download). They exist so a plain navigation, which can't set an Authorization header, never carries the reusable session JWT in a URL where it would land in proxy/access logs.
A ticket is an opaque random string bound to {uid, scope}. It is consumed (deleted) on first use and expires after ttl. In-memory, no DB.
func NewTicketStore ¶
func NewTicketStore(ttl time.Duration) *TicketStore
type TrustedDevice ¶
type TrustedDevice struct {
ID string `json:"id"`
HashedToken string `json:"hashedToken"`
Label string `json:"label"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
}
TrustedDevice is a "remember this device" record letting a browser skip the TOTP step. The cookie value is "id:plainToken"; only the SHA-256 of the token is stored.
func PruneExpiredDevices ¶
func PruneExpiredDevices(devices []TrustedDevice) []TrustedDevice
PruneExpiredDevices drops expired entries in place.