Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions CHANGELOG.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,44 @@
"grokify"
],
"releases": [
{
"version": "v0.30.5",
"date": "2026-11-02",
"highlights": [
{
"description": "A signed-in person can link another provider's sign-in to their account at `/account/sign-in-methods`, so either reaches the same account",
"commit": "18333eb"
}
],
"added": [
{
"description": "`/account/sign-in-methods` and `POST /account/sign-in-methods/{provider}`: link a provider's sign-in to the signed-in account (`SignInMethodsPath`)",
"commit": "18333eb"
},
{
"description": "The security event `systemauth.sign_in_method.linked`, for a completed link and a link the callback refuses, as OCSF User Management, Update",
"commit": "18333eb"
}
],
"security": [
{
"description": "Linking needs a session for the account that started it and an allowed origin, never moves an identity from another account, never creates an account and never changes its email",
"commit": "18333eb"
}
],
"tests": [
{
"description": "Linking, relinking, signing in with a linked identity, and refusals for another account's identity, a missing session and a cross-origin start",
"commit": "1dc9898"
}
],
"documentation": [
{
"description": "The social login guide describes linking sign-in methods; the OCSF guide lists the new event",
"commit": "2159827"
}
]
},
{
"version": "v0.30.4",
"date": "2026-11-02",
Expand Down
26 changes: 25 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

## [Unreleased]

## [v0.30.5] - 2026-11-02

### Highlights

- A signed-in person can link another provider's sign-in to their account at `/account/sign-in-methods`, so either reaches the same account

### Security

- Linking needs a session for the account that started it and an allowed origin, never moves an identity from another account, never creates an account and never changes its email ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))

### Added

- `/account/sign-in-methods` and `POST /account/sign-in-methods/{provider}`: link a provider's sign-in to the signed-in account (`SignInMethodsPath`) ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))
- The security event `systemauth.sign_in_method.linked`, for a completed link and a link the callback refuses, as OCSF User Management, Update ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))

### Documentation

