Help us shape the future of Go! Take the 2026 Go Developer Survey

sigil

package module
v0.1.0-alpha.9 Latest Latest
Warning

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

Go to latest
Published: Sep 23, 2026 License: MIT Imports: 36 Imported by: 0

README

Sigil

Authentication you embed, not a service you trust

Sigil is a Go library that gives an application you already run registration, verified password sign-in, magic links, OAuth, revocable sessions and a durable audit trail — over your own PostgreSQL, behind your own router, with a separately versioned TypeScript client for the browser.

CI Go 1.27.1 PostgreSQL 17 TypeScript client v0.1.0-alpha.1, unreleased

Quickstart  ·  Documentation  ·  API contract  ·  Threat model  ·  Changelog


[!IMPORTANT] v0.1.0-alpha.1 is prepared and validated, but not published. There is no Go module on the proxy and no npm release, so go get github.com/brewgenixapp/sigil will not resolve — clone the repository and point at it with a replace directive. Nothing in this release carries a compatibility promise; see alpha means no promises and releasing.


Why Sigil

Sigil is a library, not a service. You add it to an application you already run, give it a PostgreSQL schema and a way to send mail, and mount its handler on a path you choose. Nothing else starts: no goroutine, no timer, no listener of its own, no vendor to call.

Principle What it means in practice
Your database, your transactions Identity lives in a schema you own, reached through a public adapter contract with a reusable conformance gate. A verification mail is queued inside the same transaction that created the user: if the insert rolls back, no mail exists.
Fails closed, on purpose Without limiter storage, HTTP requests are refused rather than waved through. sigil.New refuses storage that cannot record an audit event. Sign-in before verification is refused. An invalid Config fails at construction, not at the first request.
One authorization path Typed Go calls and HTTP go through the same checks, held there by the contract package and conformance tests rather than by convention (core contracts).
The browser part is designed, not bolted on Origin and CSRF policy, cookie policy, proxy-address handling, bounded bodies and redirect policy are one documented surface (HTTP security) — with @sigil/client and @sigil/react generated from the same route table.
Operable by someone who is not you An operator CLI with documented exit codes applies migrations, diagnoses a deployment without printing secrets, and prunes expired rows. Key rotation is a written procedure, not a migration script.
Nothing publishes from a laptop Releases are cut only by a workflow: full CI, a reproducible build, an SBOM, signed SLSA provenance, verification, then the tag. No long-lived credential exists.

Quickstart

The fastest way to see the whole thing work is the browser example. It uses only the public HTTP API and needs no mail account, no provider and no credentials.

git clone https://github.com/brewgenixapp/sigil.git && cd sigil
make pg-up                      # disposable PostgreSQL on 127.0.0.1:55432
make browser
export SIGIL_DATABASE_URL='postgres://auth_ci:auth_ci@127.0.0.1:55432/auth_ci?sslmode=disable'
./bin/browser --migrate

Open http://127.0.0.1:8081/ (that exact host, not localhost) and walk the journey: register → Check mail → verification link → Confirm email → sign in → reset the password and do it again. examples/browser explains what each step proves and where the boundaries are.

Adding Sigil to your own application

store, err := postgres.New(db, "auth")            // db is your *sql.DB; Sigil borrows it
protector, err := sigil.NewAESProtector("v1", map[string]sigil.Secret{
    "v1": sigil.NewSecret(mailKey),               // a separate random 32-byte key
})
delivery, err := sigil.NewMailDelivery(store, protector, sender) // sender is yours

auth, err := sigil.New(ctx, sigil.Config{
    BaseURL: "https://app.example.com",           // the public origin, without the mount path
    Secret:  sigil.SecretFromEnv("SIGIL_SECRET"), // 32+ random bytes, identical on every replica
    Store:   store,
    Registration: sigil.RegistrationConfig{
        Delivery:            delivery,
        RenderVerification:  renderVerificationMail,
        RenderPasswordReset: renderResetMail,
        RenderEmailChange:   renderEmailChangeMail,
    },
})

Mount it wherever you like. Sigil owns every path under the mount, including its own 404 and 405 answers.

mux.Handle("/api/auth/", http.StripPrefix("/api/auth", auth.Handler()))
mux.Handle("GET /account", auth.RequireSession(http.HandlerFunc(account)))

Then run the migrations as an operator, never from a serving process, and check the deployment before it takes traffic:

$ ./bin/sigil migrate apply --schema auth
$ ./bin/sigil check --schema auth --base-url https://app.example.com --trusted-proxy 10.0.0.0/24
ok      secret: SIGIL_SECRET is set with at least 32 bytes (length only; entropy is not measurable here)
ok      plugins: none selected; pass --plugin for each plugin the deployment enables
ok      configuration: HTTPS deployment: base URL, trusted origins, proxies, cookie policy and session lifetimes are valid
ok      database: connected to the auth schema's server
ok      core schema: version 5 of 5, history and catalog match this binary

A deployment owns exactly two loops, because Sigil starts nothing on a timer: one dispatches mail from the outbox, one prunes expired rows. Both are safe on every replica — each claim takes FOR UPDATE SKIP LOCKED. The full walkthrough, with the reasoning, is installation and quickstart; examples/router is the same wiring as working code.


Architecture

flowchart LR
    B["Browser<br/>@sigil/client · @sigil/react"] -->|cookie + CSRF proof| R

    subgraph HOST["Your Go application"]
        direction TB
        R["Your router<br/>http.StripPrefix on a path you choose"] --> S
        subgraph SIGIL["sigil.Auth"]
            direction TB
            S["HTTP surface<br/>route table · origin · CSRF · cookies · bodies"] --> T["Typed services<br/>Sessions · Passwords · Audit · Retention · Admin"]
            T --> PL["Compiled plugins<br/>magic-link · oauth"]
        end
        W["Two host-owned workers<br/>mail dispatch · expired-row cleanup"]
    end

    T --> ST["storage adapter"]
    PL --> ST
    W --> ST
    ST --> DB[("PostgreSQL<br/>your schema")]
    CLI["sigil CLI<br/>migrate · check · cleanup · openapi"] -.-> DB

    classDef brand fill:#5B2BFF,stroke:#4D20F4,color:#ffffff
    classDef plain fill:none,stroke:#8B5CF6,color:#8B5CF6
    class S,T brand
    class B,R,PL,W,ST,CLI,DB plain

What's in the box

Area Implemented Contract
Accounts Registration, email verification, email change, account deletion behind recent authentication credentials · email verification · deletion
Administration Host-only suspension and reinstatement across every sign-in method and API keys, per-account session and key revocation, operator step-up and attribution administration
Impersonation Optional, host-authorized support sessions for another account: operator step-up, target exclusions, bounded duration, per-use operator rechecks, no sensitive operations, operator-attributed audit events, cookie save and restore impersonation
Passwords Verified sign-in, reset, authenticated change, versioned Argon2id with revision-checked upgrade on verification passwords · recovery
Sessions Opaque, storage-authoritative, revocable, renewable, with step-up reauthentication sessions
Other sign-in Magic links, and OAuth/OIDC with PKCE, Google and GitHub adapters, explicit linking and last-method-safe unlinking — as compiled plugins magic links · OAuth
Organization SSO Owner-configured OpenID Connect and SAML 2.0 connections per organization (Okta, Entra ID, Google Workspace shapes), DNS-verified domains, per-connection identity namespaces and audiences, claim/attribute, linking and membership policy organization SSO
SCIM provisioning Organization-bound SCIM 2.0 users and groups for Okta- and Entra ID-shaped clients: domain-proven provisioning, opt-in adoption, suspension on deactivation, SSO links, checked by an independent RFC checker SCIM
Authorization server Operate an OAuth 2.0 authorization server and OpenID Connect provider for your users: host-registered clients, consent and grants, code flow with S256 PKCE, RFC 9207 iss, single-audience opaque access tokens, rotating refresh-token families revoked on replay, ID tokens, UserInfo, discovery, JWKS rotation, revocation and introspection, checked by go-oidc and Chromium authorization server
Browser security Origin and CSRF policy, cookie policy, trusted proxies, bounded bodies, safe redirects HTTP security
Abuse control Distributed budgets over shared storage, failing closed when the limiter store is unavailable abuse controls
Audit Every acknowledged security operation commits with its event; New refuses storage that cannot record one audit events
Mail Durable encrypted outbox, host-owned dispatcher, local development sink, rotation over live payloads delivery · key rotation
Storage PostgreSQL and SQLite core adapters, versioned migrations, validated history and schema fingerprints, no down migrations migrations · PostgreSQL · SQLite
Operations sigil migrate status|apply|sql, check, cleanup, openapi, version, with documented exit codes CLI
Clients @sigil/client over generated transport types; @sigil/react session hooks with server-rendered hydration client · React
The HTTP surface — one exported route table, deterministically exported as OpenAPI 3.1

sigil.Describe and sigil openapi produce docs/openapi.json without a database, secret or mail; the TypeScript transport types are generated from that export and drift-checked in CI.

