All notable changes to theauth-go are recorded below. The format follows Keep a Changelog and the project adheres to Semantic Versioning from v1.0 forward.
The authoritative source file is CHANGELOG.md in the repository root.
All notable changes to theauth-go are recorded here. The format follows
Keep a Changelog and the project
adheres to Semantic Versioning from v1.0 forward.
Fixed
PasswordPolicy.OnLegacyHashAccepted is now invoked (in a goroutine, after the new Argon2id hash is persisted) on signin and step-up; it was declared but never called.
Added
OAuthConfig.RedirectURI, RedirectURIAllowedHosts and AllowInsecureRedirectURI to override and validate the OAuth redirect URI, plus (*TheAuth).OAuthStart and OAuthCallback for apps hosting their own routes.
- Default logs no longer contain email addresses (password signin/signup/reset, magic link,
email.Noop); lines carry user_id or a 12-hex email_ref hash instead.
ResetPasswordAdmin now also clears the email’s per-IP login backoff entries (new optional LoginThrottleEntryDeleter store capability; implemented by the memory, sqlite, postgres and mysql stores).
Added
WebAuthnConfig.UserHandleResolver: lets imported passkeys whose authenticator holds a foreign user handle sign in; the credential’s stored owner stays authoritative and the resolver must agree with it.
2.6.0 - 2026-10-05
Upgrade notes
Read these before upgrading from v2.5.x. None changes the Go API of an
existing call, but each changes runtime behavior.
- Module path is now
github.com/glincker/theauth-go/v2; update imports.
The v2.x tags were never resolvable by the Go toolchain because the module
path lacked the /v2 suffix, so go get github.com/glincker/theauth-go
returned v1.0.0. Breaking for import paths only: the API is unchanged. See
Migrating to /v2.
The storage/sqlite, mcpresource and audit/sinks/otlp modules keep their
paths and now require the v2 root.
- Login throttle is on by default. Password signin is gated by per
(client IP, normalized email) backoff and a per-user lockout. Refusals return
429 with
Retry-After and code rate_limited or account_locked. Tune or
disable with Config.LoginThrottle.
- JSON error bodies. Handlers that returned plain-text
http.Error bodies
now return {"code","message"} JSON with a stable code (bad_request,
unauthorized, forbidden, not_found, conflict, rate_limited,
internal_error). Status codes are unchanged. Clients that string-matched the
old plain-text body must switch to the code field. The OAuth provider and
authorization server handlers are not yet converted.
- TOTP verify and recovery routes rotate the session cookie. They set a new
cookie and revoke the pending token, so clients must keep the cookie from the
response.
- The OAuth state cookie value no longer equals the
state parameter. The
theauth_oauth_state cookie carries a separate browser-binding secret; code
that read or compared the two values must stop.
- OAuth callback refuses an email matching an existing account unless the
provider marks the email verified. Provider emails are lower-cased and
trimmed before lookup.
- An https
BaseURL now yields Secure cookies, even with
SecureCookie: false. Secure is set per request for https BaseURL, TLS
connections, or a TrustedProxies peer sending X-Forwarded-Proto: https.
EncryptionKey is required when a ProviderResolver is set.
- Device-request routes are restricted to the root ability by default.
GET /auth/device/requests and the approve/deny by ID routes need a session
holding APITokensConfig.DeviceRequestsAbility (empty means root), or set
DeviceRequestsAnySignedInUser. Others get 403 auth.forbidden.
- Closed signup needs
UserCountStorage. Config.Bootstrap and closed
signup require the optional capability on your storage adapter.
PasswordPolicy.AllowLegacyBcrypt is now honored at signin, step-up and
password change. With it off, a bcrypt hash returns invalid credentials
instead of a 500.
- CIMD client-metadata fetches refuse non-public addresses. Loopback,
private, link-local (including cloud metadata), CGNAT and similar ranges are
blocked at dial time, redirects are never followed and no proxy is used. A
localhost CIMD setup now needs
CIMDConfig.AllowPrivateNetworks (a dev
opt-in); CIMDConfig.DenyHost adds a host deny hook.
- Authorization error responses and SAML
RelayState are restricted.
Authorization errors redirect only when redirect_uri exactly matches a
registered URI. SAML RelayState must be a same-site path, the configured
post-login URL or an entry in SAMLConfig.AllowedRelayStates; anything else
falls back to the default.
- The repository layout changed; no exported name was removed. The module
root now holds 15 Go files (it held 80), feature code lives in
internal/
packages and black-box tests live in integration/. Exported root names are
kept through aliases, so imports and calls are unaffected, but %T and
reflect print the aliased type, go test on the root package no longer
runs the integration suite (use ./...), and sqlc.yaml moved. Details under
Changed.
- Other changes worth a look: cookie-authenticated mutating requests with a
foreign
Origin now get 403 (Config.TrustedOrigins,
Config.DisableCSRFProtection); passwords over MaxBytes (default 72) return
400 weak_password; WebAuthn credentials gain backup_eligible and
backup_state columns (migration 0017); Postgres and MySQL add migration
0018; SQLite adds migrations 0006 to 0008.
Added
Module path and HTTP embedding
Config.PathPrefix. Serve the auth routes under a custom prefix (default
/auth, validated) with no http.StripPrefix. OAuth redirect URIs, magic and
reset links, the WebAuthn challenge cookie path and the authorization server
login URL follow it. clientauth takes the same prefix via AuthPath.
(*TheAuth).Handler(). Returns an http.Handler serving every route
Mount registers, so net/http ServeMux users need no chi import.
examples/stdlib-app now uses it.
Storage
- Storage capability interfaces and
Config.CoreStorage. Storage is now
the embedding of small capability interfaces (UserStorage,
SessionStorage, MagicLinkStorage, PasswordStorage,
OAuthAccountStorage, WebAuthnStorage, TOTPStorage,
OrganizationStorage, SAMLStorage, SCIMStorage, RBACStorage,
AuditStorage) with an identical method set, so existing adapters and
callers compile unchanged. Config.CoreStorage accepts an adapter that
implements only users, sessions, magic links and passwords; New returns
the new ErrStorageMissingCapability, naming the feature and capability,
when an enabled feature needs more. storagetest gains RunCore,
RunWebAuthn, RunTOTP, RunAudit, RunRBAC and RunOAuthServer;
storagetest.Run still runs everything.
storage/sqlite adapter (separate module). Pure Go (modernc.org/sqlite)
storage for CoreStorage plus the OAuthAccount, WebAuthn, TOTP and Audit
capabilities. New(db) wraps a caller-owned *sql.DB and refuses a database
with foreign keys off. Migrations() exports numbered forward-only .sql
files for a host migrator, Migrate is an optional standalone runner with its
own version table, and WithTablePrefix keeps the tables clear of host ones.
SweepExpired is a method for a host ticker. Passes storagetest.RunCore,
RunWebAuthn, RunTOTP and RunAudit against a temp-file WAL database,
including concurrent access. Organizations, SAML, SCIM and RBAC are not
implemented and return ErrStorageMissingCapability.
- SQLite capability parity.
storage/sqlite now also implements
APITokenStorage, DeviceCodeStorage, SessionManagementStorage,
SessionLinkStorage, TOTPReplayStorage, UserCountStorage,
WebAuthnRenameStorage and RecoveryCodeStorage, with atomic single-use
claims for device codes and session links. Migrations 0006 to 0008.
- Postgres and MySQL capability parity. Both adapters now implement
APITokenStorage, DeviceCodeStorage, SessionManagementStorage,
SessionLinkStorage, TOTPReplayStorage, UserCountStorage and a shared
Store.ThrottleStore (LoginThrottleCASStore), so tokens, device login,
session management and closed signup work on them. Migration 0018 in each
adapter; atomic single-use claims. MySQL CreateUser now defaults zero
CreatedAt and UpdatedAt instead of failing.
- Shared login throttle for SQLite.
Store.ThrottleStore persists
throttle counters. New optional LoginThrottleCASStore lets the limiter
retry on conflict across processes instead of last-writer-wins.
Storage.UpdateWebAuthnBackupFlags. New storage method backing the
login-time reconciliation write for legacy WebAuthn credentials. Implemented
across the Postgres, MySQL, and in-memory backends and covered by the shared
storagetest conformance suite.
OAuth and OIDC
Config.OAuth (OAuthConfig). Pluggable OAuthStateStore (in-memory
default with expiry sweep), StateTTL, a return_to allow-list, and a
signup policy (open default, closed, allowed_domains, invite). The
default stays open so v2 callers are unchanged.
provider/oidc. Generic OIDC provider with issuer discovery, PKCE,
nonce and full ID token verification. New optional NonceProvider
interface; ProviderToken.IDToken field.
Config.ProviderResolver. Resolve OAuth/OIDC providers per request for
runtime add, edit and remove, with ProviderResolverFirst,
ProviderResolverTTL, (*TheAuth).InvalidateProvider and ListProviders.
Names are validated and resolver errors fail closed.
Sessions, step-up and MFA
- End-user session management.
GET /auth/sessions,
DELETE /auth/sessions/{id}, POST /auth/sessions/revoke-others and
POST /auth/password/change, backed by the new optional
SessionManagementStorage capability. Config.SessionIdleTimeout and
Config.SessionTouchInterval add idle expiry with throttled last-seen
writes; Session gains LastSeenAt, ElevatedUntil and CredentialID.
The admin session list is now real instead of empty. See docs/SESSIONS.md.
- Step-up re-auth.
POST /auth/step-up (password, TOTP or passkey),
RequireRecentAuth(maxAge) and WatchSession /
WatchSessionMiddleware for re-checking long-lived streams.
- Programmatic session links.
Config.SessionLinks, MintSessionLink,
ConsumeSessionLink, POST /auth/session-link/consume and the optional
SessionLinkStorage capability. Sessions tied to an upstream credential are
re-checked through CredentialChecker on every use, and
RevokeSessionsByCredential cuts them at once.
storagetest.RunSessionManagement contract suite; the memory adapter
implements both new capabilities.
- Passkey policy.
WebAuthnConfig.RequireUserVerification and
CloneWarning (reject default, flag). RP ID stays config-only.
- MFA endpoints.
GET /auth/totp, POST /auth/totp/recovery-codes,
PATCH /auth/webauthn/credentials/{id}; DELETE /auth/totp is covered for
both slash forms. New optional storage capabilities
WebAuthnRenameStorage and RecoveryCodeStorage (memory, Postgres, MySQL)
with storagetest.RunMFACaps.
- Revocation watcher.
RevocationBus (in-process default, pluggable for
Postgres NOTIFY or Redis), SubscribeRevocations, NotifyOwnerDisabled,
WatchRevocation and WatchRevocationMiddleware cancel long-lived
requests within seconds of a session, token, agent, credential or
delegation revoke. Reuses WatchSession for session polling.
API tokens, device grant, agents and CLI login
- Scoped API tokens and the RFC 8628 device grant. New
Config.APITokens
(backed by the optional APITokenStorage and DeviceCodeStorage
capabilities; the Storage method set is unchanged). Tokens are opaque,
shown once, stored only as a SHA-256 hash, carry caller-defined abilities
plus a reserved exclusive root, always expire, and record last_used_at.
Every token has an owner (a user or a service account); a user lists and
revokes only their own tokens, an admin can manage all. Tokens stop working
when the owner is deleted or reported inactive, and abilities are clamped to
the owner’s current abilities on every request. RequireAbility(name)
accepts a session or a bearer token. Routes: /auth/tokens (POST, GET,
DELETE), /auth/device/code, /auth/device/token, /auth/device/approve.
The device grant enforces slow_down, expiry, per-approver user code
attempt limits and an atomic compare-and-set redeem, and caps minted token
abilities to the approver (root only when requested and held). Memory
implementation plus storagetest.RunAPITokens and RunDeviceCodes. See
API tokens and device login.
- Legacy API token acceptance.
APITokensConfig.AcceptUnprefixed accepts
unprefixed bearer tokens by SHA-256 hash, and ImportAPIToken inserts an
existing token record by hash without seeing the secret.
- Token self-service routes.
GET /auth/tokens/current describes the
presented API bearer token and DELETE /auth/tokens/current revokes it, so
clientauth Whoami and Logout work against a real server. Bearer-only,
own record only, no secret or hash in responses.
- Device pending list.
GET /auth/device/requests lists pending device
requests (no codes or hashes) and POST /auth/device/requests/{id}/approve|deny
decides one by ID, session only, with the same capping and atomicity as the
user code route. New optional DeviceCodeLister storage extension for memory,
SQLite, Postgres and MySQL, plus ListDeviceRequests and
DecideDeviceRequestByID. Restricted by default (see Upgrade notes); the
by-code route is unchanged.
clientauth package. Client side of the device grant for host CLIs:
DeviceLogin (polling with slow_down, denial and expiry handling), a 0600
FileStore plus an injectable KeychainStore, and a Client that attaches
the bearer token and returns ErrReloginRequired on expiry or 401. See
examples/cli-login and the CLI Login guide.
- Agent identity on API tokens.
MintAgentToken mints a short-lived
kind=agent token for a human; abilities are the intersection of the
agent’s allowed set and the user’s current abilities, re-evaluated per
request. APIToken gains Kind, AgentName, DelegatedBy (shown in
listings and POST /auth/tokens), Principal.ActorChain records agent and
human, and audit events under an agent token carry both. RegisterAgent
creates an OAuth agent plus its delegation grant in one call. See
docs/AGENT-IDENTITY.md.
Policy, posture and audit
policy package. Dependency-free JSON policy engine (allow/deny
statements, action and resource globs with {var} substitution, equality,
in-list, CIDR and time-window conditions), explicit-deny-wins and
default-deny evaluation with an auditable Decision, RequirePolicy
middleware (403 policy.denied, policy.decision audit event), optional
policy.Storage capability with a memory store and storagetest.RunPolicy,
and token permission boundaries that only narrow. New
TheAuth.AuthenticatePrincipal. See the Policy Engine guide.
- Security doctor.
(*TheAuth).Doctor reports posture findings (open signup,
bootstrap gate, proxies, cookies, CSRF, throttle, password and session policy,
token hygiene, encryption key, audit, WebAuthn RP ID) with stable IDs and
severities. GET /auth/admin/doctor serves it to root callers, and
cmd/theauth-doctor prints it with --format json and --fail-on for CI.
See docs/SECURITY-DOCTOR.md.
Config.AuthEventSink. PII-minimal AuthEvent stream (user id plus IP
prefix) for login, MFA, password, passkey, TOTP, session and token events,
plus RecordTokenMinted / RecordTokenRevoked hook points and
AuthEventChannelSink. Audit log gained login.failed, mfa.verified,
mfa.failed, passkey.renamed, passkey.clone_warning,
totp.recovery_regenerated, token.minted, token.revoked; client IP and
user agent are now attached to every audited request.
- CSRF / Origin protection. Cookie-authenticated POST/PUT/PATCH/DELETE
requests whose
Origin (or Referer) is not the BaseURL origin or in the
new Config.TrustedOrigins get 403, closing the sibling-subdomain gap that
SameSite=Lax leaves. Bearer requests and GET/HEAD/OPTIONS are exempt.
Opt out with Config.DisableCSRFProtection.
- Automatic
Secure cookies. Secure is now set per request when
BaseURL is https, the connection is TLS, or a TrustedProxies peer sent
X-Forwarded-Proto: https. Config.SecureCookie: true still forces it.
TrustedProxies startup warning. A WARN is logged when it is empty
(silence with Config.SuppressTrustedProxiesWarning); behind a proxy an
empty list collapses the per-IP rate-limit bucket. See the new HTTP security
doc.
Importing existing data
sqlite.NewTx binds the SQLite Store to a caller’s *sql.Tx (SAVEPOINTs,
no nested transactions), plus ImportUserTo, ImportAPITokenTo,
ImportTOTPSecretTo and ImportWebAuthnCredentialTo (and TheAuth method
forms) for one-transaction backfills.
- Plaintext TOTP secrets are encrypted on import like enrollment; recovery codes
must be regenerated. See the SQLite guide.
Changed
-
Repository layout. The module root now holds only the public face of the
library. Feature code moved into
internal/apitokens, internal/revocation,
internal/doctor, internal/importer, internal/bootstrap,
internal/ratelimit, internal/httpsec, internal/authevents and
internal/rbac, and black-box tests moved to integration/. Root Go files
went from 80 to 15. See docs/REPO-LAYOUT.md. Every exported root identifier
keeps its name through a type alias, constant or function wrapper, and the
exported API surface was diffed before and after with go/types: no root
name, field or method was removed or renamed. What changes:
- Alias targets live in
internal/ packages. %T, reflect and compiler
messages now print apitokens.APIToken, doctor.Report, revocation.Event
and similar instead of theauth.APIToken. Code that compares type names as
strings must be updated; code using the root names is unaffected.
RevocationEvent, RevocationKind, RevocationBus, MemoryRevocationBus,
RevocationTarget and WatchOptions are now aliases of internal/revocation
types, as are APIToken, DeviceCode, Principal, Report, Finding,
Severity, AuthEvent, BootstrapConfig and RoleSeed. Method sets are
identical.
ImportUserStorage, DoctorAdminLister, DoctorSessionCounter and the
storage capability interfaces remain defined at the root.
- The
*ForTest helpers from export_test.go (never importable outside the
module) are gone from the root; in-repo tests use internal/testhooks.
go test github.com/glincker/theauth-go/v2 no longer runs the integration
suite: run go test ./... or go test ./integration/.... The fuzz targets
and the testdata/ golden files moved with it.
sqlc.yaml moved to storage/postgres/sqlc.yaml; run sqlc generate from
storage/postgres.
-
Module path is now
github.com/glincker/theauth-go/v2 (see Upgrade
notes).
-
JSON error bodies replace plain-text
http.Error bodies (see Upgrade
notes).
-
OAuth login CSRF closed with a real browser binding. The
/start cookie
now carries a secret distinct from the state parameter; the server stores
its hash and verifies it in constant time at /callback. State is burned on
any failed check.
-
Session rotation on MFA completion and password change. The TOTP verify
and recovery routes now set a new cookie and revoke the pending token.
-
Password policy.
PasswordPolicyConfig gains MinLength, MaxBytes
(default 72, the bcrypt limit; longer passwords now return 400
weak_password instead of failing later) and an optional BreachChecker.
HIBPBreachChecker implements the Have I Been Pwned k-anonymity range API,
is off by default and fails open on network errors.
-
go.mod: golang.org/x/text is now a direct dependency (NFKC).
Fixed
- Synced-passkey login failure (backup-eligible flag). WebAuthn login
failed with a generic “verification failed” for any credential whose
authenticator reports the backup-eligible (BE) flag, which is the
overwhelming majority of real-world passkeys (iCloud Keychain, Google
Password Manager, and every other synced passkey). The library never
persisted a credential’s BE / BS flags at registration nor restored them at
login, so go-webauthn’s
validateLogin BE-equality check always compared
the asserted true against a stored false and rejected the assertion.
Registration now captures the flags, login restores them, and new nullable
backup_eligible / backup_state columns (migration 0017, Postgres +
MySQL) back the round trip. Credentials registered before this fix have no
recorded flags and are reconciled on their next login via trust-on-first
use, then enforced strictly thereafter. A WebAuthn validation failure of
any kind now also logs the underlying go-webauthn error server-side (never
surfaced to the client) for diagnosability. Found via production log
analysis from a consuming application, not this repo’s issue tracker.
PasswordPolicy.AllowLegacyBcrypt is now honored at signin, step-up and
password change, with rehash to Argon2id on success. With it off, a bcrypt
hash returns invalid credentials instead of a 500.
- Existing accounts are no longer linked by an unverified provider email: a
callback whose email matches an existing user fails unless the provider marks
it verified, and provider emails are lower-cased and trimmed before lookup (no
more case-split shadow accounts).
- MySQL
CreateUser defaults zero CreatedAt and UpdatedAt instead of
failing.
Security
- Login throttle. Password signin is now gated before any credential work
by per (client IP, normalized email) exponential backoff after a grace
period, plus a per-user lockout that auto-expires and can be cleared with
TheAuth.UnlockUser. Unknown and known accounts take the same path. On by
default; tune or disable with Config.LoginThrottle, supply a persistent
LoginThrottleStore to share state across processes. Refusals return 429
with a Retry-After header and code rate_limited or account_locked.
- MFA hardening. TOTP codes are single use: the last accepted time-step is
recorded per user and replays are rejected (RFC 6238 section 5.2). TOTP and
recovery-code guesses are now capped per user, not per pending session, so
re-entering the password for fresh pending sessions no longer resets the
budget. Optional
TOTPReplayStorage makes the step durable; without it the
step is held in process memory.
- First-run bootstrap.
Config.Bootstrap closes public signup and requires
a one-time setup token (generated and logged at startup, or supplied) to
create the first user. New: TheAuth.UserCount, TheAuth.SetupToken,
GET /auth/bootstrap/status, TheAuth.ResetPasswordAdmin for a
recover-admin command, and BootstrapConfig.OnFirstUser for granting roles.
Needs the optional UserCountStorage capability.
- Email canonicalization. Every email entry point (password, magic link,
rate-limit key, SAML, SCIM) trims and lowercases, and folds Unicode
compatibility forms when
Config.EmailNFKC is set. TheAuth.NormalizeEmail
exposes the same rule.
- OAuth login CSRF and account takeover. Browser-bound state cookie and
verified-email requirement for linking existing accounts (see Changed and
Fixed).
- CSRF / Origin protection and automatic Secure cookies (see Added).
- CIMD fetches refuse non-public addresses at dial time, with no redirects or
proxy;
CIMDConfig.AllowPrivateNetworks (dev) and DenyHost added.
- Authorization error responses redirect only to a registered
redirect_uri;
SAML RelayState is restricted to same-site paths or
SAMLConfig.AllowedRelayStates.
2.5.0 - 2026-07-14
Stable release graduating v2.5.0-rc.1. Completes the Config.LifecycleHooks
surface (#76) with all six hooks fully wired, fixes a real bug where the
batteries-included Mount() HTTP routes bypassed hook dispatch entirely for
password/TOTP/WebAuthn, and closes out a batch of storage-layer correctness
fixes across Postgres and MySQL.
Added
Config.LifecycleHooks fully wired (#76). OnSignup now also fires
from SAML signup and a user’s first WebAuthn credential (passkey
registration has no true account-creation moment, so the first credential
is the closest equivalent). OnPasswordChange, OnMFAEnabled, and
OnOrgSwitch (explicit SetActiveOrganization only, not auto-provisioned
personal orgs) now fire. OnTokenIssued fires immediately before signing
on every OAuth access-token grant (authorization_code, refresh_token,
client_credentials, RFC 8693 exchange, CIBA); consumer-returned claims
merge into the JWT, and a non-nil error aborts issuance.
AuthorizationServerConfig.Clock (#38). Injectable time source for
the introspection cache and agent-chain cache, so tests can assert
revocation propagation deterministically instead of sleeping past
IntrospectionCacheTTL.
- Password policy documented in README (#39). Minimum length, Argon2id
hashing, and the anti-enumeration dummy-verify behavior.
Fixed
a.Mount() bypassed LifecycleHooks for password, TOTP, and WebAuthn.
passwordhandlers/totphandlers/webauthnhandlers held the raw internal
Service directly instead of a hook-aware adapter, unlike OAuth/
Organizations/SAML, which already routed correctly. OnSignup/OnSignin/
OnPasswordChange/OnMFAEnabled never fired for any consumer using the
batteries-included Mount() routes, even though OnSignup/OnSignin were
marked shipped in v2.5.0-rc.1. Each package now takes a root-backed
adapter, mirroring the already-correct pattern.
- SCIM
PATCH /Groups/{id} silently discarded member-write errors,
always returning 200 even when the storage write failed.
- Admin
GET /audit classified bad-cursor errors by substring-matching
err.Error() instead of errors.Is. Added a proper ErrBadCursor
sentinel.
storage/mysql.MoveTOTPSecret treated any error from its existence
check (not just “no rows”) as “primary already has a secret,” which could
silently delete a secondary user’s real TOTP secret on a transient DB
error.
storage/postgres.UpdateAgentLastActive and six storage/mysql
methods (UpdateSessionAuthLevel, SetSessionActiveOrganization,
UpdateSAMLConnectionRow, UpdateGroup, UpdateAgentLastActive,
UpdateAgentCredentialLastUsed) now return storage.ErrNotFound instead
of silently succeeding on a missing row.
mysql.Migrate() failed entirely against MySQL 8.4 (the current MySQL
major version): several migrations used TEXT/BLOB DEFAULT '', which 8.4
rejects, and the statement splitter mis-handled a semicolon appearing
inside a SQL comment. Migrations now apply cleanly end-to-end.
identitylink.MergeAccounts partial-failure behavior documented and
tested: a failure partway through its four move steps is safe to retry
with the same IDs.
[2.5.0-rc.1] - 2026-06-22
The “consumer feedback” release. Closes the three highest-impact friction points raised in the 2026-06-22 consumer-feedback session: the hook surface for lifecycle events, opt-in automatic tenant provisioning, and a unified RFC 7807 error envelope across all auth/RBAC middleware. All additions are fully additive: downstream code compiles unchanged.
This is a release candidate so consumers can road-test the new surface (LifecycleHooks contract, TenancyConfig auto-provisioning, error envelope shape) before the stable v2.5.0 tag. The remaining hook wiring (passkey, SAML, OnPasswordChange, OnMFAEnabled, OnTokenIssued, OnOrgSwitch) and the selective re-exports work (#79) ship in subsequent v2.5.x patches without API change.
Added
Config.LifecycleHooks surface (#76, partial). New optional hook bundle
lets consumers react to authentication-lifecycle events without forking
handlers or wrapping endpoints at the HTTP boundary. Six hook fields land
(OnSignup, OnSignin, OnPasswordChange, OnMFAEnabled, OnTokenIssued,
OnOrgSwitch), plus SignupMethod and MFAKind enums. Errors and panics
are recovered and logged; the triggering request never fails. This release
wires OnSignup and OnSignin at the password, magic-link, and OAuth
provider callback paths. The remaining hook points (passkey, SAML
signup; password change; MFA enable; token issuance; org switch) ship
incrementally in v2.5.x without API change. Magic-link consume and OAuth
callback both distinguish new vs returning users via an internal flag so
OnSignup only fires when the user row was actually created. See
LifecycleHooks doc comment for semantics.
Auth.UserByID (#77). Public lookup that previously forced
consumers to reach into storage directly. Forwards to Storage.UserByID.
Config.Tenancy auto-provisioning (#77). New TenancyConfig lets
consumers opt into automatic personal-organization creation on signup.
When Tenancy.AutoCreatePersonalOrg = true and Organizations is
enabled, every signup automatically creates a personal organization,
adds the user as its owner, and sets the session’s active organization
to it. Closes the SQL-seeding friction the consumer feedback flagged.
PersonalOrgNameFn and PersonalOrgSlugFn knobs override the defaults
(user.Email and "personal-" + lowercased ULID). Wired at the same
three signup paths as the hook surface (password, magic-link, OAuth).
Fixed
-
RequireAuth now emits RFC 7807 problem+json on 401 (#78). Previously
RequireAuth and RequirePendingOrFull wrote plain-text bodies ("unauthorized")
while RequirePermission already emitted RFC 7807, forcing frontends to
special-case the 401 path. All three middlewares now share a single envelope:
Two distinct codes are surfaced so frontends can distinguish “log in” from
“complete second factor”:
auth.unauthenticated for no session cookie or failed validation.
auth.step_up_required for pending_2fa sessions that still need TOTP or WebAuthn verify.
RequirePermission 500 paths also moved off plain text (rbac.disabled,
rbac.internal_error) for the same reason. Tracked in milestone v2.5.0.
[2.4.0] - 2026-06-22
The “enterprise security profile, supply chain hardening, and storage portability”
release. v2.4 closes the FAPI 2.0 baseline by combining PAR (RFC 9126), JAR
(RFC 9101), and JWT-Bearer client authentication (RFC 7523) in a single release.
A new MySQL 8.x backend, a public storagetest contract suite, CIBA backchannel
authentication (RFC 9509), and a CLI migration tool for Cognito and Auth0 round
out the release. All additions are fully additive: downstream code compiles
unchanged.
Added
-
MySQL 8.x storage backend (#62).
storage/mysql implements theauth.Storage
and OAuthServerStorage with full parity with the existing postgres backend.
The adapter uses sqlc-generated queries targeting MySQL 8.x dialect. All
dialect translations (e.g., ILIKE to LIKE BINARY, gen_random_uuid() to
UUID(), advisory locks to GET_LOCK) are handled internally. Enable the
contract test gate by setting THEAUTH_MYSQL_CONTRACT=1 when running
go test ./storage/mysql/... against a live MySQL instance. The contract suite
(storagetest.Run) is the same set used by the postgres adapter, so parity
is enforced mechanically.
- Current parity caveats:
postgres.Migrate pattern not yet mirrored; operators
apply the storage/mysql/migrations/ SQL files manually or via their
migration runner. Advisory lock serialization is weaker than Postgres
serializable transactions; avoid concurrent migrations.
-
Cognito + Auth0 migration CLI (#63). New
cmd/theauth-migrate/ binary
with sub-commands cognito and auth0 exports users from AWS Cognito (CSV
or JSON input) and Auth0 (Management API or bulk export) into an intermediate
JSON bundle, then applies the bundle to any theauth-go storage backend.
- Two-stage design:
export then apply lets operators diff and validate
the bundle with theauth-migrate validate before touching production storage.
- Auth0 path preserves bcrypt password hashes and triggers transparent
re-hashing with Argon2id on next successful login. Enable with the new
PasswordPolicy.AllowLegacyBcrypt = true config flag during the migration
window; disable once active users have been re-hashed.
- Build:
go build ./cmd/theauth-migrate.
-
PAR (RFC 9126) + JAR (RFC 9101) for FAPI-adjacent profile (#64).
Pushed Authorization Requests and JWT-Secured Authorization Requests are now
supported by the OAuth 2.1 AS.
- PAR:
POST /oauth/par accepts the full authorization request body, stores
it under a urn:ietf:params:oauth:request_uri handle (9-character random
suffix), and returns the handle with a 60-second TTL. The authorize endpoint
accepts the request_uri parameter and rejects raw parameters when PAR is
required (PARConfig.Required = true).
- JAR: the authorize endpoint verifies
request JWTs signed by the client’s
registered public key (JARConfig.AllowedAlgorithms, default ES256/RS256).
The request JWT must contain iss, aud, exp, iat, nbf, and the
standard authorization parameters.
- PAR + JAR together reach the FAPI 2.0 Security Profile baseline when
combined with the JWT-Bearer client authentication added in #65. See
the PAR + JAR concept page in the docs site for the flow narrative.
- New config:
AuthorizationServerConfig.PAR *PARConfig,
AuthorizationServerConfig.JAR *JARConfig.
- Passes the zero-dependency mcpresource contract: mcpresource gains no new
transitive deps from this change.
-
JWT-Bearer client auth + grant + token exchange polish (#65, RFC 7523).
The AS now accepts JWT client assertions (
client_assertion_type= urn:ietf:params:oauth:client-assertion-type:jwt-bearer) as a client
authentication method alongside client_secret_post and client_secret_basic.
- Issuers are registered via the new
TrustedJWTIssuer config type; a
SubjectMapper callback maps the JWT subject to a theauth client ID.
- JWT-Bearer grant (
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer):
exchange an external JWT (e.g., a Kubernetes ServiceAccount token) for a
theauth access token without a prior interactive auth step.
- Token exchange polish:
requested_token_type parameter is now respected
per RFC 8693; the response issued_token_type is set explicitly.
- Primary use case: Kubernetes workload identity. A Pod authenticates with
its projected ServiceAccount token; the AS verifies the OIDC issuer and
mints a scoped access token for the target resource. See the JWT-Bearer
concept page in the docs site.
- New config:
AuthorizationServerConfig.JWTBearer *JWTBearerConfig.
- Passes the zero-dependency mcpresource contract: no new transitive deps.
-
CIBA — backchannel authentication, RFC 9509 (#66, Poll + Ping modes).
The CIBA profile lets a consumption device (e.g., a call center agent or voice
assistant) authenticate a user via a separate authentication device (e.g., a
phone push notification) without a browser redirect.
AuthenticationDevice interface: implement Notify(ctx, req CIBARequest) error to deliver push notifications, voice prompts, or any out-of-band
channel.
- Poll mode: the client calls
POST /ciba/token periodically using the
auth_req_id returned by POST /ciba/bc-authorize. Returns
authorization_pending until the user approves (or access_denied on
denial).
- Ping mode: client registers a
client_notification_endpoint; theauth-go
POSTs the token to that endpoint when the user approves. No polling required.
- When to use: IoT device pairing, voice-channel step-up, call center
agent-assisted authentication, TV/speaker without a keyboard.
- New config:
AuthorizationServerConfig.CIBA *CIBAConfig.
-
storagetest public contract suite (#58). Any custom storage adapter can
now be verified against the canonical theauth-go contract by calling
storagetest.Run(t, factory) from its test file:
The suite covers 12 functional areas (sessions, passwords, oauth clients,
refresh tokens, JWKS rotation, agents, delegations, CIBA, storagetest
idempotency, error sentinel conformance, concurrent writes, and audit
append-only). Both in-tree adapters run this suite in CI.
-
Config.RequireState bool (#64, RFC 9700 BCP). When true, the AS rejects
/authorize requests without a non-empty state parameter. Default false
preserves backwards compatibility.
-
PasswordPolicy.AllowLegacyBcrypt bool (#63). Opt-in to accept bcrypt
password hashes imported from Auth0 (and similar systems). When a user
authenticates, theauth-go detects the bcrypt PHC prefix, verifies with bcrypt,
and re-hashes with Argon2id on success. Disable this flag once the migration
window closes.
Changed
-
AuthorizationServerConfig extended with new optional sub-configs (additive).
PAR *PARConfig, JAR *JARConfig, JWTBearer *JWTBearerConfig,
CIBA *CIBAConfig. All nil by default; nil means the feature is disabled.
Existing config structs compile and run unchanged.
-
Token exchange response now sets
issued_token_type explicitly (#65).
Previously the field was omitted. It is now set to
urn:ietf:params:oauth:token-type:access_token per RFC 8693 section 2.2.1.
The field was not part of any guarantee in previous releases, so this is
classified as a bug fix rather than a breaking change.
Fixed
gofmt fixup on jwtbearer.go and par_serialise.go (#68). Two files
landed in #64 and #65 with minor formatting inconsistencies. No logic change.
Tests
-
Root test file count: 35 to 27 (#67). Extracted
internal/theauthtest/ helper package (test fixtures, JWT minting helpers,
request builders) consumed by the remaining root tests. Reduces root noise
and makes per-package test helpers importable without init-time side effects.
-
Performance regression CI gate (#59, 12 benchmarks).
benchgate now runs
on every PR. Compares benchmark results with benchstat; any benchmark
regressing beyond the 25% default threshold fails the CI check. The diff is
posted as a PR comment. Baseline is pinned to the main branch. Override
threshold: BENCHGATE_THRESHOLD=0.30.
Internal
-
Goreleaser + SBOM + Sigstore keyless + SLSA provenance (#57). Tag pushes
now trigger
.github/workflows/release.yml, which runs goreleaser, generates
a CycloneDX SBOM, signs the SBOM and source archive with cosign keyless
signing (GitHub Actions OIDC identity), and attaches an SLSA level-3
provenance attestation. Consumers can verify releases with cosign verify-blob
and gh attestation verify. See Releases and Verification.
-
MkDocs Material docs site with
mike versioning (#60). docs-site/ now
builds a versioned docs site deployed to GitHub Pages. The mike plugin
manages version aliases (latest, stable). The GH Pages workflow deploys
on every push to main. Build locally: cd docs-site && mkdocs build --strict.
Security
-
Supply chain: goreleaser + cosign + SLSA (#57). Every release artifact
(source archive, SBOM) is now signed with Sigstore keyless signing via the
GitHub Actions OIDC identity. SLSA level-3 provenance is attached. Consumers
can cryptographically verify that a release was built by the official GitHub
Actions workflow and has not been tampered with post-build. See
Releases and Verification for the
verification commands.
-
Trust documentation (#61).
docs/THREAT-MODEL.md (STRIDE analysis across
all subsystems), docs/COMPLIANCE-SOC2.md (AICPA TSC 2017 criteria mapping),
and docs/COMPLIANCE-GDPR.md (GDPR data handling reference and
operator/controller role clarification) are now in the repository. These
documents inform operator security assessments and SOC 2 evidence packs.
[2.3.0] - 2026-06-22
The “MCP wedge deepening and enterprise feature parity” release. Three major
features land here that close the gap with Auth0, Clerk, and Better-Auth:
account linking with mandatory step-up, eight new built-in OAuth providers
(12 total), and pluggable SIEM streaming sinks for enterprise audit log
shipping. Public API stays additive; downstream code compiles unchanged.
Added
-
Account linking and identity merge (#55). Users can now bind a new
authentication method to an existing account, or merge two accounts into
one, behind mandatory step-up auth.
LinkOAuthToCurrentUser(ctx, sessionID, provider, payload) error —
bind a new OAuth provider to the signed-in user.
LinkPasswordToCurrentUser(ctx, sessionID, password) error — add a
backup password to an OAuth-only account.
MergeAccounts(ctx, primaryID, secondaryID, mergeInput) error —
destructive merge; moves OAuth accounts, passwords, WebAuthn
credentials, TOTP secrets from secondary to primary; revokes
secondary’s sessions; cross-references via merged_into in the audit
log.
- HTTP endpoints under
/account/identities: POST /oauth, GET
/oauth/callback, POST /password, POST /merge, DELETE
/{provider}.
- New errors:
ErrIdentityConflict, ErrStepUpRequired,
ErrLastAuthMethod (cannot unlink the last auth method).
- New audit event types:
identity.linked, identity.unlinked,
account.merged.
- All methods require a fully-authenticated session (no
pending_2fa);
callers receive ErrStepUpRequired otherwise.
-
Eight new built-in OAuth providers (#54). Total provider count: 4 to 12.
provider/apple — Sign in with Apple. ES256 JWT client authentication
minted from a .p8 private key per token exchange. Parse the .p8 file
with x509.ParsePKCS8PrivateKey after pem.Decode, not
x509.ParseECPrivateKey (the file is downloadable only once from
Apple’s developer console).
provider/facebook — Meta OAuth 2.0 with PKCE; email_verified
conservatively false (Graph API does not attest it).
provider/slack — Sign in with Slack via the OpenID Connect
endpoint.
provider/gitlab — GitLab OIDC; BaseURL option supports
self-hosted instances.
provider/bitbucket — Bitbucket Cloud OAuth 2.0 with HTTP Basic
auth on the token exchange per Atlassian spec.
provider/twitch — Twitch OIDC; adds the claims parameter to
surface email on the userinfo response.
provider/linkedin — Sign In with LinkedIn using OpenID Connect
(post-2023 /v2/userinfo endpoint).
provider/x — X (formerly Twitter) OAuth 2.0 with mandatory PKCE;
ExchangeCode returns an error if codeVerifier is empty.
- Each provider ships with its own
examples/oauth-<provider>/
minimal demo.
-
SIEM audit streaming sinks (#53). New
AuditSink interface on the
root package lets operators fan out audit events to external systems
without a polling job. Failed sinks never block the canonical storage
write; failures increment Stats.AuditSinkFailed.
AuditSink interface: Stream(ctx, batch []AuditEvent) error +
Name() string.
AuditConfig.Sinks []AuditSink — register zero or more sinks; root
package wires them through wiring.go.
- Built-in
audit/sinks/otlp — OTLP/HTTP logs exporter; deps in its
own go.mod (root gains zero new transitive deps).
- Built-in
audit/sinks/splunkhec — Splunk HEC envelope, token auth,
no new deps.
- Built-in
audit/sinks/webhook — generic CloudEvents 1.0 POST with
X-CloudEvents-Signature HMAC-SHA256 header.
- All three sinks support a
WithRedactor(func(AuditEvent) AuditEvent)
option for per-sink PII stripping (stricter than the default).
- New stats field:
Stats.AuditSinkFailed uint64.
Changed
- Stats counters expanded.
Stats.AuditSinkFailed added; existing
counters unchanged. Per the additive-fields contract, downstream code
consuming Stats does not need to change.
[2.2.0] - 2026-06-22
The “production-grade observability and audit closure” release. Three RFC-level
features (CIMD, DPoP, observability adapters), four security closures, four perf
items, +175pp of direct handler coverage on two highest-blast-radius packages,
and an architectural cleanup that brings theauth.go from 1,171 LOC under the
500 LOC ceiling. Public API stays byte-stable; every addition is additive.
Added
-
OAuth Client ID Metadata Documents (CIMD) per MCP spec 2025-11-25 (#42).
Clients identify themselves by HTTPS URL; theauth-go fetches and validates
the metadata JSON on first use, caches with TTL, and applies a configurable
trust policy. Default policy is
DenyAll (fail-closed) for fresh deployments.
Demotes the RFC 7591 DCR registration flow without removing it.
-
RFC 9449 DPoP (Demonstrating Proof-of-Possession) support (#43). The
authorization server can now mint sender-constrained access tokens.
Enable by setting
Config.AuthorizationServer.DPoP = &DPoPConfig{...}.
When a client presents a DPoP header on the token request, the AS
verifies the proof JWT (typ, alg, jwk, htm, htu, iat, jti, optional
nonce) and embeds an RFC 7800 cnf.jkt claim in the issued access
token. The response carries token_type: "DPoP" per RFC 9449.
Resource servers wired with mcpresource.WithDPoPVerification(...)
re-verify the proof on every request, including the ath claim that
binds the proof to the access token. A stolen token cannot be replayed
without the holder’s private key.
- New public type:
theauth.DPoPConfig (additive on
AuthorizationServerConfig; nil by default).
- New mcpresource option:
mcpresource.WithDPoPVerification(algs, proofMaxAge, jtiReplayWindow). The mcpresource module gains no new
transitive dependencies.
- AS metadata now advertises
dpop_signing_alg_values_supported.
- Supported proof algs: ES256, ES384, RS256, PS256, EdDSA. HS* and
none are unconditionally rejected.
- Deferred (forward-compatible): authorization-code binding,
refresh-token DPoP rotation.
-
Pluggable observability adapters (OTel + Prometheus) (#44). New
Tracer and Metrics interfaces on the root package plus a coalesced
Hooks bundle wired via Config.Observability. 10 spans
(theauth.oauth.token, theauth.oauth.introspect, agent + delegation
lifecycle, etc.) and 10 metrics
(theauth_oauth_token_latency_seconds{grant_type}, clientauthcache
hits/misses/size, ratelimit blocked, audit queue depth, etc.) ship live.
Example bridges to go.opentelemetry.io/otel and prometheus/client_golang
live in examples/observability-otel/ and examples/observability-prom/
with their own go.mod; root and mcpresource go.mod gain zero new deps.
-
Storage migration helper (#32).
postgres.Migrate(ctx, pool) error
embeds the migration SQL files, applies pending versions under an
advisory lock, and is idempotent on re-run. Downstream consumers can
delete their own copies of the migration code.
-
Config.RequireState knob (#45). When true, rejects /authorize
requests without a non-empty state parameter. Default false preserves
backwards compatibility. RFC 9700 best-current-practice.
-
Config.SuppressSecureCookieWarning knob (#48). Opt-out for the
deprecation warning shipped this release ahead of v3.0 default flip of
SecureCookie from false to true.
-
mcpresource.Validator.Diagnostics() (#48). Returns warnings about
validator misconfiguration (e.g., neither JWKS URL nor introspect URL
set). New public types: mcpresource.Diagnostic, severity constants.
-
AuthorizationServerNotConfigured sentinel error (#50). Replaces
six ad-hoc errors.New(...) calls in forwarders_oauth.go. Use
errors.Is(err, theauth.ErrAuthorizationServerNotConfigured).
Changed
-
theauth.go LOC: 1,171 -> 383 (#50). Wiring extracted to wiring.go,
storage interface to storage.go, config sub-structs to config.go.
OAuth provider state machine moved to internal/oauth/service.go.
internal/account/handlers/ and internal/admin/handlers/ parent
packages collapsed (handlers now live directly under internal/account/
and internal/admin/). Zero public-API impact; downstream code compiles
unchanged.
-
JWKS rotation is now transactional (#48). Migration
0015 adds a
partial unique index WHERE state='current'. Service.RotateSigningKey
serializes rotations within a process via a sync.Mutex; concurrent
rotations across processes are guaranteed to leave exactly one current
key by the database constraint. New optional storage interface
JWKSAtomicRotator lets postgres collapse the rotation into a single
serializable transaction.
-
Audit redactor uses precomputed lowercase key set (#46). Replaces
per-key
strings.ToLower allocations with strings.EqualFold against a
set built once at construction. Allocs/op drop.
-
keyedLimiter uses sync.RWMutex + atomic lastUsed (#46). Read
path no longer needs the write lock; 8-core read-heavy throughput
improves 5-10x under contention.
-
Chain-walk cache (5s TTL) (#46).
chainStillActive cached per agent;
3+ storage calls per 3-deep chain drop to ~0 within the TTL window.
Suspend / revoke invalidates the cache via the same plumbing as
clientauthcache.
-
SCIM auth uses a single storage lookup (#46).
AuthenticateSCIMToken
returns the token row; middleware reads from it directly.
TouchSCIMTokenLastUsed runs async via the audit channel-writer pattern.
Fixed
-
Magic-link endpoint is now rate-limited (#45).
POST /auth/magic-link
applies the same ipLimit and emailLimit middleware buckets as the
password endpoints. Was previously the only credential-touching route
without rate limiting (enumeration vector).
-
PKCE verifier comparison is constant-time (#45). Replaced
!= with
crypto/subtle.ConstantTimeCompare in internal/as/token.go.
-
RevokeToken walks the refresh family (#45). A revoke now invalidates
the entire token family via the same RevokeRefreshTokenFamily helper
used by the replay-detection path. RFC 7009 “unknown token returns 200”
semantics preserved.
-
InsertOAuthClient coerces nil text[] slices to empty arrays (#40).
Fixes SQLSTATE 23502 violates not-null constraint when a caller mints
an OAuth client with zero-value RedirectURIs, GrantTypes,
ResponseTypes, or Contacts. Most common hit: CreateAgent calling
MintAgentCredential on a fresh schema. Same fix applied to
UpdateOAuthClient.
Tests
-
internal/saml/handlers 0% -> 91.4% statement coverage (#47). 20
direct table tests covering happy path, signature failure, expired
NotOnOrAfter, wrong audience, and replay on /saml/acs. Plus 15
tests for the 5 CRUD endpoints.
-
internal/webauthn/handlers 0% -> 84.5% (#47). 21 tests covering
challenge cookie roundtrip, single-use guarantee, register/login Begin
and Finish, and credentials list / delete.
-
internal/organizations/handlers 0% -> 94.0% (#49). 22 tests for
the 7 endpoints (create / list / get / activate / clear-active /
add-member / remove-member) with happy paths and all documented error
codes.
-
internal/admin/handlers 0% -> 81.1% (#49). 52 tests for
requireOrgMatch middleware plus the 12 admin endpoints.
-
TestSuspendAgentBustsClientAuthCache regression guard. Confirms a
suspended agent cannot re-authenticate via a cached Argon2 entry within
the 5-minute cache TTL.
-
TestJWKSRotationConcurrentSafe (#48). 8 goroutines rotate the
signing key; asserts exactly one state=current row at the end.
Internal
-
PR H1 (#41) test relocation. 19 root-level
*_test.go files moved
into their proper internal packages, ahead of the architectural
cleanup in #50.
-
/go.work lists all 8 examples (#44 follow-up). Adds chi-app,
echo-app, gin-app, mcp-server, oauth-multi-provider,
stdlib-app, observability-otel, observability-prom.
Security
-
N1 (security re-audit). Cache invalidation gap: revoked / suspended
agents could authenticate via a cached Argon2 entry for up to 5 minutes.
Closed by an explicit
s.invalidate(cur.ClientID) call in
changeAgentStatus. Regression test ships.
-
L1-L3 + L5 (security re-audit). All re-audit lows closed; see Added
/ Fixed entries for #45 above.
-
M3-M5 (security re-audit). SecureCookie deprecation warning, JWKS
transactional rotation, mcpresource Diagnostics; see entries for #48.
[2.1.0] - 2026-06-21
Internal architecture reorganization plus the v2.0 security audit followups.
The public API is byte-stable with v2.0: every exported type, function,
and method on *theauth.TheAuth keeps the same identifier, signature, and
method set. Downstream consumers compile unchanged. Anyone relying on
unexported symbols via //go:linkname or unsafe reflection may break (we
deleted several dead unexported root methods and consolidated forwarders).
Internal package reorganization (PRs #20 through #28)
The 1.9k-line monolithic root grew to 49 non-test root files at v2.0; PRs
#20 through #28 extracted feature-by-feature implementations into
internal/<flow> subpackages while the root kept the public surface as
thin forwarders. PR G (this release) collapsed the remaining one-line
forwarders into four grouped files and removed the dead bridges that
earlier extractions left behind.
- New internal packages added across the refactor:
internal/models,
internal/as, internal/as/handlers, internal/agent,
internal/agent/handlers, internal/delegation, internal/session,
internal/password, internal/password/handlers, internal/totp,
internal/totp/handlers, internal/webauthn, internal/webauthn/handlers,
internal/magiclink, internal/oauth/handlers, internal/saml,
internal/saml/handlers, internal/scim, internal/scim/handlers,
internal/organizations, internal/organizations/handlers,
internal/rbac, internal/audit, internal/account/handlers,
internal/admin/handlers, internal/clientauthcache,
internal/jwt, internal/chain, internal/httpx, internal/wavt,
internal/ulid, internal/bench, internal/samltest.
- Root non-test
.go file count: 49 (v2.0) to 28 (v2.1). Public surface
unchanged.
Dead code purge (PR G)
PR G deleted seven unused unexported root methods that lost their last
in-tree caller during PRs B through F. Every one was forwarded to a
*as.Service or *<flow>.Service method that handler packages now reach
directly. None were on the public surface; consumers are unaffected.
(*TheAuth).currentSigningKey (was in jwks.go)
(*TheAuth).publicKeyByKID (was in jwks.go)
(*TheAuth).invalidateClientAuthCache (was in as.go)
(*TheAuth).agentBySubjectClaim (was in service_agent.go); the
AgentLookup adapter wired in theauth.New now points at
a.agentSvc.AgentBySubjectClaim directly.
(*TheAuth).authenticateClient (was in service_token.go)
(*TheAuth).finishRegistrationFromRequest (was in
service_webauthn.go); every handler now wraps the body in
http.MaxBytesReader itself.
(*TheAuth).finishLoginFromRequest (was in service_webauthn.go)
Forwarder consolidation (PR G)
Twenty-one service_*.go forwarder files became three grouped files plus
three substantive service files preserved as-is (because they still hold
local logic, not just one-line thunks).
forwarders_identity.go: session, magic-link, password, TOTP,
WebAuthn, audit.
forwarders_oauth.go: DCR, introspect, revoke, token, token v3 / v4
grants, AS metadata, protected-resource metadata, authorize.
forwarders_enterprise.go: SCIM, organizations, RBAC, delegation.
- Retained:
service_oauth.go (OAuth state cache + GC + provider flow),
service_agent.go (validateAgentConfig plus the agent CRUD
forwarders), service_saml.go (toInternal adapter plus SAML
forwarders).
Twelve root handlers_*.go files became eight: the five thinnest mount
forwarders (oauth, password, totp, webauthn, oauth_server) collapsed
into mounts_extracted.go; the six handler files that carry substantive
service adapters (handlers_account.go, handlers_admin.go,
handlers_admin_agents.go, handlers_organizations.go,
handlers_saml.go, handlers_scim.go) plus the top-level handlers.go
stay separate.
errors_v20.go merged into errors.go; models_v20.go merged into
models.go.
Security (audit 2026-06-20, shipped in PR #17)
- H1: POST
/oauth/register Bearer gate now validates the supplied token
against AuthorizationServerConfig.RegistrationTokens under
crypto/subtle.ConstantTimeCompare against pre-hashed sha256 digests.
The legacy “any non-empty bearer is accepted” behavior is gone. Empty
and unknown bearers return 401 access_denied. When RegistrationTokens
is empty and AllowAnonymousRegistration is false (the default and
production-recommended state), all registration requests are denied.
- H2: POST
/oauth/register is now rate limited per source IP. The cap
defaults to 1 req/min when AllowAnonymousRegistration is true (the
documented public-MCP profile) and 5 req/min otherwise. Operators can
override via the new
AuthorizationServerConfig.RegistrationRateLimitPerMinute field;
negative values disable the cap entirely.
- H3: organization-scoped delegation admin (POST
/admin/v1/organizations/{orgID}/delegations) now verifies that
body.userId is a member of the calling admin’s organization before
creating the grant.
- H4:
X-Forwarded-For is no longer trusted by default. The new
Config.TrustedProxies []netip.Prefix field gates XFF: the header is
consulted only when the incoming r.RemoteAddr is inside one of the
configured prefixes.
- M2:
AddOrganizationMember now refuses to demote the last owner of an
organization.
- M6: the email + password signin path pays the Argon2id verify cost on
the user-not-found and password-empty branches by verifying against a
fixed dummy PHC hash synthesized once at
New time.
Reliability (PR #18)
- Closed the handleToken / SCIM PATCH / end-to-end AS to mcpresource
reliability gaps surfaced by the 2026-06 test audit.
clientauthcache interposes between the AS handlers and the Argon2id
client_secret verifier. First verification pays the Argon2id cost; every
subsequent verification of the same (client_id, secret_hash) pair is
served from the cache until the secret rotates or the entry ages out.
- JWKS key cache: the AES-decrypted Ed25519 private key is decrypted once
per signing key version (rotation refreshes it) and stashed on
*as.Service. Cold path is unchanged; hot path drops one AES decrypt
per JWT mint.
mcpresource validator switched from sync.Mutex to sync.RWMutex on
the JWKS + introspection caches so concurrent reads no longer serialize.
Removed in PR G
- Seven unused unexported methods on
*TheAuth (see “Dead code purge”
above).
- Twenty-one
service_*.go files merged into three forwarders_*.go
files (see “Forwarder consolidation”).
- Five
handlers_*.go files merged into one mounts_extracted.go.
errors_v20.go merged into errors.go.
models_v20.go merged into models.go.
Deferred from the 2026-06-20 audit
Tracked as follow-up issues: M1 (SCIM cross-tenant email fallback), M3
(SecureCookie default), M4 (JWKS rotation transaction), M5 (mcpresource
missing-introspection startup warning), L1-L5, I1-I7.
[2.0.0] - 2026-06-20
v2.0 ships the OAuth 2.1 Authorization Server, agent identity and
delegation chains (RFC 8693), and the mcpresource validator SDK for MCP
servers. Three alpha tags shipped phases incrementally
(v2.0.0-alpha.1 through v2.0.0-alpha.3); v2.0.0 consolidated them.
Added in v2.0 phase 5 + 6 (targeted for v2.0.0)
- New separately importable Go module:
github.com/glincker/theauth-go/mcpresource.
Zero dependencies outside the standard library: a consumer importing the
package does not transitively pull theauth core or the storage adapters.
Public surface: Validator, Principal, Option, New,
PrincipalFromContext, WithJWKS, WithIntrospection, WithCacheTTL,
WithHTTPClient, WithClockSkew, (*Validator).Middleware,
(*Validator).Principal. Validates JWT signature against a cached JWKS
(refreshes on kid miss and at the configured cache TTL), enforces the
audience claim against the configured resource URI, checks expiry, nbf,
and iat with a 60 second skew tolerance by default, and walks the RFC 8693
act chain via the AS introspection endpoint so revocations propagate
inside the configured cache window. On any failure the middleware emits
HTTP 401 with WWW-Authenticate: Bearer error="invalid_token", resource_metadata="..." per RFC 6750 + RFC 9728.
- RFC 9728 OAuth 2.0 Protected Resource Metadata: the AS exposes
GET /.well-known/oauth-protected-resource (bare path returns the first
configured resource) and GET /.well-known/oauth-protected-resource/{path}
(per-resource discovery for multi-resource deployments). The document
carries resource, authorization_servers, bearer_methods_supported,
plus scopes_supported, resource_name, jwks_uri, and
resource_signing_alg_values_supported.
- Organization-scoped admin UX under
/admin/v1/organizations/{orgID}:
GET/POST/PATCH/DELETE /agents and
GET/POST/DELETE /delegations. Gated by the new seeded permissions
agents:admin and delegations:admin; both are added to the owner and
admin default org roles.
- End-user self-service UX under
/account (enabled via
Config.AccountUX): GET/POST /agents, DELETE /agents/{id},
GET/POST /delegations, POST /delegations/{id}/revoke. Session-cookie
gated; cross-user calls return 404 to avoid leaking record existence.
- New seeded RBAC permissions:
agents:admin, delegations:admin.
Existing consumers see the additional permissions on the next
SeedPermissions call; downstream role definitions that did not pre-grant
them stay valid (catalog only grows).
- New error sentinels:
ErrAccountUXRequiresAgents.
- Example
examples/mcp-server/: standalone runnable demo of the
mcpresource middleware on a tiny chi server. Shows the one-import claim:
middleware wiring plus principal extraction in roughly ten lines of Go.
- Audit emission additions: every admin and account mutation emits the same
events used by the service layer (
agent.created, agent.suspended,
agent.resumed, agent.revoked, agent_credential.minted,
agent_credential.revoked, delegation.granted, delegation.revoked).
No new action names introduced in this PR; the v2.0 phase 3 + 4 catalog
remains the source of truth.
v2.0 capability summary
- Phase 1 + 2 (
v2.0.0-alpha.1): OAuth 2.1 Authorization Server core.
/oauth/authorize, /oauth/token, /oauth/revoke, /oauth/introspect,
/oauth/register, /oauth/jwks, /.well-known/oauth-authorization-server.
RFC 9068 EdDSA JWT access tokens. RFC 8707 mandatory audience binding.
PKCE S256 mandatory. RFC 9700 refresh rotation with family revocation.
RFC 7591 dynamic client registration.
- Phase 3 + 4 (
v2.0.0-alpha.2): agent identity and delegation chains.
Agents (user or org owned) with one-shot client secrets. Delegation grants
per (user, agent, resource). client_credentials grant for agent
self-tokens. RFC 8693 token-exchange grant with strict scope narrowing,
strict duration tightening, and a hard chain-depth cap of 3.
Introspection walks the actor chain on every call.
- Phase 5 + 6 (
v2.0.0): the consumption side and the operator surface.
mcpresource SDK for MCP servers. RFC 9728 protected-resource metadata.
Admin and end-user routes for agent and delegation lifecycle.
Added in v2.0 phase 3 + 4 (released as v2.0.0-alpha.2)
- Agents:
Agent, AgentCredential, AgentOwner, CreateAgentInput,
AgentSecret models. CreateAgent, MintAgentCredential,
RotateAgentSecret, ListAgentsByOwner, GetAgent, SuspendAgent,
ResumeAgent, RevokeAgent service methods. Agent credential kinds:
secret (Argon2id-hashed), x509, jwk. The X.509 and JWK kinds return
ErrNotImplemented in this phase so callers see a typed error.
- Delegations:
DelegationGrant, GrantDelegationInput models.
GrantDelegation, ListDelegationsForUser, ListDelegationsForAgent,
RevokeDelegation service methods. Uniqueness on (user_id, agent_id, resource) enforced at the database layer. Revoking a grant invalidates
every derived token at the next introspection refresh.
- Grants on
/oauth/token:
client_credentials mints an agent self-token (sub=agent:<id>,
aud bound to the supplied resource).
urn:ietf:params:oauth:grant-type:token-exchange (RFC 8693) accepts a
user (or agent) subject_token plus an agent actor_token, looks up
the matching delegation grant, and mints a new JWT with the nested
act chain (RFC 8693 section 4.1). Chain depth is capped at 3; deeper
chains are rejected with invalid_request “actor chain depth exceeded”.
- Introspection now walks the full
act chain on every call and re-checks
the delegation grant. Any inactive actor or revoked grant flips
active=false, even on a cache hit.
- AS metadata advertises
client_credentials and the token-exchange URN in
grant_types_supported whenever Config.AgentIdentity is configured.
- Audit emissions added:
agent.created, agent.suspended, agent.resumed,
agent.revoked, agent_credential.minted, agent_credential.revoked,
agent.token_minted, delegation.granted, delegation.revoked,
token.exchanged.
- Migrations:
0012_agents.up.sql (agents + agent_credentials),
0013_delegations.up.sql (delegation_grants + audit_events.actor_agent_id).
Down migrations included.
- Storage interface:
OAuthServerStorage extended with agent + delegation
methods. v1.0 root Storage interface unchanged. Both in-tree adapters
(memory + postgres) implement the new methods.
- Errors:
ErrAgentNotFound, ErrAgentInactive, ErrDelegationNotFound,
ErrDelegationRevoked, ErrChainDepthExceeded, ErrSubjectTokenInvalid,
ErrActorTokenInvalid, ErrNotImplemented, ErrAgentRequiresAS,
ErrAgentChainDepthTooHigh.
Added in v2.0 phase 1 + 2 (released as v2.0.0-alpha.1)
- OAuth 2.1 Authorization Server with
/.well-known/oauth-authorization-server,
/oauth/authorize, /oauth/token (authorization_code + refresh_token),
/oauth/revoke, /oauth/introspect, /oauth/register (RFC 7591 dynamic
client registration), /oauth/jwks (Ed25519, 30 day rotation). Mandatory
PKCE S256; mandatory RFC 8707 resource binding; RFC 9068 JWT access
tokens; RFC 9700 refresh-token rotation with family revocation.
- New config:
AuthorizationServer *AuthorizationServerConfig. New
storage extension interface OAuthServerStorage. Migrations 0011
(oauth_clients + authorization_codes + refresh_tokens) and 0014
(jwks_keys); 0012 / 0013 reserved as placeholders for phase 3 + 4.
[1.0.0] - 2026-06-20
First production-ready release. Public API frozen per STABILITY.md. From
v1.0 forward, breaking changes require a major bump; minor releases add
optional config fields, new methods, new error sentinels, new audit
actions, and new Stats fields without breaking existing callers.
v0.1 to v1.0 capability summary
- Magic-link sign-in, opaque sessions, pluggable
Storage (memory + postgres).
- Email + password with Argon2id, password reset via single-use token.
- OAuth 2.0 + OIDC with PKCE S256; built-in GitHub, Google, Microsoft, Discord.
- WebAuthn / passkey registration and discoverable login.
- TOTP second factor with salted SHA-256 recovery codes; session step-up via
pending_2fa.
- SAML 2.0 Service Provider for enterprise IdPs (per-connection + per-org).
- SCIM 2.0 Users + Groups with eq-only filter and RFC 7644 PATCH.
- Organization multi-tenancy with
active_organization_id session scope.
- Organization-scoped RBAC with a closed permission catalog and seeded roles.
- Append-only audit log with async batched writes, default redactor, keyset cursor read API.
- Admin HTTP API at
/admin/v1 with RFC 7807 problem+json errors.
- Fuzz tests, race-clean test suite, godoc with examples, finalized
STABILITY.md.
Added in 1.0
- RBAC:
permissions, roles, role_permissions, user_roles tables and the
matching Storage methods. (*TheAuth).RequirePermission middleware with a
per-request cache. SeedPermissions, SeedOrganizationRoles,
GrantRole, RevokeRole, CreateRole, UpdateRole, DeleteRole,
PermissionsForUser, HasPermission. Twelve seeded permission constants
plus the system super_admin role (granted out-of-band only).
- Audit log:
audit_events table with org / actor / action DESC indexes.
(*TheAuth).EmitAudit is non-blocking; the writer goroutine drains in
batches and flushes on Close with a configurable timeout. Stats
exposes four atomic counters (AuditEmitted, AuditWritten,
AuditDropped, AuditFailed). DefaultRedactor masks
password / secret / token / code / refresh_token / access_token at any
nesting depth. Twenty-plus canonical actions emitted across every
state-changing handler in theauth.
- Admin API: twelve endpoints under
/admin/v1 (overridable via
AdminConfig.PathPrefix). Every endpoint requires RequireAuth plus a
catalog RequirePermission. Errors are application/problem+json per
RFC 7807 with a stable code extension; keyset pagination on the audit
read endpoint.
(*TheAuth).Start spawns the writer goroutine; New invokes Start
automatically so existing callers do not change. Close drains the
writer with a default 5 second deadline.
- New errors:
ErrAdminRequiresRBAC, ErrForbidden, ErrUnknownPermission,
ErrRoleInUse, ErrNoActiveOrg, ErrOrgMismatch, ErrRBACDisabled.
- New
admin subpackage: RFC 7807 Write helper, Problem type, keyset
cursor EncodeCursor / DecodeCursor codec, reserved problem code
constants.
- Migrations 0009 (rbac) and 0010 (audit) added under
storage/postgres/migrations/.
Tradeoffs documented
- Audit writes are async; backpressure drops events with
Stats.AuditDropped incremented. Block-on-backpressure would degrade
authentication latency under spikes; this is the documented choice.
- Audit insert failures are not retried;
Stats.AuditFailed is the ops
signal. A persistent retry queue is an order of magnitude more
complexity than v1.0 wants to absorb; SOC 2 evidence is better served
by an external audit sink (planned for v1.x).
- No wildcard permissions in v1.0 (planned for v1.1); the catalog is a
closed set so the permission check is a single set membership.
- ABAC, hierarchical roles, and time-bound permissions are explicitly
deferred to v1.x.
Migrating from v0.7
Config.AuditHook and the synchronous AuditEvent shape are removed.
Set Config.Audit = &AuditConfig{} to enable the async writer; existing
no-op deployments leave it nil and see no behavior change.
Storage gains 16 new methods (14 RBAC + 2 audit). Custom adapters
built on top of memory or postgres keep working; in-tree custom
implementations must implement the new methods. Per the new
Storage-extension rule (STABILITY.md), v1.x will introduce future
persistence operations behind optional interfaces detected via type
assertion so this kind of break does not happen again.
[0.7.0] - 2026-06-20
Added (v0.7)
- SAML 2.0 Service Provider on top of
github.com/crewjam/saml v0.5.1.
Per-connection IdP binding stored in saml_connections, signed
assertions only (raw-XML signature gate before parse), find-or-create
by (connection_id, name_id) with an email fallback, AuthnRequest
replay tracking with a configurable TTL. Public-facing flow at
/auth/saml/{connectionId}/{login,acs,metadata}. Per-organization
connection CRUD at /auth/orgs/{orgId}/saml/connections.
- SCIM 2.0 provisioning (RFC 7643 + RFC 7644). Users + Groups CRUD,
discovery endpoints (
ServiceProviderConfig, ResourceTypes,
Schemas), eq-only filter parser, RFC 7644 PATCH (add / replace /
remove). Bearer auth with sha256-hashed 256-bit tokens, HTTPS
enforcement, per-organization isolation, idempotent upsert by
externalId. PUT returns 405 (documented deviation; Okta and Azure AD
default to PATCH). Endpoints live under /scim/v2/. Per-organization
token CRUD at /auth/orgs/{orgId}/scim/tokens.
- Organizations multi-tenancy:
organizations, organization_members,
sessions.active_organization_id. Roles: owner, admin, member.
Single-tenant deployments leave Config.Organizations nil and see no
behavior change.
- Migrations:
0006_organizations, 0007_saml, 0008_scim (additive
on users.external_id, users.given_name, users.family_name,
users.display_name).
- Audit hook (
Config.AuditHook): synchronous no-op stub invoked on
every SCIM mutation and every successful SAML assertion. v1.0 replaces
the default binding with the real async writer; the consumer-facing
signature stays stable.
- Test fixtures:
internal/samltest generates a fresh IdP keypair and
signs assertions in-process for the SAML end-to-end tests. SCIM tests
exercise the Okta / Azure AD provisioning cycle (create, patch,
deactivate, idempotent re-create, delete).
Added (v0.6 hardening)
- Fuzz tests for every external byte boundary: AES-GCM encrypt and decrypt
round-trip, decrypt arbitrary input, PKCE verifier to challenge, PKCE
verifier generation, token hash round-trip, base64 URL decode, session
cookie parsing, OAuth state cookie parsing, email validation, OAuth
callback query parameters, and OAuth callback provider name routing.
- Concurrency tests covering the OAuth state map, the in-memory rate
limiter (same IP and different IP fan-out), and session creation under
contention. All assertions hold under
go test -race.
- Benchmark suite under
internal/bench measuring password sign in,
session lookup, OAuth callback storage cost, magic link consume, and
PKCE challenge derivation. Baselines are recorded in
internal/bench/BASELINES.md.
- Four new runnable example apps:
examples/gin-app,
examples/echo-app, examples/stdlib-app, and
examples/oauth-multi-provider, each with a README, single-file
main.go, go.mod, docker-compose.yml, .env.example, and
Makefile.
- Package documentation:
doc.go added for every package, doc comments
filled in on previously undocumented exports, runnable Example
functions for the most-used entry points.
STABILITY.md enumerating the stable surface and the rules that
govern future changes.
golangci.yml enables godot and godox to keep doc comments
punctuated and to flag stray TODO markers.
- CI workflow now runs a per-target fuzz job (
-fuzztime=10s) on every
PR alongside the existing race-enabled test job.
Post-G follow-up fixes (PRs #30, #31, #33)
After PR G landed, three small follow-ups also shipped in v2.1.0:
- #30 chore: em-dash sweep removed 17 surviving em or en dashes from
comments in
middleware_ratelimit.go, handlers.go, crypto/,
storage/memory/, and the related tests. Brings the repo into full
compliance with the project rule banning em and en dashes anywhere.
- #31 fix(examples): go.work hygiene added the five missing example
modules (
chi-app, echo-app, gin-app, oauth-multi-provider,
stdlib-app) to go.work so all eight examples now build cleanly
with default workspace settings. Incidental: three SAML-related deps
(beevik/etree, crewjam/saml, russellhaering/goxmldsig) were
promoted from indirect to direct in root go.mod by go work sync.
- #33 fix(security): cache bust on agent suspend/revoke (N1) closes
a v2.1 security re-audit finding:
SuspendAgent and RevokeAgent
did not call s.invalidate(cur.ClientID), so a revoked agent could
authenticate via the clientauthcache Argon2-verified snapshot for
up to the 5-minute TTL added in PR #19. The shared
changeAgentStatus now invalidates whenever the new status is not
active. Regression test:
TestSuspendAgentBustsClientAuthCache.
Notes
- No new features. Hardening only.
- No public API changes. Existing callers compile and run unchanged.
[v0.5.0] - 2026-06-19
WebAuthn passkeys, TOTP second factor with recovery codes, session
step-up via pending_2fa. See git history for the full set of changes.
[v0.4.0]
Discord OAuth provider. PKCE-aware flow shared across all four
providers.
[v0.3.0]
GitHub, Google, and Microsoft OAuth providers. AES-256-GCM at-rest
encryption for provider tokens. Per-IP rate limiter.
[v0.2.0]
Email and password credentials with Argon2id. Password reset via
single-use token.
[v0.1.0]
Initial release: magic-link email auth, opaque session tokens with
revocation, chi-friendly middleware, in-memory and Postgres storage
adapters. Last modified on October 7, 2026