- The social login guide describes linking sign-in methods; the OCSF guide lists the new event ([`2159827`](https://github.com/plexusone/systemforge/commit/2159827))

### Tests

- Linking, relinking, signing in with a linked identity, and refusals for another account's identity, a missing session and a cross-origin start ([`1dc9898`](https://github.com/plexusone/systemforge/commit/1dc9898))

## [v0.30.4] - 2026-11-02

### Highlights
Expand Down Expand Up @@ -1747,7 +1770,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro
- GitHub Actions lint workflow with golangci-lint ([`e2f98d5`](https://github.com/plexusone/systemforge/commit/e2f98d5))
- Dependabot configuration for Go modules and GitHub Actions ([`e2f98d5`](https://github.com/plexusone/systemforge/commit/e2f98d5))

[unreleased]: https://github.com/plexusone/systemforge/compare/v0.30.4...HEAD
[unreleased]: https://github.com/plexusone/systemforge/compare/v0.30.5...HEAD
[v0.30.5]: https://github.com/plexusone/systemforge/compare/v0.30.4...v0.30.5
[v0.30.4]: https://github.com/plexusone/systemforge/compare/v0.30.3...v0.30.4
[v0.30.3]: https://github.com/plexusone/systemforge/compare/v0.30.2...v0.30.3
[v0.30.2]: https://github.com/plexusone/systemforge/compare/v0.30.1...v0.30.2
Expand Down
1 change: 1 addition & 0 deletions contract/audit/ocsfaudit/details.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ var allowed = map[string]map[string]kind{
"systemauth.token_family.revoked": {"reason": kindCode, "subject": kindUUID, "client_id": kindCode},
"systemauth.email_verification.sent": {"purpose": kindCode, "sent": kindBool},
"systemauth.email.verified": {"purpose": kindCode, "method": kindCode},
"systemauth.sign_in_method.linked": {"method": kindCode, "reason": kindCode},
"systemauth.signing_key.loaded": {"key_id": kindCode, "verification_key_ids": kindCodeList, "ephemeral": kindBool},
"systemauth.production.waiver": {"check": kindCode, "setting": kindCode},

Expand Down
1 change: 1 addition & 0 deletions contract/audit/ocsfaudit/doc.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
// systemauth.email_change.requested, User Management (3007) Update Informational
// systemauth.email_change.cancelled,
// systemauth.email.changed
// systemauth.sign_in_method.linked User Management (3007) Update Informational
// systemauth.signing_key.loaded Application Lifecycle (6002) Start Informational
// systemauth.production.waiver Application Lifecycle (6002) Start Medium
// organization.membership Group Management (3006) Add User, Remove User, Update Informational; Medium for ownership_transferred
Expand Down
2 changes: 2 additions & 0 deletions contract/audit/ocsfaudit/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ func (m *mapper) toOCSF(ev *audit.Event) any {
case "systemauth.email_verification.sent", "systemauth.email.verified",
"systemauth.email_change.requested", "systemauth.email_change.cancelled", "systemauth.email.changed":
return c.email()
case "systemauth.sign_in_method.linked":
return c.userManagement(events.UserManagementActivityIDUpdate, c.subject())
case "systemauth.signing_key.loaded", "systemauth.production.waiver":
return c.startup()
case "organization.membership":
Expand Down
2 changes: 2 additions & 0 deletions contract/audit/ocsfaudit/mapping_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ func mappingCases() []mappingCase {
{"email change requested", evt("systemauth.email_change.requested", "request_email_change", ok, nil), userMg, events.UserManagementActivityIDUpdate, info},
{"email change cancelled", evt("systemauth.email_change.cancelled", "cancel_email_change", ok, nil), userMg, events.UserManagementActivityIDUpdate, info},
{"email changed", evt("systemauth.email.changed", "change_email", ok, map[string]any{"old_email": leakEmail, "new_email": leakEmail}), userMg, events.UserManagementActivityIDUpdate, info},
{"sign-in method linked", evt("systemauth.sign_in_method.linked", "link_sign_in_method", ok, map[string]any{"method": "google", "email": leakEmail}), userMg, events.UserManagementActivityIDUpdate, info},
{"sign-in method link refused", evt("systemauth.sign_in_method.linked", "link_sign_in_method", fail, map[string]any{"method": "github", "reason": "other_account"}), userMg, events.UserManagementActivityIDUpdate, low},
{"signing key loaded", withResource(withActor(evt("systemauth.signing_key.loaded", "load", ok, map[string]any{"key_id": "kid-2026", "verification_key_ids": []string{"kid-2025", "kid-2026"}, "ephemeral": false}), service), audit.Resource{Type: "signing_key", Identifier: "kid-2026"}), appLC, events.ApplicationLifecycleActivityIDStart, info},
{"production waiver", withResource(withActor(evt("systemauth.production.waiver", "waive", ok, map[string]any{"check": "dpop.single_instance", "setting": "dpop.single_instance", "risk": leakRisk}), service), audit.Resource{Type: "production_check", Identifier: "dpop.single_instance"}), appLC, events.ApplicationLifecycleActivityIDStart, medium},
{"membership added", withResource(evt("organization.membership", "added", ok, map[string]any{"organization_id": orgID.String(), "principal_id": resourceID.String(), "role": "member"}), audit.Resource{Type: "membership", ID: resourceID}), groupM, events.GroupManagementActivityIDAddUser, info},
Expand Down
26 changes: 25 additions & 1 deletion docs/releases/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,29 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro

## [Unreleased]

## [v0.30.5] - 2026-11-02

### Highlights

- A signed-in person can link another provider's sign-in to their account at `/account/sign-in-methods`, so either reaches the same account

### Security

- Linking needs a session for the account that started it and an allowed origin, never moves an identity from another account, never creates an account and never changes its email ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))

### Added

- `/account/sign-in-methods` and `POST /account/sign-in-methods/{provider}`: link a provider's sign-in to the signed-in account (`SignInMethodsPath`) ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))
- The security event `systemauth.sign_in_method.linked`, for a completed link and a link the callback refuses, as OCSF User Management, Update ([`18333eb`](https://github.com/plexusone/systemforge/commit/18333eb))

### Documentation

- The social login guide describes linking sign-in methods; the OCSF guide lists the new event ([`2159827`](https://github.com/plexusone/systemforge/commit/2159827))

### Tests

- Linking, relinking, signing in with a linked identity, and refusals for another account's identity, a missing session and a cross-origin start ([`1dc9898`](https://github.com/plexusone/systemforge/commit/1dc9898))

## [v0.30.4] - 2026-11-02

### Highlights
Expand Down Expand Up @@ -1747,7 +1770,8 @@ and this changelog is generated by [Structured Changelog](https://github.com/gro
- GitHub Actions lint workflow with golangci-lint ([`e2f98d5`](https://github.com/plexusone/systemforge/commit/e2f98d5))
- Dependabot configuration for Go modules and GitHub Actions ([`e2f98d5`](https://github.com/plexusone/systemforge/commit/e2f98d5))

[unreleased]: https://github.com/plexusone/systemforge/compare/v0.30.4...HEAD
[unreleased]: https://github.com/plexusone/systemforge/compare/v0.30.5...HEAD
[v0.30.5]: https://github.com/plexusone/systemforge/compare/v0.30.4...v0.30.5
[v0.30.4]: https://github.com/plexusone/systemforge/compare/v0.30.3...v0.30.4
[v0.30.3]: https://github.com/plexusone/systemforge/compare/v0.30.2...v0.30.3
[v0.30.2]: https://github.com/plexusone/systemforge/compare/v0.30.1...v0.30.2
Expand Down
5 changes: 4 additions & 1 deletion docs/releases/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@ Release notes, one file per version, newest first. The
[changelog](changelog.md) is generated from `CHANGELOG.json` and lists every
change with its commit.

The latest release, [v0.30.4](v0.30.4.md), adds
The latest release, [v0.30.5](v0.30.5.md), lets a signed-in person link
another provider's sign-in to their account. It adds no breaking change.

Before it, [v0.30.4](v0.30.4.md) added
`social_login.signed_out_url`, so every sign-out can land on the product's
marketing site, and links the waitlist page back to it by the product's
name. It adds no breaking change.
Expand Down
24 changes: 24 additions & 0 deletions docs/releases/v0.30.5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# v0.30.5

**Release Date:** 2026-11-02

One account, several sign-ins:

- **Linking sign-in methods.** A person who signs in with GitHub under one
email would get a second account by signing in with Google under another,
since a sign-in joins an existing account only by an email its provider is
authoritative for. Signed in, they can now open `/account/sign-in-methods`
on the auth host and link the other provider first. Either sign-in then
reaches the same account and its work. A sign-in that already has an
account of its own is not linked: merging accounts, and removing a sign-in
method, are not available yet.
- **Safe by default.** Linking needs a session for the account that started
it, is origin-checked like sign-out, never moves an identity from another
account, never creates an account and never changes the account's email.
A completed link, and a link the callback refuses, is the security event
`systemauth.sign_in_method.linked`, mapped to OCSF User Management.

It adds no breaking change and no setting: the page is served wherever
social login is.

See [Linking sign-in methods](../systemauth/social-login.md#linking-sign-in-methods).
1 change: 1 addition & 0 deletions docs/security/ocsf-events.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ selected by it as well as by OCSF class.
| `systemauth.token_family.revoked` | Authentication (3002) | Logoff, protocol OAuth 2.0 | Informational |
| `systemauth.email_verification.sent`, `systemauth.email.verified` | Authentication (3002) for purpose `verify_email`; User Management (3007) for `email_change` | Preauth; Update | Informational |
| `systemauth.email_change.requested`, `.cancelled`, `systemauth.email.changed` | User Management (3007) | Update | Informational |
| `systemauth.sign_in_method.linked` | User Management (3007) | Update | Informational; Low when refused |
| `systemauth.signing_key.loaded` | Application Lifecycle (6002) | Start | Informational |
| `systemauth.production.waiver` | Application Lifecycle (6002) | Start | Medium |
| `organization.membership` | Group Management (3006) | Add User (`added`), Remove User (`removed`), Update (`updated`, `ownership_transferred`) | Informational; Medium for `ownership_transferred` |
Expand Down
27 changes: 27 additions & 0 deletions docs/systemauth/social-login.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ endpoints:
|-------|---------|
| `GET /login?return_to=...` | The sign-in page: a link per provider, waiting for the person to choose, even with one provider. With `auto_select_single_provider`, redirects straight to the only provider. |
| `GET /login/{provider}?return_to=...` | Starts the upstream authorization-code flow (`github` or `google`). |
| `GET /account/sign-in-methods` | A signed-in person's page to link another provider's sign-in to their account (see [Linking sign-in methods](#linking-sign-in-methods)). |
| `GET /login/{provider}/callback` | Upstream callback. Resolves the principal, sets the `__Host-sf_login` session, and redirects to `return_to`. |
| `GET /logout?return_to=...` | Renders a sign-out confirmation form (never signs out by itself). |
| `POST /logout` | Deletes the login session, clears the cookie, revokes all of the principal's access and refresh tokens, and redirects (`303`) to the validated `return_to` (default `default_redirect`). |
Expand Down Expand Up @@ -192,6 +193,32 @@ email already belongs to an account, the sign-in is refused as a conflict
(`ErrEmailConflict`), and the person signs in the way they signed in before.
GitHub sign-in uses the account's primary email only.

### Linking sign-in methods

A signed-in person can link another provider's sign-in to their account at
`/account/sign-in-methods`, so either one signs them in to the same account,
whatever email each provider reports. The page lists a **Link** button per
configured provider. The button's form posts to
`/account/sign-in-methods/{provider}`, which is origin-checked like sign-out
and needs a login session. It sends the browser through the provider's
sign-in. The callback then attaches that provider identity to the account:

- The browser must still be signed in as the account that started the link.
- An identity already on another account is never moved, and the page says
so. Removing a sign-in method from an account, or merging two accounts, is
not available yet: link a provider before its first sign-in makes an
account of its own.
- An identity already on this account is a success that changes nothing.
- No account is created, no sign-up gate or age question applies, and the
account's own email is unchanged.

A completed link, and a link the callback refuses (the identity is on
another account, or the browser is no longer signed in as the account that
started it), is the security event `systemauth.sign_in_method.linked`. A
start refused before the provider, such as a cross-origin request, is not;
nor is a denial at the provider, which is a refused sign-in
(`systemauth.sign_in`).

## Security properties

- `state` is stored server-side (single use, 10 minute TTL) **and** bound to the
Expand Down
Loading
Loading