Group Routes
Session GET /session · POST /session/renew · POST /session/revoke · GET /sessions · POST /sessions/revoke · GET /csrf
Credentials POST /register · POST /sign-in/password · POST /logout
Email POST /verify-email · POST /verify-email/resend · POST /email/change · POST /email/change/confirm
Password POST /password/reset/request · POST /password/reset · POST /password/change
Account POST /account/delete
Magic link (plugin) POST /magic-link/request · POST /magic-link/redeem
OAuth (plugin) POST /oauth/start · POST /oauth/complete · POST /oauth/link/start · POST /oauth/link/complete · POST /oauth/unlink · GET /oauth/accounts
Organization SSO (plugin) POST /sso/start · POST /sso/complete · POST /sso/link/start · POST /sso/link/complete · POST /sso/connections/create|update|delete|list · POST /sso/domains/claim|verify|remove|list · POST /sso/saml/complete · POST /sso/saml/link/complete · POST /sso/saml/connections/create|update|list
SCIM (plugin) POST /scim/directories/create|update|delete|list · POST /scim/tokens/issue|revoke; the SCIM 2.0 protocol itself is a separate handler the host mounts (SCIM)
Authorization server (plugin) POST /authorization/interaction · POST /authorization/decide · POST /authorization/grants/list|revoke; the OAuth and OIDC endpoints are a separate handler the host mounts at the issuer (authorization server)

Every mutation needs CSRF proof bound to the current cookie. The session bearer is delivered only as a HttpOnly cookie; response bodies carry a public view that holds no credential material. Registration answers the same whether or not the address was already registered.


Packages

Path Responsibility and status
sigil (root) Validated configuration, lifecycle, typed services, safe DTOs and HTTP/browser security. Transport and services share one package deliberately, so typed calls and HTTP go through one authorization path (D-5/AS-37); the seam is enforced by contract and the conformance tests, not by a package boundary
storage Public adapter contract, explicitly unstable until AUTH-051
storage/conformance Reusable adapter conformance gate; a testing helper, not runtime API
storage/postgres Versioned PostgreSQL migrations and the transactional adapter
storage/sqlite SQLite core adapter, explicit migrations and local-file storage, in its own file or shared with your application's database; requires CGO
plugin Compiled registration, routes and hooks; validated for the current alpha by AUTH-045, no sandbox
contract Endpoint metadata, schema subset and OpenAPI export; imported by core and plugins
cli, cmd/sigil Operator CLI; embeddable by hosts with compiled plugins (experimental API)
packages/client Separate ESM npm package: generated transport types and the framework-neutral runtime client
packages/react React bindings: SigilProvider, useSession, useClient, useAction
Examples — six runnable hosts, each proving a different claim
Example What it proves
examples/browser The full password journey over the public API, with PostgreSQL, a local mail sink and Chromium tests
examples/nethttp Application-owned server: mounting, sessions, passwords, abuse composition and ordered shutdown
examples/router A deployment-shaped chi service with both workers — and three things chi gets wrong by default, held to executable tests. Its own Go module, so chi is not a library dependency
examples/client @sigil/client driven in Chromium against the Go server
examples/react Go plus a Node render server: hydration and isolation between concurrent users, in Chromium, Firefox and WebKit
examples/oauth-smoke The operator-run live check against the real Google and GitHub providers

The browser, client and React examples share examples/internal/loopback, an HTTPS loopback host with core credentials, mail, magic links, mock-provider OAuth and a mail preview.

All project Go packages except examples/router share the root module, and there is no go.work. The nested evaluation modules under spikes/auth-002/ are excluded from root builds and from npm workspaces; run them only with their own instructions.


Development

Go 1.27.1 or newer, Node.js 22 or newer, npm 10 or newer. No database, Docker, production credential, environment file or provider account is needed for ordinary checks.

bash scripts/check.sh    # the complete local CI equivalent
make help                # every target: checks, pg-up/pg-down, examples, contract, release
go test -race ./...      # the core security and lifecycle tests

Format changed Go files with gofmt -w. CI runs Go, TypeScript, dependency and licence inventory, vulnerability scans, workflow linting and PostgreSQL integration checks on every push and pull request, and weekly. Individual commands and the optional PostgreSQL job are in development checks.


Documentation

Start here: installation and quickstart → deployment → upgrading.

For sizing and overload behavior, see capacity measurements and operating limits. When something answers the wrong thing, troubleshooting is organised by symptom.

Getting it running Understanding it Operating it
Installation Architecture decision Operator CLI
Deployment Core contracts Migrations
Upgrading Storage contracts Key rotation
Troubleshooting Threat model Account deletion and retention · Administration · Impersonation
Development checks API contract Versioning and compatibility
Browser example HTTP security Releasing

Security and releases

Every acknowledged security operation leaves a durable audit event, secrets are protected by a versioned keyring with a written rotation procedure, and the threat model records the security requirements, the selected dependencies and the policy for updating them.

A release is declared by naming the first section of CHANGELOG.md, and is cut only by the release workflow: full CI, a reproducible build with a CycloneDX SBOM, SLSA provenance signed by a separate reusable workflow, verification of every artifact, and only then the tag and an immutable release behind an approved environment. testdata/upgrade/ holds the database each release leaves behind for the next one to move forward. To check what you run, see verifying a release; to report a vulnerability, see SECURITY.md.


Status and limitations

Current release v0.1.0-alpha.1 — built and validated, not published. Schema: core 5, magic-link 1, oauth 1. Implementation status per task lives in the task backlog.

  • Admin().CreateUser returns unavailable.
  • The storage adapter contract is versioned; plugin registration retains its alpha compatibility policy.
  • SQLite storage supports core authentication, TOTP/recovery, passkeys and organizations. The independent security review and production release gates remain open.
  • The npm packages are still private and the @sigil/ scope is unclaimed; publication and the maintenance policy remain with AUTH-040.

Licence

MIT, copyright Brewgenix. Use it, embed it in proprietary software, fork it and redistribute it; keep the copyright notice.

The licence covers this project's own source. Dependencies keep their own terms — MIT, BSD, ISC and Apache-2.0, inventoried with resolved versions and licence hashes in docs/dependency-inventory.json — and a distribution has to retain those notices too. "Sigil" and the marks in assets/ are not covered by the licence and no trademark availability is claimed.



The Go module is github.com/brewgenixapp/sigil and the root package is sigil.
The npm name @sigil/client is provisional and both manifests are private; no registry scope or trademark availability is claimed.
Naming and package metadata are development choices, not a release. See provenance for design origins.

Optional TOTP enrollment and authentication adds encrypted factor storage, confirmation, replay protection and password-plus-TOTP pending-session completion. Single-use recovery codes, explicit factor removal, and operator key rewrapping support the factor lifecycle.

Optional passkeys add user-verified WebAuthn registration, discoverable login, credential management and browser helpers, with PostgreSQL/SQLite replay protection and atomic session issuance.

Optional organizations add organizations, memberships, session-specific active tenant selection, and transaction-scoped membership checks for Go services and HTTP. Email-bound invitations, member removal and atomic ownership transfer enforce single-use tokens and last-owner protection. Owner/admin/member roles and live authorization are covered by the organization validation gate. Run the organization example for the full lifecycle.

Optional organization SSO lets each organization's owner connect the organization's own OpenID Connect provider or SAML 2.0 identity provider and prove its email domains through DNS. A connection signs in only identities linked through it, and only into its own organization: nothing proven through one organization's provider works through another's, an email is trusted for automatic linking only in a domain that organization verified, and SSO never creates accounts.

Optional SCIM provisioning lets the organization's identity provider create, deactivate and delete its users and groups through a SCIM 2.0 endpoint, with a per-directory token. It provisions only addresses in domains the organization verified, never takes over an existing account unless the owner opted in (and then only verified ones, never managed), suspends the accounts it created on deactivation, and never deprovisions the owner.

Optional authorization server support makes your application an OAuth 2.0 and OpenID Connect issuer for other applications. Clients are registered only by host code, redirect URIs match byte for byte, codes and refresh tokens are single use across instances and processes, a replayed code or refresh token revokes what it minted, and every token use and refresh is checked under the owner's lock against the grant, the client and the security epoch.

Optional scoped API keys authenticate non-interactive callers of your own API as a mode separate from sessions: owner- or organization-bound keys with catalog scopes and audiences, required expiry, per-key rate limits shared by every instance, digest-only storage and a secret shown once. Every use rechecks revocation, expiry, tenant and scope in the transaction that does the work, and is recorded in the audit trail against the key.

Administration suspends an account everywhere at once: sessions are revoked and every sign-in method, step-up and API key is refused under the account lock until reinstatement, with refusals that look like any other failed sign-in. Administrators can also list and revoke one account's sessions and keys, attributed to the host or to an operator whose own session, step-up and role are rechecked on every call. Optional impersonation gives an operator a short session as another account: it cannot do anything that needs recent authentication, ends with the operator's own session or privilege, and records the operator as the actor of everything it does.

Optional custom user fields add typed, versioned account fields declared once in Go: derived PostgreSQL/SQLite migrations, validation, owner read/update endpoints and generated Go and TypeScript types. Every field declares private, read-only or read-write access, and private fields never reach the API contract or automatic serialization. The profile example checks each generated artifact. Later versions can loosen a field or remove it (explicitly, and marked destructive), and sigil migrate plan shows exactly what an apply would run, with a digest migrate apply --plan holds it to; see migration planning.

Documentation

Overview

Package sigil provides the experimental embedded Sigil authentication foundation: immutable configuration, lifecycle and actor-authorized service contracts. Storage and authentication flows are not implemented yet. See docs/core-contracts.md for ownership, cancellation and trust boundaries.

