Releases: alex-savin/go-auth-x
Release list
v0.8.0 — opaque string entity IDs (uuid-friendly stores)
Breaking (issue #2): every public entity ID is now an opaque string across the store interfaces. AuthUser.ID, Group.ID, APIKeyInfo.ID, Passkey.ID, OrgInvite.ID, TokenClaim.UserID, SessionRecord.UserID and every uint id parameter/return on the six store interfaces are now string. The auth package never parses or does arithmetic on an ID, so a store keyed by uuid / ULID / KSUID passes its IDs straight through.
Migration
- Consumers of the reference stores: none.
gormstorekeeps itsuintprimary keys and converts to/from decimal strings at its boundary — no database migration, IDs render identically ("1","2", …).memorylikewise keepsuintkeys internally. - Custom store implementers: change your method signatures to the
stringid types (the compiler points at each one) and treat an unknown/unparseable id as the method's documented miss (ErrNoUser/ErrNoGroup/a no-op delete) rather than a real key.""is the reserved "no user" sentinel. SCIM membervalues and the{id}path segment now pass through verbatim.
Full details in CHANGELOG.md.
v0.7.0 — organizations: org-scoped resources + per-customer SCIM
The enterprise slice on top of the v0.6 org core — org-scoped groups & API keys unlocking per-customer SCIM.
- First-class invite records — org invites are now listable (
GET /auth/org/invites) and revocable (DELETE /auth/org/invites/{id}); re-inviting replaces the pending invite. Org + role bind to the stored record, so nothing in the accept link can be tampered with. - Org-scoped groups & API keys via the optional
OrgDirectoryStoreupgrade interface (type-asserted — stores that don't implement it keep compiling; the features just 501). Per-org group namespaces gated live byRequireOrgGroupsHTTP; org-bound keys refused by every global surface, honored only byValidateOrgAPIKeyScope. - Per-customer SCIM —
NewOrgScopedDirectory(dir, orgs, orgID)presents one org as aDirectoryStoreforscim.NewServer: subjects namespaced per org, no cross-tenant email adoption/rebind (SCIM 409 → use the invite flow), deprovision = org removal (never the global account; owners refused).
Security/hardening (adversarial review): closed a cross-tenant SCIM email-rebind account-takeover, refused org-bound keys at GateHTTP/empty RequireGroupsHTTP, and corrected SCIM owner-deprovision (403) and out-of-scope group (404) status codes.
Full details in CHANGELOG.md.
v0.6.0 — organizations: core (tenant/org layer)
A first-class organizations layer for B2B-shaped apps — a fourth optional capability store, additive (nil OrgStore = feature off, zero behavior change).
OrgStore(SetOrgStore) —Org+ per-org role memberships (owner/admin/member, app-extensible). Users stay global (one account, many orgs), so email uniqueness and the safe account-linking rule are untouched. Opaque string org IDs.- Active-org sessions — additive
org/orgRoleclaims carry the active org only; a sole membership auto-activates.POST /auth/org/switchre-verifies membership and re-mints (SID + remaining TTL preserved)./auth/me+/auth/configsurface org state. RequireOrgHTTP(roles…)— live re-verification per request, so off-boarding takes effect immediately, not at cookie expiry.- Email invites, self-service member management with a lock-serialized last-owner guard, and admin org CRUD (
/auth/admin/orgs, bypasses the guard as the recovery path). - GDPR parity —
DeleteUser/DeleteOrgcascade org memberships.
Full details in CHANGELOG.md.
v0.5.1
Follow-up fixes from an adversarial review of the merged v0.5.0. Each was verified against the code and has a regression test.
Fixed
- Lost
RecordSessionat login now fails the login. Sessions are recorded before the cookie is issued, so a transient store-write failure no longer mints a session the fail-closed revocation check rejects on the next request ("logged in, then instantly logged out"). Fixed at all four mint sites. adminSetBanwrites an audit record (ban/unban), matching the other admin verbs.- Passwordless (social/OIDC-only) users can delete-account / change-email. Step-up is now required only when the user has a re-authable factor (password/TOTP) — those users have neither, so the gate was a permanent lockout.
Security
Sec-Fetch-Site: cross-siteis refused on the CSRF-token-exempt login entry points (e.g./auth/email-otp/verify), closing a cross-site login-CSRF even with noTrustedOrigins/AppURLallow-list configured. Strictly additive (Fetch Metadata).
Changed
- The in-memory
SessionStoreprunes expired rows and is documented demo / single-process only — an unknown SID fails closed, so a restart (empty map) logs everyone out; usegormstorefor anything that restarts.
⚠️ Migration (from v0.5.0)
- A social-login-only deployment now enforces the gate + CSRF (
enforcing()counts social providers). Apps that made state-changingPOST /api/...calls without the double-submit token must now send theX-CSRF-Tokenheader.
Full details: CHANGELOG.md
v0.5.0
Self-service, session management, admin verbs, and a two-pass security hardening — features that fit a same-origin embedded Go BFF.
Features
- HIBP password-breach check (pluggable
BreachChecker, k-anonymity, fail-open, opt-in) SESSION_SECRETrotation via aPreviousSessionSecretsverify-only list- Email OTP (numeric sign-in code) alongside the magic link
- Optional
SessionStore— server-side revocation, list/revoke devices, sign-out-everywhere - Self-service delete-account, verified change-email, OAuth unlink + last-method guard, passkey rename
- Admin verbs — create / set-password / time-boxed ban / hard-delete / impersonate + list/revoke sessions
- Quick wins —
429 + Retry-After, IPv6/64rate-limit keying, trusted-origins allow-list
Hardening
- Microsoft/Entra multi-tenant email no longer trusted (nOAuth) — pin
MICROSOFT_TENANT - Social-login-only deployments still gate; session verify fails closed on a weak key
sanitizeNextrejects control bytes; SCIMCREATE/PUTreturns409 uniqueness; LDAP deprovision keys on presence- Hard-delete tombstones sessions; unknown SID fails closed; passkey-2FA must differ from the first factor
- bcrypt-72 cap;
PeekToken(validate reset before consuming); gorm/memory store parity
⚠️ Breaking
CredentialStore / DirectoryStore gain methods; new optional SessionStore; AuthUser gains ban fields. Both reference stores already implement them. See CHANGELOG for the full list.
Full details: CHANGELOG.md
v0.4.1 — /auth/me reports authEnabled and reads the session in local-only mode
Fixed
/auth/mereports auth as enabled and reads the session in local-only mode. TheMehandler branched on the OIDC-onlyEnabled()predicate, so a deployment with local methods on but OIDC off (SetLocalEnabled, no issuer) reportedauthEnabled:falseand never parsed the session cookie — frontends could not render the signed-in state or a logout control despite an active gated session.Menow treats local-enabled as auth-enabled and parses the session in that mode.
Full changelog: https://github.com/alex-savin/go-auth-x/blob/main/CHANGELOG.md
v0.4.0 — security-hardening audit pass
A full security-hardening pass from an audit of the library. go build, go vet, gofmt, and go test ./... (incl. -race) pass.
TwoFactorStore gains ClaimTOTPStep(userID uint, step uint64) (bool, error) — an atomic TOTP replay guard. Both reference stores are updated; custom implementations must add it.
Security
- Fail closed on a weak/absent
SESSION_SECRET— signing refuses a key <32 bytes (an unset secret previously signed cookies with an empty, publicly known HMAC key); HS256 pinned viaWithValidMethods. - Account-linking invariant enforced on the primary OIDC callback — rejects an unverified email like the social path; both stores require the incoming login to prove the email before rebinding a verified/bootstrap row; gormstore squatter-reclaim is now atomic.
- Sign in with Apple no longer blocked by CSRF —
/auth/social/*/callback(Appleform_post) is CSRF-exempt (guarded by the OAuthstate). - 2FA brute-force throttling — limiters install with a
TwoFactorStore;/auth/reauthand/auth/2fa/disableare rate-limited. - Atomic TOTP replay guard (
ClaimTOTPStep) + monotonic WebAuthn sign counts; disabled-user checks on 2FA completion. AssumeVerifiedonly fills an absentemail_verified; recovery codes widened to 80 bits.- Request body-size limits (incl. SCIM
/Bulk) + SCIM filter depth cap; open-redirect backslash fix. - SCIM —
activedefaults true, strongIf-Match, non-leaking errors, duplicate-group 409, PATCH group replace/valuePath-remove. - LDAP — paged search, honor
InsecureTLSforldaps://, DN normalization. - Email/SMTP — HTML-escaped URLs, CRLF header sanitization, magic/reset sends off the request path (anti-enumeration timing).
Added
Config.BrandName/BRAND_NAME— email + WebAuthn RP branding (replaces the hardcoded app name).- Transparent bcrypt rehash-on-login.
Authenticator.Close()stops the built-in rate-limiter GC goroutine.
Full details in CHANGELOG.md.
v0.3.0 — Microsoft/Entra + Discord + any-OIDC social, richer SCIM
go-auth-x v0.3.0
More social providers and a more conformant SCIM server. Additive at the public API — no removals or renames.
✨ Highlights
Social login: Microsoft/Entra, Discord, and any OIDC provider
- Microsoft / Entra ID (Azure AD) — work, school, and personal accounts; tenant-scoped or
common. Entra tokens often omitemail_verified, so the tenant-owned token email is trusted by default (MICROSOFT_STRICT_EMAIL_VERIFIEDforces strict). - Discord — OAuth2 + REST (
/users/@me), verified email required. - Any OIDC provider —
Config.SocialOIDCregisters GitLab, Okta, Auth0, Keycloak, or any compliant IdP as a social login at/auth/social/<name>/{login,callback}, reusing Google's PKCE +nonce+azp+email_verifiedpath (per-providerAssumeVerifiedopt-in). GET /auth/confignow reports every enabled provider in asocialProvidersarray so the SPA can render the right buttons. The route gate and config report now also cover Facebook and Apple.- New env:
MICROSOFT_CLIENT_ID/MICROSOFT_CLIENT_SECRET/MICROSOFT_TENANT,DISCORD_CLIENT_ID/DISCORD_CLIENT_SECRET.
SCIM 2.0 — more conformant
- Pagination —
startIndex/counton the Users and Groups list endpoints (RFC 7644 §3.4.2.4), with accuratetotalResults/startIndex/itemsPerPage(count=0is a valid count-only query). - Resource timestamps —
meta.created/meta.lastModifiedon Users and Groups (surfaced from the store; omitted when unavailable). - Full schema documents —
GET /Schemasreturns a ListResponse of complete User + Group schema definitions (attributes, types, mutability, uniqueness), plusGET /Schemas/{id}(RFC 7643 §7 / RFC 7644 §4).
🔎 Verified
New tests cover SCIM pagination/meta/schemas and the social routing/registry guards, plus an end-to-end mock-IdP integration test (real OIDC discovery + JWKS + RS256 verify) that confirms the generic-OIDC path rejects unverified emails, nonce mismatches, azp mismatches, and foreign signatures.
Upgrade
go get github.com/alex-savin/go-auth-x@v0.3.0Source-compatible with v0.2.x. AuthUser gains CreatedAt/UpdatedAt and Group gains UpdatedAt (additive fields for SCIM meta); Config gains the Microsoft/Discord/SocialOIDC fields.
Full changelog: https://github.com/alex-savin/go-auth-x/blob/main/CHANGELOG.md
v0.2.0 — 2FA, Apple + Facebook, richer SCIM, RFC-compliance hardening
go-auth-x v0.2.0
A big release: two-factor auth, two more social providers, a much richer SCIM server, and a full RFC-compliance hardening pass. All additive at the public-API level (no removals/renames); a few security-hardening behavior changes are noted below.
✨ Highlights
Two-factor auth (2FA) — no third-party dependency
- TOTP (RFC 4226/6238) implemented in the standard library only (verified against the published RFC test vectors) — the frontend renders the QR from an
otpauth://URI. - Recovery codes — single-use, sha256-at-rest.
- Login enforcement — when 2FA is on, the first factor sets a short-lived signed
2fa_pendingcookie instead of the session;POST /auth/2fa/verify(a TOTP or recovery code) finishes it. Replay-guarded and rate-limited. - Passkey as a second factor — complete the second step with a registered passkey (
/auth/2fa/webauthn/begin+/finish) instead of a code. - Step-up / re-auth —
RequireStepUpHTTP(maxAge)gates sensitive routes;POST /auth/reauth(password or a 2FA code) sets a short-lived step-up cookie. - Wire the optional
TwoFactorStore(gormstore + memory reference impls) withSetTwoFactorStore.
Social login: Apple + Facebook
- Sign in with Apple — the library signs the ES256 client-secret JWT from your
.p8and handles Apple'sform_postcallback (SameSite=Noneflow cookie; HTTPS required). - Facebook — Graph API
/mewith anappsecret_proof. - Both follow the same verified-email account-linking rule as Google/GitHub.
SCIM 2.0 — much more conformant
- Filters:
eq/ne/co/sw/ew/gt/ge/lt/le/prwithand/or/not, parentheses, and valuePath (emails[type eq "work"]). - Sorting (
sortBy/sortOrder), strong ETags (If-Match/If-None-Match),Location/meta.location(setSetBaseURL),uniquenessscimType, PATCHnoTarget/op validation, and the/Bulkendpoint.
🔒 Security / compliance (RFC audit)
An audit against ~27 RFCs/specs found and this release fixes 11 MUST-level gaps — all in caller-owned wiring, not the delegated crypto:
- JWT token-audience segregation — session / oauth-flow / 2fa-pending / webauthn tokens now carry a distinct validated
aud, closing a path where a2fa_pendingtoken could be replayed as a session (2FA bypass). (RFC 7519) - WebAuthn UserVerification is now Required — a passwordless passkey is a real multi-factor credential, not a bare possession factor. (WebAuthn L2)
randTokenfails closed on acrypto/randerror instead of emitting a guessable state/CSRF/API-key token. (RFC 6749)- TOTP past-leaning validation window + rate-limited verify. (RFC 4226/6238)
- LDAP StartTLS sets
ServerNameand refuses a cleartext bind. (RFC 4513) - OIDC validates
azp. (OIDC Core) - Bearer admin guard returns
401 + WWW-Authenticate/403 insufficient_scope. (RFC 6750) - SCIM strong ETags for If-Match,
Location/uniqueness/PATCH validation. (RFC 7644 / 9110)
Upgrade
go get github.com/alex-savin/go-auth-x@v0.2.0Public API is source-compatible with v0.1.x. Behavior changes to be aware of: WebAuthn logins now require user verification (a UV-capable authenticator); the admin Bearer guard returns 401 (not 403) for an invalid/missing key; SCIM ETags are now strong (no W/ prefix).
Full changelog: https://github.com/alex-savin/go-auth-x/blob/main/CHANGELOG.md
v0.1.1 — Enforce SESSION_SECRET ≥ 32 bytes (security hardening)
go-auth-x v0.1.1
A small security + docs hardening release. No API changes.
Security
SESSION_SECRET≥ 32 bytes is now enforced at boot.New()fails closed when auth will mint cookies (OIDC configured, or a secret supplied for local/social methods) and the secret is shorter than 32 bytes — the guarantee the README already described but the code didn't enforce (it only required a non-empty secret). A short, weak HMAC key is now rejected at construction instead of silently signing every session/flow/CSRF cookie.
Fixed
- Corrected the passkey
rpIDdocumentation: it's derived from the configured app origin (override viaWEBAUTHN_RPID), not the requestHostheader — so it can't be spoofed. The prior "never host-inferred" wording was inaccurate.
Upgrade
go get github.com/alex-savin/go-auth-x@v0.1.1Drop-in for v0.1.0. The one behavioral change: a SESSION_SECRET shorter than 32 bytes — or a missing secret when OIDC is configured — now returns an error from New() instead of being accepted. Set a 32+ byte secret (e.g. openssl rand -base64 32).
Full changelog: https://github.com/alex-savin/go-auth-x/blob/main/CHANGELOG.md