Skip to content

v0.26.0

Release Date: 2026-10-28

Two additions that let every application on SystemForge authenticate the same way, with no static API keys:

  • identity/appauth assembles the relying party for an application in a few lines, with the same defaults everywhere.
  • Service accounts give automation its own identity in an organization, with short-lived DPoP-bound tokens it gets by signing with a private key.

It also lets refresh tokens survive a SystemAuth restart, with a configured token HMAC secret.

The release adds packages and options and changes no existing API. The JWT-bearer grant is off until configured, applications accept service accounts only with an option, and SystemAuth starts without an HMAC secret as before. One existing response changes: relyingparty.RequestAuthenticator answers a cookie-authenticated cross-origin write with 403 instead of 401 (see Fixed); otherwise nothing changes for a deployment that does nothing.

Highlights

  • One way to authenticate an application. appauth.New builds the relying-party client, the /bff/* cookie session and one authentication chain for the API: the session or a DPoP-bound SystemAuth token, then any extra authenticators such as AAuth for agents.
  • The DPoP key is required. With persistent session and login-state stores shared by every instance (pgstore) and SystemAuth's token HMAC secret, sessions survive restarts and work on any instance.
  • Configuration errors name the setting in a form a host can relabel.
  • Memberships are filtered to the organizations where the person holds one of the application's roles.
  • Calls to SystemAuth for the signed-in person take one line with SessionToken and Transport.
  • Service accounts. An account belongs to one organization and holds application roles there. It signs a short assertion with a private key, and SystemAuth issues it a 15-minute DPoP-bound token through the RFC 7523 JWT-bearer grant. The token carries the orgs claim like a person's, so applications authorize it the same way. Removing a role takes the organization out of its memberships, and disabling the account or revoking a key ends its current tokens. Operators manage accounts with the service-account command; workloads in Go use identity/serviceclient.
  • A shared principal store. pgstore.PrincipalStore keeps an application's principals in sf_rp_principals, created by EnsureSchema, so applications stop maintaining their own.

  • Refresh tokens that survive a restart. SystemAuth signed authorization codes, refresh tokens and opaque access tokens with an HMAC secret it generated at each start, so after every restart relying parties could not refresh and sent their users through sign-in again, and a second instance refused the first one's tokens. tokens.hmac_secret sets the secret, and tokens.hmac_verification_secret rotates it without signing anyone out. Without it, SystemAuth logs a warning.

Added

  • identity/appauth:
  • Config, Validate with labels, ConfigFromEnv and RedirectURL.
  • ParseDPoPKey, ReadDPoPKeyFile and DPoPKeyCommand.
  • New, Handler, Middleware, Authenticate, Challenge and Challenges.
  • SessionToken and Transport.
  • ResourceMetadataHandler for RFC 9728.
  • pgstore.PrincipalStore on sf_rp_principals, with SetActive and Delete. NewStores now returns it as Principals.
  • storetest.PrincipalStore, a conformance suite for principal stores.
  • identity/serviceaccount: accounts, keys (generated as P-256, or supplied as ES256, RS256 with 2048 bits or more, or EdDSA), roles, and disable, enable, delete and key revocation recorded as access changes.
  • The JWT-bearer grant behind oauth.jwt_bearer:
  • enabled (default false);
  • max_assertion_lifetime (default 5m);
  • token_lifetime (default 15m).

Tokens are issued under the reserved client systemauth-jwt-bearer, with the principal_type claim and no refresh token. - identity/serviceclient: turns a private key into an http.Client that calls APIs as the account. - identity/serviceaccount/serviceaccountcmd: the service-account command, registered in systemauth. - relyingparty.BearerConfig.ServicePrincipals, VerifiedIdentity.Service and relyingparty.ServiceAccountClientID. appauth.Options.ServiceAccounts turns them on. - principal.Service.CreateServiceInTx and MemoryPrincipalStore.SetActive. - tokens.hmac_secret and tokens.hmac_verification_secret in SystemAuth's configuration (TokenConfig.HMACSecret and HMACVerificationSecret, MinHMACSecretLength): at least 32 bytes each, with environment variable expansion.

Changed

  • approle accepts service_account memberships.
  • Token introspection includes the orgs, orgs_truncated and principal_type values a token was issued with, for every principal.

Deprecated

  • appauth.Options.APIKeys. Command-line tools sign in with identity/publicclient, and automation uses service accounts (ADR-016). The option exists so an application with API keys can adopt appauth before replacing them.

Fixed

  • relyingparty.RequestAuthenticator answers a cookie-authenticated cross-origin write with 403 CSRF_REJECTED and no challenge. It used to answer with a 401, which tells a client to sign in again.

Upgrading

Nothing is required, with one response to check: an API behind relyingparty.RequestAuthenticator (or requestprincipal middleware over it) now answers a cookie-authenticated cross-origin write with 403 CSRF_REJECTED and no WWW-Authenticate challenge, where it answered 401. A client that signed in again on that 401 should treat the 403 as a refused request instead.

To adopt the additions:

  1. Set SystemAuth's token HMAC secret. Generate it once per deployment with openssl rand -hex 32, keep it in your secret store, and set tokens.hmac_secret: ${SYSTEMAUTH_TOKEN_HMAC_SECRET}. Refresh tokens issued before the first deploy with it stop working once, as they do on every restart without it. See Deploying SystemAuth.
  2. Move an application onto appauth. Follow Authenticating an Application.
  3. Generate a DPoP key once per deployment and deliver it as a file.
  4. Keep sessions and login states in persistent stores shared by every instance, such as pgstore.NewStores; the in-memory stores, allowed only on a loopback URL, lose sessions at a restart.
  5. People sign in once more after the first deploy with the key, if the application had none.
  6. An application with its own principals table can move it into sf_rp_principals with the guide's migration.
  7. Give automation service accounts.
  8. Set oauth.jwt_bearer.enabled: true in SystemAuth.
  9. Create accounts with service-account.
  10. Set appauth.Options.ServiceAccounts in the applications they call.

Service Accounts walks through it.

Documentation

For release history, see the changelog.