fiberauth

package module
v0.3.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 17, 2026 License: MIT Imports: 20 Imported by: 0

README

go-fiber-auth

CI Go Reference Go Version Release License

Shared auth building blocks for Fiber apps: JWT sessions with token-version 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 by design: every function takes closures or plain data, your app owns persistence. No database driver, no ORM, no assumptions about your user model.

Install

go get github.com/creativeyann17/go-fiber-auth
import fiberauth "github.com/creativeyann17/go-fiber-auth"

Features

File What it gives you
jwt.go HS256 session tokens (Claims with UID, Roles, Ver, Purpose), bcrypt password hashing, timing-attack DummyVerify
refresh.go Optional rotating refresh tokens (RefreshSession): SHA-256 stored, reuse detection, grace window, TokenVersion revocation
throttle.go Keyed in-memory lockout (Throttle) + two-dimension login throttle (LoginThrottle: per-account and per-IP)
middleware.go AuthMiddleware, AdminOnly, TokenVersionMiddleware, context accessors, ClientIP
otp.go TOTP secret enrollment, code validation, bcrypt-hashed one-time backup codes
trusteddevice.go "Remember this device" cookies (SHA-256 tokens, constant-time compare)
ticket.go Single-use short-lived tickets so browser navigations never carry the JWT in a URL
code.go Crypto-random 6-digit codes + the expiry/attempt-lockout validation state machine
mailer.go Minimal Resend client (no-op without an API key, dev-friendly)
loginhistory.go Capped login history + new-IP detection for alert emails

Usage

Sessions
// Signup / login
hash, _ := fiberauth.HashPassword(password)
ok := fiberauth.VerifyPassword(hash, password)

tok, _ := fiberauth.Issue(secret, user.ID, []string{"user"}, user.TokenVersion, 24*time.Hour)

// Unknown-login path: burn a bcrypt compare so response timing
// doesn't reveal whether the account exists.
fiberauth.DummyVerify(password)
Middleware
app := fiber.New()

api := app.Group("/api",
    // bootTime rejects tokens issued before this process started.
    // onReject fires on suspicious rejections (bad signature, alg
    // confusion, malformed, purpose token) for your security log.
    fiberauth.AuthMiddleware(secret, bootTime, func(reason string, c *fiber.Ctx) {
        log.Warn("auth: "+reason, "ip", c.IP())
    }),
    // Revoke-everywhere: reject tokens older than the stored version.
    fiberauth.TokenVersionMiddleware(func(uid string) (int64, bool, error) {
        u, err := store.GetByID(uid)
        if err != nil {
            return 0, false, err
        }
        return u.TokenVersion, true, nil
    }),
)

api.Get("/me", func(c *fiber.Ctx) error {
    uid := fiberauth.UID(c) // also: Roles(c), Ver(c), HasRole(c, role)
    // ...
})

admin := api.Group("/admin", fiberauth.AdminOnly)

Bumping the stored TokenVersion (logout-all, password reset, ban) instantly invalidates every outstanding token for that user.

Refresh tokens (optional)

Cookies (refresh and trusted-device) are HttpOnly, SameSite=Strict and Secure: served over plain http they are dropped by browsers, except on http://localhost.

Skip this entirely if one long-lived access token is enough. Opt in to get short access tokens plus a rotating refresh cookie: a redeploy (bootTime) or an expired access token is silently recovered, and role changes land within one access TTL.

const rtCookie, rtPath = "app_rt", "/api/auth/refresh"

// Login (after password + TOTP):
// Cap sessions per user: drops expired + least recently used.
for _, id := range fiberauth.RefreshSessionsToEvict(store.RefreshByUID(user.ID), 10) {
    store.DeleteRefresh(id)
}
sess, plain, _ := fiberauth.NewRefreshSession(user.ID, user.TokenVersion, ua, 30*24*time.Hour)
store.InsertRefresh(sess)
c.Cookie(fiberauth.RefreshCookie(rtCookie, rtPath, sess.ID, plain, 30*24*time.Hour))
tok, _ := fiberauth.Issue(secret, user.ID, user.Roles, user.TokenVersion, 15*time.Minute)

// POST /api/auth/refresh (no AuthMiddleware):
id, plain, ok := fiberauth.SplitRefreshCookie(c.Cookies(rtCookie))
sess, found := store.GetRefresh(id)
if !ok || !found {
    return fiber.ErrUnauthorized
}
user := store.GetByID(sess.UID) // fresh roles + TokenVersion
switch fiberauth.CheckRefresh(sess, plain, user.TokenVersion, 10*time.Second) {
case fiberauth.RefreshOK:
    next, newPlain, _ := fiberauth.RotateRefreshSession(sess)
    // compare-and-swap: UPDATE ... WHERE id=$1 AND hashed_token=$old
    if store.SwapRefresh(next, sess.HashedToken) {
        c.Cookie(fiberauth.RefreshCookie(rtCookie, rtPath, next.ID, newPlain, time.Until(next.ExpiresAt)))
    } // lost the race: another tab rotated, treat as grace
case fiberauth.RefreshGrace:
    // concurrent tab already rotated, browser holds the new cookie
case fiberauth.RefreshReuse:
    log.Warn("refresh token reuse", "uid", sess.UID) // likely stolen
    fallthrough
default: // RefreshExpired, RefreshRevoked
    store.DeleteRefresh(id)
    c.Cookie(fiberauth.ExpiredRefreshCookie(rtCookie, rtPath))
    return fiber.ErrUnauthorized
}
tok, _ := fiberauth.Issue(secret, user.ID, user.Roles, user.TokenVersion, 15*time.Minute)

