theauth

package module
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Jun 21, 2026 License: MIT Imports: 23 Imported by: 0

README

theauth-go

A modern auth library for Go. Magic links, sessions, OAuth, MCP OAuth 2.1. Drop-in chi/net/http middleware. Postgres or in-memory storage.

Go Reference Go Report Card CI Release

theauth-go is a small, opinionated Go auth library. Sign in with email magic links or email + password today; OAuth, passkeys, and an MCP OAuth 2.1 server land on the published roadmap below.

It is built to drop into a chi or net/http server in under twenty lines, store sessions in Postgres or memory, and grow into agent identity (the part of auth most libraries skip) without a rewrite.


Why theauth-go

Who it's for

  • Go developers building web apps or APIs who want a real auth flow without owning every line of it
  • Teams building MCP servers and AI-agent backends who need agent identity, not just human login
  • Anyone who would otherwise reach for a SaaS auth vendor and would rather self-host

What's different

  • Go-native: idiomatic net/http handlers, a tiny Storage interface, chi-friendly middleware. Not a port of a TypeScript library
  • Agent identity on the roadmap: MCP OAuth 2.1 server, delegation chains, and budget policies are first-class plans (v2.0) — not bolted on
  • Self-hosted forever: MIT-licensed library, no per-MAU pricing, no vendor lock-in, your DB

What it isn't

  • Not a SaaS (no hosted dashboard, no managed UI)
  • Not for Node — see the TypeScript sibling glincker/theauth
  • Not a full IdP yet — OAuth providers ship in v0.3, SAML in v1.0

Install

go get github.com/glincker/theauth-go

Requires Go 1.25+ (matches pgx/v5).


Quickstart

package main

import (
    "net/http"

    "github.com/glincker/theauth-go"
    "github.com/glincker/theauth-go/storage/memory"
    "github.com/go-chi/chi/v5"
)

func main() {
    a, _ := theauth.New(theauth.Config{
        Storage: memory.New(),
        BaseURL: "http://localhost:8080",
    })

    r := chi.NewRouter()
    a.Mount(r) // wires /auth/* endpoints (magic-link, email-password, me, signout, ...)

    r.With(a.RequireAuth()).Get("/me", func(w http.ResponseWriter, r *http.Request) {
        user, _ := theauth.UserFromContext(r.Context())
        w.Write([]byte("hello " + user.Email))
    })

    http.ListenAndServe(":8080", r)
}

Email + password (v0.2) calls the same mounted routes:

// signup
resp, _ := http.Post("http://localhost:8080/auth/email-password/signup",
    "application/json", strings.NewReader(`{"email":"you@example.com","password":"twelve-chars-min"}`))
// signin same shape — POST /auth/email-password/signin

Full runnable example: examples/chi-app/.


Email + password (v0.2)

Mounting a.Mount(r) also wires up the /auth/email-password route group:

Method + path Body Notes
POST /auth/email-password/signup {email, password} Creates user, sets session cookie, sends verify
POST /auth/email-password/signin {email, password} Sets session cookie
POST /auth/email-password/forgot {email} Silently 200 even for unknown emails
POST /auth/email-password/reset {token, newPassword} Revokes all of the user's existing sessions

Built-in safeguards:

  • Argon2id hashing (OWASP 2026 defaults: 64 MiB / 3 iters / 4 threads)
  • Minimum 12-char password enforced in service layer (NIST 2024 baseline)
  • Per-IP rate limit (5/min default) on every credential endpoint
  • Per-email rate limit (3/min default) on signin + forgot
  • Anti-enumeration: unknown email and wrong password return the same invalid_credentials code; /forgot silently 200s for unknown emails
  • Soft email verification: signup issues a session immediately; the verify magic link is sent but signin does not block on user.emailVerifiedAt — gate sensitive features on the client side by checking /auth/me's emailVerifiedAt

Rate limits are configurable:

theauth.New(theauth.Config{
    RateLimitPerIP:    10,  // default 5
    RateLimitPerEmail: 5,   // default 3
    // ...
})

Error responses on v0.2 endpoints use a stable {code, message} JSON shape — switch on code:

Code HTTP When
weak_password 400 password < 12 chars
email_taken 409 signup with existing email
invalid_credentials 401 wrong password OR unknown email at signin
rate_limited 429 per-IP or per-email cap hit
password_reset_invalid 401 reset token unknown or already used
password_reset_expired 401 reset token past its TTL

OAuth providers (v0.3 + v0.4)

