theauth

package module
v0.4.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: 20 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.


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 Roadmap v0.5 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 Refresh-token rotation, JWKS-backed id_token verification, SMTP sender, TOTP 2FA, WebAuthn / passkeys
  • 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.


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

Constants

View Source
const (
	CodeWeakPassword         = "weak_password"
	CodeEmailTaken           = "email_taken"
	CodeInvalidCredentials   = "invalid_credentials"
	CodeRateLimited          = "rate_limited"
	CodePasswordResetExpired = "password_reset_expired"
	CodePasswordResetInvalid = "password_reset_invalid"
)

Stable error codes that callers can switch on. New endpoints return TheAuthError; old endpoints keep returning the sentinels above.

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

Sentinel errors — retained for backward compatibility with v0.1 callers that errors.Is-check against them. New code should prefer TheAuthError + the Code* constants below.

Functions

This section is empty.

Types

type Config

type Config struct {
	Storage      Storage
	EmailSender  email.Sender
	BaseURL      string
	SigningKey   ed25519.PrivateKey
	SessionTTL   time.Duration
	MagicLinkTTL time.Duration
	CookieName   string
	SecureCookie bool
	// RateLimitPerIP is the per-IP per-minute budget applied to credential
	// endpoints (signup/signin/forgot/reset). Defaults to 5 when zero.
	RateLimitPerIP int
	// RateLimitPerEmail is the per-email per-minute budget applied to signin
	// + forgot. Defaults to 3 when zero.
	RateLimitPerEmail int

	// Providers is the list of OAuth providers exposed under
	// /auth/providers/{name}/start and /callback. Leave nil to disable
	// OAuth entirely (v0.1 / v0.2 behavior). Each provider's Name() must
	// be unique within the slice.
	Providers []Provider

	// EncryptionKey is the 32-byte AES-256 key used to encrypt provider
	// access/refresh tokens before they hit storage. Required when
	// len(Providers) > 0; New returns an error otherwise. Source this from
	// a secrets manager; never commit it.
	EncryptionKey []byte

	// PostLoginRedirect is where the OAuth callback handler 302s to after
	// a successful sign-in. Defaults to "/" when empty. Set to a path on
	// your own origin; cross-origin redirects are not validated here.
	PostLoginRedirect string
}

Config holds the wiring for a TheAuth instance.

Storage and BaseURL are required. Everything else has sensible defaults applied by New: SessionTTL=24h, MagicLinkTTL=15m, CookieName="theauth_session", EmailSender=email.Noop{}. SigningKey is reserved for future JWT signing (v0.2+); v0.1 uses opaque tokens and leaves the field nil.

type 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 Session

type Session struct {
	ID        ULID       `json:"id"`
	UserID    ULID       `json:"userId"`
	TokenHash []byte     `json:"-"` // never serialize raw hash
	UserAgent string     `json:"userAgent"`
	IP        string     `json:"ip"`
	CreatedAt time.Time  `json:"createdAt"`
	ExpiresAt time.Time  `json:"expiresAt"`
	RevokedAt *time.Time `json:"revokedAt,omitempty"`
}

func SessionFromContext

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 Storage

type Storage interface {
	// Users
	CreateUser(ctx context.Context, u User) (User, error)
	UserByEmail(ctx context.Context, email string) (*User, error)
	UserByID(ctx context.Context, id ULID) (*User, error)
	MarkEmailVerified(ctx context.Context, userID ULID) error

	// Sessions
	CreateSession(ctx context.Context, s Session) (Session, error)
	SessionByTokenHash(ctx context.Context, hash []byte) (*Session, error)
	RevokeSession(ctx context.Context, id ULID) error
	RevokeUserSessions(ctx context.Context, userID ULID) error

	// Magic links
	CreateMagicLink(ctx context.Context, ml MagicLink) error
	ConsumeMagicLink(ctx context.Context, tokenHash []byte) (*MagicLink, error)

	// Email + password (v0.2)
	SetUserPassword(ctx context.Context, userID ULID, passwordHash string) error
	// UserByEmailWithPassword fetches a user along with their stored PHC hash.
	// passwordHash is "" if the account exists but has never set a password
	// (e.g. magic-link-only signup). Callers should treat empty hash as
	// "no password credential available" and surface invalid_credentials.
	UserByEmailWithPassword(ctx context.Context, email string) (user *User, passwordHash string, err error)
	CreatePasswordResetToken(ctx context.Context, t PasswordResetToken) error
	ConsumePasswordResetToken(ctx context.Context, tokenHash []byte) (*PasswordResetToken, error)

	// OAuth accounts (v0.3)
	// UpsertOAuthAccount inserts or updates the row keyed by
	// (provider, provider_user_id). Returns the resulting row so callers
	// can use the assigned ID and timestamps. Implementations must encrypt
	// any token bytes before they reach storage; this layer only persists
	// what it is given.
	UpsertOAuthAccount(ctx context.Context, a OAuthAccount) (OAuthAccount, error)
	// OAuthAccountByProviderUserID looks up the row for a provider/user
	// pair. Returns ErrStorageNotFound when no row exists.
	OAuthAccountByProviderUserID(ctx context.Context, provider, providerUserID string) (*OAuthAccount, error)
}

Storage is the persistence contract TheAuth depends on. Adapters live in sub-packages (storage/memory, storage/postgres). Defined here so that service code in this package can reference it without importing the storage sub-package (which would create an import cycle, because storage imports this package for the model types).

The storage package re-exports this as storage.Storage so consumers can keep importing it from the conventional location.

type TheAuth

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

TheAuth is the public entry point, constructed once at app start and shared across handlers.

func New

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

New validates the Config, applies defaults, and returns a ready TheAuth.

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.

func (*TheAuth) Close added in v0.3.0

func (a *TheAuth) Close()

Close releases background resources started by New (currently: the OAuth state GC goroutine). Safe to call multiple times; safe to omit in tests that don't configure providers.

func (*TheAuth) Mount

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.

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

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.

Directories

Path Synopsis
internal
mcpresource module
provider
discord
Package discord implements theauth.Provider against Discord's OAuth 2.0 endpoints.
Package discord implements theauth.Provider against Discord's OAuth 2.0 endpoints.
github
Package github implements theauth.Provider against GitHub's OAuth 2.0 endpoints.
Package github implements theauth.Provider against GitHub's OAuth 2.0 endpoints.
google
Package google implements theauth.Provider against Google's OAuth 2.0 and OIDC endpoints.
Package google implements theauth.Provider against Google's OAuth 2.0 and OIDC endpoints.
internal/oauthtest
Package oauthtest provides shared httptest scaffolding for the OAuth provider packages under provider/.
Package oauthtest provides shared httptest scaffolding for the OAuth provider packages under provider/.
microsoft
Package microsoft implements theauth.Provider against the Microsoft identity platform v2.0 (Azure AD / Entra ID).
Package microsoft implements theauth.Provider against the Microsoft identity platform v2.0 (Azure AD / Entra ID).
postgres
Package postgres provides a Postgres-backed storage.Storage implementation built on top of sqlc-generated queries and pgx/v5.
Package postgres provides a Postgres-backed storage.Storage implementation built on top of sqlc-generated queries and pgx/v5.

Jump to

Keyboard shortcuts

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