// Logout: store.DeleteRefresh(id) + ExpiredRefreshCookie.
// Logout-all: user.TokenVersion++ already revokes every refresh session.
Login throttling
guard := fiberauth.NewLoginThrottle(fiberauth.LoginThrottleConfig{})
// defaults: 5 fails/account, 20 fails/IP, 15m lockout, 1h TTL

if locked, retry := guard.Locked(login, fiberauth.ClientIP(c)); locked {
    c.Set("Retry-After", strconv.Itoa(int(retry.Seconds())))
    return fiber.NewError(fiber.StatusTooManyRequests, "too many attempts")
}
// on failure:
count := guard.Fail(login, fiberauth.ClientIP(c)) // count paces alert emails
// on success:
guard.Succeeded(login) // clears the account counter; IP budget ages out via TTL

// Simple keyed variant for signup/resend/forgot-password spam guards:
signupGuard := fiberauth.NewThrottle(time.Hour, 6*time.Hour)
signupGuard.Fail("ip:"+ip, 10)
TOTP 2FA
// Enrollment: generate, let the user scan the QR (uri), then verify
// one code BEFORE persisting the secret.
secret, uri, _ := fiberauth.GenerateTOTPSecret(fiberauth.TOTPConfig{Issuer: "My App"}, user.Login)
if !fiberauth.ValidateTOTPCode(secret, code) { /* reject */ }

plain, hashed, _ := fiberauth.GenerateBackupCodes(8)
// store `hashed`, show `plain` exactly once

// Login with backup-code fallback:
if fiberauth.ValidateTOTPCode(user.TOTPSecret, code) { /* ok */ }
if i, ok := fiberauth.CheckBackupCode(user.TOTPBackupCodes, code); ok {
    // single-use: remove index i and persist IMMEDIATELY (replay safety)
    user.TOTPBackupCodes = append(user.TOTPBackupCodes[:i], user.TOTPBackupCodes[i+1:]...)
}
Trusted devices (skip 2FA on known browsers)
id, plain, hashed, _ := fiberauth.NewTrustedDeviceToken()
user.TrustedDevices = append(fiberauth.PruneExpiredDevices(user.TrustedDevices),
    fiberauth.TrustedDevice{ID: id, HashedToken: hashed, Label: ua,
        CreatedAt: time.Now(), ExpiresAt: time.Now().Add(30 * 24 * time.Hour)})
c.Cookie(fiberauth.DeviceCookie("app_device", id, plain, 30*24*time.Hour))

// At login:
if _, ok := fiberauth.ValidateDeviceCookie(c.Cookies("app_device"), user.TrustedDevices); ok {
    // skip the TOTP step
}

// Revocation:
c.Cookie(fiberauth.ExpiredDeviceCookie("app_device"))
Single-use download tickets

Browser navigations (zip downloads, file streams) can't set an Authorization header, and putting the JWT in a URL leaks it into access logs. Mint a ticket instead:

tickets := fiberauth.NewTicketStore(60 * time.Second)

// authenticated endpoint:
tok, _ := tickets.Issue(uid, "folder/photos")
// client then navigates to /download?ticket=<tok>

// public endpoint:
uid, scope, ok := tickets.Consume(c.Query("ticket")) // consumed on first use, replay fails
Email codes (verification / password reset)
code, _ := fiberauth.GenerateCode() // crypto-random "042917"
// store code + expiry + attempts=0 on your user record, email it

st := fiberauth.CodeState{Code: u.VerifyCode, ExpiresAt: u.VerifyExpiresAt, Attempts: u.VerifyAttempts}
switch res, remaining := fiberauth.CheckCode(st, input, 5); res {
case fiberauth.CodeOK:      // clear the three fields, mark verified
case fiberauth.CodeExpired: // prompt a resend
case fiberauth.CodeLocked:  // attempt budget spent; only a fresh code unlocks
case fiberauth.CodeWrong:   // increment Attempts, persist; `remaining` left
}
// Bound to the current TokenVersion: bumping it after use kills replays,
// and AuthMiddleware never accepts a Purpose token as a session.
tok, _ := fiberauth.IssuePurpose(secret, user.ID, "pwreset", user.TokenVersion, 30*time.Minute)

claims, err := fiberauth.Parse(secret, tok)
if err != nil || claims.Purpose != "pwreset" || claims.Ver != user.TokenVersion { /* reject */ }
user.TokenVersion++ // burns the link and every other session
Mailer (Resend)
m := fiberauth.NewMailer(os.Getenv("RESEND_API_KEY"), "noreply@example.com")
if !m.Enabled() {
    log.Info("mail disabled, code for %s: %s", email, code) // dev fallback
}
_ = m.Send(email, "Your verification code", "<p>042917</p>")

