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 EnrollTOTPResult
- type MagicLink
- type OAuthAccount
- type PasswordResetToken
- type Provider
- type ProviderToken
- type ProviderUser
- type RecoveryCode
- type Session
- type SigninStep
- type Storage
- type TOTPConfig
- type TOTPSecret
- type TheAuth
- func (a *TheAuth) Authn() func(http.Handler) http.Handler
- func (a *TheAuth) BeginPasskeyLogin(_ context.Context) (*protocol.CredentialAssertion, string, error)
- func (a *TheAuth) BeginPasskeyRegistration(ctx context.Context, userID ULID) (*protocol.CredentialCreation, string, error)
- func (a *TheAuth) BeginTOTPEnrollment(ctx context.Context, userID ULID, accountName string) (EnrollTOTPResult, error)
- func (a *TheAuth) Close()
- func (a *TheAuth) ConsumeRecoveryCode(ctx context.Context, pendingSessionToken, code string) (string, Session, error)
- func (a *TheAuth) FinishPasskeyLogin(ctx context.Context, challengeToken string, body io.Reader, ua, ip string) (string, Session, error)
- func (a *TheAuth) FinishPasskeyRegistration(ctx context.Context, userID ULID, challengeToken, name string, body io.Reader) (WebAuthnCredential, error)
- func (a *TheAuth) FinishTOTPEnrollment(ctx context.Context, userID ULID, enrollmentID, code string) ([]string, error)
- func (a *TheAuth) IssuePending2FA(ctx context.Context, userID ULID, ua, ip string) (string, Session, error)
- 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
- func (a *TheAuth) RequirePendingOrFull() func(http.Handler) http.Handler
- func (a *TheAuth) VerifyTOTP(ctx context.Context, pendingSessionToken, code string) (string, Session, error)
- type TheAuthError
- type ULID
- type User
- type WebAuthnConfig
- type WebAuthnCredential
Examples ¶
Constants ¶
const ( CodeWeakPassword = "weak_password" CodeEmailTaken = "email_taken" CodeInvalidCredentials = "invalid_credentials" CodeRateLimited = "rate_limited" CodePasswordResetExpired = "password_reset_expired" CodePasswordResetInvalid = "password_reset_invalid" // v0.5 codes. CodeTOTPRequired = "totp_required" CodeInvalidTOTP = "invalid_totp" CodeAlreadyEnrolled = "already_enrolled" CodeWebAuthn = "webauthn_error" )
Stable error codes that callers can switch on. New endpoints return TheAuthError; old endpoints keep returning the sentinels above.
const ( AuthLevelFull = "full" AuthLevelPending2FA = "pending_2fa" )
Session auth levels (v0.5). AuthLevelFull is the post-MFA, full-access state. AuthLevelPending2FA is the short-lived state after a successful password verify on an account that also has TOTP enrolled: the user has proven the first factor and may only call /auth/totp/verify or /auth/totp/recovery. RequireAuth rejects pending sessions everywhere else.
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") // ErrReplayDetected (v0.5) is returned by storage on a WebAuthn sign // count update where the new count is not strictly greater than the // stored value. The library treats this as a clone-attempt and refuses // the login (with the standard 0-stays-0 carve-out for authenticators // that do not implement counters; that case is handled by the caller). ErrReplayDetected = errors.New("theauth: webauthn sign count replay detected") // ErrAlreadyEnrolled (v0.5) is returned when /auth/totp/enroll/finish is // called against a user who already has a confirmed TOTP secret. Callers // must DELETE /auth/totp first to re-enroll. ErrAlreadyEnrolled = errors.New("theauth: totp already enrolled") )
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
// WebAuthn enables passkey registration + discoverable login when non-nil.
// RPID and RPOrigins are mandatory per spec. Leave nil to keep v0.4 behavior.
WebAuthn *WebAuthnConfig
// TOTP enables time-based second-factor enrollment + verification when non-nil.
// Requires Config.EncryptionKey (already required by v0.3 OAuth) so the stored
// secret is encrypted at rest. New returns an error if TOTP is set without a key.
TOTP *TOTPConfig
}
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 EnrollTOTPResult ¶ added in v0.5.0
type EnrollTOTPResult struct {
Secret string `json:"secret"`
OTPAuthURL string `json:"otpAuthUrl"`
EnrollmentID string `json:"enrollmentId"`
}
EnrollTOTPResult is returned by BeginTOTPEnrollment for the caller to render: Secret is the base32 string the user can type manually, OTPAuthURL is the otpauth:// URI the consumer renders as a QR. EnrollmentID is the opaque token /enroll/finish must echo back to bind the secret to the session.
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 RecoveryCode ¶ added in v0.5.0
type RecoveryCode struct {
ID ULID `json:"id"`
UserID ULID `json:"userId"`
CodeHash []byte `json:"-"`
UsedAt *time.Time `json:"usedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
}
RecoveryCode is one of N single-use codes generated at TOTP enrollment. CodeHash carries the 16-byte salt prefix followed by sha256(salt || code), produced by crypto.HashRecoveryCode. UsedAt is set the first time a user consumes the code; the same hash cannot be replayed.
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"`
// AuthLevel is "full" or "pending_2fa" (v0.5). Sessions issued by code
// paths predating v0.5 default to "full" so existing rows keep working.
AuthLevel string `json:"authLevel,omitempty"`
}
func SessionFromContext ¶
SessionFromContext returns the Session attached by Authn middleware, if any. Returns false when the request is anonymous.
type SigninStep ¶ added in v0.5.0
type SigninStep string
SigninStep is the v0.5 indicator returned alongside the session token by signinWithPassword: "full" when the cookie is immediately usable, "totp_required" when the cookie is a pending_2fa session that can only hit /auth/totp/verify and /auth/totp/recovery.
const ( SigninStepFull SigninStep = "full" SigninStepTOTPRequired SigninStep = "totp_required" )
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)
// Sessions: v0.5 step-up additions
// CreateSessionWithAuthLevel mints a session whose AuthLevel column is
// set to the supplied value (typically AuthLevelPending2FA). Mirrors
// CreateSession otherwise. CreateSession itself continues to default
// to AuthLevelFull at the DDL layer so older callers see no change.
CreateSessionWithAuthLevel(ctx context.Context, s Session) (Session, error)
// UpdateSessionAuthLevel rewrites a single session's AuthLevel column.
// Used by /auth/totp/verify to promote a pending session to full.
UpdateSessionAuthLevel(ctx context.Context, id ULID, level string) error
// WebAuthn (v0.5)
InsertWebAuthnCredential(ctx context.Context, c WebAuthnCredential) (WebAuthnCredential, error)
WebAuthnCredentialsByUserID(ctx context.Context, userID ULID) ([]WebAuthnCredential, error)
// WebAuthnCredentialByCredentialID returns the row keyed by the raw
// authenticator credential ID, or ErrStorageNotFound when missing.
WebAuthnCredentialByCredentialID(ctx context.Context, credentialID []byte) (*WebAuthnCredential, error)
// UpdateWebAuthnSignCount atomically writes a strictly greater sign
// count and bumps last_used_at. Returns ErrReplayDetected when the
// new count is not strictly greater than the stored value (the
// canonical replay signal per WebAuthn L2 / L3). Returns
// ErrStorageNotFound when the credential does not exist.
UpdateWebAuthnSignCount(ctx context.Context, credentialID []byte, newCount uint32, usedAt time.Time) error
// DeleteWebAuthnCredential removes a credential by ID, scoped to the
// owning user. Returns ErrStorageNotFound when the row does not exist
// or does not belong to the caller (no leak on cross-user lookup).
DeleteWebAuthnCredential(ctx context.Context, id ULID, userID ULID) error
// TOTP (v0.5)
// UpsertPendingTOTPSecret writes an encrypted secret with
// confirmed_at = NULL. Replaces any prior unconfirmed secret for the
// same user; preserves a confirmed one untouched (re-enrollment
// requires DeleteTOTPSecret first).
UpsertPendingTOTPSecret(ctx context.Context, s TOTPSecret) error
// ConfirmTOTPSecret sets confirmed_at on the user's pending secret.
// Returns ErrStorageNotFound when no pending row exists.
ConfirmTOTPSecret(ctx context.Context, userID ULID, at time.Time) error
TOTPSecretByUserID(ctx context.Context, userID ULID) (*TOTPSecret, error)
DeleteTOTPSecret(ctx context.Context, userID ULID) error
InsertRecoveryCodes(ctx context.Context, codes []RecoveryCode) error
// ConsumeRecoveryCode walks the user's unused codes, locates the one
// whose hash matches via crypto.VerifyRecoveryCode, and marks it used
// atomically. Returns ErrStorageNotFound when no matching unused code
// exists (covers wrong code, reused code, and cross-user mismatch).
ConsumeRecoveryCode(ctx context.Context, userID ULID, code string, at time.Time) 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 TOTPConfig ¶ added in v0.5.0
type TOTPConfig struct {
// Issuer is shown in the authenticator app, e.g. "GLINR Quarter".
Issuer string
// RecoveryCodeCount defaults to 10 when zero. Each code is 10 hex chars
// (40 bits of entropy) generated via crypto/rand.
RecoveryCodeCount int
}
TOTPConfig wires the second-factor TOTP behavior. Algorithm is fixed at SHA-1 / 30s / 6 digits for compatibility with Google Authenticator, Authy, 1Password, and every other mainstream authenticator app. Skew is fixed at one period before/after (the pquerna/otp default) to absorb client clock drift.
type TOTPSecret ¶ added in v0.5.0
type TOTPSecret struct {
UserID ULID `json:"userId"`
SecretEnc []byte `json:"-"`
ConfirmedAt *time.Time `json:"confirmedAt,omitempty"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
TOTPSecret stores the AES-GCM encrypted shared secret for one user. The secret is base32 plaintext only at enroll-begin and verify time; on disk it is always ciphertext (nonce prepended, courtesy of crypto.Encrypt). ConfirmedAt is NULL until the user proves possession by entering one valid code in /auth/totp/enroll/finish. Unconfirmed rows are overwritten by a subsequent /enroll/begin (no half-state can survive).
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 New ¶
New validates the Config, applies defaults, and returns a ready TheAuth.
Example ¶
ExampleNew shows the minimum wiring required to construct a TheAuth instance. Storage and BaseURL are the only mandatory fields; everything else takes a documented default.
package main
import (
"fmt"
"github.com/glincker/theauth-go"
"github.com/glincker/theauth-go/storage/memory"
)
func main() {
a, err := theauth.New(theauth.Config{
Storage: memory.New(),
BaseURL: "http://localhost:8080",
SecureCookie: false,
})
if err != nil {
panic(err)
}
defer a.Close()
fmt.Println("ready")
}
Output: ready
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 (full) or RequirePendingOrFull (TOTP verify routes only).
Example ¶
ExampleTheAuth_Authn wraps a handler with the Authn middleware, which resolves the session cookie (when present) and attaches the user to the request context without rejecting anonymous traffic.
package main
import (
"fmt"
"net/http"
"github.com/glincker/theauth-go"
"github.com/glincker/theauth-go/storage/memory"
)
func main() {
a, err := theauth.New(theauth.Config{
Storage: memory.New(),
BaseURL: "http://localhost:8080",
SecureCookie: false,
})
if err != nil {
panic(err)
}
defer a.Close()
handler := a.Authn()(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if _, ok := theauth.UserFromContext(r.Context()); ok {
_, _ = w.Write([]byte("authed"))
return
}
_, _ = w.Write([]byte("anon"))
}))
_ = handler
fmt.Println("wired")
}
Output: wired
func (*TheAuth) BeginPasskeyLogin ¶ added in v0.5.0
func (a *TheAuth) BeginPasskeyLogin(_ context.Context) (*protocol.CredentialAssertion, string, error)
BeginPasskeyLogin starts a discoverable-credential login. The user is not identified up-front; the authenticator returns its userHandle inside the assertion, which we look up in FinishPasskeyLogin.
func (*TheAuth) BeginPasskeyRegistration ¶ added in v0.5.0
func (a *TheAuth) BeginPasskeyRegistration(ctx context.Context, userID ULID) (*protocol.CredentialCreation, string, error)
BeginPasskeyRegistration starts the registration ceremony for a signed-in user. Returns the upstream CredentialCreation options for the browser to pass to navigator.credentials.create plus the opaque challenge token the handler binds in a cookie.
func (*TheAuth) BeginTOTPEnrollment ¶ added in v0.5.0
func (a *TheAuth) BeginTOTPEnrollment(ctx context.Context, userID ULID, accountName string) (EnrollTOTPResult, error)
BeginTOTPEnrollment generates a fresh shared secret and otpauth URL, returns them to the caller (the secret is displayed exactly once), and stashes the plaintext in the in-memory pending map keyed by the returned EnrollmentID. The DB row is written immediately with confirmed_at = NULL so a server restart does not strand a half-enrolled account, but verify is gated on the in-memory secret to keep the AES-GCM Decrypt cost off the verify hot path.
func (*TheAuth) Close ¶ added in v0.3.0
func (a *TheAuth) Close()
Close releases background resources started by New: the OAuth state GC loop (v0.3) and the WebAuthn challenge / TOTP enrollment GC loops (v0.5). Safe to call multiple times; safe to omit in tests that don't configure the corresponding feature.
func (*TheAuth) ConsumeRecoveryCode ¶ added in v0.5.0
func (a *TheAuth) ConsumeRecoveryCode(ctx context.Context, pendingSessionToken, code string) (string, Session, error)
ConsumeRecoveryCode upgrades a pending session by consuming one unused recovery code. Five failures revoke the session just like VerifyTOTP.
func (*TheAuth) FinishPasskeyLogin ¶ added in v0.5.0
func (a *TheAuth) FinishPasskeyLogin(ctx context.Context, challengeToken string, body io.Reader, ua, ip string) (string, Session, error)
FinishPasskeyLogin completes a discoverable login. The handler hands us the browser's JSON, the challenge token, and the request metadata. On success we issue a full session directly: per NIST SP 800-63B rev 4, a passkey login is a single strong factor that satisfies AAL2 by itself, so we do not gate it on a second TOTP step (single-factor-strong model). See https://pages.nist.gov/800-63-4/sp800-63b.html .
func (*TheAuth) FinishPasskeyRegistration ¶ added in v0.5.0
func (a *TheAuth) FinishPasskeyRegistration(ctx context.Context, userID ULID, challengeToken, name string, body io.Reader) (WebAuthnCredential, error)
FinishPasskeyRegistration completes the registration ceremony. The body is the raw JSON the browser POSTs from navigator.credentials.create. On success the new row is inserted and returned to the caller for display (nickname, AAGUID, transports). Single-use: the challenge entry is removed before the library is invoked so a failed verify burns it.
func (*TheAuth) FinishTOTPEnrollment ¶ added in v0.5.0
func (a *TheAuth) FinishTOTPEnrollment(ctx context.Context, userID ULID, enrollmentID, code string) ([]string, error)
FinishTOTPEnrollment validates one code against the pending secret, confirms the row, generates RecoveryCodeCount single-use recovery codes, and returns them to the caller (the only time the plaintext codes are visible). Idempotency: a second /finish call with the same enrollmentID fails with ErrAlreadyEnrolled.
func (*TheAuth) IssuePending2FA ¶ added in v0.5.0
func (a *TheAuth) IssuePending2FA(ctx context.Context, userID ULID, ua, ip string) (string, Session, error)
IssuePending2FA mints a short-lived session whose AuthLevel is AuthLevelPending2FA. The cookie name is identical to the full session cookie so the browser sends it everywhere; middleware/RequireAuth rejects it except on the two TOTP verify endpoints (handled by the requirePendingOrFull tag in handlers_totp.go).
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.
Example ¶
ExampleTheAuth_Mount mounts the standard authentication routes onto a chi router. The routes appear under /auth (see Mount godoc for the complete list).
package main
import (
"fmt"
"github.com/glincker/theauth-go"
"github.com/glincker/theauth-go/storage/memory"
"github.com/go-chi/chi/v5"
)
func main() {
a, err := theauth.New(theauth.Config{
Storage: memory.New(),
BaseURL: "http://localhost:8080",
SecureCookie: false,
})
if err != nil {
panic(err)
}
defer a.Close()
r := chi.NewRouter()
a.Mount(r)
fmt.Println("mounted")
}
Output: mounted
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.
func (*TheAuth) RequireAuth ¶
RequireAuth runs Authn, then rejects requests that don't have a FULL session. Pending_2fa sessions are treated as unauthorized here. The two TOTP verify routes opt in to RequirePendingOrFull instead.
func (*TheAuth) RequirePendingOrFull ¶ added in v0.5.0
RequirePendingOrFull (v0.5) accepts both pending_2fa and full sessions. Used exclusively by /auth/totp/verify and /auth/totp/recovery so a user mid-step-up can complete the second factor.
func (*TheAuth) VerifyTOTP ¶ added in v0.5.0
func (a *TheAuth) VerifyTOTP(ctx context.Context, pendingSessionToken, code string) (string, Session, error)
VerifyTOTP consumes a 6-digit code against the user's confirmed secret, upgrades their pending session to full, and returns the (same) token with the upgraded session row. On five consecutive failures the pending session is revoked.
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.
type User ¶
type WebAuthnConfig ¶ added in v0.5.0
type WebAuthnConfig struct {
// RPID is the Relying Party Server ID. e.g. "glinr.com" (eTLD+1 of the
// origin, no scheme, no port). Required.
RPID string
// RPDisplayName is shown by browsers and authenticators. Required.
RPDisplayName string
// RPOrigins is the fully qualified origins permitted to invoke the
// API, e.g. ["https://glinr.com"]. At least one required.
RPOrigins []string
// ChallengeTTL caps how long the in-memory challenge session is valid.
// Defaults to 5 minutes; challenges are single-use regardless.
ChallengeTTL time.Duration
}
WebAuthnConfig wires the Relying Party identity. Field names mirror the upstream go-webauthn/webauthn.Config so consumers reading either set of docs see the same vocabulary.
type WebAuthnCredential ¶ added in v0.5.0
type WebAuthnCredential struct {
ID ULID `json:"id"`
UserID ULID `json:"userId"`
CredentialID []byte `json:"credentialId"`
PublicKey []byte `json:"-"`
SignCount uint32 `json:"signCount"`
Transports []string `json:"transports"`
AAGUID []byte `json:"aaguid"`
Name string `json:"name"`
CreatedAt time.Time `json:"createdAt"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
}
WebAuthnCredential mirrors the persistent subset of webauthn.Credential. We store the COSE-encoded public key plus the metadata we own (nickname, timestamps, sign count). One row per registered authenticator; the same user can register many. CredentialID is the raw byte string the authenticator returned at registration and must be globally unique to prevent a stolen credential being re-registered against a different user.
SignCount is monotonic per credential. A login that supplies a non-greater value than the stored count is treated as a clone-attempt and refused via ErrReplayDetected (carve-out: an authenticator that never implements sign counts always returns 0, which the library handles as a per-spec exception).
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
Package crypto contains the primitives theauth-go uses for password hashing (Argon2id), opaque session and reset tokens (crypto/rand plus SHA-256), AES-256-GCM symmetric encryption for OAuth tokens at rest, PKCE code verifier and S256 challenge generation, and salted SHA-256 recovery-code hashing.
|
Package crypto contains the primitives theauth-go uses for password hashing (Argon2id), opaque session and reset tokens (crypto/rand plus SHA-256), AES-256-GCM symmetric encryption for OAuth tokens at rest, PKCE code verifier and S256 challenge generation, and salted SHA-256 recovery-code hashing. |
|
Package email defines the Sender interface theauth-go uses to deliver magic-link and password-reset messages, plus a Noop implementation suitable for local development and tests.
|
Package email defines the Sender interface theauth-go uses to deliver magic-link and password-reset messages, plus a Noop implementation suitable for local development and tests. |
|
examples
|
|
|
totp-stepup
command
totp-stepup is a runnable demo of the v0.5 password + TOTP step-up flow.
|
totp-stepup is a runnable demo of the v0.5 password + TOTP step-up flow. |
|
webauthn-passkey
command
webauthn-passkey is a runnable demo of v0.5 passkey registration + login.
|
webauthn-passkey is a runnable demo of v0.5 passkey registration + login. |
|
internal
|
|
|
wavt
Package wavt is the WebAuthn virtual test helper used to drive the v0.5 passkey ceremony tests.
|
Package wavt is the WebAuthn virtual test helper used to drive the v0.5 passkey ceremony tests. |
|
mcpresource
module
|
|
|
Package provider is the namespace root for theauth-go's OAuth 2.0 implementations.
|
Package provider is the namespace root for theauth-go's OAuth 2.0 implementations. |
|
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 OpenID Connect endpoints.
|
Package google implements theauth.Provider against Google's OAuth 2.0 and OpenID Connect 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 Microsoft Entra ID (Azure AD) OAuth 2.0 endpoints.
|
Package microsoft implements theauth.Provider against Microsoft Entra ID (Azure AD) OAuth 2.0 endpoints. |
|
Package storage re-exports the theauth.Storage interface and the canonical ErrNotFound sentinel that adapters return on lookup misses.
|
Package storage re-exports the theauth.Storage interface and the canonical ErrNotFound sentinel that adapters return on lookup misses. |
|
memory
Package memory provides an in-process implementation of theauth.Storage backed by Go maps protected by a sync.RWMutex.
|
Package memory provides an in-process implementation of theauth.Storage backed by Go maps protected by a sync.RWMutex. |
|
postgres
Package postgres provides a production-grade implementation of theauth.Storage backed by PostgreSQL through pgx and sqlc-generated query bindings.
|
Package postgres provides a production-grade implementation of theauth.Storage backed by PostgreSQL through pgx and sqlc-generated query bindings. |