Skip to content

Feat/v0.29.0 - #75

Merged
grokify merged 24 commits into
mainfrom
feat/v0.29.0
Oct 29, 2026
Merged

grokify merged 24 commits into
mainfrom
feat/v0.29.0

Conversation

@grokify

@grokify grokify commented Oct 29, 2026

Copy link
Copy Markdown
Member

No description provided.

grokify and others added 24 commits October 29, 2026 04:57
An OAuth application now records its party in sf_oauth_apps.party:
first_party when the service provider owns it, second_party when the
customer owns it, third_party when a partner owns it. The customer owns
the data in every case. The column is nullable so rows written before it
existed stay readable; for them the first_party flag decides (true is
first_party, false third_party). first_party is deprecated in favour of
party and kept equal to party == first_party.

BREAKING CHANGE: run migrations/0012_oauth_app_party.sql on an existing
database. It adds the column and fills it from first_party. Without it,
reading or writing an OAuth application fails on the missing column.

Refs: RMI-SYSTEMFORGE-065
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A SystemAuth client now says who owns it: Client.Party and the static
client's party are first_party (the service provider), second_party (the
customer) or third_party (a partner); the customer owns the data in every
case. A client declared in the configuration file defaults to first_party,
since the file declares the operator's own applications. A client
registered at runtime (RegisterClient, Storage, EmbeddedOAuthClientStore)
without a party is third_party. Unknown parties are refused by
Config.Validate, NewClientFromConfig, RegisterClient and both storages.

Ent storage writes the party and keeps the superseded first_party flag
equal to party == first_party; a row without a party is read by its flag.
OAuthClient carries the party, and an update without one keeps it. The
identity/oauth server reads the party the same way.

A client may also carry an optional logo URL (logo_url), which must be an
absolute https URL; Ent storage keeps it in sf_oauth_apps.logo_url.

Refs: RMI-SYSTEMFORGE-065
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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, asked for and recorded by the consent page. A client
from a custom Storage that does not report its party is treated as
third-party.

skip_consent is still accepted but ignored and deprecated; SystemAuth
logs a warning at startup when it is set. WithSkipConsent is a
deprecated no-op, and DefaultSessionProvider, which records no consent,
reports none.

BREAKING CHANGE: skip_consent and WithSkipConsent no longer grant
consent. Clients in the configuration file are first_party unless they
set party, so they still sign in without a consent screen. Users of a
second- or third-party client, including any client registered at
runtime without a party, are now asked for consent; set party:
first_party on a client the operator owns. A deployment using
DefaultSessionProvider with such clients needs a SessionProvider that
records consent.

Refs: RMI-SYSTEMFORGE-065
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The authorization endpoint now honours prompt (OIDC Core 3.1.2.1), so a
first-party application can sign a user in silently:

- none shows no page: without a login session it answers login_required,
  and when consent is needed (a second- or third-party client without
  recorded consent) consent_required, at the client's redirect URI with
  its state; otherwise the request completes without any page.
- 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, since the client asked for it.
- select_account is accepted and has no effect.

none combined with another value, or an unknown value, is
invalid_request. The request the login or consent page returns to no
longer carries the prompt value that page satisfied, so it does not loop.

Refs: RMI-SYSTEMFORGE-083
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the login chooser is reached from an authorization request, it names
the client ("to continue to ...") and shows the client's logo_url when it
has one. The name and logo are rendered by html/template; the logo must
be an https URL, and only the chooser that shows it allows images from its
origin in the content security policy.

Refs: RMI-SYSTEMFORGE-083
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Describe the client party (first_party, second_party, third_party) and
its consent policy, the party and logo_url client settings, the
deprecated skip_consent, the OIDC prompt values the authorization
endpoint honours, the client name and logo on the login page, and the
sf_oauth_apps.party upgrade. The example configuration and the CLI help
no longer mention skip_consent as a way to skip consent.

Refs: RMI-SYSTEMFORGE-065
Refs: RMI-SYSTEMFORGE-083
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the secrets package: a Resolver over an omnivault vault that expands
${NAME} and $NAME references with os.Expand's syntax. The default (Env)
is backed by omnivault's environment provider and matches os.ExpandEnv
exactly, including the whole-reference rule that keeps "$" inside PEM
data. Missing secrets expand to the empty string; any other vault error
is returned.