Index

Constants

View Source
const (
	// MaxPasswordBytes bounds input before UTF-8 scanning or copying.
	MaxPasswordBytes = 1024
	// MinPasswordRunes applies to new passwords, not historical verification.
	MinPasswordRunes = 15
)

Variables

View Source
var (
	ErrRateLimited                  = &Error{Code: CodeRateLimited, Message: "too many attempts"}
	ErrOverloaded                   = &Error{Code: CodeOverloaded, Message: "authentication capacity exhausted"}
	ErrAbuseUnavailable             = &Error{Code: CodeAbuseUnavailable, Message: "abuse controls unavailable"}
	ErrInvalidConfiguration         = &Error{Code: CodeInvalidConfiguration, Message: "invalid configuration"}
	ErrUnauthenticated              = &Error{Code: CodeUnauthenticated, Message: "authentication required"}
	ErrAssuranceRequired            = &Error{Code: CodeAssuranceRequired, Message: "stronger authentication required"}
	ErrForbidden                    = &Error{Code: CodeForbidden, Message: "operation forbidden"}
	ErrRecentAuthenticationRequired = &Error{Code: CodeRecentAuthenticationRequired, Message: "recent authentication required"}
	ErrUnavailable                  = &Error{Code: CodeUnavailable, Message: "authentication is not implemented"}
	ErrClosed                       = &Error{Code: CodeClosed, Message: "authentication service is closed"}
	ErrBadRequest                   = &Error{Code: CodeBadRequest, Message: "invalid request"}
	ErrNotFound                     = &Error{Code: CodeNotFound, Message: "endpoint not found"}
	ErrMethodNotAllowed             = &Error{Code: CodeMethodNotAllowed, Message: "method not allowed"}
	ErrBodyTooLarge                 = &Error{Code: CodeBodyTooLarge, Message: "request body too large"}
	ErrUnsupportedMediaType         = &Error{Code: CodeUnsupportedMediaType, Message: "unsupported content type"}
)
View Source
var ErrDelivery = errors.New("sigil: email delivery failed")
View Source
var ErrProtection = errors.New("sigil: secret protection failed")

Functions

func Describe

func Describe(c Config) (contract.Document, error)

Describe exports the API contract for a configuration without side effects. It compiles the plugin registry and resolves the browser cookie policy, and reads nothing else: no storage connection, secret load, startup check or mail. The same description is validated by New, so a configuration that starts also exports, and an export never requires production credentials.

func ImpersonatorFromContext

func ImpersonatorFromContext(ctx context.Context) (string, bool)

ImpersonatorFromContext returns the operator's user ID when ctx's validated session is an impersonation session. The session's user is the target. Hosts use it to show that a session is impersonated and to refuse their own sensitive actions, as the library refuses its own.

func IsAccountSuspended

func IsAccountSuspended(err error) bool

IsAccountSuspended reports whether a direct call failed only because its account is suspended. The error still matches ErrUnauthenticated and HTTP responses never distinguish it; hosts may use this to explain the refusal to a caller they have otherwise authenticated, and plugins use it to avoid recording a second failure event for the same attempt.

func IsEmailTaken

func IsEmailTaken(err error) bool

IsEmailTaken reports whether WithNewAccount failed only because another account already has the address. It still matches ErrBadRequest.

func Refuse

func Refuse(reason AuditReason, err *Error) error

Refuse returns an error that a transactional callback of WithCredential, WithAssurance, WithRecentSession or WithAdministration returns to reject the operation while committing what it already wrote, such as revoking the tokens a replayed authorization code minted. The operation's failure event, with reason, commits in the same transaction in place of its success event; plugin hooks do not run; the caller receives err. Use it only when the rejection itself must change state; every other rejection rolls back, as a refusal does under an empty operation.

func Version

func Version() string

Version reports the release this build was compiled from, exactly as the Go toolchain recorded it and with the leading "v" the module version carries, so it is the Git tag rather than a transformation of it. It is empty when the toolchain had nothing to record.

Nothing in the repository states a release number. The tag is the only source of truth, which is why no file has to be remembered at release time and why nothing can disagree with the tag. The cost is that this is best-effort: it reports a published version to a consumer who resolved one, a pseudo-version with a "+dirty" suffix inside a working checkout, and nothing at all under `go run`, in a test binary, with -buildvcs=false, or when a replace directive reaches this module. Log it; never decide on it.

Compatibility for each surface is docs/versioning.md.

func WithAuditMethod

func WithAuditMethod(ctx context.Context, method AuditMethod) context.Context

WithAuditMethod names, for events recorded under ctx, the credential or method an operation concerns, such as "api_key.<id>" for a session-authorized API-key issuance. It is for trusted code only; flows that establish their own sign-in method override it. The value must match ^[a-z][a-z0-9_.-]{0,79}$ or the operation fails as a bad request when its event is written.

Types

type AbuseConfig

type AbuseConfig struct {
	Store                     storage.AbuseStore
	Policies                  map[AbuseFlow]AbusePolicy
	MaxRequests, MaxExpensive int
}

AbuseConfig limits each Auth instance's active requests and expensive work. There are no waiting queues. Store defaults to Config.Store when supported; without a shared store, protected operations fail closed (including in development). All replicas must use identical secrets and policies.

type AbuseFlow

type AbuseFlow string
const (
	AbuseRequest      AbuseFlow = "request"
	AbuseSignIn       AbuseFlow = "sign_in"
	AbuseRegistration AbuseFlow = "registration"
	AbuseRecovery     AbuseFlow = "recovery"
	AbuseVerification AbuseFlow = "verification"
	AbuseToken        AbuseFlow = "token"
	AbuseMail         AbuseFlow = "mail"
	// AbuseClient budgets the authentication of machine callers, such as an
	// authorization server's clients and resource servers, per address and
	// per client identifier. Its defaults suit steady machine traffic.
	AbuseClient AbuseFlow = "client"
)

type AbusePolicy

type AbusePolicy struct{ IPAttempts, IdentifierAttempts int }

type AbuseService

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

func (AbuseService) Do

func (s AbuseService) Do(ctx context.Context, flow AbuseFlow, ip netip.Addr, identifier string, expensive bool, work func(context.Context) error) error

Do reserves local capacity, consumes shared budgets, then runs work once. The callback must honor context cancellation; slots remain held until it returns. Use expensive=false with Passwords(), which acquires its own KDF slot. Use expensive=true for other expensive callbacks. A flow inside the HTTP request callback reuses its request slot, as do nested service flow budgets. Do not recursively nest expensive work.

type Account

type Account struct {
	User        User       `json:"user"`
	SuspendedAt *time.Time `json:"suspendedAt"`
}

Account is the administrative view of an account's state.

type AdminService

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

AdminService is a host-only capability for explicit administrative operations. Possession of Auth grants access; never expose this capability to untrusted plugins or dispatch it from browser routes. The host must authorize its admin caller. Request authentication is not an administrative authorization grant.

func (AdminService) Account

func (s AdminService) Account(ctx context.Context, userID string) (Account, error)

Account reads an account's administrative state. It records no event.

func (AdminService) CompleteStepUp

func (s AdminService) CompleteStepUp(ctx context.Context, method AuditMethod, verify func(storage.Tx, storage.User, storage.Session) (AssuranceLevel, error)) (IssuedSession, error)

CompleteStepUp is a trusted host/plugin capability, never a browser DTO. verify must verify/consume fresh proof bound to this user, session and epoch inside tx and return the assurance actually proven. MultiFactor requires two independent factors (or a user-verified multi-factor authenticator); a method name or two proofs of the same factor is insufficient. Stale existing evidence cannot be counted. Proof consumption, old-bearer revocation and issuance commit together. All returned credentials must be discarded on error. An impersonation session cannot step up: forbidden.

func (AdminService) CreateUser

func (s AdminService) CreateUser(ctx context.Context, input CreateUser) (User, error)

CreateUser is the administrative import contract, not browser registration. It remains unavailable until transactional identity storage is implemented.

func (AdminService) DeleteUser

func (s AdminService) DeleteUser(ctx context.Context, userID string) error

DeleteUser is the administrative deletion for operator-driven removal, such as a data-subject request. The host authorizes its caller; no session is involved. It performs the same transactional cascade, mail withdrawal and audit event as DeleteCurrent, attributed to the host or to the administrator of an Operator context. A suspended account can be deleted. Like RevokeAllSessions, an unknown user is reported as unauthenticated.

func (AdminService) Impersonate

func (s AdminService) Impersonate(ctx context.Context, targetID string, duration time.Duration) (IssuedSession, error)

Impersonate starts an impersonation of targetID by the administrator whose validated session ctx carries (a request behind RequireSession, or from Sessions().Authenticate), and returns a new session for the target. It requires Config.Impersonation. The operator's session must be live, not suspended, not itself an impersonation, and satisfy the policy's assurance and recency; the target must exist, not be the operator and not be suspended; and Authorize must allow the pair. The operator's session must also prove at least the assurance the target's AssurancePolicy requires, so impersonation never bypasses the target's MFA policy, and the new session carries the operator's assurance, never more.

