Skip to content

Latest commit

 

History

History
964 lines (817 loc) · 50.2 KB

File metadata and controls

964 lines (817 loc) · 50.2 KB

Changelog

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.

[Unreleased]

[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:

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

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

Performance (PR #19)

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