Documentation
¶
Overview ¶
Package auth implements user authentication with JWT access tokens + opaque refresh tokens.
Design:
- Access token: stateless JWT, short TTL (15min). Validated cryptographically per-request.
- Refresh token: opaque random string, long TTL (1 day normal, 30 days "remember me"), stored hashed in DB so we can revoke (logout = delete row).
- Login returns both. Frontend hits /refresh when access expires to get a new pair (rolling refresh).
Package auth implements user authentication with JWT access tokens + opaque refresh tokens.
Design:
- Access token: stateless JWT, short TTL (15min). Validated cryptographically per-request.
- Refresh token: opaque random string, long TTL (1 day normal, 30 days "remember me"), stored hashed in DB so we can revoke (logout = delete row).
- Login returns both. Frontend hits /refresh when access expires to get a new pair (rolling refresh).
Index ¶
- Constants
- func AdminOnly() gin.HandlerFunc
- func GenerateTOTPSecret() (string, error)
- func GuestRestrict() gin.HandlerFunc
- func Optional(tm *TokenManager) gin.HandlerFunc
- func Required(tm *TokenManager) gin.HandlerFunc
- func TOTPURI(secret, issuer, account string) string
- func UserIDFromCtx(c *gin.Context) (int, bool, bool)
- func ValidateTOTP(secret, code string) bool
- type Claims
- type Lockout
- type RefreshOutcome
- type Role
- type SessionInfo
- type Status
- type Store
- func (s *Store) AddCredential(userID int, cred *webauthn.Credential) error
- func (s *Store) Bootstrap(adminUser, adminPass string) error
- func (s *Store) ChangePassword(userID int, current, new string) error
- func (s *Store) CleanupExpired() error
- func (s *Store) ConsumeBackupCode(userID int, code string) bool
- func (s *Store) ConsumeRefreshToken(plain string) error
- func (s *Store) ConsumeRefreshTokenOnce(plain string) (bool, error)
- func (s *Store) ConsumeToken(plain, purpose string) (*TokenInfo, error)
- func (s *Store) CountBackupCodes(userID int) int
- func (s *Store) CreateRefreshToken(userID int, ttl time.Duration, remember bool, userAgent, ip string) (string, error)
- func (s *Store) CreateToken(purpose string, userID int, email string, ttl time.Duration) (string, error)
- func (s *Store) CreateUser(username, password string, role Role) (int, error)
- func (s *Store) CreateUserFull(username, email, password string, role Role, status Status) (int, error)
- func (s *Store) Credentials(userID int) ([]webauthn.Credential, error)
- func (s *Store) DeleteCredential(userID int, credIDB64 string) error
- func (s *Store) DeleteUser(id int) error
- func (s *Store) DisableTOTP(userID int) error
- func (s *Store) EmailInUse(email string, excludeID int) (bool, error)
- func (s *Store) EnableTOTP(userID int) error
- func (s *Store) Exists(username, email string) (bool, error)
- func (s *Store) GenerateBackupCodes(userID, n int) ([]string, error)
- func (s *Store) GetTOTPSecret(userID int) (secret string, enabled bool, err error)
- func (s *Store) GetUserByEmail(email string) (*User, error)
- func (s *Store) GetUserByID(id int) (*User, error)
- func (s *Store) GetUserByUsername(username string) (*User, error)
- func (s *Store) HasPasskey(userID int) bool
- func (s *Store) ListSessions(userID int, currentPlain string) ([]SessionInfo, error)
- func (s *Store) ListUsers() ([]User, error)
- func (s *Store) RevokeAllSessions(userID int) error
- func (s *Store) RevokeOtherSessions(userID int, currentPlain string) (int, error)
- func (s *Store) RevokeSession(userID int, id string) error
- func (s *Store) RotateRefreshToken(plain string, grace time.Duration) (*User, bool, RefreshOutcome, error)
- func (s *Store) SetEmailVerified(userID int, promoteTo Status) error
- func (s *Store) SetNtfyTopic(userID int, topic string) error
- func (s *Store) SetPassword(userID int, password string) error
- func (s *Store) SetStatus(userID int, status Status) error
- func (s *Store) SetTOTPSecret(userID int, secret string) error
- func (s *Store) UpdateCredential(cred *webauthn.Credential) error
- func (s *Store) UpdateEmail(userID int, email string) error
- func (s *Store) ValidateRefreshToken(plain string) (*User, bool, error)
- func (s *Store) VerifyPassword(username, password string) (*User, error)
- type TokenInfo
- type TokenManager
- type User
- type WAManager
- func (m *WAManager) BeginLogin(id int, name string, creds []webauthn.Credential) (*protocol.CredentialAssertion, string, error)
- func (m *WAManager) BeginRegister(id int, name string, creds []webauthn.Credential) (*protocol.CredentialCreation, string, error)
- func (m *WAManager) FinishLogin(id int, name string, creds []webauthn.Credential, sessionID string, ...) (*webauthn.Credential, error)
- func (m *WAManager) FinishRegister(id int, name string, creds []webauthn.Credential, sessionID string, ...) (*webauthn.Credential, error)
Constants ¶
const ( HeaderAuthorization = "Authorization" BearerPrefix = "Bearer " )
const ( TokenInvite = "invite" // authorizes a registration (no user yet) TokenVerifyEmail = "verify_email" // confirms a user's email address TokenResetPassword = "reset_password" // password recovery )
Token purposes — single-use, TTL'd links sent by email (or copied by an admin).
const ScopeMedia = "media"
ScopeMedia marca tokens emitidos por SignMedia — usados como ?token= em <video>/<track>/<img> que precisam sobreviver a refreshes do access token regular durante uma sessão de playback longa.
Variables ¶
This section is empty.
Functions ¶
func AdminOnly ¶
func AdminOnly() gin.HandlerFunc
AdminOnly aborts with 403 unless the request was authenticated as an admin. Must be chained after Required.
func GenerateTOTPSecret ¶
GenerateTOTPSecret returns a fresh base32 secret (no padding) for enrollment.
func GuestRestrict ¶
func GuestRestrict() gin.HandlerFunc
GuestRestrict blocks mutating methods (POST, DELETE, PUT, PATCH) for guests. Playback-only mutations under /api/stream are allowlisted via guestStreamAllowed; self-service account management via guestAuthSelfAllowed. /api/local/file is NOT exempt: its only mutating method is DELETE (LocalDelete), which a read-only guest must never reach. GET on any media route is already unaffected (it isn't a mutating method).
func Optional ¶
func Optional(tm *TokenManager) gin.HandlerFunc
Optional attaches claims if a valid token is present but never blocks. Useful for endpoints where behavior changes based on auth state (e.g., admin sees more). Aplica o mesmo gate de scope que Required pra evitar elevação de privilégio silenciosa via media token em rotas sensíveis.
func Required ¶
func Required(tm *TokenManager) gin.HandlerFunc
Required is the Gin middleware that rejects requests without a valid Bearer token. On success, the parsed Claims are attached to the context and available via FromCtx. Media tokens (scope="media") only valem em rotas de mídia chamadas via ?token=; rejeitadas aqui mesmo que a assinatura seja válida.
func TOTPURI ¶
TOTPURI builds the otpauth:// URI that authenticator apps consume (also used to render a QR). issuer/account label the entry in the app.
func UserIDFromCtx ¶
UserIDFromCtx returns (userID, isAdmin, isAuthenticated). Use in handlers that filter by ownership.
func ValidateTOTP ¶
ValidateTOTP checks a code against the secret, allowing ±1 step (clock skew / the user typing as the window rolls).
Types ¶
type Claims ¶
type Claims struct {
UserID int `json:"uid"`
Username string `json:"u"`
Role Role `json:"r"`
// Scope distingue access token regular ("") de tokens especiais. Hoje só
// "media" — TTL longo, válido apenas em rotas servidas via ?token=
// (isMediaPath). Middleware Required rejeita tokens com scope="media"
// pra impedir uso em rotas sensíveis via header Authorization.
Scope string `json:"scope,omitempty"`
jwt.RegisteredClaims
}
Claims is what we encode inside the JWT access token.
type Lockout ¶
type Lockout struct {
MaxFailures int
LockWindow time.Duration
// contains filtered or unexported fields
}
Lockout is an in-memory brute-force guard keyed by username. After MaxFailures consecutive failed login attempts the key is locked for LockDuration. A successful login (or the lock expiring) resets the counter.
In-memory is deliberate: a single-instance self-hosted app doesn't need a shared store, and losing the state on restart only ever HELPS a legitimate user (a restart clears a lock) — it never weakens the guard against a live attacker, who can't trigger restarts.
func NewLockout ¶
NewLockout builds a limiter. maxFailures<=0 disables locking entirely.
type RefreshOutcome ¶
type RefreshOutcome int
RefreshOutcome is the decision of a rotation attempt (RotateRefreshToken).
const ( RefreshInvalid RefreshOutcome = iota // unknown or expired token RefreshRotated // we won the race; the token was consumed now RefreshGraceReissue // recently consumed by a concurrent refresh — reissue, don't revoke RefreshReuse // consumed long ago and presented again — treat as theft )
type SessionInfo ¶
type SessionInfo struct {
ID string `json:"id"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
Remember bool `json:"remember"`
Current bool `json:"current"`
UserAgent string `json:"userAgent"`
IP string `json:"ip"`
}
SessionInfo is one active refresh-token session, safe to show its owner. ID is the token_hash — exposing the HASH to the authenticated owner is harmless (it can't be used to authenticate, only to revoke that same session).
type Status ¶
type Status string
Status is the account lifecycle state. Only "active" users may log in.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store wraps the PostgreSQL-backed user + refresh token persistence.
func New ¶
New wires the auth store onto the shared Postgres pool. The schema is applied centrally (internal/db migrations), so there's no per-store migrate here.
func (*Store) AddCredential ¶
func (s *Store) AddCredential(userID int, cred *webauthn.Credential) error
AddCredential persists a newly-registered passkey for a user.
func (*Store) Bootstrap ¶
Bootstrap ensures an admin user exists. If no users at all, creates "admin" with the given password. Use this once at startup with the password from config/env.
func (*Store) ChangePassword ¶
ChangePassword verifies the current password and sets a new one (self-service).
func (*Store) CleanupExpired ¶
CleanupExpired removes refresh tokens past their TTL, plus soft-consumed tokens older than an hour (well past the rotation grace window) so they don't linger until their original TTL. Call periodically.
func (*Store) ConsumeBackupCode ¶
ConsumeBackupCode validates a code for a user and marks it used (single-use). Returns true on a successful redemption.
func (*Store) ConsumeRefreshToken ¶
ConsumeRefreshToken deletes a refresh token (use on logout, or rolling rotation).
func (*Store) ConsumeRefreshTokenOnce ¶
ConsumeRefreshTokenOnce atomically deletes a refresh token and reports whether THIS call was the one that removed it (RowsAffected == 1). Used by rotation to close the validate-then-delete TOCTOU: with two concurrent refreshes of the same token, only one gets `true` and may issue a new pair; the loser gets `false` (the token was already consumed) and must be rejected.
func (*Store) ConsumeToken ¶
ConsumeToken validates a token for the given purpose (exists, right purpose, not used, not expired) and marks it used (single-use). Returns its payload.
func (*Store) CountBackupCodes ¶
CountBackupCodes returns how many unused backup codes a user has left.
func (*Store) CreateRefreshToken ¶
func (s *Store) CreateRefreshToken(userID int, ttl time.Duration, remember bool, userAgent, ip string) (string, error)
CreateRefreshToken generates a fresh random token, stores its hash, returns the plain string. `remember` controls TTL behavior on refresh: when true, every successful refresh re-extends the expiration by 30 days from now (sliding window — only logs out after 30d of inactivity). userAgent/ip identify the creating device so sessions are recognizable in the UI.
func (*Store) CreateToken ¶
func (s *Store) CreateToken(purpose string, userID int, email string, ttl time.Duration) (string, error)
CreateToken issues a single-use token for a purpose (invite/verify/reset) and returns the PLAINTEXT (only its SHA-256 is stored). userID 0 → NULL row.
func (*Store) CreateUser ¶
CreateUser hashes the password and inserts a new user. Returns the inserted ID.
func (*Store) CreateUserFull ¶
func (s *Store) CreateUserFull(username, email, password string, role Role, status Status) (int, error)
CreateUserFull creates a user with email + lifecycle status (used by the registration flow). Returns the new id. Username uniqueness is enforced by the table; email uniqueness is checked by the caller (Register handler).
func (*Store) Credentials ¶
func (s *Store) Credentials(userID int) ([]webauthn.Credential, error)
Credentials returns all passkeys registered by a user (empty slice if none).
func (*Store) DeleteCredential ¶
DeleteCredential removes one passkey (by base64url id) owned by a user.
func (*Store) DeleteUser ¶
DeleteUser removes a user (and cascades refresh tokens via FK).
func (*Store) DisableTOTP ¶
DisableTOTP clears the secret + disables MFA, and drops any backup codes (they're meaningless once MFA is off).
func (*Store) EmailInUse ¶
EmailInUse reports whether a non-empty email belongs to any user other than excludeID (so changing the case of your own address never collides).
func (*Store) EnableTOTP ¶
EnableTOTP marks MFA active (after the user confirms a code during enrollment).
func (*Store) GenerateBackupCodes ¶
GenerateBackupCodes replaces a user's backup codes with n fresh ones and returns the PLAINTEXT (formatted "xxxx-xxxx") — shown once, never recoverable.
func (*Store) GetTOTPSecret ¶
GetTOTPSecret returns the stored secret + whether MFA is enabled.
func (*Store) GetUserByEmail ¶
GetUserByEmail returns the (verified-or-not) user with a given email, or nil when none. Used by password recovery. Empty email never matches.
func (*Store) GetUserByID ¶
GetUserByID is used by middleware after JWT validation to load current user state.
func (*Store) GetUserByUsername ¶
GetUserByUsername loads a user by login name (no password check). Used by the passkey login flow, which authenticates via the authenticator assertion rather than a password. Returns nil when no such user.
func (*Store) HasPasskey ¶
HasPasskey reports whether a user has at least one registered passkey.
func (*Store) ListSessions ¶
func (s *Store) ListSessions(userID int, currentPlain string) ([]SessionInfo, error)
ListSessions returns a user's active sessions, newest first. currentPlain (the caller's own refresh token, may be empty) flags which row is "this device".
func (*Store) RevokeAllSessions ¶
RevokeAllSessions deletes every session for a user (used when an admin disables the account so existing logins can't keep refreshing).
func (*Store) RevokeOtherSessions ¶
RevokeOtherSessions deletes every session for a user EXCEPT the caller's own (identified by currentPlain). Returns how many were dropped.
func (*Store) RevokeSession ¶
RevokeSession deletes one session by its id (token_hash), scoped to the owner so a user can't revoke another account's session.
func (*Store) RotateRefreshToken ¶
func (s *Store) RotateRefreshToken(plain string, grace time.Duration) (*User, bool, RefreshOutcome, error)
RotateRefreshToken atomically decides what to do with a presented refresh token, replacing the validate-then-consume sequence in the handler:
- Invalid: unknown/expired token → reject (no revoke).
- Rotated: the token was active and THIS call consumed it → issue a fresh pair.
- GraceReissue: the token was consumed within `grace` (a concurrent refresh from another tab, or the request burst when the backend returns from a deploy) → issue a fresh pair WITHOUT revoking. This is what stops the re-login-after-deploy: the loser of a concurrent rotation no longer nukes the whole session family.
- Reuse: the token was consumed BEFORE the grace window → a real replay of a rotated (possibly stolen) token → caller revokes all sessions.
Returns the owning user + remember flag for the issue-tokens outcomes.
func (*Store) SetEmailVerified ¶
SetEmailVerified flips a user's email_verified flag (after they click the confirmation link). Optionally promotes the account to a new status (an invited user becomes active on confirmation).
func (*Store) SetNtfyTopic ¶
SetNtfyTopic updates a user's ntfy.sh notification topic.
func (*Store) SetPassword ¶
SetPassword overwrites a user's password hash (used by ChangePassword + reset).
func (*Store) SetStatus ¶
SetStatus changes an account's lifecycle state (approve/disable/re-enable).
func (*Store) SetTOTPSecret ¶
SetTOTPSecret stores a (not-yet-enabled) TOTP secret during enrollment.
func (*Store) UpdateCredential ¶
func (s *Store) UpdateCredential(cred *webauthn.Credential) error
UpdateCredential rewrites a credential after a successful login (the sign counter advances and must be persisted to detect cloned authenticators).
func (*Store) UpdateEmail ¶
UpdateEmail changes a user's email and resets email_verified — the new address must be (re)confirmed via the verify-email link.
func (*Store) ValidateRefreshToken ¶
ValidateRefreshToken looks up a token, checks expiry, returns the owning user plus the `remember` flag that the session was created with.
type TokenInfo ¶
type TokenInfo struct {
UserID int // 0 when the token isn't tied to a user (invites)
Email string // optional pre-set email (invites)
Purpose string
}
TokenInfo is the resolved payload of a consumed single-use token.
type TokenManager ¶
type TokenManager struct {
// contains filtered or unexported fields
}
TokenManager signs and validates access tokens with HMAC-SHA256.
func NewTokenManager ¶
func NewTokenManager(secret []byte, accessTTL time.Duration) *TokenManager
NewTokenManager — secret must be at least 32 random bytes for HS256 to be safe. accessTTL controls how often the frontend must hit /refresh. mediaTTL é o TTL dos tokens de mídia (SignMedia); default 6h se zero.
func (*TokenManager) ParseAccess ¶
func (t *TokenManager) ParseAccess(raw string) (*Claims, error)
ParseAccess validates the JWT and returns its claims. Returns error if expired or tampered.
func (*TokenManager) SetMediaTTL ¶
func (t *TokenManager) SetMediaTTL(d time.Duration)
SetMediaTTL ajusta o TTL dos media tokens. 0 = default 6h.
func (*TokenManager) SignAccess ¶
SignAccess creates a new short-lived access JWT for the user.
func (*TokenManager) SignMedia ¶
SignMedia emite um JWT scope="media" com TTL longo, pra ser usado em URLs de mídia (<video src>, <track src>) que sobrevivem ao refresh do access token regular durante uma sessão de playback. Carrega as mesmas claims de usuário que SignAccess pra que os handlers continuem identificando o requester. NÃO é aceito em rotas que usam header Authorization (ver middleware Required).
type User ¶
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Role Role `json:"role"`
Status Status `json:"status"`
EmailVerified bool `json:"emailVerified"`
MfaEnabled bool `json:"mfaEnabled"`
NtfyTopic string `json:"ntfyTopic"`
CreatedAt time.Time `json:"createdAt"`
}
User is the public, password-less representation.
type WAManager ¶
type WAManager struct {
// contains filtered or unexported fields
}
WAManager wraps go-webauthn plus a short-lived in-memory store for the challenge (SessionData) that bridges the begin/finish steps of each ceremony. Sessions are keyed by an opaque id returned to the client and echoed back.
func NewWAManager ¶
NewWAManager builds the manager. rpID is the effective domain (no scheme/port, e.g. "jackui.example.com"); origin is the full URL the browser uses (e.g. "https://jackui.example.com"). Returns nil if config is incomplete.
func (*WAManager) BeginLogin ¶
func (m *WAManager) BeginLogin(id int, name string, creds []webauthn.Credential) (*protocol.CredentialAssertion, string, error)
BeginLogin starts a passkey assertion for a known user.
func (*WAManager) BeginRegister ¶
func (m *WAManager) BeginRegister(id int, name string, creds []webauthn.Credential) (*protocol.CredentialCreation, string, error)
BeginRegister starts adding a passkey. Returns the creation options (to pass to navigator.credentials.create) and a session id to echo back on finish.
func (*WAManager) FinishLogin ¶
func (m *WAManager) FinishLogin(id int, name string, creds []webauthn.Credential, sessionID string, r *http.Request) (*webauthn.Credential, error)
FinishLogin verifies the assertion; returns the matched credential (with an updated sign count to persist).
func (*WAManager) FinishRegister ¶
func (m *WAManager) FinishRegister(id int, name string, creds []webauthn.Credential, sessionID string, r *http.Request) (*webauthn.Credential, error)
FinishRegister verifies the attestation in the request body against the saved session and returns the new credential to persist.