The session lasts duration (zero is the policy's MaxDuration, which is also the upper bound) and never outlives the operator's session. It is the target's session in every ordinary respect, except that each use revalidates the operator and Authorize first, every event it causes is attributed to the operator with the target as subject, and it can never perform an operation that requires recent authentication, step up, or sign the target out everywhere: those are forbidden. Neither the operator's nor the target's own sessions change. Records admin.impersonation_start, whose actor session is the operator's own and whose session is the impersonation session. A denial by Authorize is recorded as a forbidden failure on that operation.

func (AdminService) IssueSession

func (s AdminService) IssueSession(ctx context.Context, userID string, method AuditMethod) (IssuedSession, error)

IssueSession is a host-only authentication capability. The host must have just authenticated this user and names how (MethodPassword, MethodMagicLink, OAuthMethod or a host identifier) so the audit event records the method. Never dispatch it from a user-ID browser input. The impersonation method is reserved for Impersonate and is a bad request here.

func (AdminService) Operator

Operator binds the administrative calls made with the returned context to the administrator whose validated session ctx carries, for example a request behind RequireSession. The session is checked now and again at the start of every administrative call: it must be live, not suspended, satisfy the policy's assurance and recency, and pass Authorize. The events of those calls are attributed to that user and session instead of the host. The operator cannot suspend their own account. Only one account can be locked per transaction, so the recheck commits before the target account is locked; revoke an administrator's sessions to stop calls already admitted.

func (AdminService) Reinstate

func (s AdminService) Reinstate(ctx context.Context, userID string) error

Reinstate lifts a suspension. It restores nothing the suspension revoked: the user signs in again with any credential that still exists, and API keys that were neither revoked nor expired work again. Reinstating an active account succeeds without change of state. Records admin.user_reinstate.

func (AdminService) RevokeAllSessions

func (s AdminService) RevokeAllSessions(ctx context.Context, userID string) error

RevokeAllSessions advances the account's security epoch, revoking every session and outstanding proof without suspending it. Records admin.sessions_revoke. An unknown user is unauthenticated.

func (AdminService) RevokeSession

func (s AdminService) RevokeSession(ctx context.Context, userID, sessionID string) error

RevokeSession revokes one of an account's sessions by its public ID. Revoking an already revoked session succeeds; a session the account does not own is unauthenticated, like an unknown account. Records admin.session_revoke with the session.

func (AdminService) Sessions

func (s AdminService) Sessions(ctx context.Context, userID, afterID string, limit int) ([]Session, error)

Sessions lists at most limit (1..100) of an account's active sessions in ID order after afterID, for an administrator deciding what to revoke.

func (AdminService) Suspend

func (s AdminService) Suspend(ctx context.Context, userID string) error

Suspend withdraws every form of access from an account until Reinstate. In one transaction it marks the account suspended and advances its security epoch, which revokes every session (including pending MFA sessions) and every outstanding verification, reset, email-change and magic-link proof. Afterwards no supported method can authenticate the account: password, magic link, OAuth, passkey, TOTP and recovery step-up, host IssueSession, and API keys through WithCredential are all refused, and reset, verification and magic-link requests send nothing. API keys are blocked, not revoked. Suspending a suspended account succeeds, keeps the original time and revokes again. Records admin.user_suspend.

func (AdminService) VerifyAndIssueSession

func (s AdminService) VerifyAndIssueSession(ctx context.Context, userID string, method AuditMethod, verify func(storage.Tx, storage.User) (AssuranceLevel, error)) (IssuedSession, error)

VerifyAndIssueSession is a trusted authentication plugin capability. userID only selects the account to lock; verify must authenticate that account using fresh proof and consume the proof in tx. The proof and session commit together. Never expose a callback that accepts browser claims as verified assurance.

type AssuranceLevel

type AssuranceLevel int

AssuranceLevel describes verified evidence, not a claim about NIST compliance.

const (
	SingleFactor AssuranceLevel = 1
	MultiFactor  AssuranceLevel = 2
)

type AssurancePolicy

type AssurancePolicy func(storage.Tx, storage.User) (AssuranceLevel, error)

AssurancePolicy reads factor enrollment under the user's transaction lock. It must return a valid minimum on every call; errors fail closed. It must not perform network I/O or retain tx. Nil preserves single-factor deployments.

type AssuranceRequirement

type AssuranceRequirement struct {
	Level  AssuranceLevel
	MaxAge time.Duration
}

AssuranceRequirement is checked against authoritative state within a mutation. MaxAge zero imposes no age limit; positive ages are exclusive upper bounds.

type AuditEvent

type AuditEvent = storage.AuditEvent

AuditEvent and AuditQuery are the storage records: identifiers, timestamps and closed vocabularies only. See docs/audit-events.md for the field contract.

type AuditFailure

type AuditFailure struct {
	Operation     plugin.Operation
	Reason        AuditReason
	Method        AuditMethod
	SubjectUserID string
	AsHost        bool
}

AuditFailure describes a rejected attempt for RecordFailure. Method names the sign-in method when the attempt was a sign-in; SubjectUserID names the account the attempt targeted when the caller established it. AsHost attributes the failure to the host, as a refused administrative or provisioning call made with WithAdministration is, ignoring any session or credential in the context.

type AuditMethod

type AuditMethod string

AuditMethod names how a user authenticated on sign-in events. Use the constants, OAuthMethod, or a host-defined identifier matching ^[a-z][a-z0-9_.-]{0,79}$. Events that are not sign-ins carry no method.

const (
	MethodPassword  AuditMethod = "password"
	MethodMagicLink AuditMethod = "magic_link"
)
const MethodImpersonation AuditMethod = "impersonation"

MethodImpersonation is the public method of an impersonation session and the audit method of the events recorded through one. It and every method under "impersonation." are reserved: host and plugin issuance reject them.

func OAuthMethod

func OAuthMethod(providerID string) AuditMethod

OAuthMethod names a sign-in through the configured provider identifier, so events distinguish providers ("oauth.google", "oauth.github").

type AuditQuery

type AuditQuery = storage.AuditQuery

type AuditReason

type AuditReason string

AuditReason is the closed failure vocabulary. Reasons describe why a presented credential or proof was rejected; they never carry input values.

const (
	ReasonInvalidCredentials AuditReason = "invalid_credentials" // password mismatch or malformed password
	ReasonUnknownIdentity    AuditReason = "unknown_identity"    // no account, or no password for it
	ReasonEmailUnverified    AuditReason = "email_unverified"    // a correct password for an unverified account
	ReasonInvalidProof       AuditReason = "invalid_proof"       // unknown, expired, consumed or mismatched proof or provider transaction
)
const (
	ReasonAccountSuspended AuditReason = "account_suspended"
	ReasonForbidden        AuditReason = "forbidden"
)

ReasonAccountSuspended records an otherwise valid authentication, proof or credential refused because an administrator suspended the account. ReasonForbidden records an administrator whose own authorization failed.

type AuditService

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

AuditService is a host-only capability over the durable trail. Possession of Auth grants it; never dispatch it from browser routes without the host's own administrative authorization. Events contain no credential material.

func (AuditService) Events

func (s AuditService) Events(ctx context.Context, q AuditQuery) ([]AuditEvent, error)

Events lists events in sequence order after the query cursor. Pages are current views; use the final Sequence as the next AfterSequence.

func (AuditService) Prune

func (s AuditService) Prune(ctx context.Context, before time.Time, limit int) (int64, error)

Prune deletes at most limit events recorded before the cutoff and returns the count. The library never prunes on its own; the host schedules retention.

func (AuditService) RecordFailure

func (s AuditService) RecordFailure(ctx context.Context, f AuditFailure) error

RecordFailure records a rejected attempt from trusted host or plugin code, attributing actor and client address from ctx. The event is durable in its own transaction; a storage failure is returned after being logged with the operation and reason only.

type Auth

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

Auth is safe for concurrent use. It owns its cancellation scope and admitted work, but never closes host-owned storage, loggers or secret providers.

func New

func New(ctx context.Context, c Config) (*Auth, error)

New validates and freezes configuration without starting workers or changing schema. ctx is the instance lifetime, not an individual HTTP request context. Dependency implementations must honor cancellation and must not panic.

func (*Auth) Abuse

func (a *Auth) Abuse() AbuseService

func (*Auth) Admin

func (a *Auth) Admin() AdminService

func (*Auth) Audit

func (a *Auth) Audit() AuditService

func (*Auth) AuthenticateRequest

func (a *Auth) AuthenticateRequest(r *http.Request) (*http.Request, error)

AuthenticateRequest attaches the session that r's session cookie carries, verified against authoritative storage, as RequireSession does, without requiring one: with no cookie it returns r unchanged. A malformed, duplicated, expired or revoked cookie is an error. It is for plugin endpoints outside Handler that act differently for signed-in browsers; it applies no origin or CSRF policy.

func (*Auth) Close

func (a *Auth) Close(ctx context.Context) error

Close refuses new work, cancels admitted work, and waits for it to finish. A deadline only bounds this wait; another Close can continue waiting. Closing is idempotent. Future durable workers must requeue or drain before returning.

func (*Auth) Contract

func (a *Auth) Contract() contract.Document

Contract returns the description validated at startup for this instance's configured core and plugin endpoints. Treat the result as read-only.

func (*Auth) Credentials

func (a *Auth) Credentials() CredentialsService

func (*Auth) Deployment

func (a *Auth) Deployment() Deployment

Deployment returns the instance's validated topology.

func (*Auth) EndImpersonation

func (a *Auth) EndImpersonation(w http.ResponseWriter, r *http.Request) error

EndImpersonation is the browser form of Sessions().EndImpersonation. It revokes the impersonation session in the session cookie, if it still authenticates, and restores the operator's own session from the cookie StartImpersonation saved; if that session has meanwhile ended, the browser is signed out instead. It also recovers a browser whose impersonation already expired or was revoked. A live session that is not an impersonation is a bad request and no cookie changes. The host route owns CSRF protection.

func (*Auth) Handler

func (a *Auth) Handler() http.Handler

Handler serves the authentication API. Mount with http.StripPrefix. Session verification uses authoritative storage; GET requests never renew sessions.

func (*Auth) Passwords

func (a *Auth) Passwords() PasswordsService

func (*Auth) PluginIDs

func (a *Auth) PluginIDs() []string

PluginIDs returns a copy of the frozen dependency order.

func (*Auth) ProtocolRequest

func (a *Auth) ProtocolRequest(r *http.Request, origin string) (*http.Request, netip.Addr, error)

ProtocolRequest validates the transport of a request that a plugin serves outside Handler at origin, which must satisfy ServesOrigin: the forwarding headers from trusted proxies, the public origin and, in Development, the loopback peer, exactly as Handler does. It returns the request with the client address attached, so audit events recorded under its context name it, and the address for abuse budgets. It applies no origin, CSRF, body or cookie policy: the caller owns those.

func (*Auth) RequireSession

func (a *Auth) RequireSession(next http.Handler) http.Handler

RequireSession checks shared service authorization. It does not apply the auth API's host/body/CSRF policies to host application routes; those are owned by the host. Session cookies are verified against authoritative storage.

func (*Auth) Retention

func (a *Auth) Retention() RetentionService

func (*Auth) SafeRedirect

func (a *Auth) SafeRedirect(target string) (string, error)

SafeRedirect returns an absolute URL under BaseURL. Redirects deliberately permit only local absolute paths, never a caller-supplied external origin. Host and forwarding headers cannot affect the destination.

func (*Auth) ServesOrigin

func (a *Auth) ServesOrigin(origin string) bool

ServesOrigin reports whether origin is a canonical origin that receives the session cookie: BaseURL's origin, or with a cookie domain (T2) an origin of the same scheme whose host is inside it.

func (*Auth) Sessions

func (a *Auth) Sessions() SessionsService

func (*Auth) SetSessionCookie

func (a *Auth) SetSessionCookie(w http.ResponseWriter, issued IssuedSession) error

SetSessionCookie delivers a committed issuance result using the configured browser policy. Host authentication flows must not serialize Token themselves.

func (*Auth) StartImpersonation

func (a *Auth) StartImpersonation(w http.ResponseWriter, r *http.Request, targetID string, duration time.Duration) (IssuedSession, error)

StartImpersonation is the browser form of Impersonate for a host's own administrative route. r must carry the operator's validated session from the session cookie (behind RequireSession). On success the operator's bearer is moved to a separate HttpOnly cookie and the session cookie carries the impersonation session, so EndImpersonation can restore the operator. The host route owns CSRF protection and must authorize the request like any other administrative action.

func (*Auth) Users

func (a *Auth) Users() UsersService

func (*Auth) WithAdministration

func (a *Auth) WithAdministration(ctx context.Context, operation plugin.Operation, userID string, fn func(storage.Tx, storage.User) error) error

WithAdministration runs a trusted administrative mutation against userID's account under that account's lock. It is how plugins add administrative capabilities with the core's attribution: the host by default, or the administrator of an Operator context, rechecked first. A non-empty operation records a success event whose subject is the account. Suspension does not block administration. An unknown account is unauthenticated. fn must not perform network I/O or retain the transaction. Possession of Auth is a trusted host capability; never accept userID from a caller the host has not authorized to administer it.

func (*Auth) WithAdministrationTx

func (a *Auth) WithAdministrationTx(ctx context.Context, operation plugin.Operation, fn func(storage.Tx) error) error

WithAdministrationTx runs trusted administrative work that concerns no single account, such as a directory's group records, with the attribution of WithAdministration: the host, or the administrator of an Operator context, rechecked first. No account is locked, so fn must not call storage methods that require one. A non-empty operation records a success event without a subject. fn must not perform network I/O or retain the transaction.

func (*Auth) WithAssurance

WithAssurance runs a trusted mutation and its assurance check atomically.

func (*Auth) WithCredential

func (a *Auth) WithCredential(ctx context.Context, operation plugin.Operation, credential AuditMethod, owner func(storage.Tx) (string, error), fn func(storage.Tx, storage.User) error) error

WithCredential runs trusted work authenticated by a non-session credential, such as an API key, that acts for one account. It is a distinct authentication mode: any session in ctx is ignored, and the audit event is attributed to the owning account as a user actor with no session, naming credential (for example "api_key.<id>") as its method. owner reads, without locking, the account the presented credential claims; the core then locks that account and calls fn, which must verify the credential and every restriction on it under that lock before acting. A suspended account is refused before fn runs, with an account_suspended failure event. An empty operation records no event. fn must not perform network I/O or retain the transaction. Possession of Auth is a trusted host/plugin capability; never accept an owner or credential identifier from request input as proof.

func (*Auth) WithNewAccount

func (a *Auth) WithNewAccount(ctx context.Context, operation plugin.Operation, input CreateUser, fn func(storage.Tx, storage.User) error) (User, error)

WithNewAccount creates an account for trusted provisioning code, such as a directory that proved control of the address's domain, and runs fn under the new account's lock in the same transaction. The account has no password or linked identity; its address is marked verified when input.EmailVerified, which is the caller's vouching for it. Attribution is administrative, as for WithAdministration: the host, or the administrator of an Operator context, rechecked first. A non-empty operation records a success event whose subject is the new account, and the plugin hooks see it. An address another account has, verified or not, is refused without change (IsEmailTaken): an existing account is never adopted or merged here. fn may be nil; it must not perform network I/O or retain the transaction. Possession of Auth is a trusted capability; never create accounts from input the host has not authorized.

func (*Auth) WithRecentSession

func (a *Auth) WithRecentSession(ctx context.Context, operation plugin.Operation, fn func(storage.Tx, storage.User, storage.Session) error) error

WithRecentSession runs a trusted plugin mutation under the actor's owner lock. It revalidates the live session and five-minute authentication age in the same transaction as fn and plugin hooks. fn must not perform network I/O or retain the transaction. Possession of Auth is a trusted host/plugin capability.

type AuthenticationState

type AuthenticationState string

AuthenticationState deliberately distinguishes a pending MFA flow from a usable session. Unknown values must fail closed just like Pending.

const (
	Unauthenticated AuthenticationState = "unauthenticated"
	Pending         AuthenticationState = "pending"
	Authenticated   AuthenticationState = "authenticated"
)

type Clock

type Clock interface{ Now() time.Time }

Clock implementations must be safe for concurrent use. Production defaults to the system clock. Tests may inject a clock without sleeping.

type Code

type Code string

Code is a stable machine-readable error category. New codes are additive.

const (
	CodeRateLimited                  Code = "rate_limited"
	CodeOverloaded                   Code = "overloaded"
	CodeAbuseUnavailable             Code = "abuse_unavailable"
	CodeInvalidConfiguration         Code = "invalid_configuration"
	CodeUnauthenticated              Code = "unauthenticated"
	CodeForbidden                    Code = "forbidden"
	CodeAssuranceRequired            Code = "assurance_required"
	CodeRecentAuthenticationRequired Code = "recent_authentication_required"
	CodeUnavailable                  Code = "unavailable"
	CodeClosed                       Code = "closed"
	CodeBadRequest                   Code = "bad_request"
	CodeNotFound                     Code = "not_found"
	CodeMethodNotAllowed             Code = "method_not_allowed"
	CodeBodyTooLarge                 Code = "body_too_large"
	CodeUnsupportedMediaType         Code = "unsupported_media_type"
)

type Config

type Config struct {
	AssurancePolicy    AssurancePolicy
	SensitiveAssurance AssuranceLevel // zero defaults to SingleFactor
	Plugins            []plugin.Definition
	Registration       RegistrationConfig
	Abuse              AbuseConfig
	BaseURL            string
	Secret             SecretSource
	TrustedOrigins     []string
	TrustedProxies     []string
	Cookie             CookieConfig
	Session            SessionConfig
	Development        bool // Explicit HTTP loopback exception; never for deployment.
	Clock              Clock
	Random             io.Reader // Test override, permitted only in Development mode.
	Logger             *slog.Logger
	Store              storage.Store // Optional; session operations fail closed without storage.
	Startup            StartupChecker
	Protector          Protector
	// Impersonation enables Admin().Impersonate; nil (the default) disables
	// it. See docs/impersonation.md.
	Impersonation *ImpersonationPolicy
}

Config is copied and validated by New. Mutating its slices afterwards cannot alter an instance. Injected interfaces remain host-owned and must be safe for concurrent use. No request-specific state belongs in Config.

func (Config) Format

func (Config) Format(w fmt.State, verb rune)

Formatting a Config never traverses injected dependencies or credentials.

func (Config) LogValue

func (Config) LogValue() slog.Value

func (Config) MarshalJSON

func (Config) MarshalJSON() ([]byte, error)

type CookieConfig

type CookieConfig struct {
	Name, Domain string
	SameSite     http.SameSite
}

CookieConfig is optional; zero values select Secure, HttpOnly, Lax, Path=/ and a host-only __Host-sigil_session cookie. Insecure flags aren't exposed.

type CreateUser

type CreateUser struct {
	Email         string
	EmailVerified bool
}

type Credentials

type Credentials struct {
	Email    string
	Password Secret
}

Credentials contains untrusted input. Passwords are never normalized.

type CredentialsService

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

func (CredentialsService) ChangePassword

func (s CredentialsService) ChangePassword(ctx context.Context, ip netip.Addr, current, password Secret) error

ChangePassword requires a verified, recently authenticated actor and the current password. All proof bindings are rechecked after hashing, under the user lock. Success invalidates every session (including the caller) and pending challenge.

func (CredentialsService) ConfirmEmailChange

func (s CredentialsService) ConfirmEmailChange(ctx context.Context, ip netip.Addr, token Secret) error

ConfirmEmailChange proves control of the pending address. Changing and verifying the address, consuming the proof and invalidating all sessions/challenges commit together. Sign in again after success; this method never issues a session.

func (CredentialsService) Register

func (s CredentialsService) Register(ctx context.Context, ip netip.Addr, input Credentials) error

Register returns the same success for a new and an existing identity, and never issues a session. User, password, challenge and mail commit together. ip must come from the host's trusted client-IP policy; HTTP derives it itself.

func (CredentialsService) RequestEmailChange

func (s CredentialsService) RequestEmailChange(ctx context.Context, ip netip.Addr, target string) error

RequestEmailChange requires a verified account and authentication less than five minutes old, rechecked under the user lock. The old address remains authoritative until confirmation. Existing targets receive the same accepted result and no mail.

func (CredentialsService) RequestPasswordReset

func (s CredentialsService) RequestPasswordReset(ctx context.Context, ip netip.Addr, target string) error

RequestPasswordReset accepts all canonical addresses uniformly. Only verified accounts with a password receive a durable, fifteen-minute recovery proof.

func (CredentialsService) ResendVerification

func (s CredentialsService) ResendVerification(ctx context.Context, ip netip.Addr, target string) error

ResendVerification accepts existing, missing and already verified addresses uniformly. Only an unverified current identity receives a new proof; issuing a resend does not revoke older, still valid verification links.

func (CredentialsService) ResetPassword

func (s CredentialsService) ResetPassword(ctx context.Context, ip netip.Addr, token, password Secret) error

ResetPassword consumes a recovery proof, replaces the password and invalidates all sessions and outstanding challenges in one transaction. It issues no session.

func (CredentialsService) SignIn

SignIn verifies outside the database transaction, then revalidates all proof bindings and issues under one user lock. Email verification is required; unverified accounts receive the same failure as invalid credentials.

func (CredentialsService) VerifyEmail

func (s CredentialsService) VerifyEmail(ctx context.Context, ip netip.Addr, token Secret) error

VerifyEmail consumes the registration proof and verifies its bound address. It never issues or renews a session. The host must require explicit confirmation.

type Deployment

type Deployment struct {
	// BaseURL is the canonical origin of Handler.
	BaseURL string
	// CookieDomain is the session cookie's Domain attribute, empty for a
	// host-only cookie (T1).
	CookieDomain string
	// TrustedOrigins are the exact origins whose pages may call Handler.
	TrustedOrigins []string
	Development    bool
}

Deployment is the validated browser topology, for plugins that serve endpoints outside Handler and must agree with the core's cookie and origin policy. It is a copy; changing it changes nothing.

type Error

type Error struct {
	Code    Code   `json:"code"`
	Message string `json:"message"`
	Field   string `json:"field,omitempty"`
}

Error contains only library-controlled diagnostics, never dependency errors, request values or credentials. Field identifies an invalid configuration field.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

type ImpersonationPolicy

type ImpersonationPolicy struct {
	Assurance   AssuranceRequirement
	MaxDuration time.Duration
	Authorize   func(ctx context.Context, operator, target User) error
}

ImpersonationPolicy enables impersonation (Config.Impersonation); without one, Impersonate is unavailable and any stored impersonation session is refused. Assurance is what the operator's own session must prove to start: MaxAge must be positive, and Level is raised to Config.SensitiveAssurance. MaxDuration bounds every impersonation session (zero is 15 minutes, at most one hour). Authorize is the host's privilege check and target exclusion: it runs when impersonation starts and again, with the freshly revalidated operator, before every request the impersonation session makes. A non-nil error denies, except ErrUnavailable, which is passed through. Authorize must be safe for concurrent use and must refuse any target the operator must not act as, such as another administrator.

type IssuedSession

type IssuedSession struct {
	View  SessionView `json:"view"`
	Token Secret      `json:"-"`
}

IssuedSession exposes the bearer only through Secret.Reveal. It must be sent to its intended client only after the issuing transaction has committed.

type LocalMailSink

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

LocalMailSink is a bounded, in-memory DEVELOPMENT sender. It has no HTTP route, disk writes or automatic logging. Drain explicitly transfers sensitive previews to host code and clears retained messages. Full sinks fail so work is retried.

func NewLocalMailSink

func NewLocalMailSink(capacity int) (*LocalMailSink, error)

func (*LocalMailSink) Drain

func (s *LocalMailSink) Drain() []Mail

func (*LocalMailSink) Send

func (s *LocalMailSink) Send(ctx context.Context, m Mail) error

type Mail

type Mail struct {
	ID                      string
	To, Subject, Text, HTML Secret
}

Mail contains rendered, sensitive content. Reveal only inside the sender or an explicitly host-controlled local preview. ID is a provider idempotency key.

type MailDelivery

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

func NewMailDelivery

func NewMailDelivery(store storage.DeliveryStore, protector Protector, sender MailSender) (*MailDelivery, error)

func (*MailDelivery) Cleanup

func (d *MailDelivery) Cleanup(ctx context.Context, limit int) (int64, error)

func (*MailDelivery) DispatchOne

func (d *MailDelivery) DispatchOne(ctx context.Context) (bool, error)

DispatchOne synchronously claims and attempts one message, with a 30-second send deadline (shorter than the lease). The host schedules calls and cleanup. A failed send is durably rescheduled and returns ErrDelivery, with no provider text. A crash after provider acceptance can cause duplicate delivery.

func (*MailDelivery) Enqueue

func (d *MailDelivery) Enqueue(ctx context.Context, tx storage.Tx, id string, template MailTemplate, ttl time.Duration) error

Enqueue must be called inside Store.WithinTx. Use a fresh random operation ID and persist it with the business operation. A conflict aborts the transaction; an ambiguous commit must be reconciled, never retried with a new operation ID.

func (*MailDelivery) Rotate

func (d *MailDelivery) Rotate(ctx context.Context, cursor string, limit int) (Rotation, error)

Rotate re-encrypts up to limit live payloads sealed under a key other than the protector's active key, scanning IDs after cursor. Run it from a trusted process after every replica holds the new keyring, repeating from "" until a full pass reports nothing rewrapped and nothing unreadable; only then retire the old key. Replacement is compare-and-swap, so a concurrent send or another pass is never overwritten. Plaintext exists only in this process's memory.

type MailSender

type MailSender interface {
	Send(context.Context, Mail) error
}

MailSender must honor cancellation; success means provider acceptance, not inbox receipt. Providers should deduplicate Mail.ID whenever supported.

type MailTemplate

type MailTemplate interface {
	Render(context.Context) (Mail, error)
}

MailTemplate renders before enqueue, freezing template content across retries. Hosts own URLs, escaping (use html/template), localization and sender address.

type OperatorPolicy

type OperatorPolicy struct {
	Assurance AssuranceRequirement
	Authorize func(context.Context, User) error
}

OperatorPolicy is what an administrator's own session must prove before administrative calls made with an Operator context proceed. Assurance must name a positive MaxAge; its Level is raised to the configured sensitive assurance. Authorize is the host's privilege check (for example an admin role) and runs on every call with the freshly revalidated operator; a non-nil error denies the call as forbidden, except ErrUnavailable, which is passed through.

type PasswordVerification

type PasswordVerification struct {
	Match   bool
	Upgrade storage.Material
}

PasswordVerification contains no plaintext. Upgrade is nonempty only after a matching historical verifier, and must be persisted with a credential revision check under the user lock. This result alone never authorizes session issuance.

type PasswordsService

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

PasswordsService is a host-only cryptographic primitive, not a sign-in flow. Network-facing callers must charge Abuse().Do with expensive=false first; these methods reserve the shared expensive slot themselves, with no queue. Do not invoke them inside an expensive=true callback.

func (PasswordsService) Hash

func (s PasswordsService) Hash(ctx context.Context, password Secret) (storage.Material, error)

Hash validates new-password policy and returns a randomly salted PHC verifier.

func (PasswordsService) Verify

func (s PasswordsService) Verify(ctx context.Context, password Secret, verifier storage.Material) (PasswordVerification, error)

Verify accepts only bounded Argon2id v19 PHC records. Mismatch returns an empty result and nil error; malformed/unsupported records return ErrBadRequest. Matching older parameters are rehashed within the same admission slot.

type ProtectionBinding

type ProtectionBinding struct{ Purpose, RecordID string }

ProtectionBinding must be authenticated along with the envelope. A ciphertext for one purpose or record must never decrypt under another binding.

type Protector

type Protector interface {
	Seal(context.Context, ProtectionBinding, Secret) ([]byte, error)
	Open(context.Context, ProtectionBinding, []byte) (Secret, error)
}

Protector is the host-injectable recoverable-secret boundary. Implementations must use versioned authenticated encryption, key IDs and binding metadata; fail closed on tampering/unknown keys; and respect cancellation. AUTH-016 supplies the AES-256-GCM envelope; the Rotator extension and MailDelivery.Rotate (AUTH-031) re-encrypt live ciphertext under a new key. Ciphertext is internal persistence material, never a public response DTO.

func NewAESProtector

func NewAESProtector(active string, keys map[string]Secret) (Protector, error)

NewAESProtector copies a keyring of raw 32-byte AES keys. Key IDs are public, immutable identifiers; keep old keys while their ciphertext remains live. This constructor starts no workers and never derives keys from passwords.

type Pruned

type Pruned = storage.Pruned

Pruned is the storage record of one cleanup pass.

type RegistrationConfig

type RegistrationConfig struct {
	Delivery           *MailDelivery
	RenderVerification func(context.Context, string, Secret) (Mail, error)
	// RenderEmailChange optionally enables pending address changes. It has the
	// same rendering-only contract and must link to the change confirmation flow.
	RenderEmailChange func(context.Context, string, Secret) (Mail, error)
	// RenderPasswordReset optionally enables password recovery, using the same rendering-only contract.
	RenderPasswordReset func(context.Context, string, Secret) (Mail, error)
}

RegistrationConfig enables registration with durable verification mail. RenderVerification must be concurrency-safe, honor cancellation, and only render content (no sending or other external side effects). The host owns safe confirmation URLs and escaping. Sigil sets the canonical recipient. Delivery's dispatcher must use the same store as Config.Store.

type RequestContext

type RequestContext struct {
	State   AuthenticationState `json:"state"`
	User    User                `json:"user"`
	Session Session             `json:"session"`
}

RequestContext is a credential-free snapshot of validated authentication. It is not an input capability: constructing one does not authorize a call.

func RequestFromContext

func RequestFromContext(ctx context.Context) RequestContext

RequestFromContext returns a value copy. Missing authentication is explicit.

type RetentionService

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

RetentionService is the host-only capability over expired-row cleanup. The library never prunes on its own; the host schedules it alongside MailDelivery.Cleanup, Audit().Prune and the plugin pruners.

func (RetentionService) PruneExpired

func (s RetentionService) PruneExpired(ctx context.Context, before time.Time, limit int) (Pruned, error)

PruneExpired deletes from each core table at most limit (1..1000) rows that expired at or before the cutoff (clamped to database time, so live rows are never removed) and returns the counts. Repeat while any count equals limit. A store without the extension is unavailable.

type RetryError

type RetryError struct {
	After time.Duration
	Cause *Error
}

RetryError exposes a bounded retry hint without identity or storage diagnostics.

func (*RetryError) Error

func (e *RetryError) Error() string

func (*RetryError) Unwrap

func (e *RetryError) Unwrap() error

type Rotation

type Rotation struct {
	Next       string // cursor for the next call; empty once the scan reached the end
	Visited    int
	Rewrapped  int
	Unreadable int // malformed envelopes or keys missing from the keyring; retire nothing while nonzero
}

Rotation reports one re-encryption pass over live outbox payloads.

type Rotator

type Rotator interface {
	Protector
	ActiveKeyID() string
	KeyID([]byte) (string, error)
}

Rotator is an optional Protector extension for operational key rotation. ActiveKeyID names the key new ciphertext uses. KeyID names the key an existing envelope was sealed under without decrypting it, whether or not that key is still in the keyring, so rotation can find ciphertext that depends on a key scheduled for retirement; a malformed envelope is ErrProtection.

type Secret

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

Secret prevents accidental disclosure through formatting, JSON and slog. Reveal is an explicit trust boundary; callers must not log its result. Go cannot guarantee erasure of strings or protect against in-process code.

func NewSecret

func NewSecret(value string) Secret

func (Secret) Format

func (s Secret) Format(w fmt.State, verb rune)

func (Secret) Load

func (s Secret) Load(ctx context.Context) (Secret, error)

func (Secret) LogValue

func (s Secret) LogValue() slog.Value

func (Secret) MarshalJSON

func (s Secret) MarshalJSON() ([]byte, error)

func (Secret) Reveal

func (s Secret) Reveal() string

type SecretSource

type SecretSource interface {
	Load(context.Context) (Secret, error)
}

SecretSource resolves deployment key material at startup, respecting ctx. It must return at least 32 random bytes (encoded text is allowed). Length validation cannot establish entropy. Sources are host-owned and not closed.

func SecretFromEnv

func SecretFromEnv(name string) SecretSource

SecretFromEnv reads the environment at construction time, not on each request.

type Session

type Session struct {
	Assurance       AssuranceLevel      `json:"assurance"`
	Method          string              `json:"method"`
	ID              string              `json:"id"`
	UserID          string              `json:"userId"`
	State           AuthenticationState `json:"state"`
	CreatedAt       time.Time           `json:"createdAt"`
	AuthenticatedAt time.Time           `json:"authenticatedAt"`
	ExpiresAt       time.Time           `json:"expiresAt"`
}

Session is a public view. ID is a non-bearer identifier; no raw token or digest may be assigned to it. AuthenticatedAt is the last completed authentication, not session renewal. Pending timestamps describe the first-factor proof only.

type SessionConfig

type SessionConfig struct{ AbsoluteLifetime, IdleTimeout, RenewAfter time.Duration }

type SessionView

type SessionView struct {
	User    User    `json:"user"`
	Session Session `json:"session"`
}

type SessionsService

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

func (SessionsService) Authenticate

func (s SessionsService) Authenticate(ctx context.Context, token Secret) (context.Context, error)

Authenticate validates an opaque bearer against authoritative storage without renewing it. The resulting context is instance-bound; service calls revalidate it so a saved context cannot bypass subsequent revocation.

func (SessionsService) Authentication

func (s SessionsService) Authentication(ctx context.Context) (SessionView, error)

Authentication returns the live flow state, including pending. It grants no ordinary access. Factor routes may use it to render a challenge.

func (SessionsService) Current

func (s SessionsService) Current(ctx context.Context) (SessionView, error)

func (SessionsService) EndImpersonation

func (s SessionsService) EndImpersonation(ctx context.Context) error

EndImpersonation revokes the impersonation session ctx carries. The operator's and the target's own sessions are untouched. A session that is not an impersonation is a bad request. Records admin.impersonation_end, attributed like every event of the impersonation session. The operator can also end it from their console with Admin().RevokeSession.

func (SessionsService) List

func (s SessionsService) List(ctx context.Context, afterID string, limit int) ([]Session, error)

List returns at most 100 active, owned sessions in ID order. Use the final ID as afterID for the next page. The empty cursor starts a new listing.

func (SessionsService) Renew

Renew extends idle expiry only at/after RenewAfter. It preserves the bearer, absolute deadline and authentication time, and never renews on read requests.

func (SessionsService) RequireAssurance

func (s SessionsService) RequireAssurance(ctx context.Context, r AssuranceRequirement) error

RequireAssurance is a read-only check. Mutations must use WithAssurance to avoid revocation, enrollment and check/use races.

func (SessionsService) RequireRecent

func (s SessionsService) RequireRecent(ctx context.Context, maxAge time.Duration) error

RequireRecent authenticates from storage, then requires age strictly below maxAge. Renewal never refreshes this evidence. Future sensitive mutations must apply this predicate inside their own transaction to avoid a check/use race.

func (SessionsService) RevokeAll

func (s SessionsService) RevokeAll(ctx context.Context) error

RevokeAll invalidates all existing sessions and challenges through the user's security epoch. Issuance serialized after this operation belongs to the new epoch. An impersonation session cannot sign its target out everywhere: forbidden.

func (SessionsService) RevokeCurrent

func (s SessionsService) RevokeCurrent(ctx context.Context) error

type StartupChecker

type StartupChecker interface{ Check(context.Context) error }

StartupChecker checks connectivity/schema compatibility without writes or migrations. The host owns the dependency and its shutdown. AUTH-008/010 will connect this contract to the storage adapter.

type User

type User struct {
	ID            string    `json:"id"`
	Email         string    `json:"email"`
	EmailVerified bool      `json:"emailVerified"`
	CreatedAt     time.Time `json:"createdAt"`
}

User is a public identity view, never a credential persistence record.

func UserFromContext

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

type UsersService

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

UsersService and SessionsService enforce actor authorization for both HTTP and direct calls. They never accept caller-asserted actors or admin flags.

func (UsersService) Current

func (s UsersService) Current(ctx context.Context) (User, error)

func (UsersService) DeleteCurrent

func (s UsersService) DeleteCurrent(ctx context.Context) error

DeleteCurrent deletes the signed-in user's account. It requires an active session authenticated strictly less than five minutes ago, rechecked under the user lock in the deleting transaction. The user row and everything that cascades from it (password, linked identities, sessions including the caller's, pending proofs and cascading plugin rows) go in one transaction with the account.delete audit event; queued proof mail addressed to the account is withdrawn. Afterwards no supported method can authenticate the account, the address is free to register again, and the trail keeps the account's events until the host prunes them. Deletion is final.

