Skip to content

v0.29.0

Release Date: 2026-10-29

A release that decides consent by who owns a client, proves email addresses, and reads secrets from a vault:

  • Consent by party. Each client says whether the service provider, a customer or a partner owns it. Only first-party clients skip the consent screen; social_login.skip_consent no longer grants consent for every client.
  • OIDC prompt and a named login page. The authorization endpoint honours prompt, so a first-party application can sign a user in silently, and the login chooser names the requesting client and shows its logo.
  • Verified email. With a new mail section, SystemAuth verifies an unverified provider email at sign-in, lets a signed-in person change their email, and tells relying parties when the address was verified.
  • Secrets through omnivault. Secret references in the configuration resolve through a vault named in secrets.vault, with the environment as the default.

It also adds systemauth org member add|remove|list, organization slug resolution for relying parties, and read-only application vocabularies in the product contract API.

The release is breaking. An existing database needs the sf_oauth_apps.party column, and users of a client registered at runtime without a party are now asked for consent. Clients declared in the configuration file stay first-party, so they keep signing in without a consent screen. Read Upgrading first.

Highlights

  • Client parties: party is first_party, second_party or third_party, with an optional logo_url. Configuration-file clients default to first_party; clients registered at runtime default to third_party. See Client party and Parties.
  • prompt: none, login, consent and select_account. See Silent sign-in and prompt.
  • Verified email: a verify-email step at sign-in, email change at /account/email, and the email_verified_at claim, with mail sent through Amazon SES, SMTP or, in development, the log.
  • secrets package: a resolver over an omnivault vault that expands ${NAME} and $NAME exactly like os.ExpandEnv by default. See Secrets.
  • systemauth org member: add, remove and list an organization's members and their application roles from the command line.
  • Slug resolution: GET /oauth/userinfo/memberships/resolve?slug= and relyingparty.Client.ResolveOrganization map a slug, or the old slug of a renamed organization, to the caller's membership.

Security

  • Consent. social_login.skip_consent granted consent for every client to users signed in through social login, and DefaultSessionProvider's WithSkipConsent did the same for its users. Once a client the operator does not own was registered, its users were never asked. The authorization endpoint now decides by the client's party, whatever the SessionProvider: a first-party client needs no consent, and a second- or third-party client needs the user's recorded consent to every requested scope. A client from a custom Storage that does not report its party is treated as third-party.
  • Verification links. Tokens are single-use, scoped to a purpose (verify_email or email_change) and stored as SHA-256 hashes. A verify_email link completes only in the browser that started the sign-in, bound to a __Host- cookie; an email_change link only from a session of the principal that asked. Each confirmation is a POST, origin-checked with a CSRF token, so a forwarded or scanned link, or another person's pending sign-in, cannot complete it, and a wrong binding leaves the token usable for its owner. A link verifies an address; it is not a way to sign in.
  • Mail limits. Verification mail is limited per recipient address and per client address on the shared rate limit storage. A used-up recipient limit only suppresses the message, and requests answer the same whether or not a message was sent or the address belongs to an account.
  • No links in production logs. Production mode refuses mail.provider: log, with no waiver, because it writes verification links to the server log.
  • Client logo. logo_url must be an absolute https URL. It is rendered by html/template, and only the login chooser that shows it allows images from its origin in the content security policy.
  • Security events record verification mail sent, emails verified, and email change requests, cancellations and changes, without addresses. See Security events.

Breaking Changes

  • sf_oauth_apps.party. Reading or writing an OAuth application fails without the column. Startup migration adds it; otherwise run migrations/0012_oauth_app_party.sql, which also fills it from first_party.
  • skip_consent and WithSkipConsent no longer grant consent. Users of a second- or third-party client, including any client registered at runtime without a party, are asked for consent.
  • DefaultSessionProvider records no consent and reports none. A deployment using it with second- or third-party clients needs a SessionProvider that records consent.
  • rate_limit.redis_url is expanded like the other secret fields. It was documented as expanded but was not, so a literal $ in it is now expanded.

