Documentation
¶
Overview ¶
Package theauth provides session-based authentication for Go applications.
TheAuth ships magic-link email auth, opaque session tokens with revocation, and chi-friendly middleware. Storage backends include in-memory and Postgres (pgx + sqlc). OAuth providers, TOTP, WebAuthn, and MCP OAuth 2.1 land in future versions — see the README roadmap.
Index ¶
- Constants
- Variables
- type Config
- type MagicLink
- type PasswordResetToken
- type Session
- type Storage
- type TheAuth
- func (a *TheAuth) Authn() func(http.Handler) http.Handler
- func (a *TheAuth) Mount(r chi.Router)
- func (a *TheAuth) RateLimitByEmail(perMinute int) func(http.Handler) http.Handler
- func (a *TheAuth) RateLimitByIP(perMinute int) func(http.Handler) http.Handler
- func (a *TheAuth) RequireAuth() func(http.Handler) http.Handler
- type TheAuthError
- type ULID
- type User
Constants ¶
const ( CodeWeakPassword = "weak_password" CodeEmailTaken = "email_taken" CodeInvalidCredentials = "invalid_credentials" CodeRateLimited = "rate_limited" CodePasswordResetExpired = "password_reset_expired" CodePasswordResetInvalid = "password_reset_invalid" )
Stable error codes that callers can switch on. New endpoints return TheAuthError; old endpoints keep returning the sentinels above.
const MinPasswordLength = 12
MinPasswordLength is enforced at the library level (NIST 2024 baseline). No composition rules — NIST recommends against them. Consumers wanting stricter policies should layer them on top.
const PasswordResetTTL = time.Hour
PasswordResetTTL is how long a reset token stays valid after issuance. Reset tokens are single-use; this is also enforced atomically in storage.
Variables ¶
var ( ErrInvalidToken = errors.New("theauth: invalid token") ErrSessionExpired = errors.New("theauth: session expired") ErrUserNotFound = errors.New("theauth: user not found") ErrMagicLinkExpired = errors.New("theauth: magic link expired") ErrMagicLinkUsed = errors.New("theauth: magic link already used") ErrEmailNotVerified = errors.New("theauth: email not verified") // ErrStorageNotFound is the canonical "row missing" sentinel that storage // adapters return on lookup misses. Lives in the root package so service // code can errors.Is-check without importing the storage package // (which would create an import cycle). ErrStorageNotFound = errors.New("theauth: storage row not found") )
Sentinel errors — retained for backward compatibility with v0.1 callers that errors.Is-check against them. New code should prefer TheAuthError + the Code* constants below.
Functions ¶
This section is empty.
Types ¶
type Config ¶
type Config struct {
Storage Storage
EmailSender email.Sender
BaseURL string
SigningKey ed25519.PrivateKey
SessionTTL time.Duration
MagicLinkTTL time.Duration
CookieName string
SecureCookie bool
// RateLimitPerIP is the per-IP per-minute budget applied to credential
// endpoints (signup/signin/forgot/reset). Defaults to 5 when zero.
RateLimitPerIP int
// RateLimitPerEmail is the per-email per-minute budget applied to signin
// + forgot. Defaults to 3 when zero.
RateLimitPerEmail int
}
Config holds the wiring for a TheAuth instance.
Storage and BaseURL are required. Everything else has sensible defaults applied by New: SessionTTL=24h, MagicLinkTTL=15m, CookieName="theauth_session", EmailSender=email.Noop{}. SigningKey is reserved for future JWT signing (v0.2+); v0.1 uses opaque tokens and leaves the field nil.
type PasswordResetToken ¶ added in v0.2.0
type PasswordResetToken struct {
ID ULID `json:"id"`
UserID ULID `json:"userId"`
TokenHash []byte `json:"-"`
ExpiresAt time.Time `json:"expiresAt"`
UsedAt *time.Time `json:"usedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
PasswordResetToken backs the /auth/email-password/forgot+reset flow. Shape mirrors MagicLink but binds to a known user_id (resets always operate on an existing account), and lives in its own table to keep flows isolated.
type Session ¶
type Session struct {
ID ULID `json:"id"`
UserID ULID `json:"userId"`
TokenHash []byte `json:"-"` // never serialize raw hash
UserAgent string `json:"userAgent"`
IP string `json:"ip"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
RevokedAt *time.Time `json:"revokedAt,omitempty"`
}
func SessionFromContext ¶
SessionFromContext returns the Session attached by Authn middleware, if any. Returns false when the request is anonymous.
type Storage ¶
type Storage interface {
// Users
CreateUser(ctx context.Context, u User) (User, error)
UserByEmail(ctx context.Context, email string) (*User, error)
UserByID(ctx context.Context, id ULID) (*User, error)
MarkEmailVerified(ctx context.Context, userID ULID) error
// Sessions
CreateSession(ctx context.Context, s Session) (Session, error)
SessionByTokenHash(ctx context.Context, hash []byte) (*Session, error)
RevokeSession(ctx context.Context, id ULID) error
RevokeUserSessions(ctx context.Context, userID ULID) error
// Magic links
CreateMagicLink(ctx context.Context, ml MagicLink) error
ConsumeMagicLink(ctx context.Context, tokenHash []byte) (*MagicLink, error)
// Email + password (v0.2)
SetUserPassword(ctx context.Context, userID ULID, passwordHash string) error
// UserByEmailWithPassword fetches a user along with their stored PHC hash.
// passwordHash is "" if the account exists but has never set a password
// (e.g. magic-link-only signup). Callers should treat empty hash as
// "no password credential available" and surface invalid_credentials.
UserByEmailWithPassword(ctx context.Context, email string) (user *User, passwordHash string, err error)
CreatePasswordResetToken(ctx context.Context, t PasswordResetToken) error
ConsumePasswordResetToken(ctx context.Context, tokenHash []byte) (*PasswordResetToken, error)
}
Storage is the persistence contract TheAuth depends on. Adapters live in sub-packages (storage/memory, storage/postgres). Defined here so that service code in this package can reference it without importing the storage sub-package (which would create an import cycle, because storage imports this package for the model types).
The storage package re-exports this as storage.Storage so consumers can keep importing it from the conventional location.
type TheAuth ¶
type TheAuth struct {
// contains filtered or unexported fields
}
TheAuth is the public entry point — constructed once at app start and shared across handlers.
func (*TheAuth) Authn ¶
Authn looks for a session cookie, validates it, and adds the user + session to the request context. Does NOT reject anonymous requests — pair with RequireAuth.
func (*TheAuth) Mount ¶
Mount wires TheAuth's HTTP routes onto the supplied chi router under /auth. Routes:
POST /auth/magic-link request a magic link GET /auth/magic-link/verify consume a magic link, set session cookie POST /auth/email-password/signup create user with email + password (rate-limited) POST /auth/email-password/signin sign in with email + password (rate-limited) POST /auth/email-password/forgot request a password reset link (rate-limited) POST /auth/email-password/reset consume a reset token + set new password (rate-limited) GET /auth/me return the authenticated user (RequireAuth) DELETE /auth/sessions/current revoke the current session (RequireAuth)
Default rate limits: 5/min per source IP on every credential endpoint, plus 3/min per email on signin + forgot (most attack-surface). All limits are in-memory + per-process; replace at the LB layer for multi-instance deploys.
func (*TheAuth) RateLimitByEmail ¶ added in v0.2.0
RateLimitByEmail returns a middleware that limits requests per email body field. Reads the JSON body up to 16 KiB, extracts "email", restores the body so downstream handlers can re-read it. Requests without a parseable email are passed through unlimited (handler will reject them on its own).
func (*TheAuth) RateLimitByIP ¶ added in v0.2.0
RateLimitByIP returns a middleware that limits requests per source IP to perMinute per minute. Use on credential endpoints (signin, signup, forgot, reset). The limiter lives on the returned handler — multiple calls produce independent buckets, so wire it once per route group at startup.
type TheAuthError ¶ added in v0.2.0
TheAuthError is the structured error type returned by v0.2+ service methods. Callers can errors.As-extract it and switch on Code for stable handling, or errors.Is-check against a value of the same Code for shorter paths.
func NewError ¶ added in v0.2.0
func NewError(code, message string, inner error) *TheAuthError
NewError constructs a TheAuthError with the supplied code, message, and optional wrapped cause.
func (*TheAuthError) Error ¶ added in v0.2.0
func (e *TheAuthError) Error() string
func (*TheAuthError) Is ¶ added in v0.2.0
func (e *TheAuthError) Is(target error) bool
Is reports whether target is a *TheAuthError with the same Code, OR is the Inner cause. This lets callers do errors.Is(err, &TheAuthError{Code: ...}) for code-only comparisons without caring about the message or inner cause.
func (*TheAuthError) Unwrap ¶ added in v0.2.0
func (e *TheAuthError) Unwrap() error
Unwrap exposes the inner error for errors.Is/errors.As traversal.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
mcpresource
module
|
|
|
postgres
Package postgres provides a Postgres-backed storage.Storage implementation built on top of sqlc-generated queries and pgx/v5.
|
Package postgres provides a Postgres-backed storage.Storage implementation built on top of sqlc-generated queries and pgx/v5. |