func (UsersService) Get

func (s UsersService) Get(ctx context.Context, userID string) (User, error)

Get currently supports only the validated actor's own snapshot. An explicit target check is shared by direct and future HTTP routes.

Directories

Path Synopsis
Package cli implements the operator command set behind cmd/sigil: contract export, migration status, planning, apply and SQL export, deployment diagnostics and bounded expired-row cleanup.
Package cli implements the operator command set behind cmd/sigil: contract export, migration status, planning, apply and SQL export, deployment diagnostics and bounded expired-row cleanup.
cmd
sigil command
Command sigil is the operator tool for this repository: contract export, migrations, diagnostics and cleanup over the core and the in-repo plugins.
Command sigil is the operator tool for this repository: contract export, migrations, diagnostics and cleanup over the core and the in-repo plugins.
Package contract describes the HTTP API as data: endpoint metadata, JSON schemas, the safe error catalogue and a deterministic OpenAPI 3.1 export.
Package contract describes the HTTP API as data: endpoint metadata, JSON schemas, the safe error catalogue and a deterministic OpenAPI 3.1 export.
examples
authserver command
Command authserver runs a disposable host that operates an OAuth 2.0 authorization server and OpenID Connect provider with plugin/authserver, together with a demo relying party on a second origin.
Command authserver runs a disposable host that operates an OAuth 2.0 authorization server and OpenID Connect provider with plugin/authserver, together with a demo relying party on a second origin.
browser command
Command browser runs the local password authentication journey.
Command browser runs the local password authentication journey.
browser/testdb command
Command testdb removes only a uniquely named browser, client or react test schema.
Command testdb removes only a uniquely named browser, client or react test schema.
client command
Command client hosts the framework-neutral TypeScript client against the real Go server: the shared HTTPS loopback host (core credentials, the magic-link plugin, the OAuth plugin bound to a built-in mock OIDC provider and a local mail preview) plus a page that talks to the API only through @sigil-auth/client.
Command client hosts the framework-neutral TypeScript client against the real Go server: the shared HTTPS loopback host (core credentials, the magic-link plugin, the OAuth plugin bound to a built-in mock OIDC provider and a local mail preview) plus a page that talks to the API only through @sigil-auth/client.
internal/loopback
Package loopback is the HTTPS loopback host shared by the browser examples that drive the TypeScript packages against the real Go server: core credentials with verification and reset mail, the magic-link plugin, the OAuth plugin bound to a built-in mock OIDC provider, and a development mail preview, served under a throwaway certificate on a loopback address.
Package loopback is the HTTPS loopback host shared by the browser examples that drive the TypeScript packages against the real Go server: core credentials with verification and reset mail, the magic-link plugin, the OAuth plugin bound to a built-in mock OIDC provider, and a development mail preview, served under a throwaway certificate on a loopback address.
nethttp command
Command nethttp demonstrates embedding Sigil in an application-owned net/http server: validated configuration, the mounted authentication API, host routes protected by RequireSession, trusted host capabilities, abuse admission and graceful shutdown.
Command nethttp demonstrates embedding Sigil in an application-owned net/http server: validated configuration, the mounted authentication API, host routes protected by RequireSession, trusted host capabilities, abuse admission and graceful shutdown.
oauth-smoke command
Command oauth-smoke drives the link, sign-in and unlink journeys of the Google and GitHub adapters against the real providers from an operator's browser.
Command oauth-smoke drives the link, sign-in and unlink journeys of the Google and GitHub adapters against the real providers from an operator's browser.
organization command
Command organization runs a disposable, host-side organization journey.
Command organization runs a disposable, host-side organization journey.
profile command
Command profile runs a disposable custom-field journey against PostgreSQL: operator migrations, an owner update over HTTP, a back-office write of private and read-only fields, and the typed Go view.
Command profile runs a disposable custom-field journey against PostgreSQL: operator migrations, an owner update over HTTP, a back-office write of private and read-only fields, and the typed Go view.
profile/fields
Package fields declares the example application's custom account fields.
Package fields declares the example application's custom account fields.
profile/generate command
Command generate rewrites the profile example's generated Go types and its OpenAPI export from fields.Schema.
Command generate rewrites the profile example's generated Go types and its OpenAPI export from fields.Schema.
react command
Command react is the Go-plus-React example: the shared HTTPS loopback host (core credentials, magic links, the OAuth plugin with a mock provider and a local mail preview) in front of a Node renderer that server-renders the React pages with @sigil-auth/react and forwards each request's session cookie to the API through a per-request server client.
Command react is the Go-plus-React example: the shared HTTPS loopback host (core credentials, magic links, the OAuth plugin with a mock provider and a local mail preview) in front of a Node renderer that server-renders the React pages with @sigil-auth/react and forwards each request's session cookie to the API through a per-request server client.
internal
plugintest
Package plugintest supplies the same plugin fixtures with real engine stores.
Package plugintest supplies the same plugin fixtures with real engine stores.
Package plugin defines explicitly registered, compiled extensions.
Package plugin defines explicitly registered, compiled extensions.
apikey
Package apikey provides optional PostgreSQL/SQLite scoped API keys: an authentication mode distinct from browser sessions, for non-interactive callers of the host's own API.
Package apikey provides optional PostgreSQL/SQLite scoped API keys: an authentication mode distinct from browser sessions, for non-interactive callers of the host's own API.
authserver
Package authserver lets a host operate an OAuth 2.0 authorization server and OpenID Connect provider for its own users, on PostgreSQL and SQLite.
Package authserver lets a host operate an OAuth 2.0 authorization server and OpenID Connect provider for its own users, on PostgreSQL and SQLite.
internal/idtoken
Package idtoken holds the OpenID Connect ID-token claim contract shared by the provider sign-in plugins, so provider sign-in and organization SSO accept exactly the same tokens.
Package idtoken holds the OpenID Connect ID-token claim contract shared by the provider sign-in plugins, so provider sign-in and organization SSO accept exactly the same tokens.
magiclink
Package magiclink provides optional, existing-account email sign-in using public plugin, storage, mail and session contracts.
Package magiclink provides optional, existing-account email sign-in using public plugin, storage, mail and session contracts.
oauth
Package oauth implements optional authorization-code sign-in for already linked external identities.
Package oauth implements optional authorization-code sign-in for already linked external identities.
oauth/github
Package github configures GitHub OAuth 2.0 sign-in for the oauth plugin.
Package github configures GitHub OAuth 2.0 sign-in for the oauth plugin.
oauth/google
Package google configures Google OpenID Connect sign-in for the oauth plugin.
Package google configures Google OpenID Connect sign-in for the oauth plugin.
oauth/internal/providertest
Package providertest is the PostgreSQL fixture shared by the provider adapter integration tests.
Package providertest is the PostgreSQL fixture shared by the provider adapter integration tests.
organization
Package organization provides optional PostgreSQL/SQLite tenant membership and session context.
Package organization provides optional PostgreSQL/SQLite tenant membership and session context.
passkey
Package passkey provides user-verified discoverable WebAuthn credentials.
Package passkey provides user-verified discoverable WebAuthn credentials.
portability
Package portability holds the cross-engine plugin fixtures that keep the PostgreSQL and SQLite adapters honest about the same behavior: baseline migration, real factor/tenant/credential data preserved across disablement, and re-enablement after an upgrade.
Package portability holds the cross-engine plugin fixtures that keep the PostgreSQL and SQLite adapters honest about the same behavior: baseline migration, real factor/tenant/credential data preserved across disablement, and re-enablement after an upgrade.
scim
Package scim provides optional organization-bound SCIM 2.0 provisioning and deprovisioning for PostgreSQL and SQLite (RFC 7643 and RFC 7644).
Package scim provides optional organization-bound SCIM 2.0 provisioning and deprovisioning for PostgreSQL and SQLite (RFC 7643 and RFC 7644).
sso
Package sso provides optional organization-bound OpenID Connect and SAML 2.0 single sign-on for PostgreSQL and SQLite.
Package sso provides optional organization-bound OpenID Connect and SAML 2.0 single sign-on for PostgreSQL and SQLite.
totp
Package totp implements optional password-plus-TOTP authentication on PostgreSQL.
Package totp implements optional password-plus-TOTP authentication on PostgreSQL.
userfields
Package userfields adds explicit, typed custom fields to user accounts.
Package userfields adds explicit, typed custom fields to user accounts.
Package storage defines identity records and transactional adapter guarantees.
Package storage defines identity records and transactional adapter guarantees.
conformance
Package conformance is the reusable storage gate described in docs/storage-conformance.md.
Package conformance is the reusable storage gate described in docs/storage-conformance.md.
postgres
Package postgres provides explicit versioned core PostgreSQL migrations and the transactional storage.Store adapter over them.
Package postgres provides explicit versioned core PostgreSQL migrations and the transactional storage.Store adapter over them.
sqlite
Package sqlite implements Sigil storage on a local SQLite database.
Package sqlite implements Sigil storage on a local SQLite database.

Jump to

Keyboard shortcuts

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