v0.14.0¶
Release Date: 2026-10-11
A breaking release with five themes. Principal is the sole identity
root: the legacy User model and its tables are gone, so password login,
social login, federation sync, and SCIM all resolve a person to the same
principal (ADR-004). Managed principals let an application create humans
without an email, such as young learners, whom guardian and educator
managers control and sign in with QR-friendly login tokens (ADR-005), and
management is now derived from manager relationships (ADR-009). A
security pass makes the marketplace, organization, and invite APIs act
for the authenticated caller and check organization permissions, instead of
trusting the IDs a request names. The organization model is completed:
organizations without a plan, signup on first login, slugs, application
roles and entitlements carried in an orgs claim, collaborators and
resource grants (ADR-006 to ADR-008), and real-time invalidation of access
changes (ADR-010). Finally, the authorization modes are named rbac and
rebac and chosen by configuration.
This release covers everything since v0.13.0. Read the upgrade notes before deploying; they are in two parts, one for the identity and security changes and one for the organization model.
Highlights¶
- One identity model —
Principal(type=human)+Humaneverywhere;systemauth.Storageworks onHumanPrincipal. - Managed principals — opt-in per deployment and per client;
guardianandeducatormanagers; login tokens, a rate-limited exchange endpoint, manager visibility and revocation, guardian claims, and attaching an email later. See Managed Principals. - Caller-bound, authorized APIs — the marketplace, organization, and invite APIs identify the caller through
requestprincipaland check organization permissions; without an authorizer they refuse rather than trust the request. - Provider parity — the simple and SpiceDB providers grant identical organization and manager permissions, enforced by a parity test against the base schema and a shared conformance suite.
- AI agents at resource servers —
identity/requestprincipalwith an AAuth authenticator (aauthrs) next to bearer, API-key, and BFF-session authentication. - No organization plan —
sf_organizations.planandorganization.Plan*are gone; organization responses carryentitlementsderived from the marketplace. See Application roles and entitlements. - Slugs — reserved names, rename with a 7-day cooling-off period, aliases for old links, and personal-to-team conversion. See Organization slugs and Converting a personal organization to a team.
- Application roles — assigned per principal, organization and application, limited to entitled applications and seat limits, and granted by invites. See Invites.
orgsclaim and membership sources —ClaimsMembershipsis the default relying-party source;RemoteMembershipsreadsGET /oauth/userinfo/memberships.- Real-time invalidation — access tokens default to 10 minutes (at most 15), every access-reducing change is recorded in
sf_access_changes, and relying parties refuse earlier tokens; polling, replay and back-channel delivery cover replicas and separate deployments. See Access-change invalidation. - Collaborators and resource grants —
viewer,contributorandmaintaineron one resource, inrbacandrebac, and invites that target a resource. See Collaborators and resource grants. - Authorization mode as configuration — the
authzkey andauthz/mode.New. See Configuring the authorization mode. - Management is a relationship — release, manager invitations (accept or decline), authorized managing operations and deletion.
Security¶
These fix vulnerabilities present in v0.13.0. Upgrade promptly.
- Marketplace checkout took the purchaser from the request body, so anyone could start a purchase in another principal's name for any organization.
- Marketplace listing, license, and seat endpoints acted on any IDs a request named.
- The organization API listed every organization and let any caller read, change, or delete organizations, change members, and transfer ownership.
- Invite management needed no permission, and anyone holding an invite link could accept it as any principal.
- Invite tokens were stored in plain text; they are now SHA-256 hashes.
- SpiceDB principal and organization cleanup ignored failures and missed editor and manager relations, leaving access behind while reporting success.
- Refresh tokens could be used more than once, and refreshing near expiry extended a session indefinitely; rotation is now atomic, a session keeps its original expiry, and reusing a rotated refresh token revokes the whole token family.
- Stripe checkout granted a license before a delayed payment cleared, an invalid seat count produced an unlimited seat-based license, and cancelling a subscription left its license in place.
- Deactivating a principal did not stop its tokens: access tokens stayed valid and refresh tokens kept issuing new ones for up to 30 days. Tokens of an inactive principal are now refused wherever they are used or refreshed.
- Agent delegation failed open: revoking a delegation gave the agent unrestricted access, constraints that did not overlap allowed everything, and expiry and action limits were never stored.
- Revoking platform administration through
principal.UpdateHumanwas not synced to SpiceDB. - Signing up with an invite accepted any email, and
SignupOrGetExistingreturned existing accounts for unverified emails. - Anyone who could reach SystemAuth's authorization endpoint, or
identity/oauth's, could obtain tokens for any user by sending anX-User-IDheader. Header identity is now an explicit, gated opt-in, andidentity/oauthalso requires consent for third-party clients. - Rate limits, BFF sessions, and API key usage records believed client-supplied
X-Forwarded-For,X-Real-IP, and Cloudflare headers, so a client could evade a limit or forge its address. They now key on the direct peer and believe forwarding headers only from proxies you configure. - SCIM listings skipped authorization, and a SCIM API mounted without a hook was open; the federation identity sync endpoint trusted its request body from anyone; token introspection accepted any token or public client as the caller; and the default storage accepted replayed client assertions.
- API keys could be managed by ID alone and granted any scope when no allow-list was set; the BFF proxy could forward the browser's session cookie upstream; concurrent logins could exceed the lockout limit; and
multiapptrusted theX-App-IDheader. - BFF cookies could lose
SecureandHttpOnly; cookie-authenticated writes skipped the same-origin check; JWTs withoutexpwere accepted; client secret checks revealed which client IDs exist; andmultiappcould leave one application's schema on a pooled connection.
Upgrade Notes¶
1. Database¶
Run these against an existing PostgreSQL database; a fresh database needs
only systemauth migrate.
| Script | When | What |
|---|---|---|
migrations/0002_drop_legacy_user_tables.sql |
Before starting this release | Creates the system service principal, reassigns OAuth apps owned by legacy users, drops the legacy tables |
migrations/0003_hash_invite_tokens.sql |
When deploying | Hashes stored invite tokens so outstanding invite links keep working |
migrations/0004_drop_organization_plan.sql |
When deploying | Drops sf_organizations.plan |
migrations/0005_subscriptions_per_listing.sql |
When deploying | Adds listing_id to subscriptions and allows one subscription per listing beside the platform subscription |
migrations/0006_invite_app_roles.sql |
When deploying | Adds sf_invites.app_roles |
migrations/0007_oauth_app_backchannel_logout_uri.sql |
When deploying | Adds sf_oauth_apps.backchannel_logout_uri |
migrations/0008_drop_editor_viewer_roles.sql |
When deploying | Maps editor to member and viewer to collaborator in memberships and pending invites, recording an access change for each active membership it changes |
migrations/0009_invite_resource_target.sql |
When deploying | Adds the resource target columns to sf_invites |
Run them in order; each is safe to run more than once. Then run
systemauth migrate (or systemauthsvc.Service.Migrate) for the new tables and columns:
sf_manager_relationships, sf_manager_claims, sf_client_assertion_jtis,
the login_token credential type, and OAuthApp.managed_principals_enabled
and login_token_ttl.
The organization model adds further tables, created by the same command: sf_app_roles, sf_slug_reservations, sf_access_changes, sf_resource_grants and sf_manager_invites.
2. Configure authorization for the HTTP APIs¶
The marketplace, organization, and invite APIs now require an
authenticated caller, set by requestprincipal.Middleware, and an
authorizer. Until the authorizer is configured, organization-scoped
endpoints answer 501.
orgs := organization.NewService(db)
authorizer := rbac.New(
rbac.WithRoleGetter(organization.MembershipRoleGetter(orgs)),
rbac.WithPlatformAdminChecker(isPlatformAdmin), // optional
)
orgCfg := organization.DefaultAPIConfig()
orgCfg.Authorizer = authorizer
orgAPI := organization.NewAPI(orgs, orgCfg)
inviteCfg := invite.DefaultAPIConfig()
inviteCfg.Authorizer = authorizer
inviteAPI := invite.NewAPI(invite.NewService(db, baseURL), orgs, inviteCfg)
marketplace.NewAPI(humaAPI, marketplace.APIConfig{}, svc,
marketplace.WithAuthorizer(authorizer))
// Mount each behind requestprincipal.Middleware so requests carry the caller.
With SpiceDB, pass the SpiceDB provider instead. MembershipRoleGetter
counts only active memberships; a role getter built on GetMembership
directly would also count deactivated ones.
3. Client changes¶
- Request fields that named the acting principal are optional and deprecated, and when sent must equal the caller: marketplace
purchaser_id,owner_id,assigned_by; inviteinviter_principal_idandprincipal_id; theemailquery on pending invites. - Accepting or declining an invite requires the caller's email to match the invite.
- Listing organizations returns the caller's organizations (all of them for platform administrators).
- Creating an organization makes the caller its owner.
- Unpublished marketplace listings are visible only to the creator organization.
4. Go API changes¶
- Removed Ent schemas and packages:
user,membership,oauthaccount,refreshtoken,apikey,serviceaccount,serviceaccountkeypair, andmixin.UserBase/mixin.MembershipBase. systemauth.Storageuser methods becameCreateHuman,GetHuman,GetHumanByEmail,GetHumanByExternalIdentity,LinkExternalIdentity,UpdateHuman,DeleteHumanonHumanPrincipal;systemauth.UserandErrUserNotFound/ErrUserExistsare replaced byHumanPrincipal,ErrPrincipalNotFound,ErrPrincipalExists.- OAuth tokens and authorization codes use the
subjectcolumn;OAuthConsent.user_idisprincipal_id;OAuthApp.ownerreferencesPrincipal. Human.emailis optional;HumanPrincipal.Email,principal.Human.Email, and contract identity types may be empty for a managed principal. Existing creation paths still require an email.invite.Invitehas noTokenfield; the token is only inInviteResult.InviteURL.- The
authzproviders are named for the mode they evaluate:authz/simpleis nowauthz/rbac(role-based: roles plus direct grants), andauthz/spicedbis nowauthz/rebac/spicedb(relationship-based, with SpiceDB as the engine). The old paths remain as deprecated forwarding packages for this release; change imports to the new paths, andstaticcheckreports any you miss. spicedb.BaseSchemaequalsauthz.BaseSpiceDBSchemaand is deprecated: members holdcontribute, notedit, on an organization, and editors are recognized.- Interfaces gained methods:
authz.RelationshipSyncer(AddPrincipalManager,RemovePrincipalManager) andcredential.Service(IssueLoginToken,VerifyLoginToken). organization.ServiceListMembersandListMembershipsexclude inactive memberships unlessIncludeInactiveis passed.identity/tokenreturns typed errors:ErrInvalidAccessToken,ErrInvalidRefreshToken,ErrRefreshTokenReused,ErrDPoPMismatch,ErrScopeNotGranted.marketplace.LicenseServicegainsGetByStripeSubscriptionID.identity/delegationreturns typed errors,LinkgainsActive, and an agent without a delegation is denied.signup.SignupOrGetExistingreturns an existing account only whenSignupInput.EmailVerifiedis set;SignupWithInviterequires the invited email.apikey.ServiceGet,Revoke, andDeletetake the owning principal first; useRevokeInOrganizationfor an administrator of an organization-scoped key. A service with noAllowedScopesrefuses keys that request scopes unlessAllowAnyScopeis set, andMaxKeysPerUseris enforced.rls.Helper.SetTenantandSetUserreturnErrNotInTransactionfor*sql.DBand*sql.Conn, becauseSET LOCALoutside a transaction scopes nothing.rls.Middleware.SetRLSContextonly puts the caller on the request context; run queries throughMiddleware.WithTxorrls.WithTenant.principallookups returnprincipal.ErrNotFound; type-specific updates returnErrWrongType;CreateAgentreturnsErrInvalidDelegator.
5. Behavior changes¶
- The default BFF session cookie is
sf_session(wascf_session); browsers holding the old cookie sign in again. - SCIM users are human principals; users provisioned before this release are not visible through SCIM. Managed principals are never SCIM users.
6. Stripe webhooks¶
Subscribe your Stripe webhook endpoint to
checkout.session.async_payment_succeeded in addition to
checkout.session.completed: licenses for delayed payment methods are now
granted when the payment clears, not when checkout completes. Subscribing
to customer.subscription.deleted makes cancellation revoke the license.
7. User authentication for the authorization endpoint¶
systemauth.WithUserIDHeader is removed, and DefaultSessionProvider no
longer believes any header on its own: it sends every authorization request
to the login URL. A deployment that relied on a gateway setting X-User-ID
must say so, and say which requests may be believed:
gate, err := systemauth.TrustedProxyNetworks("10.0.0.0/8") // the gateway's addresses
if err != nil { return err }
provider := systemauth.NewDefaultSessionProvider(
systemauth.WithTrustedUserIDHeader("X-User-ID", gate),
)
Use systemauth.TrustAllRequests only when the server is reachable solely
through a gateway that overwrites the header; the gate judges the direct
peer address, never X-Forwarded-For. Social login and custom
SessionProviders need no change. identity/oauth takes
oauth.WithUserAuthenticator (or WithTrustedUserIDHeader) and
WithConsent; without them its authorization endpoint answers
server_error or access_denied.
8. Proxies, SCIM, federation, and introspection¶
- Behind a reverse proxy, tell SystemForge which proxies to believe, or every client will share the proxy's address:
ratelimit.ForwardedIPKey("10.0.0.0/8")for limits,bff.ClientIPConfig{TrustProxy: true, TrustedProxies: ...}for sessions (andCloudflareIPRangesfor Cloudflare),systemauth.WithLoginTokenClientIP(...), and anIPExtractorfor the API key middleware. - SCIM now answers 403 until you pass
scim.WithAuthorizationHook(for examplescim.NewScopedAuthorizationHook(true)). - Federation: pass
systemauth.WithFederationAuthenticator(systemauth.SharedSecretAuthenticator(secret))toNewFederationEndpoints, or the sync and lookup routes are not registered. - Introspection: resource servers must authenticate as a confidential client or present a token carrying the
introspectscope. - Rate limiting fails closed: a storage outage answers 503. Pass
ratelimit.WithFailOpen()to keep the old behavior. - Tokens:
session/jwtand the contract middleware requireexp; usejwt.ValidateAccessTokenForAudienceormiddleware.HTTPAuthForAudienceon routes that serve one kind of client. - Lockout: call
Lockout.Beginbefore verifying a password andAttempt.SuccessorFailureafter;CheckAndRecordis deprecated. - multiapp apps that share a signing key should set
MiddlewareConfig.AppAudiencesand mint tokens withjwt.TokenOptions{Audience: ...}; apps with their own keys need nothing. - multiapp:
AppAwareDB.ForSchemareturns an error;SchemaDB.AcquireConnis deprecated in favor ofBegin.
9. Managed principals (optional)¶
Nothing changes unless you opt in. To enable, set managed_principals.enabled
in the SystemAuth configuration and managed_principals: true on each client
that may use them, then wire identity/managedprincipal. If you use the
marketplace, add stripe.WithPurchaserChecker(managedPrincipalService) so
managed principals cannot purchase.
Organization model¶
The next sections cover the organization model; they apply to every deployment.
10. Organization roles and the plan¶
editorandviewerare no longer organization roles. The roles areowner,admin,memberandcollaborator; any other value is refused withErrInvalidRole. Replace them in your own tables, vocabularies and invites; use resource roles for access to one resource.- Remove
Planfrom organization inputs and filters, and from API requests. SupplyAPIConfig.Entitlementswithmarketplace.EntitlementSourceto returnentitlements.
11. Entitlements fail closed¶
An approle.Service with no entitlement source entitles no organization to
any application. Build it with marketplace.ApproleOptions(src), and declare
applications free to everyone with marketplace.WithDefaultEntitlements.
approle.EntitleAll is for development and tests. marketplace.WarnIfNoEntitlements
logs a startup warning when nothing entitles anyone.
12. Relying parties read claims¶
The default membership source is ClaimsMemberships, which decodes the
orgs claim. LocalMemberships is no longer picked up implicitly; pass it
explicitly for development and tests. Use ClaimsOrRemoteMemberships when
the claim may be truncated or absent.
13. Token lifetime and revocation¶
The default access token lifetime is 10 minutes (it was 15), a configured
value above 15 minutes is refused at load, and per-client values are capped.
Pass a relyingparty.RevocationCache to BearerMiddleware and NewBFF, and
feed it as described in Access-change invalidation.
14. Authorization schema and packages¶
- The marketplace SpiceDB fragment attaches to the base organization through a
marketplace_orgfacet and no longer redefinesprincipalororganization. Compose schemas withMergeSchema, or letauthz/mode.Newcompose and write them. - Build the provider from configuration with
authz/mode.New. Theauthz/simpleandauthz/spicedbforwarding packages (renamed in this release) remain deprecated and will be removed in the next major release; importauthz/rbacandauthz/rebac/spicedb. - Switching to
rebacstarts with an empty graph; write existing memberships, application roles and grants through the syncer before serving requests.
New Features¶
identity/managedprincipal:Create,AddManager,RemoveManager,ListManagers,ManagerRoles,IsManaged,IssueLoginToken,ListLoginTokens,RevokeLoginToken,ListSessions,RevokeSession,RevokeAll,IssueClaim,RedeemClaim,AttachVerifiedEmail, andNewHandler.- SystemAuth:
POST /login-token, themanagedclaim,Server.ManagedPrincipalsAllowed,Server.LoginTokenPolicy,WithCredentialService,WithLoginTokenRateLimit. identity/credential: login tokens andLoginTokenPolicy.authz:OrgRolePermissions,PermOrg*,ManagerRolePermissions,PermManage*,NewPrincipalResource;rbac.WithManagerRoleGetter;providertest.RunOrgPermissionTestsandRunManagerRelationTests;spicedb.Client.DeleteRelationships(now underauthz/rebac/spicedb).identity/requestprincipal:RequestPrincipal, authenticator chain and middleware,PrincipalUUID;aauthrsfor AAuth agents;relyingparty.NewRequestAuthenticator.marketplace:WithAuthorizer,WithPurchaseAuthorizer,OrgRolePurchaseAuthorizer,WithCallerResolver,PurchaserChecker;stripe.WithPurchaserChecker.organization:MembershipRoleGetter,ErrMembershipNotFound,ListOrganizationsInput.MemberPrincipalID.identity/pgschema(PostgreSQLlower(email)unique index),identity/clientassertionJWT ID replay protection, andstore.Config.TrustProvisionedEmailfor SCIM.- Signup on first social login (
signup.SlugForLogin,CreatePersonalOrgInTx,EnsurePersonalOrgInTx); reserved slugs (organization.ReservedSlugs,WithReservedSlugs);Rename,ResolveSlug,WithSlugCoolingOff;ConvertToTeam; personal organization rules (ErrPersonalOrganization). identity/approle:Assign,Change,Remove,RemoveForMember,EffectiveRoles, endpoints under/organizations/{slug}/members/{principal_id}/app-roles, andauthz.AppRoleSyncer.marketplace:EntitlementSource,ApproleOptions,WithDefaultEntitlements,WarnIfNoEntitlements; listings may name an application and subscriptions may be per listing.systemauth: theorgsclaim,GET /oauth/userinfo/memberships,GET /access-changes,NewBackchannel, and thebackchannel_logout_uriclient setting.identity/accesschange,relyingparty.RevocationCache,NewAccessChangePoller,Replay,BackchannelHandler; relying-party membership sourcesClaimsMemberships,RemoteMemberships,ClaimsOrRemoteMemberships,ProvisioningMemberships.identity/resourcegrant, the resource-role ladder andauthz.ResourceGranter;rbac.WithResourceGrants; resource-targeted invites.authz.ModeConfig,authz/mode.Newandsystemauth.Config.Authz.identity/managedprincipal:Release, manager invitations with accept and decline,Delete, and the managing operations over HTTP.
Fixes¶
- The marketplace, organization, and invite APIs panicked at registration.
- The simple provider denied every organization permission check.
- SCIM listing panicked on every ascending sort.
identity/oauth.NewProviderpanicked on construction, and token revocation had no effect.private_key_jwtclient authentication failed under foreign-key enforcement.- A managed principal's first relying-party sign-in failed; empty emails could be matched or adopted; invite emails were compared case-sensitively; organizations created without a plan failed.
- Accepting an invite, and social login signup, register memberships, principals and organizations with the authorization graph.
- Signup creates the principal and personal organization in one transaction.
- The application-role seat limit cannot be exceeded by concurrent assignments, and assigning with no seat left answers
409. - Manager authority checks for
AddManager,RemoveManagerandReleaserun inside the row-locked transaction, a managed principal is never a valid manager, and access-change reasons are accurate.
The complete list is in the changelog.
Documentation¶
- Managed Principals, and updates to Users, Account Security, and authorization conventions.
- ADRs live in
docs/design/adrs/, with an index of statuses and a Design section in the docs nav. - Architecture pages describe the implemented behavior: Organizations and Membership, Managed Principals and Authorization.
- New guides: Invites, Application roles and entitlements, Collaborators and resource grants, Organization slugs, Personal to team conversion, Access-change invalidation and Authorization mode.
- Design of record: ADR-004, ADR-005, ADR-006, ADR-007, ADR-008 (with guidance on choosing
rbacorrebac), ADR-009 and ADR-010. - Feature specifications moved from
docs/design/todocs/specs/.
What's Next¶
Verified email for SystemAuth through omnimail (INIT-SYSTEMFORGE-006), which also drives the managed-principal attach-email flow.