Development

make check         # gofmt + vet + race tests
make install-hooks # pre-commit hook running the same

License

MIT

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

View Source
const (
	CtxUID   ctxKey = "uid"
	CtxRoles ctxKey = "roles"
	CtxVer   ctxKey = "ver"
)
View Source
const RoleAdmin = "admin"

RoleAdmin is the shared role-name convention across apps.

Variables

This section is empty.

Functions

func AdminOnly

func AdminOnly(c *fiber.Ctx) error

AdminOnly gates a route to tokens carrying RoleAdmin.

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

func CheckBackupCode(hashed []string, input string) (matchIndex int, ok bool)

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

func ClientIP(c *fiber.Ctx) string

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

func CookieDeviceID(cookieValue string) string

CookieDeviceID returns the device id embedded in a cookie value, or "" if absent/malformed.

func DeviceCookie

func DeviceCookie(name, id, plain string, ttl time.Duration) *fiber.Cookie

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

func ExpiredDeviceCookie(name string) *fiber.Cookie

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

func ExpiredRefreshCookie(name, path string) *fiber.Cookie

ExpiredRefreshCookie tells the browser to drop the refresh cookie. name and path must match RefreshCookie's.

func GenerateBackupCodes

func GenerateBackupCodes(n int) (plaintext []string, hashed []string, err error)

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

func GenerateCode() (string, error)

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 HasRole

func HasRole(c *fiber.Ctx, r string) bool

HasRole reports whether the request's token carries role r.

func HashPassword

func HashPassword(pw string) (string, error)

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 Issue

func Issue(secret, uid string, roles []string, ver int64, ttl time.Duration) (string, error)

func IssuePurpose

func IssuePurpose(secret, uid, purpose string, ver int64, ttl time.Duration) (string, error)

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

func NewTrustedDeviceToken() (id, plain, hashed string, err error)

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

func RefreshCookie(name, path, id, plain string, ttl time.Duration) *fiber.Cookie

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 Roles

func Roles(c *fiber.Ctx) []string

Roles returns the token's roles set by AuthMiddleware, or nil.

func SplitRefreshCookie added in v0.2.0

func SplitRefreshCookie(v string) (id, plain string, ok bool)

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 UID

func UID(c *fiber.Ctx) string

UID returns the authenticated user id set by AuthMiddleware, or "".

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

func ValidateTOTPCode(secret, code string) bool

ValidateTOTPCode checks a 6-digit authenticator code against secret.

func Ver

func Ver(c *fiber.Ctx) int64

Ver returns the token's version claim set by AuthMiddleware, or 0.

func VerifyPassword

func VerifyPassword(hash, pw string) bool

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
}

func Parse

func Parse(secret, token string) (*Claims, error)

func (*Claims) HasRole

func (c *Claims) HasRole(r string) bool

HasRole reports whether the token's Roles claim carries r, the token is the sole source of truth for role checks on a request (see AuthMiddleware), never re-checked against the user store per-request.

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

func CheckCode(st CodeState, input string, maxAttempts int) (result CodeCheck, remaining int)

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

type CodeState struct {
	Code      string
	ExpiresAt *time.Time
	Attempts  int
}

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 LoginThrottleConfig struct {
	LockFor       time.Duration // lockout duration once tripped (default 15m)
	TTL           time.Duration // idle entries pruned after this (default 1h)
	LoginMaxFails int           // per-account threshold (default 5)
	IPMaxFails    int           // per-IP threshold, higher: shared NATs (default 20)
}

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.

func NewMailer

func NewMailer(apiKey, from string) *Mailer

func (*Mailer) Enabled

func (m *Mailer) Enabled() bool

func (*Mailer) Send

func (m *Mailer) Send(to, subject, html string) error

Send emails to via Resend, HTML body. No-op (nil error) when APIKey is empty.

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 NewThrottle(lockFor, ttl time.Duration) *Throttle

func (*Throttle) Fail

func (t *Throttle) Fail(key string, maxFails int) int

Fail records a failed attempt for key and returns the running count. Once the count reaches maxFails the key is locked for lockFor.

func (*Throttle) Locked

func (t *Throttle) Locked(key string) (bool, time.Duration)

Locked reports whether key is currently locked out and, if so, how long until it may try again.

func (*Throttle) Reset

func (t *Throttle) Reset(key string)

Reset clears any failure state for key (called on successful auth).

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

func (*TicketStore) Consume

func (d *TicketStore) Consume(tok string) (uid, scope string, ok bool)

Consume validates a ticket and returns its bound identity. The ticket is deleted unconditionally on lookup (single-use), so a replay, even within the TTL, fails.

func (*TicketStore) Issue

func (d *TicketStore) Issue(uid, scope string) (string, error)

Issue mints a ticket bound to uid+scope. The window is short because the client navigates to the target URL immediately after minting.

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL