# Browser-direct linked identities

Provider links belong to a tenant member **within an organization**, not just
the current application. All routes below are SSO POSTs below the
[browser-direct base](https://m7.org/docs/api/sso.user.m7.org/browser-direct.md), with selected session handles, admitted
Origin and fresh endpoint DPoP. They need no additional profile/email scope.
The application and member must remain active/admitted. Starting/completing
provider linking and provider sign-in require current federation policy. Listing
and unlink through another retained method are not a blanket federation-policy
override; a provider can serve as retained proof only while currently permitted.

| Relative path | Additional input | Success |
| --- | --- | --- |
| `/identities/list` | None | Organization identity view |
| `/identities/start` | Authorization context plus `provider: github`, `discord` or `m7` | `transaction`, `state`, `url` for provider navigation |
| `/identities/status` | `transaction` | Ceremony status, provider display and resulting identity ID if any |
| `/identities/confirm` | `transaction`, `code_verifier` | `status: completed`, `identity_id` plus identity view |
| `/identities/cancel` | `transaction` | `status` only, cancelled or already completed |
| `/identities/unlink` | `identity_id` local UUID | `removed`, identity view, `sessions_revoked`, `revocation_pending` |

All rows above include session handles in addition to the listed fields.
Start, confirm and unlink require primary authentication no older than five
minutes; list/status/cancel do not. Refresh and selection cannot manufacture
fresh authentication. The [same-carrier reauthentication flow](https://m7.org/docs/api/sso.user.m7.org/browser-direct-passwords.md#fresh-sign-in-without-duplicate-remembered-accounts)
replaces old entries only after successful activation.

## Inspect and link

The identity view contains `sub`, `scope: organization`, `identities`, allowed
`providers`, `reauthentication_required`, `can_link`,
`multiple_linked_identities_allowed`, and `message`. Identity rows expose only
local `id`, `provider`, display `handle`, `linked_at`, `last_login`, `verified`
and `can_unlink`. Raw provider subjects, remote credentials and tokens are absent.

```json
{"bd_carrier":"<carrier>","bd_session":"<session>","bd_key":"<carrier secret>","identity_id":"eeeeeeee-eeee-4eee-8eee-eeeeeeeeeeee"}
```

That body unlinks only the reviewed local identity; never construct it from a
provider display handle. For linking, start with new state/PKCE but the selected
session's signing key. Follow `url` in a full page or popup. The callback returns
`identity=continue,state` or an error. Validate state and call status. The status
projection has `status` (`draft`, `ready`, `completed`, `cancelled`),
`provider` (null or `{vendor,handle}`), and `identity_id` (null or UUID).

```mermaid
sequenceDiagram
  participant B as Browser
  participant S as SSO
  participant P as Provider
  B->>S: identities/start + context + selected handles
  S-->>B: transaction + url
  B->>P: Fresh provider authentication
  P->>S: Validated provider result
  S-->>B: Callback identity=continue + state
  B->>S: identities/status
  S-->>B: ready + provider display
  Note over B: Explicitly confirm reviewed link
  B->>S: identities/confirm + verifier + handles
  S-->>B: completed + local identity ID
```

Provider proof alone does not insert a link. The ten-minute ceremony binds org,
member, app, origin, key and the original selected session. Explicit confirm
requires the original verifier and current policy/ownership; a changed session
requires restarting. Completed confirm is a receipt retry, even if its original
freshness window has passed, while current selected-session authority remains
required. After a lost response use status before creating anything else.
Cancel before completion discards the pending proof; cancelling a completed
ceremony returns its completed receipt and does not undo the link. Expiry needs
a new ceremony.

Provider subject ownership is unique within the organization. An existing
identity owned by another member is never moved, even if email or display names
match. With org `tenant_policy_allow_multiple_linked_identities` off, the first
federated link is allowed but additional ones are blocked. Password and email
do not count. Existing multiple links remain usable. On permits up to 16
federated links. Current policy is rechecked before insertion, including during
a previously started ceremony. See [org policy](https://m7.org/docs/api/api.user.m7.org/tenant-policy).

## Unlink safely

Unlink requires fresh proof of a sign-in method that remains after deletion:
a native password, another currently permitted verified provider, a consumer
link, or eligible email sign-in with current unique verified ownership and policy.
You cannot sign in through the provider being removed and use that alone to
remove it. At least one usable method must remain.

Successful unlink returns `removed: true` and the refreshed view. Repeating for
an already-absent ID returns `removed: false`; it cannot remove a replacement
identity by provider name. A successful link does not rotate tokens or close
sessions. Unlink immediately closes sessions attributed to the removed identity
and legacy unattributed tenant sessions, across hosted SSO and all applications.
Known sessions from other retained methods remain. `sessions_revoked` counts
local closures; `revocation_pending` distinguishes unfinished remote token-family
cleanup. Each operation attempts a bounded batch of eight remote revocations;
the existing reconciliation process handles remaining work. It does not imply
immediate application-cookie clearing or offline JWT invalidation everywhere.

Malformed IDs produce `invalid_request`; unavailable/expired transactions can
produce `invalid_grant`. `identity_transaction_invalid` (400) covers rejected
provider confirmation/verifier/phase. `identity_unavailable` is 403 for denied
link availability or 409 for a current ownership/limit/service conflict.
`remaining_signin_method_required` (403) needs fresh proof of a retained method;
`reauthentication_required` (403) needs primary sign-in again. `session_changed`
(409) requires the original selected session or a new ceremony; provider policy
denial is `access_denied` (403). Reload the view instead of choosing another
member or suppressing denial. Network errors need status/list reconciliation
with a fresh proof. No provider token or proof should be logged.

Managers can [inspect and unlink](https://m7.org/docs/api/api.user.m7.org/tenant-member-security)
using separate manager authority and a recorded reason. New identity linking
remains member self-service. Manager last-method protection does not count
email alone as a retained method; do not reuse self-service eligibility in a
manager UI.