Every secret field the SystemAuth configuration expanded with
os.ExpandEnv is now resolved through the resolver, plus
rate_limit.redis_url, which was documented as expanded but was not.
LoadConfig, ParseConfig, DecodeConfig and DecodeConfigFile keep their
signatures and gain WithResolver variants; systemauthsvc gains
LoadConfigWithResolver and LoadConfigBytesWithResolver. A new
secrets.vault setting names an omnivault URI to read references from, so
a deployment can register a cloud provider scheme with
omnivault.RegisterProvider without SystemForge importing a cloud SDK.

Refs: RMI-SYSTEMFORGE-068
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Extend the repository-wide legacy-name gate:

- coreauth, corecontrol, core_control and core_auth, in any case, in Go,
  Markdown, SQL, YAML and JSON outside the historical paths.
- The Core-era cf_ table, cookie and API-key prefix. Cloudflare's own
  session metadata keys (cf_ray, cf_country, cf_visitor, cf_bot_score)
  copied from CF-* request headers stay allowed.
- Upstream GitHub/Google OAuth endpoints (authorize, token and userinfo
  URLs, and the golang.org/x/oauth2/github and /google endpoint packages)
  in Go code outside identity/oauthclient.

A table-driven test pins what each rule flags and allows. The README's
v0.11.0 migration note is reworded so it no longer spells the retired
names.

Conformance of sf_principal_id across consuming applications is checked
in those repositories, not here.

Refs: RMI-SYSTEMFORGE-074
Refs: RMI-SYSTEMFORGE-078
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add identity/organization/membercmd, the "org member" command a host CLI
mounts so an operator can manage an organization's members and their
application roles without the HTTP API's authorization checks, for
recovery and bootstrap. It opens the database directly and applies
changes through organization.Service and approle.Service, so removals
record access changes exactly as the API does.

- add <org> <person> adds a member (--org-role, default member), or
  changes an existing member's role when --org-role is given, and
  assigns or changes each --role app:role.
- remove <org> <person> removes the membership, or with --app only the
  roles in those applications.
- list <org> prints members with their emails, roles and stored
  application roles (--json, --include-inactive).

Organizations resolve through ResolveSlug, so a renamed organization's
old slug works during its cooling-off period. The standalone binary
mounts the command without vocabularies, like service-account; a host
passes DefaultRoles with its registry and entitlements to grant roles.

Refs: RMI-SYSTEMFORGE-167
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Relying parties that put organization slugs in their URLs had no way to
map a slug, or the old slug of a renamed organization, to its ID: the
organization API's GET /organizations/{slug} follows aliases but is
served by a host to signed-in people, not by SystemAuth to relying
parties.

Add GET /oauth/userinfo/memberships/resolve?slug=, authenticated like
the memberships endpoint (an access token granted openid). It returns
the token principal's membership in the organization the slug names, in
the full orgs-claim form with the current slug, and alias true when the
slug is one the organization gave up within its cooling-off period. An
unknown slug and an organization the principal does not belong to both
answer 404, so other organizations are not revealed.

EntOrgsSource implements the new optional MembershipResolver interface;
an OrgsSource without it answers 501, so existing implementations keep
compiling. relyingparty.Client.ResolveOrganization calls the endpoint
and returns ErrOrganizationNotResolved for a 404.

Refs: RMI-SYSTEMFORGE-161
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add GET {base}/policy/vocabularies to the product contract API. It lists
each application vocabulary registered with the new
contract.WithVocabularies provider option: the application's roles with
their hierarchy levels and permissions (lowest level first), the union
of its permissions, and its OAuth scopes. Without a registry the list is
empty. It is gated by policy:read like the other policy reads.

