auth

package
v0.97.2 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 24 Imported by: 0

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

View Source
const (
	HeaderAuthorization = "Authorization"
	BearerPrefix        = "Bearer "
)
View Source
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).

View Source
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

func GenerateTOTPSecret() (string, error)

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

func TOTPURI(secret, issuer, account string) string

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

func UserIDFromCtx(c *gin.Context) (int, bool, bool)

UserIDFromCtx returns (userID, isAdmin, isAuthenticated). Use in handlers that filter by ownership.

func ValidateTOTP

func ValidateTOTP(secret, code string) bool

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.

func ClaimsFromCtx

func ClaimsFromCtx(c *gin.Context) (*Claims, bool)

ClaimsFromCtx retrieves the authenticated Claims previously attached by Required/Optional.

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

func NewLockout(maxFailures int, lockWindow time.Duration) *Lockout

NewLockout builds a limiter. maxFailures<=0 disables locking entirely.

func (*Lockout) Fail

func (l *Lockout) Fail(key string)

Fail records a failed attempt, locking the key once it hits MaxFailures.

func (*Lockout) Locked

func (l *Lockout) Locked(key string) (bool, time.Duration)

Locked reports whether key is currently locked and, if so, how long remains.

func (*Lockout) Reset

func (l *Lockout) Reset(key string)

Reset clears all failure state for a key (call on a successful login).

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 Role

type Role string

Role identifies a user's authorization level.

const (
	RoleAdmin Role = "admin"
	RoleUser  Role = "user"
	RoleGuest Role = "guest"
)

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.

const (
	StatusActive   Status = "active"   // can log in
	StatusPending  Status = "pending"  // self-registered, awaiting admin approval
	StatusDisabled Status = "disabled" // blocked by an admin
)

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store wraps the PostgreSQL-backed user + refresh token persistence.

func New

func New(pool *sql.DB) (*Store, error)

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

func (s *Store) Bootstrap(adminUser, adminPass string) error

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

func (s *Store) ChangePassword(userID int, current, new string) error

ChangePassword verifies the current password and sets a new one (self-service).

func (*Store) CleanupExpired

func (s *Store) CleanupExpired() error

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) Close

func (s *Store) Close()

Close is a no-op: the shared pool's lifecycle is owned by main.

func (*Store) ConsumeBackupCode

func (s *Store) ConsumeBackupCode(userID int, code string) bool

ConsumeBackupCode validates a code for a user and marks it used (single-use). Returns true on a successful redemption.

func (*Store) ConsumeRefreshToken

func (s *Store) ConsumeRefreshToken(plain string) error

ConsumeRefreshToken deletes a refresh token (use on logout, or rolling rotation).

func (*Store) ConsumeRefreshTokenOnce

func (s *Store) ConsumeRefreshTokenOnce(plain string) (bool, error)

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

func (s *Store) ConsumeToken(plain, purpose string) (*TokenInfo, error)

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

func (s *Store) CountBackupCodes(userID int) int

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

func (s *Store) CreateUser(username, password string, role Role) (int, error)

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

func (s *Store) DeleteCredential(userID int, credIDB64 string) error

DeleteCredential removes one passkey (by base64url id) owned by a user.

func (*Store) DeleteUser

func (s *Store) DeleteUser(id int) error

DeleteUser removes a user (and cascades refresh tokens via FK).

func (*Store) DisableTOTP

func (s *Store) DisableTOTP(userID int) error

DisableTOTP clears the secret + disables MFA, and drops any backup codes (they're meaningless once MFA is off).

func (*Store) EmailInUse

func (s *Store) EmailInUse(email string, excludeID int) (bool, error)

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

func (s *Store) EnableTOTP(userID int) error

EnableTOTP marks MFA active (after the user confirms a code during enrollment).

func (*Store) Exists

func (s *Store) Exists(username, email string) (bool, error)

Exists reports whether a username or (non-empty) email is already taken.

func (*Store) GenerateBackupCodes

func (s *Store) GenerateBackupCodes(userID, n int) ([]string, error)

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

func (s *Store) GetTOTPSecret(userID int) (secret string, enabled bool, err error)

GetTOTPSecret returns the stored secret + whether MFA is enabled.

func (*Store) GetUserByEmail

func (s *Store) GetUserByEmail(email string) (*User, error)

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

func (s *Store) GetUserByID(id int) (*User, error)

GetUserByID is used by middleware after JWT validation to load current user state.

func (*Store) GetUserByUsername

func (s *Store) GetUserByUsername(username string) (*User, error)

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

func (s *Store) HasPasskey(userID int) bool

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) ListUsers

func (s *Store) ListUsers() ([]User, error)

ListUsers returns all users (admin only).

func (*Store) RevokeAllSessions

func (s *Store) RevokeAllSessions(userID int) error

RevokeAllSessions deletes every session for a user (used when an admin disables the account so existing logins can't keep refreshing).

func (*Store) RevokeOtherSessions

func (s *Store) RevokeOtherSessions(userID int, currentPlain string) (int, error)

RevokeOtherSessions deletes every session for a user EXCEPT the caller's own (identified by currentPlain). Returns how many were dropped.

func (*Store) RevokeSession

func (s *Store) RevokeSession(userID int, id string) error

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

func (s *Store) SetEmailVerified(userID int, promoteTo Status) error

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

func (s *Store) SetNtfyTopic(userID int, topic string) error

SetNtfyTopic updates a user's ntfy.sh notification topic.

func (*Store) SetPassword

func (s *Store) SetPassword(userID int, password string) error

SetPassword overwrites a user's password hash (used by ChangePassword + reset).

func (*Store) SetStatus

func (s *Store) SetStatus(userID int, status Status) error

SetStatus changes an account's lifecycle state (approve/disable/re-enable).

func (*Store) SetTOTPSecret

func (s *Store) SetTOTPSecret(userID int, secret string) error

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

func (s *Store) UpdateEmail(userID int, email string) error

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

func (s *Store) ValidateRefreshToken(plain string) (*User, bool, error)

ValidateRefreshToken looks up a token, checks expiry, returns the owning user plus the `remember` flag that the session was created with.

func (*Store) VerifyPassword

func (s *Store) VerifyPassword(username, password string) (*User, error)

VerifyPassword loads a user by username and checks bcrypt against the supplied password. Returns the User on match.

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

func (t *TokenManager) SignAccess(u *User) (string, time.Time, error)

SignAccess creates a new short-lived access JWT for the user.

func (*TokenManager) SignMedia

func (t *TokenManager) SignMedia(u *User) (string, time.Time, error)

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

func NewWAManager(rpID, rpDisplayName, origin string) (*WAManager, error)

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.

Jump to

Keyboard shortcuts

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