Skip to content

Releases: alex-savin/go-auth-x

v0.8.0 — opaque string entity IDs (uuid-friendly stores)

Choose a tag to compare

@alex-savin alex-savin released this 16 Jul 19:30

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. gormstore keeps its uint primary keys and converts to/from decimal strings at its boundary — no database migration, IDs render identically ("1", "2", …). memory likewise keeps uint keys internally.
  • Custom store implementers: change your method signatures to the string id 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 member values and the {id} path segment now pass through verbatim.

Full details in CHANGELOG.md.

v0.7.0 — organizations: org-scoped resources + per-customer SCIM

Choose a tag to compare

@alex-savin alex-savin released this 16 Jul 19:30

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 OrgDirectoryStore upgrade interface (type-asserted — stores that don't implement it keep compiling; the features just 501). Per-org group namespaces gated live by RequireOrgGroupsHTTP; org-bound keys refused by every global surface, honored only by ValidateOrgAPIKeyScope.
  • Per-customer SCIMNewOrgScopedDirectory(dir, orgs, orgID) presents one org as a DirectoryStore for scim.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)

Choose a tag to compare

@alex-savin alex-savin released this 16 Jul 19:30

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/orgRole claims carry the active org only; a sole membership auto-activates. POST /auth/org/switch re-verifies membership and re-mints (SID + remaining TTL preserved). /auth/me + /auth/config surface 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 parityDeleteUser/DeleteOrg cascade org memberships.

Full details in CHANGELOG.md.

v0.5.1

Choose a tag to compare

@alex-savin alex-savin released this 15 Jul 06:12

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 RecordSession at 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.
  • adminSetBan writes 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-site is refused on the CSRF-token-exempt login entry points (e.g. /auth/email-otp/verify), closing a cross-site login-CSRF even with no TrustedOrigins/AppURL allow-list configured. Strictly additive (Fetch Metadata).

Changed

  • The in-memory SessionStore prunes expired rows and is documented demo / single-process only — an unknown SID fails closed, so a restart (empty map) logs everyone out; use gormstore for 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-changing POST /api/... calls without the double-submit token must now send the X-CSRF-Token header.

Full details: CHANGELOG.md

v0.5.0

Choose a tag to compare

@alex-savin alex-savin released this 15 Jul 06:12

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_SECRET rotation via a PreviousSessionSecrets verify-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 wins429 + Retry-After, IPv6 /64 rate-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
  • sanitizeNext rejects control bytes; SCIM CREATE/PUT returns 409 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

Choose a tag to compare

@alex-savin alex-savin released this 02 Jul 19:30

Fixed

  • /auth/me reports auth as enabled and reads the session in local-only mode. The Me handler branched on the OIDC-only Enabled() predicate, so a deployment with local methods on but OIDC off (SetLocalEnabled, no issuer) reported authEnabled:false and never parsed the session cookie — frontends could not render the signed-in state or a logout control despite an active gated session. Me now 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

Choose a tag to compare

@alex-savin alex-savin released this 02 Jul 04:40

A full security-hardening pass from an audit of the library. go build, go vet, gofmt, and go test ./... (incl. -race) pass.

⚠️ Breaking: 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 via WithValidMethods.
  • 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 (Apple form_post) is CSRF-exempt (guarded by the OAuth state).
  • 2FA brute-force throttling — limiters install with a TwoFactorStore; /auth/reauth and /auth/2fa/disable are rate-limited.
  • Atomic TOTP replay guard (ClaimTOTPStep) + monotonic WebAuthn sign counts; disabled-user checks on 2FA completion.
  • AssumeVerified only fills an absent email_verified; recovery codes widened to 80 bits.
  • Request body-size limits (incl. SCIM /Bulk) + SCIM filter depth cap; open-redirect backslash fix.
  • SCIMactive defaults true, strong If-Match, non-leaking errors, duplicate-group 409, PATCH group replace/valuePath-remove.
  • LDAP — paged search, honor InsecureTLS for ldaps://, 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

Choose a tag to compare

@alex-savin alex-savin released this 01 Jul 04:47

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 omit email_verified, so the tenant-owned token email is trusted by default (MICROSOFT_STRICT_EMAIL_VERIFIED forces strict).
  • Discord — OAuth2 + REST (/users/@me), verified email required.
  • Any OIDC providerConfig.SocialOIDC registers 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_verified path (per-provider AssumeVerified opt-in).
  • GET /auth/config now reports every enabled provider in a socialProviders array 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

  • PaginationstartIndex / count on the Users and Groups list endpoints (RFC 7644 §3.4.2.4), with accurate totalResults / startIndex / itemsPerPage (count=0 is a valid count-only query).
  • Resource timestampsmeta.created / meta.lastModified on Users and Groups (surfaced from the store; omitted when unavailable).
  • Full schema documentsGET /Schemas returns a ListResponse of complete User + Group schema definitions (attributes, types, mutability, uniqueness), plus GET /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.0

Source-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

Choose a tag to compare

@alex-savin alex-savin released this 01 Jul 03:56

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_pending cookie 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-authRequireStepUpHTTP(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) with SetTwoFactorStore.

Social login: Apple + Facebook

  • Sign in with Apple — the library signs the ES256 client-secret JWT from your .p8 and handles Apple's form_post callback (SameSite=None flow cookie; HTTPS required).
  • Facebook — Graph API /me with an appsecret_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/pr with and/or/not, parentheses, and valuePath (emails[type eq "work"]).
  • Sorting (sortBy/sortOrder), strong ETags (If-Match/If-None-Match), Location/meta.location (set SetBaseURL), uniqueness scimType, PATCH noTarget/op validation, and the /Bulk endpoint.

🔒 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 a 2fa_pending token 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)
  • randToken fails closed on a crypto/rand error 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 ServerName and 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.0

Public 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)

Choose a tag to compare

@alex-savin alex-savin released this 30 Jun 22:18

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 rpID documentation: it's derived from the configured app origin (override via WEBAUTHN_RPID), not the request Host header — 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.1

Drop-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