Role-assignment CRUD (the organization API's app-role routes) and
scope-bounded token issuance already exist and are unchanged.

Refs: RMI-SYSTEMFORGE-005
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Memberships (organization id, slug and role) already reach the ID
token, JWT access tokens, userinfo and the memberships endpoint, and
product entitlements reach them through each organization's apps map:
approle.EffectiveRoles lists only applications the organization is
entitled to, so apps is the per-principal view of the marketplace
entitlements. No claim change is needed.

Add a test that switches an organization's entitlements and asserts,
across all four surfaces, that apps carries exactly the entitled
applications with the principal's role, that a stored role in an
application the organization lost drops out, and that the owner's
implicit admin follows the entitlement. Document this in the
application roles guide.

Refs: RMI-SYSTEMFORGE-082
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the end-to-end case the appauth design promises: against a packaged
SystemAuth (systemauthsvc, SQLite, managed principals enabled, DPoP
nonces required), the application signs a person in through its BFF,
takes the session's token with SessionToken, and creates and lists a
managed principal with mpclient over Authenticator.Transport. The same
token sent without Transport's DPoP proofs is refused with
ErrUnauthorized.

No top-level examples directory is added: the package's Example in
example_test.go remains the runnable example.

Refs: RMI-SYSTEMFORGE-193
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A client from the configuration file that was stored before parties
existed is third_party after the party migration. The test checks that
the next start rewrites it from the configuration as first_party, so its
users keep skipping the consent screen.

Refs: RMI-SYSTEMFORGE-065
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…acyguard

The appauth managed-principal test's client comes from the configuration
file, so it is first-party and needs no deprecated skip_consent. The
legacy-name gate now skips the .claude directory, which holds local agent
settings and worktrees rather than committed code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VerificationToken (sf_verification_tokens) stores a SHA-256 hash of a
single-use email verification token with its purpose (verify_email or
email_change), the address it verifies, the browser session or principal
a confirmation must present, an optional payload, its expiry and when it
was consumed. The table is created by the schema migration, and no
application login is granted it.

Refs: RMI-SYSTEMFORGE-086
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
VerificationTokenStore keeps single-use, purpose-scoped verification
tokens (verify_email, email_change) as SHA-256 hashes with an expiry that
defaults to 30 minutes. Consume checks purpose, binding, expiry and prior
use in one conditional update, so concurrent confirmations succeed at most
once and a confirmation with the wrong binding leaves the token usable for
its owner. Latest and Revoke serve resends and cancellation, and
DeleteExpired removes expired rows. Memory and Ent implementations pass
the new storetest.VerificationTokenStore conformance suite.

Refs: RMI-SYSTEMFORGE-086
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The mail section configures verification mail: provider (ses, smtp or
log), sender and reply-to addresses, branding (product name, help URL),
link lifetime, and send limits per recipient address and per client
address on the shared rate limit storage. A used-up recipient limit only
suppresses the message, so responses do not depend on the address.
Messages are built-in text and html/template pairs rendered through the
omnimail core Sender, set with WithMailSender; the library packages do
not import a vendor SDK. systemauthsvc builds the log and smtp senders
and takes others from Options.MailSender, and the systemauth binary
builds the Amazon SES sender from mail.ses with omni-aws. The SMTP
username and password are expanded from the environment. Production
mode refuses the log provider, with no waiver, because it writes
verification links to the server log.

Refs: RMI-SYSTEMFORGE-089
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With mail configured, a social sign-in whose provider email is not
verified is held instead of refused. The pending sign-in is stored with
a verify_email token bound to a hash of a __Host- cookie in the browser
that started it, and SystemAuth mails a link. Opening the link shows a
confirmation form; its POST, origin-checked with a cookie-derived CSRF
token, uses the token only with that browser's cookie, so a forwarded or
scanned link, or another person's pending sign-in, cannot complete it,
and a wrong cookie leaves the token usable. The verify-email page offers
a rate-limited resend that replaces earlier links and answers the same
whether or not a message was sent.

A confirmed address counts as proven (ExternalLogin.EmailProven): it is
treated as verified and authoritative, so the sign-in may create an
account or link to an existing account whose email is verified and the
same, because the person showed control of the mailbox. An account
with an unverified email is still refused as a conflict. Without mail,
sign-ins with an unverified email are refused as before.

Refs: RMI-SYSTEMFORGE-087
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signed-in person requests an email change at /account/email. SystemAuth
mails an email_change link to the new address and a notice, with the new
address masked, to the current one. The link's confirmation form uses
the token only from a session of the principal that asked, origin-checked
with a session-derived CSRF token. The email changes, marked verified at
that moment, only if it is still the address the change was requested
from and no other account has the new one, and a notice goes to the old
address. A new request replaces the pending one, and cancelling revokes
it. Requests answer the same whether or not the address belongs to an
account or was sent to.

PrincipalEmailChanger is implemented by the Ent directory, which changes
the human's email, its verification time and the principal's identifier
in one transaction, and by the memory directories. Requests,
cancellations, verifications and changes are recorded as security events
without addresses.

Refs: RMI-SYSTEMFORGE-088
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…UserInfo

With the email scope, ID tokens and UserInfo carry email_verified_at,
a SystemForge extension: when the current address was verified, as a
NumericDate, present only with email_verified true and a known time. It
is listed in discovery's claims_supported and filtered by scope in
UserInfo. HumanPrincipal gains EmailVerifiedAt, and relyingparty decodes
the claim into VerifiedIdentity.EmailVerifiedAt, preferring UserInfo.

The relying-party guide explains how to read the claims, and a new guide
sets up Amazon SES for verification mail: domain identity, Easy DKIM,
SPF through a custom MAIL FROM domain, DMARC, a configuration set with
event destinations, leaving the sandbox and a least-privilege policy.

Refs: RMI-SYSTEMFORGE-090
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SystemAuth's JWT encoder can write the email_verified_at claim in
floating-point form (1.79e+09), which is a valid NumericDate but failed
to decode into an int64, so relying parties rejected the ID token of any
user with a verified email. IDTokenClaims.EmailVerifiedAt and
UserInfo.EmailVerifiedAt are now *jwt.NumericDate, which accepts integer
and floating-point values.

Refs: RMI-SYSTEMFORGE-090
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ticks the roadmap items that have shipped: the Core-era naming and
social login gates, the social login core and relying-party contract,
the verified-email chain with its mail library and SES adapter, the
vocabulary endpoint, and the legacy-user and managed-principal release
items.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 148 files, which is 48 over the limit of 100.

To get a review, reduce the PR to 100 files or fewer by splitting it into smaller PRs or changing its base branch.

Upgrade to a paid plan to raise the limit.

This review couldn't start because sufficient usage credits or metered capacity aren't available. Add credits or update usage-based reviews in the billing tab, then retry.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: d2f5235f-39f5-4eed-be8f-154d05e90631
Commits

Reviewing files that changed from the base of the PR and between 3e8431c and c86fd4f.

Files ignored due to path filters: 1
  • go.sum is excluded by !**/*.sum
Files selected for processing: 148
  • CHANGELOG.json
  • CHANGELOG.md
  • README.md
  • cmd/systemauth/app.go
  • cmd/systemauth/commands.go
  • cmd/systemauth/config.example.yaml
  • cmd/systemauth/invite.go
  • cmd/systemauth/mail.go
  • cmd/systemauth/mail_test.go
  • cmd/systemauth/org_test.go
  • contract/operations_policy.go
  • contract/provider.go
  • contract/types.go
  • contract/vocabularies_test.go
  • docs/getting-started/quickstart.md
  • docs/guides/application-roles.md
  • docs/guides/dpop-clients.md
  • docs/guides/organization-slugs.md
  • docs/oauth/apps.md
  • docs/oauth/authorization-code.md
  • docs/platform/product-contract.md
  • docs/reference/schemas.md
  • docs/releases/changelog.md
  • docs/releases/index.md
  • docs/releases/v0.29.0.md
  • docs/security/production-checklist.md
  • docs/security/security-events.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-001/ROADMAP.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-004/ROADMAP.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-005/ROADMAP.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-006/ROADMAP.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-007/ROADMAP.md
  • docs/specs/initiatives/INIT-SYSTEMFORGE-008/ROADMAP.md
  • docs/systemauth/deployment.md
  • docs/systemauth/overview.md
  • docs/systemauth/relying-party.md
  • docs/systemauth/ses-deployment.md
  • docs/systemauth/social-login.md
  • docs/systemauth/verified-email.md
  • go.mod
  • identity/appauth/managed_test.go
  • identity/ent/client.go
  • identity/ent/ent.go
  • identity/ent/entql.go
  • identity/ent/hook/hook.go
  • identity/ent/internal/schema.go
  • identity/ent/migrate/schema.go
  • identity/ent/mutation.go
  • identity/ent/oauthapp.go
  • identity/ent/oauthapp/oauthapp.go
  • identity/ent/oauthapp/where.go
  • identity/ent/oauthapp_create.go
  • identity/ent/oauthapp_update.go
  • identity/ent/predicate/predicate.go
  • identity/ent/privacy/privacy.go
  • identity/ent/runtime.go
  • identity/ent/schema/oauth_app.go
  • identity/ent/schema/verification_token.go
  • identity/ent/tx.go
  • identity/ent/verificationtoken.go
  • identity/ent/verificationtoken/verificationtoken.go
  • identity/ent/verificationtoken/where.go
  • identity/ent/verificationtoken_create.go
  • identity/ent/verificationtoken_delete.go
  • identity/ent/verificationtoken_query.go
  • identity/ent/verificationtoken_update.go
  • identity/oauth/flow_test.go
  • identity/oauth/handler_fosite.go
  • identity/organization/membercmd/membercmd.go
  • identity/organization/membercmd/membercmd_test.go
  • identity/relyingparty/client.go
  • identity/relyingparty/email_verified_at_test.go
  • identity/relyingparty/memberships_claims.go
  • identity/relyingparty/principal.go
  • identity/relyingparty/resolve_organization_test.go
  • identity/systemauth/authorize_prompt.go
  • identity/systemauth/authorize_prompt_test.go
  • identity/systemauth/client.go
  • identity/systemauth/client_party.go
  • identity/systemauth/client_party_test.go
  • identity/systemauth/client_secret_ent_test.go
  • identity/systemauth/config.go
  • identity/systemauth/config_loader.go
  • identity/systemauth/config_secrets_test.go
  • identity/systemauth/consent_policy_test.go
  • identity/systemauth/email_change.go
  • identity/systemauth/email_change_directory.go
  • identity/systemauth/email_change_test.go
  • identity/systemauth/email_verification.go
  • identity/systemauth/email_verification_test.go
  • identity/systemauth/handler_discovery.go
  • identity/systemauth/handler_fosite.go
  • identity/systemauth/handler_userinfo.go
  • identity/systemauth/hardening_test.go
  • identity/systemauth/hmac_secret_test.go
  • identity/systemauth/human_principal.go
  • identity/systemauth/login_branding_test.go
  • identity/systemauth/login_token_exchange.go
  • identity/systemauth/mail.go
  • identity/systemauth/mail_test.go
  • identity/systemauth/oidc_test.go
  • identity/systemauth/orgs_claim.go
  • identity/systemauth/orgs_claim_test.go
  • identity/systemauth/orgs_entitlements_test.go
  • identity/systemauth/production.go
  • identity/systemauth/production_test.go
  • identity/systemauth/provider.go
  • identity/systemauth/provider_oauth.go
  • identity/systemauth/resolve_membership_test.go
  • identity/systemauth/schema/config.schema.json
  • identity/systemauth/security_headers.go
  • identity/systemauth/security_headers_test.go
  • identity/systemauth/security_log_test.go
  • identity/systemauth/server.go
  • identity/systemauth/session.go
  • identity/systemauth/signup_age_test.go
  • identity/systemauth/signup_gate_test.go
  • identity/systemauth/social_directory.go
  • identity/systemauth/social_directory_memory_storage.go
  • identity/systemauth/social_login.go
  • identity/systemauth/social_login_test.go
  • identity/systemauth/social_store_ent_internal_test.go
  • identity/systemauth/social_store_ent_test.go
  • identity/systemauth/storage_ent.go
  • identity/systemauth/storage_ent_human.go
  • identity/systemauth/storage_memory.go
  • identity/systemauth/storetest/storetest.go
  • identity/systemauth/systemauth_test.go
  • identity/systemauth/userinfo_test.go
  • identity/systemauth/verification_token.go
  • identity/systemauth/verification_token_ent.go
  • identity/systemauthsvc/config.go
  • identity/systemauthsvc/config_secrets_test.go
  • identity/systemauthsvc/console_test.go
  • identity/systemauthsvc/mail.go
  • identity/systemauthsvc/mail_test.go
  • identity/systemauthsvc/managedprincipals_test.go
  • identity/systemauthsvc/production.go
  • identity/systemauthsvc/service.go
  • identity/systemauthsvc/service_test.go
  • identity/systemauthsvc/signup_test.go
  • internal/legacyguard/doc.go
  • internal/legacyguard/legacy_test.go
  • migrations/0012_oauth_app_party.sql
  • mkdocs.yml
  • multiapp/provision.go
  • secrets/secrets.go
  • secrets/secrets_test.go

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@grokify
grokify merged commit d4bed7a into main Oct 29, 2026
1 of 9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant