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.
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.
Config.LifecycleHookssurface (#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), plusSignupMethodandMFAKindenums. Errors and panics are recovered and logged; the triggering request never fails. This release wiresOnSignupandOnSigninat 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. SeeLifecycleHooksdoc comment for semantics.Auth.UserByID(#77). Public lookup that previously forced consumers to reach into storage directly. Forwards toStorage.UserByID.Config.Tenancyauto-provisioning (#77). NewTenancyConfiglets consumers opt into automatic personal-organization creation on signup. WhenTenancy.AutoCreatePersonalOrg = trueandOrganizationsis 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.PersonalOrgNameFnandPersonalOrgSlugFnknobs override the defaults (user.Emailand"personal-" + lowercased ULID). Wired at the same three signup paths as the hook surface (password, magic-link, OAuth).
-
RequireAuthnow emits RFC 7807 problem+json on 401 (#78). PreviouslyRequireAuthandRequirePendingOrFullwrote plain-text bodies ("unauthorized") whileRequirePermissionalready emitted RFC 7807, forcing frontends to special-case the 401 path. All three middlewares now share a single envelope:HTTP/1.1 401 Unauthorized Content-Type: application/problem+json WWW-Authenticate: Session realm="theauth", error="auth.unauthenticated" {"type":"https://theauth.dev/problems/auth.unauthenticated", "title":"Unauthorized","status":401, "detail":"Missing or invalid session","code":"auth.unauthenticated"}Two distinct codes are surfaced so frontends can distinguish "log in" from "complete second factor":
auth.unauthenticatedfor no session cookie or failed validation.auth.step_up_requiredfor pending_2fa sessions that still need TOTP or WebAuthn verify.
RequirePermission500 paths also moved off plain text (rbac.disabled,rbac.internal_error) for the same reason. Tracked in milestone v2.5.0.
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.
-
MySQL 8.x storage backend (#62).
storage/mysqlimplementstheauth.StorageandOAuthServerStoragewith full parity with the existing postgres backend. The adapter usessqlc-generated queries targeting MySQL 8.x dialect. All dialect translations (e.g.,ILIKEtoLIKE BINARY,gen_random_uuid()toUUID(), advisory locks toGET_LOCK) are handled internally. Enable the contract test gate by settingTHEAUTH_MYSQL_CONTRACT=1when runninggo 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.Migratepattern not yet mirrored; operators apply thestorage/mysql/migrations/SQL files manually or via their migration runner. Advisory lock serialization is weaker than Postgres serializable transactions; avoid concurrent migrations.
- Current parity caveats:
-
Cognito + Auth0 migration CLI (#63). New
cmd/theauth-migrate/binary with sub-commandscognitoandauth0exports 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:
exportthenapplylets operators diff and validate the bundle withtheauth-migrate validatebefore 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 = trueconfig flag during the migration window; disable once active users have been re-hashed. - Build:
go build ./cmd/theauth-migrate.
- Two-stage design:
-
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/paraccepts the full authorization request body, stores it under aurn:ietf:params:oauth:request_urihandle (9-character random suffix), and returns the handle with a 60-second TTL. The authorize endpoint accepts therequest_uriparameter and rejects raw parameters when PAR is required (PARConfig.Required = true). - JAR: the authorize endpoint verifies
requestJWTs signed by the client's registered public key (JARConfig.AllowedAlgorithms, default ES256/RS256). TherequestJWT must containiss,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.
- PAR:
-
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 alongsideclient_secret_postandclient_secret_basic.- Issuers are registered via the new
TrustedJWTIssuerconfig type; aSubjectMappercallback 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_typeparameter is now respected per RFC 8693; the responseissued_token_typeis 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.
- Issuers are registered via the new
-
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.
AuthenticationDeviceinterface: implementNotify(ctx, req CIBARequest) errorto deliver push notifications, voice prompts, or any out-of-band channel.- Poll mode: the client calls
POST /ciba/tokenperiodically using theauth_req_idreturned byPOST /ciba/bc-authorize. Returnsauthorization_pendinguntil the user approves (oraccess_deniedon 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.
-
storagetestpublic contract suite (#58). Any custom storage adapter can now be verified against the canonical theauth-go contract by callingstoragetest.Run(t, factory)from its test file:func TestConformance(t *testing.T) { storagetest.Run(t, func() theauth.Storage { return mystorage.New() }) }
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/authorizerequests without a non-emptystateparameter. 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.
-
AuthorizationServerConfigextended 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_typeexplicitly (#65). Previously the field was omitted. It is now set tourn:ietf:params:oauth:token-type:access_tokenper 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.
gofmtfixup onjwtbearer.goandpar_serialise.go(#68). Two files landed in #64 and #65 with minor formatting inconsistencies. No logic change.
-
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).
benchgatenow runs on every PR. Compares benchmark results withbenchstat; any benchmark regressing beyond the 25% default threshold fails the CI check. The diff is posted as a PR comment. Baseline is pinned to themainbranch. Override threshold:BENCHGATE_THRESHOLD=0.30.
-
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 withcosignkeyless signing (GitHub Actions OIDC identity), and attaches an SLSA level-3 provenance attestation. Consumers can verify releases withcosign verify-blobandgh attestation verify. See Releases and Verification. -
MkDocs Material docs site with
mikeversioning (#60).docs-site/now builds a versioned docs site deployed to GitHub Pages. Themikeplugin manages version aliases (latest,stable). The GH Pages workflow deploys on every push tomain. Build locally:cd docs-site && mkdocs build --strict.
-
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), anddocs/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.
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.
-
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 viamerged_intoin 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 receiveErrStepUpRequiredotherwise.
-
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 withx509.ParsePKCS8PrivateKeyafterpem.Decode, notx509.ParseECPrivateKey(the file is downloadable only once from Apple's developer console).provider/facebook-- Meta OAuth 2.0 with PKCE;email_verifiedconservatively false (Graph API does not attest it).provider/slack-- Sign in with Slack via the OpenID Connect endpoint.provider/gitlab-- GitLab OIDC;BaseURLoption 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 theclaimsparameter to surface email on the userinfo response.provider/linkedin-- Sign In with LinkedIn using OpenID Connect (post-2023/v2/userinfoendpoint).provider/x-- X (formerly Twitter) OAuth 2.0 with mandatory PKCE;ExchangeCodereturns an error ifcodeVerifieris empty.- Each provider ships with its own
examples/oauth-<provider>/minimal demo.
-
SIEM audit streaming sinks (#53). New
AuditSinkinterface 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 incrementStats.AuditSinkFailed.AuditSinkinterface:Stream(ctx, batch []AuditEvent) error+Name() string.AuditConfig.Sinks []AuditSink-- register zero or more sinks; root package wires them throughwiring.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 withX-CloudEvents-SignatureHMAC-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.
- Stats counters expanded.
Stats.AuditSinkFailedadded; existing counters unchanged. Per the additive-fields contract, downstream code consuming Stats does not need to change.
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.
-
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 aDPoPheader on the token request, the AS verifies the proof JWT (typ, alg, jwk, htm, htu, iat, jti, optional nonce) and embeds an RFC 7800cnf.jktclaim in the issued access token. The response carriestoken_type: "DPoP"per RFC 9449. Resource servers wired withmcpresource.WithDPoPVerification(...)re-verify the proof on every request, including theathclaim 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 onAuthorizationServerConfig; 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
noneare unconditionally rejected. - Deferred (forward-compatible): authorization-code binding, refresh-token DPoP rotation.
- New public type:
-
Pluggable observability adapters (OTel + Prometheus) (#44). New
TracerandMetricsinterfaces on the root package plus a coalescedHooksbundle wired viaConfig.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 togo.opentelemetry.io/otelandprometheus/client_golanglive inexamples/observability-otel/andexamples/observability-prom/with their own go.mod; root andmcpresourcego.mod gain zero new deps. -
Storage migration helper (#32).
postgres.Migrate(ctx, pool) errorembeds 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.RequireStateknob (#45). When true, rejects/authorizerequests without a non-emptystateparameter. Default false preserves backwards compatibility. RFC 9700 best-current-practice. -
Config.SuppressSecureCookieWarningknob (#48). Opt-out for the deprecation warning shipped this release ahead of v3.0 default flip ofSecureCookiefromfalsetotrue. -
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. -
AuthorizationServerNotConfiguredsentinel error (#50). Replaces six ad-hocerrors.New(...)calls inforwarders_oauth.go. Useerrors.Is(err, theauth.ErrAuthorizationServerNotConfigured).
-
theauth.goLOC: 1,171 -> 383 (#50). Wiring extracted towiring.go, storage interface tostorage.go, config sub-structs toconfig.go. OAuth provider state machine moved tointernal/oauth/service.go.internal/account/handlers/andinternal/admin/handlers/parent packages collapsed (handlers now live directly underinternal/account/andinternal/admin/). Zero public-API impact; downstream code compiles unchanged. -
JWKS rotation is now transactional (#48). Migration
0015adds a partial unique indexWHERE state='current'.Service.RotateSigningKeyserializes rotations within a process via async.Mutex; concurrent rotations across processes are guaranteed to leave exactly one current key by the database constraint. New optional storage interfaceJWKSAtomicRotatorlets postgres collapse the rotation into a single serializable transaction. -
Audit redactor uses precomputed lowercase key set (#46). Replaces per-key
strings.ToLowerallocations withstrings.EqualFoldagainst a set built once at construction. Allocs/op drop. -
keyedLimiterusessync.RWMutex+ atomiclastUsed(#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).
chainStillActivecached 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 asclientauthcache. -
SCIM auth uses a single storage lookup (#46).
AuthenticateSCIMTokenreturns the token row; middleware reads from it directly.TouchSCIMTokenLastUsedruns async via the audit channel-writer pattern.
-
Magic-link endpoint is now rate-limited (#45).
POST /auth/magic-linkapplies the sameipLimitandemailLimitmiddleware 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
!=withcrypto/subtle.ConstantTimeCompareininternal/as/token.go. -
RevokeTokenwalks the refresh family (#45). A revoke now invalidates the entire token family via the sameRevokeRefreshTokenFamilyhelper used by the replay-detection path. RFC 7009 "unknown token returns 200" semantics preserved. -
InsertOAuthClientcoerces nil text[] slices to empty arrays (#40). FixesSQLSTATE 23502 violates not-null constraintwhen a caller mints an OAuth client with zero-valueRedirectURIs,GrantTypes,ResponseTypes, orContacts. Most common hit:CreateAgentcallingMintAgentCredentialon a fresh schema. Same fix applied toUpdateOAuthClient.
-
internal/saml/handlers0% -> 91.4% statement coverage (#47). 20 direct table tests covering happy path, signature failure, expiredNotOnOrAfter, wrong audience, and replay on/saml/acs. Plus 15 tests for the 5 CRUD endpoints. -
internal/webauthn/handlers0% -> 84.5% (#47). 21 tests covering challenge cookie roundtrip, single-use guarantee, register/login Begin and Finish, and credentials list / delete. -
internal/organizations/handlers0% -> 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/handlers0% -> 81.1% (#49). 52 tests forrequireOrgMatchmiddleware plus the 12 admin endpoints. -
TestSuspendAgentBustsClientAuthCacheregression 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 onestate=currentrow at the end.
-
PR H1 (#41) test relocation. 19 root-level
*_test.gofiles moved into their proper internal packages, ahead of the architectural cleanup in #50. -
/go.worklists all 8 examples (#44 follow-up). Addschi-app,echo-app,gin-app,mcp-server,oauth-multi-provider,stdlib-app,observability-otel,observability-prom.
-
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 inchangeAgentStatus. 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.
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).
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
.gofile count: 49 (v2.0) to 28 (v2.1). Public surface unchanged.
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 injwks.go)(*TheAuth).publicKeyByKID(was injwks.go)(*TheAuth).invalidateClientAuthCache(was inas.go)(*TheAuth).agentBySubjectClaim(was inservice_agent.go); theAgentLookupadapter wired intheauth.Newnow points ata.agentSvc.AgentBySubjectClaimdirectly.(*TheAuth).authenticateClient(was inservice_token.go)(*TheAuth).finishRegistrationFromRequest(was inservice_webauthn.go); every handler now wraps the body inhttp.MaxBytesReaderitself.(*TheAuth).finishLoginFromRequest(was inservice_webauthn.go)
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(validateAgentConfigplus the agent CRUD forwarders),service_saml.go(toInternaladapter 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.
- H1: POST
/oauth/registerBearer gate now validates the supplied token againstAuthorizationServerConfig.RegistrationTokensundercrypto/subtle.ConstantTimeCompareagainst pre-hashed sha256 digests. The legacy "any non-empty bearer is accepted" behavior is gone. Empty and unknown bearers return 401 access_denied. WhenRegistrationTokensis empty andAllowAnonymousRegistrationis false (the default and production-recommended state), all registration requests are denied. - H2: POST
/oauth/registeris now rate limited per source IP. The cap defaults to 1 req/min whenAllowAnonymousRegistrationis true (the documented public-MCP profile) and 5 req/min otherwise. Operators can override via the newAuthorizationServerConfig.RegistrationRateLimitPerMinutefield; negative values disable the cap entirely. - H3: organization-scoped delegation admin (POST
/admin/v1/organizations/{orgID}/delegations) now verifies thatbody.userIdis a member of the calling admin's organization before creating the grant. - H4:
X-Forwarded-Foris no longer trusted by default. The newConfig.TrustedProxies []netip.Prefixfield gates XFF: the header is consulted only when the incomingr.RemoteAddris inside one of the configured prefixes. - M2:
AddOrganizationMembernow 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
Newtime.
- Closed the handleToken / SCIM PATCH / end-to-end AS to mcpresource reliability gaps surfaced by the 2026-06 test audit.
clientauthcacheinterposes 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. mcpresourcevalidator switched fromsync.Mutextosync.RWMutexon the JWKS + introspection caches so concurrent reads no longer serialize.
- Seven unused unexported methods on
*TheAuth(see "Dead code purge" above). - Twenty-one
service_*.gofiles merged into threeforwarders_*.gofiles (see "Forwarder consolidation"). - Five
handlers_*.gofiles merged into onemounts_extracted.go. errors_v20.gomerged intoerrors.go.models_v20.gomerged intomodels.go.
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.
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.
- 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 8693actchain via the AS introspection endpoint so revocations propagate inside the configured cache window. On any failure the middleware emits HTTP 401 withWWW-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) andGET /.well-known/oauth-protected-resource/{path}(per-resource discovery for multi-resource deployments). The document carriesresource,authorization_servers,bearer_methods_supported, plusscopes_supported,resource_name,jwks_uri, andresource_signing_alg_values_supported. - Organization-scoped admin UX under
/admin/v1/organizations/{orgID}:GET/POST/PATCH/DELETE /agentsandGET/POST/DELETE /delegations. Gated by the new seeded permissionsagents:adminanddelegations:admin; both are added to the owner and admin default org roles. - End-user self-service UX under
/account(enabled viaConfig.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 nextSeedPermissionscall; 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.
- 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_credentialsgrant 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.mcpresourceSDK for MCP servers. RFC 9728 protected-resource metadata. Admin and end-user routes for agent and delegation lifecycle.
- Agents:
Agent,AgentCredential,AgentOwner,CreateAgentInput,AgentSecretmodels.CreateAgent,MintAgentCredential,RotateAgentSecret,ListAgentsByOwner,GetAgent,SuspendAgent,ResumeAgent,RevokeAgentservice methods. Agent credential kinds:secret(Argon2id-hashed),x509,jwk. The X.509 and JWK kinds returnErrNotImplementedin this phase so callers see a typed error. - Delegations:
DelegationGrant,GrantDelegationInputmodels.GrantDelegation,ListDelegationsForUser,ListDelegationsForAgent,RevokeDelegationservice 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_credentialsmints an agent self-token (sub=agent:<id>,audbound to the supplied resource).urn:ietf:params:oauth:grant-type:token-exchange(RFC 8693) accepts a user (or agent)subject_tokenplus an agentactor_token, looks up the matching delegation grant, and mints a new JWT with the nestedactchain (RFC 8693 section 4.1). Chain depth is capped at 3; deeper chains are rejected withinvalid_request"actor chain depth exceeded".
- Introspection now walks the full
actchain on every call and re-checks the delegation grant. Any inactive actor or revoked grant flipsactive=false, even on a cache hit. - AS metadata advertises
client_credentialsand the token-exchange URN ingrant_types_supportedwheneverConfig.AgentIdentityis 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:
OAuthServerStorageextended with agent + delegation methods. v1.0 rootStorageinterface unchanged. Both in-tree adapters (memory + postgres) implement the new methods. - Errors:
ErrAgentNotFound,ErrAgentInactive,ErrDelegationNotFound,ErrDelegationRevoked,ErrChainDepthExceeded,ErrSubjectTokenInvalid,ErrActorTokenInvalid,ErrNotImplemented,ErrAgentRequiresAS,ErrAgentChainDepthTooHigh.
- 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 interfaceOAuthServerStorage. Migrations 0011 (oauth_clients + authorization_codes + refresh_tokens) and 0014 (jwks_keys); 0012 / 0013 reserved as placeholders for phase 3 + 4.
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.
- 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_idsession 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/v1with RFC 7807 problem+json errors. - Fuzz tests, race-clean test suite, godoc with examples, finalized
STABILITY.md.
- RBAC:
permissions,roles,role_permissions,user_rolestables and the matchingStoragemethods.(*TheAuth).RequirePermissionmiddleware with a per-request cache.SeedPermissions,SeedOrganizationRoles,GrantRole,RevokeRole,CreateRole,UpdateRole,DeleteRole,PermissionsForUser,HasPermission. Twelve seeded permission constants plus the systemsuper_adminrole (granted out-of-band only). - Audit log:
audit_eventstable with org / actor / action DESC indexes.(*TheAuth).EmitAuditis non-blocking; the writer goroutine drains in batches and flushes onClosewith a configurable timeout.Statsexposes four atomic counters (AuditEmitted,AuditWritten,AuditDropped,AuditFailed).DefaultRedactormaskspassword / secret / token / code / refresh_token / access_tokenat any nesting depth. Twenty-plus canonical actions emitted across every state-changing handler in theauth. - Admin API: twelve endpoints under
/admin/v1(overridable viaAdminConfig.PathPrefix). Every endpoint requiresRequireAuthplus a catalogRequirePermission. Errors areapplication/problem+jsonper RFC 7807 with a stablecodeextension; keyset pagination on the audit read endpoint. (*TheAuth).Startspawns the writer goroutine;NewinvokesStartautomatically so existing callers do not change.Closedrains the writer with a default 5 second deadline.- New errors:
ErrAdminRequiresRBAC,ErrForbidden,ErrUnknownPermission,ErrRoleInUse,ErrNoActiveOrg,ErrOrgMismatch,ErrRBACDisabled. - New
adminsubpackage: RFC 7807Writehelper,Problemtype, keyset cursorEncodeCursor/DecodeCursorcodec, reserved problem code constants. - Migrations 0009 (rbac) and 0010 (audit) added under
storage/postgres/migrations/.
- Audit writes are async; backpressure drops events with
Stats.AuditDroppedincremented. Block-on-backpressure would degrade authentication latency under spikes; this is the documented choice. - Audit insert failures are not retried;
Stats.AuditFailedis 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.
Config.AuditHookand the synchronousAuditEventshape are removed. SetConfig.Audit = &AuditConfig{}to enable the async writer; existing no-op deployments leave it nil and see no behavior change.Storagegains 16 new methods (14 RBAC + 2 audit). Custom adapters built on top ofmemoryorpostgreskeep working; in-tree custom implementations must implement the new methods. Per the newStorage-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.
- SAML 2.0 Service Provider on top of
github.com/crewjam/samlv0.5.1. Per-connection IdP binding stored insaml_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 byexternalId. 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 leaveConfig.Organizationsnil and see no behavior change. - Migrations:
0006_organizations,0007_saml,0008_scim(additive onusers.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/samltestgenerates 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).
- 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/benchmeasuring password sign in, session lookup, OAuth callback storage cost, magic link consume, and PKCE challenge derivation. Baselines are recorded ininternal/bench/BASELINES.md. - Four new runnable example apps:
examples/gin-app,examples/echo-app,examples/stdlib-app, andexamples/oauth-multi-provider, each with a README, single-filemain.go,go.mod,docker-compose.yml,.env.example, andMakefile. - Package documentation:
doc.goadded for every package, doc comments filled in on previously undocumented exports, runnableExamplefunctions for the most-used entry points. STABILITY.mdenumerating the stable surface and the rules that govern future changes.golangci.ymlenablesgodotandgodoxto keep doc comments punctuated and to flag strayTODOmarkers.- CI workflow now runs a per-target fuzz job (
-fuzztime=10s) on every PR alongside the existing race-enabled test job.
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) togo.workso 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 rootgo.modbygo work sync. - #33 fix(security): cache bust on agent suspend/revoke (N1) closes
a v2.1 security re-audit finding:
SuspendAgentandRevokeAgentdid not calls.invalidate(cur.ClientID), so a revoked agent could authenticate via theclientauthcacheArgon2-verified snapshot for up to the 5-minute TTL added in PR #19. The sharedchangeAgentStatusnow invalidates whenever the new status is not active. Regression test:TestSuspendAgentBustsClientAuthCache.
- No new features. Hardening only.
- No public API changes. Existing callers compile and run unchanged.
WebAuthn passkeys, TOTP second factor with recovery codes, session
step-up via pending_2fa. See git history for the full set of changes.
Discord OAuth provider. PKCE-aware flow shared across all four providers.
GitHub, Google, and Microsoft OAuth providers. AES-256-GCM at-rest encryption for provider tokens. Per-IP rate limiter.
Email and password credentials with Argon2id. Password reset via single-use token.
Initial release: magic-link email auth, opaque session tokens with revocation, chi-friendly middleware, in-memory and Postgres storage adapters.