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 OAuthAccount
- type PasswordResetToken
- type Provider
- type ProviderToken
- type ProviderUser
- type Session
- type Storage
- type TheAuth
- func (a *TheAuth) Authn() func(http.Handler) http.Handler
- func (a *TheAuth) Close()
- 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
// Providers is the list of OAuth providers exposed under
// /auth/providers/{name}/start and /callback. Leave nil to disable
// OAuth entirely (v0.1 / v0.2 behavior). Each provider's Name() must
// be unique within the slice.
Providers []Provider
// EncryptionKey is the 32-byte AES-256 key used to encrypt provider
// access/refresh tokens before they hit storage. Required when
// len(Providers) > 0; New returns an error otherwise. Source this from
// a secrets manager; never commit it.
EncryptionKey []byte
// PostLoginRedirect is where the OAuth callback handler 302s to after
// a successful sign-in. Defaults to "/" when empty. Set to a path on
// your own origin; cross-origin redirects are not validated here.
PostLoginRedirect string
}
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 OAuthAccount ¶ added in v0.3.0
type OAuthAccount struct {
ID ULID `json:"id"`
UserID ULID `json:"userId"`
Provider string `json:"provider"`
ProviderUserID string `json:"providerUserId"`
AccessTokenEnc []byte `json:"-"`
RefreshTokenEnc []byte `json:"-"`
ExpiresAt *time.Time `json:"expiresAt,omitempty"`
Scope string `json:"scope"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
OAuthAccount records the linkage between one of our Users and a remote OAuth provider identity (e.g. user X authenticates via GitHub). The (provider, provider_user_id) pair is unique; re-running the OAuth flow for the same provider account upserts this row rather than creating a duplicate. Tokens are encrypted at rest via crypto.Encrypt and are never serialized over JSON.
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 Provider ¶ added in v0.3.0
type Provider interface {
// Name returns the stable registry key, e.g. "github". Routes mount as
// /auth/providers/{name}/start and /auth/providers/{name}/callback, and
// it lands in oauth_accounts.provider so it must be URL-safe and lower
// case.
Name() string
// AuthURL builds the absolute authorization URL the user agent will be
// redirected to. state and codeChallenge are generated by the caller;
// the implementation only assembles the query string.
AuthURL(state, codeChallenge, redirectURI string, scopes []string) string
// ExchangeCode trades an authorization code (and PKCE verifier) for a
// ProviderToken. Implementations should set a request timeout via ctx;
// the caller will not.
ExchangeCode(ctx context.Context, code, codeVerifier, redirectURI string) (*ProviderToken, error)
// UserInfo returns the canonical user profile for the supplied token.
// Implementations are responsible for picking the best email (verified +
// primary if the provider distinguishes them) and reflecting that in
// ProviderUser.EmailVerified.
UserInfo(ctx context.Context, token *ProviderToken) (*ProviderUser, error)
}
Provider is the contract every OAuth 2.0 / OIDC provider implements. Each concrete provider lives in its own sub-package under provider/<name>/ so consumers can pick what to import (avoids dragging in HTTP clients for providers they will never use).
All methods are called from the OAuth start/callback service flow:
- AuthURL builds the redirect target for /auth/providers/{name}/start.
- ExchangeCode swaps the authorization code (plus the PKCE verifier stored at /start time) for a *ProviderToken.
- UserInfo turns that token into a normalized *ProviderUser used by the find-or-create logic.
Implementations should be safe to share across goroutines; the service only holds one instance per provider name for the process lifetime.
type ProviderToken ¶ added in v0.3.0
type ProviderToken struct {
AccessToken string
RefreshToken string
ExpiresAt time.Time
Scope string
TokenType string
}
ProviderToken is the normalized shape of an OAuth token exchange response. Providers vary in which fields they populate (e.g. GitHub typically omits RefreshToken and ExpiresAt for "no-expiry" tokens). Storage encrypts the access/refresh tokens at rest via crypto.Encrypt.
type ProviderUser ¶ added in v0.3.0
ProviderUser is the normalized shape of a provider's userinfo response. ID is the provider-stable user identifier (e.g. GitHub numeric id as a string) and is what oauth_accounts.provider_user_id stores. Email may be empty when the user denied the email scope or has no public email on the provider; EmailVerified is true only when the provider attests to it.
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)
// OAuth accounts (v0.3)
// UpsertOAuthAccount inserts or updates the row keyed by
// (provider, provider_user_id). Returns the resulting row so callers
// can use the assigned ID and timestamps. Implementations must encrypt
// any token bytes before they reach storage; this layer only persists
// what it is given.
UpsertOAuthAccount(ctx context.Context, a OAuthAccount) (OAuthAccount, error)
// OAuthAccountByProviderUserID looks up the row for a provider/user
// pair. Returns ErrStorageNotFound when no row exists.
OAuthAccountByProviderUserID(ctx context.Context, provider, providerUserID string) (*OAuthAccount, 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) Close ¶ added in v0.3.0
func (a *TheAuth) Close()
Close releases background resources started by New (currently: the OAuth state GC goroutine). Safe to call multiple times; safe to omit in tests that don't configure providers.
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
|
|
|
provider
|
|
|
discord
Package discord implements theauth.Provider against Discord's OAuth 2.0 endpoints.
|
Package discord implements theauth.Provider against Discord's OAuth 2.0 endpoints. |
|
github
Package github implements theauth.Provider against GitHub's OAuth 2.0 endpoints.
|
Package github implements theauth.Provider against GitHub's OAuth 2.0 endpoints. |
|
google
Package google implements theauth.Provider against Google's OAuth 2.0 and OIDC endpoints.
|
Package google implements theauth.Provider against Google's OAuth 2.0 and OIDC endpoints. |
|
internal/oauthtest
Package oauthtest provides shared httptest scaffolding for the OAuth provider packages under provider/.
|
Package oauthtest provides shared httptest scaffolding for the OAuth provider packages under provider/. |
|
microsoft
Package microsoft implements theauth.Provider against the Microsoft identity platform v2.0 (Azure AD / Entra ID).
|
Package microsoft implements theauth.Provider against the Microsoft identity platform v2.0 (Azure AD / Entra ID). |
|
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. |