Repository navigation
Feat/v0.29.0 - #75
Merged
Merged
Feat/v0.29.0#75
Conversation
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>
|
Important Review skippedToo 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.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.