Each provider lives in its own sub-package so consumers only pull in the ones they want. v0.3 shipped GitHub; v0.4 adds Google, Microsoft, and Discord. All four implement the same theauth.Provider interface and mount the same /start + /callback route shape.

GitHub (v0.3)

Register an OAuth app at https://github.com/settings/developers with a callback of https://yourapp.com/auth/providers/github/callback, then wire the provider into theauth.New:

import (
    "github.com/glincker/theauth-go"
    "github.com/glincker/theauth-go/provider/github"
)

key := mustGetenv("THEAUTH_ENCRYPTION_KEY") // 32 raw bytes (AES-256)

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    EncryptionKey: key,
    Providers: []theauth.Provider{
        github.New(github.Config{
            ClientID:     os.Getenv("GITHUB_CLIENT_ID"),
            ClientSecret: os.Getenv("GITHUB_CLIENT_SECRET"),
        }),
    },
    PostLoginRedirect: "/dashboard",
})
Google (v0.4)

Register at https://console.cloud.google.com/apis/credentials, set the authorized redirect URI to https://yourapp.com/auth/providers/google/callback, then add the provider to the slice above:

import "github.com/glincker/theauth-go/provider/google"

google.New(google.Config{
    ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
})

Default scopes are openid email profile. EmailVerified is mapped from Google's email_verified userinfo claim (true only when Google attests).

Microsoft (v0.4)

Register at https://entra.microsoft.com/ under App registrations, add the redirect URI https://yourapp.com/auth/providers/microsoft/callback, then:

import "github.com/glincker/theauth-go/provider/microsoft"

microsoft.New(microsoft.Config{
    ClientID:     os.Getenv("MS_CLIENT_ID"),
    ClientSecret: os.Getenv("MS_CLIENT_SECRET"),
    Tenant:       os.Getenv("MS_TENANT"), // optional; defaults to "common"
})

Tenant is substituted into the authorize and token URLs. Valid values:

  • common (default): any work, school, or personal Microsoft account
  • organizations: work / school only
  • consumers: personal only
  • a tenant GUID or verified domain (e.g. contoso.onmicrosoft.com) for single-tenant apps

Microsoft's OIDC userinfo endpoint does not return an email_verified claim, so ProviderUser.EmailVerified is always false. The find or create flow treats this as soft verification: the address is surfaced for matching, but the user record is created with EmailVerifiedAt=nil, and your app can require an extra verification step before granting elevated permissions.

Discord (v0.4)

Register at https://discord.com/developers/applications, add the redirect URI https://yourapp.com/auth/providers/discord/callback, then:

import "github.com/glincker/theauth-go/provider/discord"

discord.New(discord.Config{
    ClientID:     os.Getenv("DISCORD_CLIENT_ID"),
    ClientSecret: os.Getenv("DISCORD_CLIENT_SECRET"),
})

Default scopes are identify email. The avatar URL is synthesized from the user's avatar hash (https://cdn.discordapp.com/avatars/{id}/{hash}.png); users without a custom avatar receive an empty AvatarURL so consumers can render their own placeholder.

Routes (any provider)

a.Mount(r) adds two routes per registered provider, keyed by Provider.Name():

Method + path Notes
GET /auth/providers/{name}/start 302 to the provider's authorize URL, sets state cookie
GET /auth/providers/{name}/callback exchanges code, sets session cookie

{name} is github, google, microsoft, or discord depending on which providers you wired up.

Built-in safeguards:

  • PKCE S256 on every flow (no plaintext code_verifier on the wire)
  • State cookie + query parameter must match before code exchange (CSRF)
  • State entries expire after 10 minutes and are GC-swept every minute
  • Tokens encrypted at rest with AES-256-GCM via Config.EncryptionKey
  • Primary verified email only counts as verified on User.EmailVerifiedAt
  • Same rate limit as credential endpoints (5/min per IP by default)

Find-or-create resolves a returning OAuth user in this order: (1) existing oauth_accounts row by provider + provider user id, (2) match by verified email on the existing users table, (3) create a brand new user. The upstream access/refresh tokens are persisted (encrypted) so v0.4 refresh rotation can light up without a schema change.


WebAuthn / passkeys (v0.5)

WebAuthn ships behind Config.WebAuthn. Routes mount only when the block is non-nil, so existing apps see no behavior change after upgrading.