Added

  • Client party and logo: party and logo_url on Client, static clients, OAuthClient and Ent storage (sf_oauth_apps.party, sf_oauth_apps.logo_url). Unknown parties are refused by Config.Validate, NewClientFromConfig, RegisterClient and both storages.
  • OIDC prompt:
  • none shows no page: without a login session it answers login_required, and when consent is needed consent_required, at the client's redirect URI with its state;
  • login sends the user to the login page even with a login session;
  • consent shows the consent page even when consent is recorded, for first-party clients too;
  • select_account is accepted and has no effect;
  • none with another value, or an unknown value, is invalid_request.
  • Login chooser names the client ("to continue to ...") and shows its logo when the page is reached from an authorization request.
  • Mail settings: mail.provider (ses, smtp or log), from, reply_to, product_name, support_url, token_lifetime (5m to 24h, default 30m), per_address (default 5 an hour), per_client (default 20 an hour), smtp.*, ses.region and ses.configuration_set; systemauth.WithMailSender and systemauthsvc.Options.MailSender. The library packages depend only on the omnimail Sender; the systemauth binary builds the Amazon SES sender from mail.ses.
  • Verification tokens: VerificationTokenStore with memory and Ent implementations (sf_verification_tokens) and the storetest.VerificationTokenStore conformance suite.
  • Verify-email step at sign-in: with mail configured, a social sign-in whose provider email is not verified is held, a link is mailed, and the verify-email page offers a rate-limited resend. A confirmed address counts as proven (ExternalLogin.EmailProven), so the sign-in may create an account or link to an existing account with the same verified email.
  • Email change at /account/email, with a notice to the current address; PrincipalEmailChanger, implemented by the Ent and memory directories.
  • email_verified_at: with the email scope, ID tokens and UserInfo carry when the current address was verified, as a NumericDate, and discovery lists it in claims_supported. HumanPrincipal.EmailVerifiedAt; relyingparty.IDTokenClaims.EmailVerifiedAt and UserInfo.EmailVerifiedAt (*jwt.NumericDate) and VerifiedIdentity.EmailVerifiedAt.
  • Security events systemauth.email_verification.sent, systemauth.email.verified, systemauth.email_change.requested, systemauth.email_change.cancelled and systemauth.email.changed, and the sign-in refusal reason verification_invalid.
  • secrets: Resolver, New and Env; the secrets.vault setting; systemauth.LoadConfigWithResolver, ParseConfigWithResolver, DecodeConfigWithResolver and DecodeConfigFileWithResolver; systemauthsvc.LoadConfigWithResolver and LoadConfigBytesWithResolver.
  • identity/organization/membercmd: the org member command a host CLI mounts. add <org> <person> adds a member or changes their role and assigns --role app:role; remove removes the membership, or with --app only those applications' roles; list prints members with their roles (--json, --include-inactive). It applies changes through organization.Service and approle.Service, so removals record access changes as the API does.
  • Slug resolution: GET /oauth/userinfo/memberships/resolve?slug=, authenticated like the memberships endpoint, returns the caller's membership with the current slug and alias true for a slug given up within its cooling-off period; an unknown slug and an organization the caller does not belong to both answer 404. The optional MembershipResolver interface, implemented by EntOrgsSource; relyingparty.Client.ResolveOrganization and ErrOrganizationNotResolved.
  • Application vocabularies: GET {base}/policy/vocabularies in the product contract API lists each application's roles with their levels and permissions, the union of its permissions and its OAuth scopes, from the new contract.WithVocabularies option. It is gated by policy:read.
  • Dependencies: github.com/plexusone/omnimail v0.1.0 and github.com/plexusone/omni-aws v0.11.0.

Changed

  • Clients declared in the configuration file default to first_party and are rewritten from it at startup; clients registered at runtime default to third_party.
  • The example configuration and the CLI help no longer mention skip_consent.

Deprecated

  • social_login.skip_consent: ignored; SystemAuth logs a warning at startup when it is set.
  • WithSkipConsent on DefaultSessionProvider: a no-op.
  • sf_oauth_apps.first_party: use party. It is kept equal to party == first_party.

Upgrading

New SystemAuth settings. Values reach the YAML through ${VAR} references, so the variable name is a convention.

YAML key Suggested variable Needed when
clients[].party — A configuration-file client is not the operator's own
clients[].logo_url — The login chooser should show the client's logo
mail.provider, mail.from — Unverified provider emails should be verified
mail.smtp.password SYSTEMAUTH_SMTP_PASSWORD mail.provider: smtp with authentication
mail.ses.region — mail.provider: ses
secrets.vault SECRETS_VAULT Secrets live in a vault other than the environment
  1. Migrate the database. Run systemauth migrate (or start with --migrate, the default). It adds sf_oauth_apps.party and sf_oauth_apps.logo_url and creates sf_verification_tokens. Without startup migration, also run migrations/0012_oauth_app_party.sql, which fills party from first_party. See Client party column.
  2. Check the party of runtime-registered clients. Clients in the configuration file are rewritten as first_party at startup unless they set party, so existing suite applications keep signing in without a consent screen. A client registered at runtime is third_party unless its first_party flag was set. Set party: first_party on clients the operator owns and second_party on clients a customer owns; otherwise their users now see the consent screen.
  3. Remove skip_consent. social_login.skip_consent and WithSkipConsent are ignored. A deployment that relied on DefaultSessionProvider for second- or third-party clients needs a SessionProvider that records consent.
  4. Add mail to verify emails. It is opt-in; without it, sign-ins with an unverified provider email are refused as before. The systemauth binary builds the ses sender from mail.ses with the AWS default credential chain; an application using systemauthsvc passes it as Options.MailSender, and one embedding systemauth passes it with WithMailSender. smtp and log are built from the configuration, and production refuses log:

    mail:
      provider: ses
      from: "Example <[email protected]>"
      ses:
        region: us-east-1
    

    See Verified email and Sending Mail with Amazon SES. 5. Read secrets from a vault, if you want to. Nothing changes by default: references expand from the environment as before. To read them from another vault, set secrets.vault to an omnivault URI (env://, file:// and memory:// are built in). For a cloud secret manager, register its scheme with omnivault.RegisterProvider in your own binary, or pass a resolver to systemauthsvc.LoadConfigWithResolver. SystemForge imports no cloud SDK for this. See Secrets. 6. Check rate_limit.redis_url. It is now expanded, so a literal $ in it is replaced; write it as a ${VAR} reference instead.

Walk the production checklist before going live.

Documentation

For release history, see the changelog.