theauth

package module
v1.0.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: 37 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.

Production status: v1.0.0 is the production-ready release. Public API frozen per STABILITY.md; breaking changes from here forward require a major bump. See CHANGELOG.md for the v0.1 to v1.0 capability summary.

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.


Organizations (v0.7)

Multi-tenancy is opt-in. Set Config.Organizations to a non-nil value and the /auth/orgs/* routes mount. Existing single-tenant deployments leave the field nil and see zero behavior change.

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    Organizations: &theauth.OrganizationsConfig{},
})

Routes (all require an authenticated session):

Method + path Auth Purpose
POST /auth/orgs RequireAuth Create org, caller becomes owner
GET /auth/orgs RequireAuth List orgs the caller belongs to
GET /auth/orgs/{id} member Read one org
POST /auth/orgs/{id}/members owner or admin Add a member, body {userId, role}
DELETE /auth/orgs/{id}/members/{userId} owner Remove a member (refuses last owner)
POST /auth/orgs/{id}/activate member Bind current session to this org
POST /auth/orgs/clear-active RequireAuth Clear the active-org binding

Roles: owner, admin, member. Sessions gain a nullable ActiveOrganizationID so downstream application code can scope queries without re-resolving membership.


SAML 2.0 SP (v0.7)

The library is a Service Provider only. Each saml_connections row binds one organization to one IdP, signed assertions only, find-or-create by (connection_id, name_id) with an email fallback.

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    Organizations: &theauth.OrganizationsConfig{},
    SAML: &theauth.SAMLConfig{
        SPCertificatePEM: spCertPEM, // PEM-encoded SP signing cert
        SPPrivateKeyPEM:  spKeyPEM,  // PEM-encoded RSA key
        // AuthnRequestTTL defaults to 10m; ClockSkew defaults to 30s
    },
})

Public-facing flow routes:

Method + path Auth Purpose
GET /auth/saml/{connectionId}/login none SP-initiated SSO, 302 to IdP
POST /auth/saml/{connectionId}/acs none Assertion Consumer Service, runs find-or-create
GET /auth/saml/{connectionId}/metadata none SP metadata XML for the IdP admin

Per-organization connection CRUD (owner-only):

Method + path Purpose
POST /auth/orgs/{orgId}/saml/connections Create connection
GET /auth/orgs/{orgId}/saml/connections List connections
GET /auth/orgs/{orgId}/saml/connections/{id} Read one
PUT /auth/orgs/{orgId}/saml/connections/{id} Replace (cert rotation)
DELETE /auth/orgs/{orgId}/saml/connections/{id} Delete

Attribute mapping is per-connection JSON with WS-Federation defaults (Microsoft, Okta, OneLogin). Override only the keys that differ for a specific IdP. Standards: SAML 2.0 Core (OASIS), implemented via github.com/crewjam/saml v0.5.1.


SCIM 2.0 provisioning (v0.7)

Hand-rolled SCIM 2.0 (RFC 7643 + RFC 7644). Bearer auth, one or more sha256-hashed tokens per organization, idempotent upsert by externalId, PATCH per RFC 7644 §3.5.2.

a, _ := theauth.New(theauth.Config{
    Storage:       postgres.New(pool),
    BaseURL:       "https://yourapp.com",
    Organizations: &theauth.OrganizationsConfig{},
    SCIM: &theauth.SCIMConfig{
        RequireHTTPS: true, // default true; flip false only behind a TLS-terminating proxy that strips X-Forwarded-Proto
        MaxPageSize:  200,  // default 200
    },
})

Token CRUD lives under the org tree (owner-only):

Method + path Purpose
POST /auth/orgs/{orgId}/scim/tokens Mint a token, plaintext returned once
GET /auth/orgs/{orgId}/scim/tokens List tokens (hash never exposed)
DELETE /auth/orgs/{orgId}/scim/tokens/{id} Revoke

Resource endpoints at /scim/v2/ (bearer required):

Method + path Purpose
GET /scim/v2/ServiceProviderConfig RFC 7644 §5
GET /scim/v2/ResourceTypes RFC 7644 §6
GET /scim/v2/Schemas RFC 7644 §6
GET /scim/v2/Users List users in org. eq filter on userName, externalId, emails.value
POST /scim/v2/Users Create or upsert by externalId
GET /scim/v2/Users/{id} Read one
PATCH /scim/v2/Users/{id} RFC 7644 §3.5.2 add/replace/remove
PUT /scim/v2/Users/{id} 405, documented deviation (Okta and Azure AD default to PATCH)
DELETE /scim/v2/Users/{id} Soft delete (removes from org, preserves user row)
GET /scim/v2/Groups List groups
POST /scim/v2/Groups Create or upsert
GET /scim/v2/Groups/{id} Read
PATCH /scim/v2/Groups/{id} Members add/remove
PUT /scim/v2/Groups/{id} 405
DELETE /scim/v2/Groups/{id} Delete

Documented deviations from RFC 7643:

  • password on User POST/PATCH is rejected with 400 invalidValue. Auth credentials in theauth-go are owned by the user-facing endpoints, not by SCIM clients.
  • Nested groups (a members[i].type=="Group") are rejected with 400 invalidValue. v0.7 supports flat user membership only.
  • Only equality filters are accepted; other operators return 400 invalidFilter with the documented body.

Audit hook: Config.AuditHook is invoked synchronously for every SCIM mutation and every successful SAML assertion. v0.7 ships a no-op default; v1.0 replaces this binding with the real async writer.


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
SAML 2.0 SP Shipping Shipping Shipping Shipping Shipping
SCIM 2.0 provisioning Shipping Shipping Shipping Shipping Shipping
Multi-tenancy (organizations) 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 Hardening pass: fuzz matrix, race tests, examples, benchmarks, godoc, STABILITY.md (shipped)
  • v0.7 SAML 2.0 SP, SCIM 2.0 provisioning, organizations multi-tenancy (shipped)
  • v1.0 RBAC, async audit log writer, admin API surface
  • 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 (
	OrgRoleOwner  = "owner"
	OrgRoleAdmin  = "admin"
	OrgRoleMember = "member"
)

Organization role constants. Scoped to one organization each.

View Source
const (
	PermissionBillingRead    = "billing:read"
	PermissionBillingWrite   = "billing:write"
	PermissionBillingAdmin   = "billing:admin"
	PermissionUsersRead      = "users:read"
	PermissionUsersInvite    = "users:invite"
	PermissionUsersAdmin     = "users:admin"
	PermissionRolesRead      = "roles:read"
	PermissionRolesAdmin     = "roles:admin"
	PermissionAuditRead      = "audit:read"
	PermissionSAMLAdmin      = "saml:admin"
	PermissionSCIMAdmin      = "scim:admin"
	PermissionSessionsRevoke = "sessions:revoke"
)

Seeded permission catalog. Every consumer's permission set extends (never shrinks) this list; New returns an error if a custom Permission name duplicates a seeded one with a different description (defensive against silent overrides).

The list is intentionally finite and small. Wildcards and ABAC are deferred to v1.x per the v1.0 design document.

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.

View Source
const SystemRoleSuperAdmin = "super_admin"

SystemRoleSuperAdmin is the global role whose presence on a user bypasses every permission check. It is created at seed time with a NULL organization_id and is granted via direct DB insert or a CLI; the admin API never exposes a path to grant it.

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

	// ErrSCIMRequiresOrganizations is returned by New when Config.SCIM is
	// non-nil but Config.Organizations is nil. SCIM is meaningless without
	// multi-tenancy because every token is bound to one organization.
	ErrSCIMRequiresOrganizations = errors.New("theauth: SCIM requires Organizations to be enabled")
	// ErrSAMLRequiresOrganizations is returned by New when Config.SAML is
	// non-nil but Config.Organizations is nil. Single-tenant SAML is
	// meaningless; per-connection routing keys off organization ownership.
	ErrSAMLRequiresOrganizations = errors.New("theauth: SAML requires Organizations to be enabled")
	// ErrSAMLUnsignedAssertion is returned by FinishSAMLLogin when the
	// inbound SAML Response parses but its assertion is not signed.
	ErrSAMLUnsignedAssertion = errors.New("theauth: saml assertion not signed")
	// ErrSAMLMissingEmail is returned by FinishSAMLLogin when the mapped
	// email attribute is empty (the find-or-create email fallback path
	// cannot proceed).
	ErrSAMLMissingEmail = errors.New("theauth: saml assertion missing email attribute")
	// ErrSAMLInvalidAssertion wraps the underlying crewjam/saml validation
	// error for failed signature / conditions / replay checks.
	ErrSAMLInvalidAssertion = errors.New("theauth: saml assertion invalid")
	// ErrLastOwner is returned when an org member removal would leave the
	// organization with zero owners.
	ErrLastOwner = errors.New("theauth: cannot remove the last owner")
	// ErrUnsupportedFilter is returned by the SCIM filter parser on any
	// filter shape outside the documented eq-only whitelist.
	ErrUnsupportedFilter = errors.New("theauth: scim filter not supported")
	// ErrSlugTaken is returned when the supplied organization slug already
	// exists.
	ErrSlugTaken = errors.New("theauth: organization slug already taken")

	// ErrAdminRequiresRBAC is returned by New when Config.Admin is non-nil
	// but Config.RBAC is nil. The admin endpoints are permission-gated and
	// meaningless without RBAC.
	ErrAdminRequiresRBAC = errors.New("theauth: Admin requires RBAC to be enabled")
	// ErrForbidden indicates the caller lacks a required permission for
	// the requested operation. Handlers map this to 403 with code
	// "rbac.forbidden" in the RFC 7807 problem body.
	ErrForbidden = errors.New("theauth: forbidden")
	// ErrUnknownPermission indicates a permission name not present in the
	// seeded or extended catalog. Mapped to 400 "rbac.unknown_permission".
	ErrUnknownPermission = errors.New("theauth: unknown permission")
	// ErrRoleInUse indicates a DELETE role would leave the organization
	// without any user holding the "users:admin" permission. Mapped to
	// 409 "rbac.role_in_use".
	ErrRoleInUse = errors.New("theauth: role in use")
	// ErrNoActiveOrg indicates the session has no active_organization_id
	// set. Mapped to 403 "rbac.no_active_org".
	ErrNoActiveOrg = errors.New("theauth: session has no active organization")
	// ErrOrgMismatch indicates the session's active_organization_id does
	// not match the resource organization. Mapped to 403 "rbac.org_mismatch".
	ErrOrgMismatch = errors.New("theauth: session active organization does not match resource")
	// ErrRBACDisabled is returned by RBAC service methods invoked when
	// Config.RBAC is nil. The RequirePermission middleware short-circuits
	// to 500 rather than returning this error directly.
	ErrRBACDisabled = errors.New("theauth: RBAC disabled (Config.RBAC is nil)")
)

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

func DefaultRedactor added in v1.0.0

func DefaultRedactor(metadata map[string]any) map[string]any

DefaultRedactor masks values for keys named password, secret, token, code, refresh_token, access_token (case-insensitive) at any nesting depth. Nested maps and []any are descended; primitive values are kept as-is. Returns the same map (mutated in place) for chaining.

Applied once at emit time. A custom Config.Audit.Redactor receives the raw metadata and may strip/rename additional fields.

func HashEmailForAudit added in v1.0.0

func HashEmailForAudit(email string) string

HashEmailForAudit returns sha256(lowercase(email)) hex-encoded. Used by emit sites that want to correlate audit rows by email without storing the raw plaintext.

func SeededSecretKeys added in v1.0.0

func SeededSecretKeys() []string

SeededSecretKeys returns a copy of the case-insensitive key blocklist applied by the default redactor. Callers extending the redactor should merge this slice with their own additions.

func WithAuditMetadata added in v1.0.0

func WithAuditMetadata(ctx context.Context, md AuditMetadata) context.Context

WithAuditMetadata attaches an AuditMetadata to ctx; EmitAudit reads it. Overrides values derived from UserFromContext / SessionFromContext when the corresponding field is non-zero.

Types

type AdminConfig added in v1.0.0

type AdminConfig struct {
	// PathPrefix where admin routes mount. Defaults to "/admin/v1".
	PathPrefix string
}

AdminConfig mounts /admin/v1/* when non-nil on the same chi router passed to Mount.

type AuditConfig added in v1.0.0

type AuditConfig struct {
	// BufferSize is the channel buffer between EmitAudit and the writer
	// goroutine. Defaults to 4096. When full, EmitAudit drops the event
	// and increments Stats.AuditDropped.
	BufferSize int

	// BatchSize is the maximum events per INSERT. Defaults to 100.
	BatchSize int

	// FlushInterval is the maximum delay before a partial batch flushes.
	// Defaults to 1 second.
	FlushInterval time.Duration

	// Redactor optionally transforms metadata before storage. The default
	// redactor strips the keys "password", "secret", "token", "code",
	// "refresh_token", "access_token" (case-insensitive) at any nesting
	// depth and replaces their values with the string "[REDACTED]".
	Redactor func(metadata map[string]any) map[string]any

	// DrainTimeout caps how long Close waits for the writer goroutine to
	// drain remaining events. Defaults to 5 seconds.
	DrainTimeout time.Duration
}

AuditConfig configures the async audit writer. All fields have safe defaults; the zero value AuditConfig{} is valid.

type AuditEvent added in v0.7.0

type AuditEvent struct {
	ID             ULID           `json:"id"`
	OrganizationID *ULID          `json:"organizationId,omitempty"`
	ActorUserID    *ULID          `json:"actorUserId,omitempty"`
	ActorSessionID *ULID          `json:"actorSessionId,omitempty"`
	Action         string         `json:"action"`
	TargetType     string         `json:"targetType,omitempty"`
	TargetID       string         `json:"targetId,omitempty"`
	Metadata       map[string]any `json:"metadata,omitempty"`
	IP             string         `json:"ip,omitempty"`
	UserAgent      string         `json:"userAgent,omitempty"`
	CreatedAt      time.Time      `json:"createdAt"`
}

AuditEvent is one append-only row in audit_events. Metadata is arbitrary jsonb; the default redactor masks secret-flavored keys at any depth before the value reaches storage.

type AuditMetadata added in v1.0.0

type AuditMetadata struct {
	OrganizationID *ULID
	ActorUserID    *ULID
	IP             string
	UserAgent      string
}

AuditMetadata is the optional bundle a caller may attach to a context to override the auto-derived actor / org / IP / UA. Useful in tests and in background jobs that do not pass through the HTTP middleware.

type AuditQuery added in v1.0.0

type AuditQuery struct {
	OrganizationID *ULID
	ActorUserID    *ULID
	Action         string
	TargetType     string
	TargetID       string
	Since          *time.Time
	Until          *time.Time
	Limit          int
	After          string
}

AuditQuery filters and paginates audit_events reads. Zero-valued fields are ignored. Limit is capped at 200 and defaults to 50; After is the opaque cursor returned by the previous page.

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

	// Organizations (v0.7) enables multi-tenancy when non-nil. Single-tenant
	// deployments leave this nil; organization-scoped routes (SAML connection
	// CRUD, SCIM token CRUD, /auth/orgs/*) are not mounted.
	Organizations *OrganizationsConfig

	// SAML (v0.7) enables the per-connection Service Provider routes when
	// non-nil. The SP keypair lives on the config so multi-tenant deployments
	// can rotate it centrally; every connection signs AuthnRequests with this
	// single keypair. Requires Organizations to be non-nil.
	SAML *SAMLConfig

	// SCIM (v0.7) enables the /scim/v2 endpoints when non-nil. Requires
	// Organizations to be non-nil.
	SCIM *SCIMConfig

	// RBAC (v1.0) enables organization-scoped role and permission
	// management when non-nil. The zero value RBACConfig{} accepts the
	// seeded permissions and default org roles documented in
	// service_rbac.go; consumers extend (never shrink) the seeded lists.
	// New returns an error if Admin is non-nil and RBAC is nil because the
	// admin endpoints are permission-gated and meaningless without RBAC.
	RBAC *RBACConfig

	// Audit (v1.0) enables the async batched audit writer when non-nil.
	// When nil, EmitAudit is a silent no-op and no writer goroutine starts;
	// the v0.7 stub call sites keep working as no-ops, so deployments that
	// do not configure audit continue to behave exactly as before.
	Audit *AuditConfig

	// Admin (v1.0) mounts /admin/v1/* when non-nil. Requires RBAC to be
	// non-nil. The PathPrefix can be moved (e.g. "/api/admin/v1") but the
	// trailing version segment is always v1.
	Admin *AdminConfig
}

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 Group added in v0.7.0

type Group struct {
	ID             ULID      `json:"id"`
	OrganizationID ULID      `json:"organizationId"`
	DisplayName    string    `json:"displayName"`
	ExternalID     string    `json:"externalId,omitempty"`
	CreatedAt      time.Time `json:"createdAt"`
	UpdatedAt      time.Time `json:"updatedAt"`
}

Group is a SCIM-first concept. v0.7 stores them flat (no nesting), scoped to one organization. Application semantics (mapping a group to a role) land in v0.8 RBAC.

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 Organization added in v0.7.0

type Organization struct {
	ID        ULID      `json:"id"`
	Name      string    `json:"name"`
	Slug      string    `json:"slug"`
	CreatedAt time.Time `json:"createdAt"`
	UpdatedAt time.Time `json:"updatedAt"`
}

Organization is the top-level multi-tenant container. Slug is a citext unique URL-safe handle (lowercased on write).

type OrganizationMember added in v0.7.0

type OrganizationMember struct {
	OrganizationID ULID      `json:"organizationId"`
	UserID         ULID      `json:"userId"`
	Role           string    `json:"role"`
	JoinedAt       time.Time `json:"joinedAt"`
}

OrganizationMember binds a user to an organization with a single role. Role values: "owner", "admin", "member". owner can manage SAML and SCIM, admin can manage SCIM only, member is read-only against org metadata.

type OrganizationsConfig added in v0.7.0

type OrganizationsConfig struct{}

OrganizationsConfig is currently empty: there are no tunables for v0.7. The presence of a non-nil value is the signal that multi-tenancy is on. Future fields (default member role, invitation TTL, etc.) land here without breaking the existing zero-value wiring.

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 Permission added in v1.0.0

type Permission struct {
	ID          ULID      `json:"id"`
	Name        string    `json:"name"`
	Description string    `json:"description"`
	CreatedAt   time.Time `json:"createdAt"`
}

Permission is one entry in the closed permission catalog. Names are program identifiers (ASCII, no whitespace) of the shape "domain:verb" or "domain:verb:scope". Seeded permissions live in service_rbac.go.

func SeededPermissions added in v1.0.0

func SeededPermissions() []Permission

SeededPermissions returns the v1.0 canonical permission catalog. The slice is returned by value (callers may not mutate the library state).

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 RBACConfig added in v1.0.0

type RBACConfig struct {
	// Permissions extends the seeded catalog. Names are deduped by case-
	// sensitive equality with the seeded list. New returns an error if any
	// supplied name contains whitespace or non-ASCII (permission names are
	// program identifiers, not user input).
	Permissions []Permission

	// DefaultRoles seeds every new organization. When empty the three
	// default roles ("owner", "admin", "member") are used. Reserved names
	// ("owner", "admin", "member") must remain present when consumers
	// override; New returns an error otherwise.
	DefaultRoles []RoleSeed
}

RBACConfig configures organization-scoped roles and permissions. The zero value is valid and accepts the seeded permission list plus the seeded "owner", "admin", "member" roles.

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 Role added in v1.0.0

type Role struct {
	ID             ULID      `json:"id"`
	OrganizationID *ULID     `json:"organizationId,omitempty"`
	Name           string    `json:"name"`
	Description    string    `json:"description"`
	Permissions    []string  `json:"permissions"`
	CreatedAt      time.Time `json:"createdAt"`
	UpdatedAt      time.Time `json:"updatedAt"`
}

Role binds a permission set to an organization (or to the global namespace when OrganizationID is nil, i.e. system roles such as super_admin). Permissions is the hydrated permission-name slice and is not persisted on the Role row itself; storage adapters fill it on read via PermissionsByRole.

type RoleSeed added in v1.0.0

type RoleSeed struct {
	Name        string
	Description string
	Permissions []string
}

RoleSeed describes one organization-scoped role created at SeedOrganizationRoles time. Permissions are permission names; unknown names cause New to return a validation error so typos surface at startup instead of on the first permission check.

func DefaultRoleSeeds added in v1.0.0

func DefaultRoleSeeds() []RoleSeed

DefaultRoleSeeds returns the three default organization roles seeded into every new organization. Consumers may extend with additional roles via Config.RBAC.DefaultRoles; the three reserved names ("owner", "admin", "member") must always remain present.

type SAMLAttributeMap added in v0.7.0

type SAMLAttributeMap struct {
	Email      string            `json:"email"`
	Name       string            `json:"name,omitempty"`
	GivenName  string            `json:"givenName,omitempty"`
	FamilyName string            `json:"familyName,omitempty"`
	Groups     string            `json:"groups,omitempty"`
	Extra      map[string]string `json:"extra,omitempty"`
}

SAMLAttributeMap projects SAML attribute names to canonical user fields. Stored as jsonb in saml_connections.attribute_map.

func DefaultSAMLAttributeMap added in v0.7.0

func DefaultSAMLAttributeMap() SAMLAttributeMap

DefaultSAMLAttributeMap returns the WS-Federation claim URIs that Microsoft, Okta, and OneLogin emit by default. A per-connection map can override any of these by writing a non-empty string for the field.

type SAMLConfig added in v0.7.0

type SAMLConfig struct {
	// SPCertificatePEM and SPPrivateKeyPEM are PEM-encoded; they sign every
	// outbound AuthnRequest and identify the SP in metadata XML. New returns
	// an error if either is missing or unparseable.
	SPCertificatePEM []byte
	SPPrivateKeyPEM  []byte

	// AuthnRequestTTL caps how long an outstanding SP-initiated AuthnRequest
	// ID is tracked for replay protection. Defaults to 10 minutes.
	AuthnRequestTTL time.Duration

	// ClockSkew accepted on Conditions.NotBefore / NotOnOrAfter. Defaults to
	// 30 seconds (matches crewjam default). Some IdPs (Okta on slow clocks)
	// need 60s.
	ClockSkew time.Duration
}

SAMLConfig wires the Service Provider keypair and base behavior. Per-IdP configuration lives in saml_connections rows, not here.

type SAMLConnection added in v0.7.0

type SAMLConnection struct {
	ID             ULID             `json:"id"`
	OrganizationID ULID             `json:"organizationId"`
	IdPEntityID    string           `json:"idpEntityId"`
	IdPSSOURL      string           `json:"idpSsoUrl"`
	IdPX509Cert    string           `json:"idpX509Cert"`
	SPEntityID     string           `json:"spEntityId"`
	SPACSURL       string           `json:"spAcsUrl"`
	AttributeMap   SAMLAttributeMap `json:"attributeMap"`
	CreatedAt      time.Time        `json:"createdAt"`
	UpdatedAt      time.Time        `json:"updatedAt"`
}

SAMLConnection is one IdP binding for one organization. An organization can hold multiple connections (e.g. two distinct Okta tenants for subsidiaries), each routed by id in the URL.

type SAMLConnectionInput added in v0.7.0

type SAMLConnectionInput struct {
	OrganizationID ULID
	IdPEntityID    string
	IdPSSOURL      string
	IdPX509Cert    string
	SPEntityID     string
	SPACSURL       string
	AttributeMap   SAMLAttributeMap
}

SAMLConnectionInput is the consumer-facing payload for create / update.

type SAMLIdentity added in v0.7.0

type SAMLIdentity struct {
	ID           ULID       `json:"id"`
	ConnectionID ULID       `json:"connectionId"`
	UserID       ULID       `json:"userId"`
	NameID       string     `json:"nameId"`
	NameIDFormat string     `json:"nameIdFormat"`
	LastLoginAt  *time.Time `json:"lastLoginAt,omitempty"`
	CreatedAt    time.Time  `json:"createdAt"`
}

SAMLIdentity links a successful SAML login to a user. Lookup key is (connection_id, name_id); name_id is whatever Subject.NameID.Value the IdP emitted, opaque and stable across sessions for most IdPs.

type SCIMConfig added in v0.7.0

type SCIMConfig struct {
	// RequireHTTPS rejects requests whose r.TLS is nil and whose
	// X-Forwarded-Proto header is not "https". Default true. Set false only
	// when SCIM is fronted by a TLS-terminating proxy that strips the header
	// (in which case the proxy is responsible for TLS).
	RequireHTTPS bool

	// MaxPageSize caps the count parameter on list endpoints. Defaults to
	// 200. RFC 7644 section 3.4.2 lets the server enforce a maximum.
	MaxPageSize int
}

SCIMConfig wires the SCIM 2.0 endpoint behavior.

type SCIMGroupFilter added in v0.7.0

type SCIMGroupFilter struct {
	DisplayName string
	ExternalID  string
}

SCIMGroupFilter is the equality-only filter accepted on /scim/v2/Groups.

type SCIMToken added in v0.7.0

type SCIMToken struct {
	ID             ULID       `json:"id"`
	OrganizationID ULID       `json:"organizationId"`
	Name           string     `json:"name"`
	TokenHash      []byte     `json:"-"`
	CreatedAt      time.Time  `json:"createdAt"`
	LastUsedAt     *time.Time `json:"lastUsedAt,omitempty"`
	RevokedAt      *time.Time `json:"revokedAt,omitempty"`
}

SCIMToken is a hashed bearer token bound to one organization. Plaintext is only ever returned from CreateSCIMToken; subsequent reads only ever see the hash. Hash is sha256(token); rationale documented in the v0.7 spec.

type SCIMUserFilter added in v0.7.0

type SCIMUserFilter struct {
	UserName   string
	ExternalID string
	Email      string
}

SCIMUserFilter is the equality-only filter accepted on /scim/v2/Users. Anything outside this whitelist returns 400 invalidFilter.

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"`
	// ActiveOrganizationID (v0.7) scopes a session to one organization for
	// the duration of the session. Nil in single-tenant deployments and on
	// any session that has not picked an org yet.
	ActiveOrganizationID *ULID `json:"activeOrganizationId,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 Stats added in v1.0.0

type Stats struct {
	AuditEmitted uint64 `json:"auditEmitted"`
	AuditWritten uint64 `json:"auditWritten"`
	AuditDropped uint64 `json:"auditDropped"`
	AuditFailed  uint64 `json:"auditFailed"`
}

Stats holds runtime counters useful for ops dashboards and tests. Counters are monotonically non-decreasing; reads are atomic snapshots. AuditFailed counts batches whose INSERT returned an error; the writer goroutine logs the error and does not retry (the v1.0 tradeoff documented in docs/2026-06-20-theauth-go-v1.0-design.md section 4.4).

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

	// Organizations + membership
	InsertOrganization(ctx context.Context, o Organization) (Organization, error)
	OrganizationByID(ctx context.Context, id ULID) (*Organization, error)
	OrganizationBySlug(ctx context.Context, slug string) (*Organization, error)
	UpdateOrganization(ctx context.Context, o Organization) error
	DeleteOrganization(ctx context.Context, id ULID) error

	UpsertOrganizationMember(ctx context.Context, m OrganizationMember) error
	DeleteOrganizationMember(ctx context.Context, orgID, userID ULID) error
	OrganizationMembersByOrg(ctx context.Context, orgID ULID) ([]OrganizationMember, error)
	OrganizationsByUser(ctx context.Context, userID ULID) ([]Organization, error)
	OrganizationMemberRole(ctx context.Context, orgID, userID ULID) (string, error)

	SetSessionActiveOrganization(ctx context.Context, sessionID ULID, orgID *ULID) error

	// SAML connections and identities
	InsertSAMLConnection(ctx context.Context, c SAMLConnection) (SAMLConnection, error)
	UpdateSAMLConnectionRow(ctx context.Context, c SAMLConnection) error
	DeleteSAMLConnection(ctx context.Context, id ULID) error
	SAMLConnectionByID(ctx context.Context, id ULID) (*SAMLConnection, error)
	SAMLConnectionsByOrg(ctx context.Context, orgID ULID) ([]SAMLConnection, error)

	UpsertSAMLIdentity(ctx context.Context, i SAMLIdentity) (SAMLIdentity, error)
	SAMLIdentityByConnectionAndNameID(ctx context.Context, connectionID ULID, nameID string) (*SAMLIdentity, error)
	TouchSAMLIdentityLastLogin(ctx context.Context, id ULID, at time.Time) error

	// SCIM tokens
	InsertSCIMToken(ctx context.Context, t SCIMToken) (SCIMToken, error)
	SCIMTokenByHash(ctx context.Context, hash []byte) (*SCIMToken, error)
	SCIMTokensByOrg(ctx context.Context, orgID ULID) ([]SCIMToken, error)
	RevokeSCIMTokenByID(ctx context.Context, id ULID, at time.Time) error
	TouchSCIMTokenLastUsed(ctx context.Context, id ULID, at time.Time) error

	// SCIM user + group lookups scoped to a single organization
	ListUsersByOrganization(ctx context.Context, orgID ULID, offset, limit int, filter SCIMUserFilter) (users []User, total int, err error)
	ListGroupsByOrganization(ctx context.Context, orgID ULID, offset, limit int, filter SCIMGroupFilter) (groups []Group, total int, err error)
	UserByExternalIDInOrg(ctx context.Context, orgID ULID, externalID string) (*User, error)
	UpdateUserSCIM(ctx context.Context, u User) error

	// Groups (SCIM)
	InsertGroup(ctx context.Context, g Group) (Group, error)
	GroupByID(ctx context.Context, id ULID) (*Group, error)
	GroupByExternalIDInOrg(ctx context.Context, orgID ULID, externalID string) (*Group, error)
	UpdateGroup(ctx context.Context, g Group) error
	DeleteGroup(ctx context.Context, id ULID) error
	SetGroupMembers(ctx context.Context, groupID ULID, userIDs []ULID) error
	AddGroupMembers(ctx context.Context, groupID ULID, userIDs []ULID) error
	RemoveGroupMembers(ctx context.Context, groupID ULID, userIDs []ULID) error
	GroupMembers(ctx context.Context, groupID ULID) ([]ULID, error)

	// ---------- v1.0 RBAC ----------
	// Permissions form a global catalog (no org scope). Insert is idempotent
	// on the name unique index; duplicate names return the existing row
	// rather than an error so seed runs at app start are safe.
	InsertPermission(ctx context.Context, p Permission) (Permission, error)
	PermissionByName(ctx context.Context, name string) (*Permission, error)
	ListPermissions(ctx context.Context) ([]Permission, error)

	InsertRole(ctx context.Context, r Role) (Role, error)
	UpdateRoleRow(ctx context.Context, r Role) (Role, error)
	DeleteRole(ctx context.Context, id ULID) error
	RoleByID(ctx context.Context, id ULID) (*Role, error)
	RoleByOrgAndName(ctx context.Context, orgID *ULID, name string) (*Role, error)
	RolesByOrganization(ctx context.Context, orgID *ULID) ([]Role, error)

	SetRolePermissions(ctx context.Context, roleID ULID, permissionIDs []ULID) error
	// PermissionsByRole returns the permission-name slice for one role.
	// The names are looked up by joining role_permissions to permissions.
	PermissionsByRole(ctx context.Context, roleID ULID) ([]string, error)

	GrantUserRole(ctx context.Context, ur UserRole) error
	RevokeUserRole(ctx context.Context, userID, roleID ULID) error
	RolesForUser(ctx context.Context, userID ULID, orgID *ULID) ([]Role, error)
	PermissionsForUser(ctx context.Context, userID ULID, orgID *ULID) ([]string, error)
	CountUsersWithPermissionInOrg(ctx context.Context, orgID ULID, perm string) (int, error)

	// ---------- v1.0 audit log ----------
	// InsertAuditEvents writes a batch in one round trip. Append-only;
	// adapters MUST NOT expose UPDATE or DELETE for audit rows. Failure
	// returns an error; the writer goroutine logs it and increments
	// Stats.AuditFailed without retrying (documented tradeoff).
	InsertAuditEvents(ctx context.Context, events []AuditEvent) error
	QueryAuditEvents(ctx context.Context, q AuditQuery) (events []AuditEvent, nextCursor string, err 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 TargetRef added in v1.0.0

type TargetRef struct {
	Type string `json:"type,omitempty"`
	ID   string `json:"id,omitempty"`
}

TargetRef points at the resource an audit event acts on. ID is a string (not ULID) because some targets are external identifiers (e.g. SCIM externalId) rather than ULIDs.

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) AddOrganizationMember added in v0.7.0

func (a *TheAuth) AddOrganizationMember(ctx context.Context, orgID, userID ULID, role string) error

AddOrganizationMember adds (or updates the role of) a user inside an organization. Roles must be one of "owner", "admin", "member".

func (*TheAuth) AuthenticateSCIMToken added in v0.7.0

func (a *TheAuth) AuthenticateSCIMToken(ctx context.Context, presented string) (ULID, error)

AuthenticateSCIMToken is the entry point invoked by the SCIM bearer middleware on every request. Returns the bound organization or an error. Touches last_used_at on success.

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) BeginSAMLLogin added in v0.7.0

func (a *TheAuth) BeginSAMLLogin(ctx context.Context, connectionID ULID, relayState string) (string, error)

BeginSAMLLogin returns the redirect URL for an SP-initiated SSO and records the AuthnRequest ID in the in-memory tracker for replay protection.

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), the WebAuthn challenge / TOTP enrollment GC loops (v0.5), the SAML AuthnRequest GC loop (v0.7), and the audit writer goroutine (v1.0). Audit drain waits up to Config.Audit.DrainTimeout (default 5 seconds) for the writer to flush. Safe to call multiple times.

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) CreateOrganization added in v0.7.0

func (a *TheAuth) CreateOrganization(ctx context.Context, name, slug string, ownerUserID ULID) (Organization, error)

CreateOrganization writes a new org row and adds the supplied user as its owner. Slug is lowercased and validated against the slug rules in validateSlug; the storage layer enforces uniqueness.

func (*TheAuth) CreateRole added in v1.0.0

func (a *TheAuth) CreateRole(ctx context.Context, orgID ULID, name, description string, perms []string) (Role, error)

CreateRole adds a new role to orgID with the listed permissions. Names are unique per org; returns the storage error (typically a unique-violation translation) on collision. Emits "role.created".

func (*TheAuth) CreateSAMLConnection added in v0.7.0

func (a *TheAuth) CreateSAMLConnection(ctx context.Context, in SAMLConnectionInput) (SAMLConnection, error)

CreateSAMLConnection inserts a fresh saml_connections row. AttributeMap is normalised: empty fields fall back to the WS-Federation defaults.

func (*TheAuth) CreateSCIMToken added in v0.7.0

func (a *TheAuth) CreateSCIMToken(ctx context.Context, orgID ULID, name string) (string, SCIMToken, error)

CreateSCIMToken mints a fresh 256-bit token, stores its sha256 hash, and returns the plaintext to the caller. The plaintext is the only point at which it leaves the library; subsequent reads only ever see the hash.

func (*TheAuth) DeleteRole added in v1.0.0

func (a *TheAuth) DeleteRole(ctx context.Context, roleID ULID) error

DeleteRole removes a role. Returns ErrRoleInUse when the role is the sole grantor of users:admin in its organization (lockout protection). Emits "role.deleted".

func (*TheAuth) DeleteSAMLConnection added in v0.7.0

func (a *TheAuth) DeleteSAMLConnection(ctx context.Context, id ULID) error

DeleteSAMLConnection removes the connection + cascades its identities.

func (*TheAuth) EmitAudit added in v1.0.0

func (a *TheAuth) EmitAudit(ctx context.Context, action string, target TargetRef, metadata map[string]any)

EmitAudit enqueues an audit event for asynchronous batched write. Non- blocking: when the writer channel is full the event is dropped and Stats.AuditDropped increments by 1. The actor user / session id and the request IP / user agent are pulled from ctx when present (Authn populates them); callers may also pass a context with those values explicitly attached via WithAuditMetadata.

EmitAudit may be called from any goroutine. The metadata map is mutated in place by the redactor; callers must not retain references after the call (or, equivalently, must defensively copy before passing in).

When Config.Audit is nil EmitAudit is a silent no-op. When Close has been called EmitAudit is also a silent no-op (the writer goroutine has drained and the channel is closed; pushing would panic).

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) FinishSAMLLogin added in v0.7.0

func (a *TheAuth) FinishSAMLLogin(ctx context.Context, connectionID ULID, samlResponseB64 string, ua, ip string) (string, Session, error)

FinishSAMLLogin validates an inbound SAMLResponse, runs find-or-create, issues a session, and returns its token. ua + ip annotate the session for audit.

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) GrantRole added in v1.0.0

func (a *TheAuth) GrantRole(ctx context.Context, actor, target, roleID ULID) error

GrantRole assigns roleID to the target user. The actor must already have permission to grant; callers should run the RequirePermission middleware upstream of this method. Idempotent on (user_id, role_id). Emits action "role.granted" with role_id and role_name in metadata.

func (*TheAuth) HasPermission added in v1.0.0

func (a *TheAuth) HasPermission(ctx context.Context, userID ULID, orgID *ULID, perm string) (bool, error)

HasPermission returns true when the user holds the named permission in the given organization, OR when the user holds the system super_admin role (which bypasses every check). orgID may be nil to ask only about system permissions.

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) ListOrganizationMembers added in v0.7.0

func (a *TheAuth) ListOrganizationMembers(ctx context.Context, orgID ULID) ([]OrganizationMember, error)

ListOrganizationMembers returns every member of the supplied organization.

func (*TheAuth) ListSAMLConnections added in v0.7.0

func (a *TheAuth) ListSAMLConnections(ctx context.Context, orgID ULID) ([]SAMLConnection, error)

ListSAMLConnections returns every connection for one organization.

func (*TheAuth) ListSCIMTokens added in v0.7.0

func (a *TheAuth) ListSCIMTokens(ctx context.Context, orgID ULID) ([]SCIMToken, error)

ListSCIMTokens returns every token (revoked or not) for the supplied org.

func (*TheAuth) ListUserOrganizations added in v0.7.0

func (a *TheAuth) ListUserOrganizations(ctx context.Context, userID ULID) ([]Organization, error)

ListUserOrganizations returns every organization the user is a member of.

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) OrganizationByID added in v0.7.0

func (a *TheAuth) OrganizationByID(ctx context.Context, id ULID) (*Organization, error)

OrganizationByID looks up an organization by ULID.

func (*TheAuth) OrganizationBySlug added in v0.7.0

func (a *TheAuth) OrganizationBySlug(ctx context.Context, slug string) (*Organization, error)

OrganizationBySlug looks up an organization by URL-safe slug.

func (*TheAuth) PermissionsForUser added in v1.0.0

func (a *TheAuth) PermissionsForUser(ctx context.Context, userID ULID, orgID *ULID) ([]string, error)

PermissionsForUser returns the user's permission set scoped to orgID (nil orgID returns only system-role permissions, i.e. super_admin). Sorted alphabetically for deterministic output.

func (*TheAuth) QueryAudit added in v1.0.0

func (a *TheAuth) QueryAudit(ctx context.Context, q AuditQuery) ([]AuditEvent, string, error)

QueryAudit returns up to q.Limit events plus an opaque cursor for the next page. Caller is responsible for permission-checking; this method does no auth.

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) RemoveOrganizationMember added in v0.7.0

func (a *TheAuth) RemoveOrganizationMember(ctx context.Context, orgID, userID ULID) error

RemoveOrganizationMember removes a user from an organization. Refuses to remove the last remaining owner (returns ErrLastOwner).

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) RequirePermission added in v1.0.0

func (a *TheAuth) RequirePermission(perms ...string) func(http.Handler) http.Handler

RequirePermission (v1.0) returns a middleware that enforces the caller holds every named permission inside session.active_organization_id. A session without active_organization_id is 403 rbac.no_active_org. A user with the system super_admin role bypasses the check entirely. The permission lookup hits storage once per request; subsequent middleware in the same chain hit the per-request cache attached to ctx.

When Config.RBAC is nil the middleware short-circuits to 500: an admin trying to wire RequirePermission without enabling RBAC is a programming error worth surfacing loudly.

func (*TheAuth) RevokeRole added in v1.0.0

func (a *TheAuth) RevokeRole(ctx context.Context, actor, target, roleID ULID) error

RevokeRole removes roleID from the target user. Emits "role.revoked".

func (*TheAuth) RevokeSCIMToken added in v0.7.0

func (a *TheAuth) RevokeSCIMToken(ctx context.Context, id ULID) error

RevokeSCIMToken marks the named token as revoked. Revoked tokens still resolve via SCIMTokenByHash but the middleware refuses them.

func (*TheAuth) SAMLConnectionByID added in v0.7.0

func (a *TheAuth) SAMLConnectionByID(ctx context.Context, id ULID) (*SAMLConnection, error)

SAMLConnectionByID looks up one connection.

func (*TheAuth) SAMLMetadataXML added in v0.7.0

func (a *TheAuth) SAMLMetadataXML(ctx context.Context, connectionID ULID) ([]byte, error)

SAMLMetadataXML serialises the per-connection SP metadata as XML, ready to hand to an IdP admin.

func (*TheAuth) SeedOrganizationRoles added in v1.0.0

func (a *TheAuth) SeedOrganizationRoles(ctx context.Context, orgID ULID) error

SeedOrganizationRoles creates the three default roles (or whatever the consumer configured) for one organization. Idempotent on (organization_id, name); existing roles keep their IDs and have their permission set reconciled against the seed.

Called automatically from handlers_organizations.go::CreateOrganization; safe to call again manually (e.g. after extending the seed list).

func (*TheAuth) SeedPermissions added in v1.0.0

func (a *TheAuth) SeedPermissions(ctx context.Context) ([]Permission, error)

SeedPermissions ensures every seeded + consumer-extended permission row exists in storage. Idempotent on the permissions.name unique index. Returns the canonical permission rows (with their persisted IDs) so the caller can reuse them for role assignments.

SeedPermissions runs lazily on first SeedOrganizationRoles / CreateRole invocation; consumers wanting eager seeding at app start may call it directly from their bootstrap.

func (*TheAuth) SetActiveOrganization added in v0.7.0

func (a *TheAuth) SetActiveOrganization(ctx context.Context, sessionID ULID, orgID *ULID) error

SetActiveOrganization sets (or clears, when orgID is nil) the active organization on a session. The caller is responsible for verifying that the session's user is a member of orgID before calling.

func (*TheAuth) Start added in v1.0.0

func (a *TheAuth) Start() error

Start spawns the audit writer goroutine when Config.Audit is non-nil and the writer is not already running. Idempotent; safe to call multiple times. New calls Start automatically so existing callers that never invoked Start keep working.

func (*TheAuth) Stats added in v1.0.0

func (a *TheAuth) Stats() Stats

Stats returns a snapshot of runtime counters.

func (*TheAuth) UpdateRole added in v1.0.0

func (a *TheAuth) UpdateRole(ctx context.Context, roleID ULID, name, description string, perms []string) (Role, error)

UpdateRole rewrites name / description / permissions on an existing role. nil-valued fields are not touched (caller passes the current value). Emits "role.updated".

func (*TheAuth) UpdateSAMLConnection added in v0.7.0

func (a *TheAuth) UpdateSAMLConnection(ctx context.Context, id ULID, in SAMLConnectionInput) (SAMLConnection, error)

UpdateSAMLConnection rewrites an existing connection in place.

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"`
	// ExternalID (v0.7 SCIM) stores the SCIM client's stable identifier so
	// upsert by externalId works. Empty for users not created via SCIM.
	ExternalID string `json:"externalId,omitempty"`
	// GivenName / FamilyName / DisplayName (v0.7 SCIM) capture the structured
	// name attributes SCIM clients provision; they are best-effort projections
	// alongside Name and may be empty for users created via other flows.
	GivenName   string `json:"givenName,omitempty"`
	FamilyName  string `json:"familyName,omitempty"`
	DisplayName string `json:"displayName,omitempty"`
}

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 UserRole added in v1.0.0

type UserRole struct {
	UserID    ULID      `json:"userId"`
	RoleID    ULID      `json:"roleId"`
	GrantedAt time.Time `json:"grantedAt"`
	GrantedBy *ULID     `json:"grantedBy,omitempty"`
}

UserRole records a grant of one role to one user. GrantedBy is the actor that issued the grant; nil indicates a system grant (e.g. seeded by SeedOrganizationRoles itself).

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 admin holds shared helpers for the /admin/v1 HTTP surface: RFC 7807 problem+json marshalling and keyset cursor encoding.
Package admin holds shared helpers for the /admin/v1 HTTP surface: RFC 7807 problem+json marshalling and keyset cursor encoding.
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
samltest
Package samltest provides an in-process SAML IdP for end-to-end testing of the SP integration.
Package samltest provides an in-process SAML IdP for end-to-end testing of the SP integration.
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