import "github.com/glincker/theauth-go"

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    EncryptionKey: key, // 32 bytes; reuses the same key OAuth tokens ride on
    WebAuthn: &theauth.WebAuthnConfig{
        RPID:          "yourapp.com",            // eTLD+1, no scheme, no port
        RPDisplayName: "Your App",                // shown in browser prompts
        RPOrigins:     []string{"https://yourapp.com"},
    },
})

Routes:

Method + path Auth Purpose
POST /auth/webauthn/register/begin RequireAuth (full) Returns protocol.CredentialCreation, sets challenge cookie
POST /auth/webauthn/register/finish RequireAuth (full) Validates attestation, stores credential row
POST /auth/webauthn/login/begin none Returns discoverable protocol.CredentialAssertion
POST /auth/webauthn/login/finish none Validates assertion, issues full session cookie
GET /auth/webauthn/credentials RequireAuth (full) Lists the caller's registered credentials
DELETE /auth/webauthn/credentials/{id} RequireAuth (full) Removes one credential

Built-in safeguards:

  • PKCE-equivalent challenge binding via an HttpOnly cookie scoped to /auth/webauthn
  • Single-use challenge entries (LoadAndDelete before validate)
  • 5 minute TTL on every challenge, swept every 60s
  • Sign-count replay protection via an atomic UPDATE ... WHERE sign_count < $new plus a lookup follow-up to disambiguate replay from missing row
  • Discoverable-only login so the begin endpoint reveals nothing about user existence
  • Single-factor-strong per NIST SP 800-63B rev 4: a passkey login is a strong factor by itself, so the flow does NOT step up to TOTP

Passkey ceremonies on the client side use the standard navigator.credentials.create and navigator.credentials.get APIs; the server returns the unmodified protocol.CredentialCreation / protocol.CredentialAssertion JSON for the browser to consume directly. See examples/webauthn-passkey/ for a small single-page demo with a working register and login button pair.


TOTP 2FA + recovery codes (v0.5)

TOTP ships behind Config.TOTP. Like WebAuthn, the routes only mount when the block is non-nil.

import "github.com/glincker/theauth-go"

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    EncryptionKey: key, // 32 bytes; required, used to encrypt totp_secrets.secret_enc
    TOTP: &theauth.TOTPConfig{
        Issuer: "Your App", // shown in Google Authenticator, Authy, 1Password
        // RecoveryCodeCount defaults to 10
    },
})

Routes:

Method + path Auth Purpose
POST /auth/totp/enroll/begin RequireAuth (full) Generates secret, returns {secret, otpAuthUrl, enrollmentId}
POST /auth/totp/enroll/finish RequireAuth (full) Body {enrollmentId, code}. Confirms secret, returns 10 recovery codes (once)
POST /auth/totp/verify RequirePendingOrFull Body {code}. Promotes pending session to full
POST /auth/totp/recovery RequirePendingOrFull Body {code}. Consumes one recovery code, promotes session
DELETE /auth/totp RequireAuth (full) Drops the secret + all recovery codes

When Config.TOTP != nil and a user has confirmed TOTP, the POST /auth/email-password/signin response shape becomes {"step":"totp_required"} and the session cookie carries a pending_2fa AuthLevel that only the two verify routes accept.

Session state machine:

anon
  |
  | POST /auth/email-password/signin (no TOTP enrolled)
  | GET  /auth/magic-link/verify
  | GET  /auth/providers/{name}/callback
  | POST /auth/webauthn/login/finish
  v
full  (auth_level = 'full', all RequireAuth routes)
  |
  | DELETE /auth/sessions/current
  v
anon

anon
  |
  | POST /auth/email-password/signin (TOTP confirmed for user)
  v
pending_2fa  (auth_level = 'pending_2fa', 10 min TTL)
  |
  | POST /auth/totp/verify   (valid code)
  | POST /auth/totp/recovery (valid unused recovery code)
  v
full

Built-in safeguards:

  • AES-GCM at rest for totp_secrets.secret_enc (mandatory; New errors if EncryptionKey is missing)
  • Per-code salted SHA-256 for recovery codes (40 bit crypto/rand entropy makes Argon2id wasted latency; see crypto/recoverycode.go)
  • Five-strike pending session revocation: five consecutive wrong codes revoke the pending session and force a fresh password verify
  • WebAuthn login bypasses TOTP: NIST SP 800-63B rev 4 treats passkey as a single strong factor
  • OAuth callbacks bypass TOTP: the IdP already enforced its own factors

See examples/totp-stepup/ for a single-page demo of the full flow.


Comparison

How theauth-go stacks up against the libraries and services you would actually consider in 2026:

Feature theauth-go better-auth Auth0 SDK Stytch Ory Kratos
Language Go TypeScript Multiple Multiple Go
Magic links Shipping Shipping Shipping Shipping Shipping
Email / password Shipping Shipping Shipping Shipping Shipping
OAuth providers 4 17 30+ 20+ 10+
Passkeys / WebAuthn Shipping Shipping Shipping Shipping Shipping
TOTP 2FA + recovery codes Shipping Shipping Shipping Shipping Shipping
Self-hosted Yes Yes No No Yes
MCP OAuth 2.1 server Roadmap v2.0 Roadmap v2.0 No No No
Agent identity + delegation Roadmap v2.0 Roadmap v2.0 No No No
Hosting model Library Library SaaS SaaS Service
Cost Free (MIT) Free (MIT) Paid Paid Free (Apache)

Honest legend: Shipping = available today, Roadmap vX = planned for that version, No = not planned. Numbers reflect each vendor's 2026 documentation.


Architecture

                ┌──────────────────────────────┐
   HTTP req ─►  │  chi / net/http router       │
                │   ├── a.Mount(r)             │  /auth/* handlers
                │   └── a.RequireAuth()        │  middleware
                └──────────────┬───────────────┘
                               │
                ┌──────────────▼───────────────┐
                │  theauth core                │
                │   ├── magic-link service     │
                │   ├── session service        │  opaque tokens, hashed in DB
                │   ├── password service       │  argon2id, rate-limited (v0.2)
                │   └── oauth / MCP / agents   │  (v0.3 → v2.0)
                └──────────────┬───────────────┘
                               │
                ┌──────────────▼───────────────┐
                │  Storage interface           │  pluggable
                │   ├── storage/memory         │  tests, demos
                │   └── storage/postgres       │  pgx + sqlc
                └──────────────────────────────┘

Sessions are opaque tokens — the raw token lives only in the user's cookie; only a SHA-256 hash is persisted. Revocation is a single UPDATE. The Storage interface is the only surface a custom backend needs to implement.


Storage backends

  • storage/memory — in-memory, zero deps. Use for tests, local demos, and quickstarts
  • storage/postgrespgx/v5 + sqlc-generated queries. Migrations live in storage/postgres/migrations/ and run via golang-migrate
  • Custom — implement the Storage interface (one type, focused method set) to back theauth with anything: SQLite, MySQL, DynamoDB, your existing ORM

Postgres example:

pool, _ := pgxpool.New(ctx, "postgres://...")
a, _ := theauth.New(theauth.Config{
    Storage: postgres.New(pool),
    BaseURL: "https://myapp.com",
})

Email senders

  • email.Noop — logs to stdout. Default. Good for local dev; never ship to production
  • email.SMTP — minimal SMTP sender (host, port, from). Lands in v0.3
  • Custom — implement email.Sender to wire Resend, Postmark, SES, SendGrid, etc.

Roadmap

  • v0.1 Magic links, sessions, chi middleware, Postgres + in-memory storage (shipped)
  • v0.2 Email + password (signup, signin, forgot, reset), argon2id, per-IP + per-email rate limiting, structured TheAuthError type (shipped)
  • v0.3 GitHub OAuth (Provider interface + PKCE S256), AES-GCM token-at-rest encryption (shipped)
  • v0.4 Google + Microsoft + Discord OAuth (shipped)
  • v0.5 WebAuthn / passkeys, TOTP 2FA + recovery codes, session step-up state machine (shipped)
  • v0.6 Refresh-token rotation, JWKS-backed id_token verification, SMTP sender
  • v1.0 All 17 OAuth providers + SAML 2.0
  • v2.0 MCP OAuth 2.1 server, agent identity, delegation chains, budget policies

Track the work in GitHub Issues and Releases.


FAQ

Is theauth-go production-ready?

v0.1 ships sessions and magic links and is covered by unit + integration tests against Postgres. v0.2 adds email + password with argon2id hashing, rate limiting, and anti-enumeration safeguards. It is appropriate for greenfield projects and side projects today. OAuth lands in v0.3. If you need OAuth, passkeys, or 2FA right now, check the roadmap and pick the right version — or use one of the alternatives above and migrate later.

Why not just use Auth0 or Clerk?

Both are excellent if you are happy paying per monthly active user and letting a third party hold your identity data. theauth-go exists for teams that want self-hosted, MIT-licensed, no-per-MAU-cost auth they fully control — including the code path that runs at login.

Why not Ory Kratos?

Kratos is a separate service you run alongside your app. theauth-go is a library you import — same process, same DB, same deploy. Fewer moving parts, less ops burden, but you give up Kratos's UI flows and multi-language SDK. Pick Kratos if you need a polyglot stack; pick theauth-go if your backend is Go and you want library-grade simplicity.

Why not better-auth?

better-auth is the TypeScript reference for this design. If your stack is Node/Next.js, use it (or use the sibling glincker/theauth TS implementation). theauth-go exists because the Go ecosystem deserves the same ergonomics natively, not via a Node sidecar.

Why a new Go auth library?

No Go library today combines agent identity + MCP OAuth 2.1 + traditional human auth in a single package. theauth-go is built for the moment human and agent auth converge — sessions, magic links, and email + password today, OAuth and passkeys in 2026, MCP OAuth 2.1 server + delegation in v2.0.

What is MCP OAuth 2.1?

The Model Context Protocol's authorization spec — built on RFC 9728 (Protected Resource Metadata), RFC 8707 (Resource Indicators), RFC 8414 (Authorization Server Metadata), and RFC 7591 (Dynamic Client Registration). It lets AI agents authenticate to MCP servers with proper scope, audience, and delegation. v2.0 of theauth-go will be the Go reference implementation.

Does it work with net/http only, no chi?

Yes. chi is recommended because middleware composition is cleaner, but Mount accepts anything that satisfies http.Handler registration, and RequireAuth() returns a standard func(http.Handler) http.Handler.

How are sessions stored?

Sessions are opaque tokens. The raw token is set in an HttpOnly, Secure, SameSite=Lax cookie. The DB only stores a SHA-256 hash plus metadata (user ID, created/expires, revoked flag). Revocation is a single UPDATE.


Stability and benchmarks

Contributing

  • Bug reports and feature requests: GitHub Issues
  • Questions, design discussion, RFC threads: GitHub Discussions
  • Pull requests welcome — please open an issue first for anything beyond a typo or one-file fix
  • Run go test ./... and go vet ./... before pushing

Sibling project

github.com/glincker/theauth — TypeScript implementation (formerly kavachos, rebranded 2026-06). Shares the same design language and roadmap; pick the one that matches your backend.

License

MIT — see LICENSE.

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

Examples

Constants

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

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

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

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

View Source
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 MagicLink struct {
	ID        ULID       `json:"id"`
	Email     string     `json:"email"`
	TokenHash []byte     `json:"-"`
	ExpiresAt time.Time  `json:"expiresAt"`
	UsedAt    *time.Time `json:"usedAt,omitempty"`
	CreatedAt time.Time  `json:"createdAt"`
}

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

type ProviderUser struct {
	ID            string
	Email         string
	EmailVerified bool
	Name          string
	AvatarURL     string
}

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

func SessionFromContext(ctx context.Context) (*Session, bool)

SessionFromContext returns the Session attached by Authn middleware, if any. Returns false when the request is anonymous.

func (Session) Expired

func (s Session) Expired(now time.Time) bool

Expired reports whether the session is no longer usable at the given time.

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

func New(cfg Config) (*TheAuth, error)

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

func (a *TheAuth) Authn() func(http.Handler) http.Handler

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

func (a *TheAuth) Mount(r chi.Router)

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

func (a *TheAuth) RateLimitByEmail(perMinute int) func(http.Handler) http.Handler

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

func (a *TheAuth) RateLimitByIP(perMinute int) func(http.Handler) http.Handler

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

func (a *TheAuth) RequireAuth() func(http.Handler) http.Handler

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

func (a *TheAuth) RequirePendingOrFull() func(http.Handler) http.Handler

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

type TheAuthError struct {
	Code    string
	Message string
	Inner   error
}

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 ULID

type ULID = ulid.ULID

ULID is the canonical ID type — generated in app, stored as uuid in Postgres.

type User

type User struct {
	ID              ULID       `json:"id"`
	Email           string     `json:"email"`
	EmailVerifiedAt *time.Time `json:"emailVerifiedAt,omitempty"`
	Name            string     `json:"name"`
	AvatarURL       string     `json:"avatarUrl"`
	CreatedAt       time.Time  `json:"createdAt"`
	UpdatedAt       time.Time  `json:"updatedAt"`
}

func UserFromContext

func UserFromContext(ctx context.Context) (*User, bool)

UserFromContext returns the authenticated User attached by Authn middleware, if any. Returns false when the request is anonymous.

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

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.

Jump to

Keyboard shortcuts

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