# Organization memberships and applications

```text
GET or POST https://sso.user.m7.org/userinfo/orgs
```

This M7 extension is separate from standard `/userinfo`. Request the registered
OAuth scope `orgs` explicitly. Consumer authorization requires hosted consent;
client-credentials grants use the application's registered scope policy. An
`orgs`-only token can call this endpoint; standard UserInfo still requires `openid`. Omission of scope does not
implicitly grant `orgs`. Active consumer users and ordinary client-credentials
applications can discover their own Management memberships across organizations.
Tenant member tokens and personal machine-owner proxies are ineligible. Tenant
identity/groups remain fenced to their own directory. This is a
caller-requested lookup of existing organization facts. SSO does not push
organizations into a consuming server or grant that server access to its
resources.
Device authorization currently rejects `orgs` until its approval flow supports
the permission. See the [normative provider profile](https://m7.org/docs/api/sso.user.m7.org/provider-profile.md#consumer-organization-discovery).

## Request and trust

Resolve `m7_orgs_endpoint` from trusted exact-issuer discovery. For an unbound
token:

```bash
curl --fail-with-body --silent --show-error \
  --request POST 'https://sso.user.m7.org/userinfo/orgs' \
  --header 'Authorization: Bearer ACCESS_TOKEN'
```

Use the [UserInfo request contract](https://m7.org/docs/api/sso.user.m7.org/userinfo.md#request) for owning `client_id`
assertions, token transport, fingerprint and sender bindings. The provider derives
the client from the token and checks live token/session/account state, registered
scope/audience policy, and active organization ownership of a client where relevant.
No separate SSO resource audience is added by this endpoint. The credential
must be an access token; an ID token, refresh token, authorization code or API key
does not authorize this lookup. Verify the response `sub` exactly against the
already validated user subject or, for an ordinary app, its public Client ID.

For DPoP use `Authorization: DPoP ACCESS_TOKEN` and a fresh `DPoP` proof binding
this endpoint's exact URL, actual GET/POST method and access token `ath`; a proof
for `/userinfo` or API BigFS cannot be reused. For certificate-bound tokens,
present the bound certificate at the top-level `m7_mtls_orgs_endpoint` from
discovery. It is an M7 extension outside the standard `mtls_endpoint_aliases`
object. When both bindings apply, both are required. See
[discovery](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#organization-endpoint-metadata).

## Current projection

```json
{
  "sub": "22222222-2222-4222-8222-222222222222",
  "client": null,
  "orgs": [
    {
      "id": "11111111-1111-4111-8111-111111111111",
      "name": "Example",
      "slug": "example",
      "access_level": "member",
      "consumer": [
        {
          "id": "66666666-6666-4666-8666-666666666666",
          "client_id": "docs-client",
          "name": "Docs"
        }
      ],
      "tenant": [],
      "management_groups": [
        {
          "id": "33333333-3333-4333-8333-333333333333",
          "name": "Management",
          "membership_id": "44444444-4444-4444-8444-444444444444",
          "role": "member",
          "pub": {
            "quota": 7
          },
          "tags": [
            {
              "id": "55555555-5555-4555-8555-555555555555",
              "name": "BigFS read",
              "slug": "bigfs-read",
              "data": {
                "actions": [
                  "read"
                ]
              }
            }
          ]
        }
      ]
    }
  ]
}
```

These UUIDs are valid illustrative identifiers; replace them with actual records.
Each org currently has one implicit Management group. This response does not
introduce arbitrary management-group creation. `access_level` and membership
`role` are the caller's live stored `owner`, `admin` or `member` role. The list
is sorted by org ID and can be empty (`orgs: []`). Only eligible active,
non-archived orgs, active Management groups/principals and matching current
user or application memberships survive the live lookup. A departure or removed
membership is omitted on the next lookup.

`client` describes the OAuth registration that issued this token, independently
of the user's `orgs` list. A personal consumer registration returns
`{"org":null,"tenant":{"id":"00000000-0000-0000-0000-000000000000","slug":"m7-identity-consumer"}}`.
For an org-owned registration, the owning org's disclosure setting controls
whether `client` contains `org: {id, slug}` and `tenant: null` (consumer app) or
`tenant: {id, slug}` (tenant app). A policy-hidden org-owned registration returns
`client: null`; it does not change which Management memberships appear in `orgs`.

The org owner also controls disclosure to the **requesting OAuth registration**.
SSO resolves its row `id` and current owning `org` from the validated token's
`client_id`; request-supplied identities and token `tenant` cannot select the
policy identity. Requesting apps may be personal or org-owned. The caller's
owner/admin/member role does not bypass disclosure policy.

For each eligible membership, SSO evaluates the org's ordered app/org ACL; the
first matching rule wins, without app-specific precedence. App rules match the
registration row UUID, and org rules match its non-nil owning org. With no
match, a requester owned by the disclosed org uses `same_org_discoverable`;
other requesters use `discoverable`. Both switches default off, including on
existing orgs with no saved policy. An explicit deny overrides the switches.
Only stored org owners can manage these settings through API User and the
User M7 **Access → Policy** tab.

Denied orgs are omitted entirely before loading public membership data, tags
or app inventories. An allow rule permits disclosure of existing membership
facts; it cannot create membership or grant product access. Invalid policy or
failed storage fails the response. SDK and consuming applications receive the
same projection shape, filtered by this live policy; a successful empty list
may mean no memberships are discoverable to this app.

For a consumer token, `sub` is the user UUID. For an application token, `sub` is
its public `client_id`; the internal Management principal source is the OAuth
registration's row UUID. The provider authenticates the app as itself and checks
its owning org remains active, when it has one. App access additionally requires
at least one live human Management member in each returned org, matching API
User's application-access rule. The creator `uid` and owning `org` do not grant
Management membership. Add the app explicitly to Management to grant access;
its own membership supplies its role, `pub` and tags. Ordinary apps may back
either directory and may hold memberships in orgs other than their owning org;
tenant identity binding is validated independently of those memberships.

| Check | What it establishes |
| --- | --- |
| Registration `org` | Who owns/manages the app; not the caller's memberships |
| Caller Management membership | The live role, membership `pub` and assigned tags in this org |
| Owner discovery policy | Whether those facts may be disclosed to this requesting app |
| Consuming-service policy | Whether the authenticated caller may operate on a bucket or other resource |

Both `consumer` and `tenant` are always arrays, including when empty. They list
live **app registrations owned by each returned org**, not users, tenant-account
memberships or the caller's memberships in those apps:

- `consumer`: `oauth_clients.org = org.id` and nil `tenant`.
- `tenant`: `oauth_clients.org = org.id` and non-nil `tenant`.

Only active, unarchived, unexpired registrations appear, sorted by row UUID.
Each descriptor contains `id` (row UUID), `client_id` (public OAuth identifier)
and `name`. OAuth registrations have no `slug`; `client_id` need not be a UUID.
Personal apps (`org` nil) are excluded even when created by the caller. Secrets,
creator IDs, configuration and notes are excluded. Inventory shows which apps
share ownership; it does not grant those apps access to each other or substitute
for a caller's Management membership.

| Data | Meaning |
| --- | --- |
| Membership `pub` | `organization_manage_group_meta.pub`, with `asset` equal to the caller's Management membership UUID. |
| Tag `data` | JSON on this individual tag definition, shared by assignments in its org's separate tag pool. |
| Organization `data` | Separate organization-level data; read through authorized API User org properties, not this projection. |

Tags use scope `organization_management` / org UUID and assignment asset
`organization_management_member` / membership UUID, not the global principal.
`pub` and tag `data` accept object/array/null. Missing public data is null;
no assignments gives `tags: []`. Current associative PHP decoding serializes
empty objects as `[]`. The SDK preserves service-defined JSON contents without
giving them universal product permission semantics.

Other principals' memberships, `pri`, internal owner/principal/creator fields,
timestamps, assignment metadata and expiry fields are excluded. Membership expiry
was not added. Public means exportable through this authorized call, not anonymous
org access. Storage failures return an error, never a successful empty list.

## Response policy and failures

HTTP 200 normally uses `application/json`. Registered UserInfo signing policy
returns `application/jwt` with RS256/RS512 signature, exact provider issuer,
client-ID audience and subject; registered encryption applies to that signed
response. Decrypt/authenticate first, then verify signature, issuer, audience,
time and exact subject. Policy, signer or encryption failures never fall back to
plaintext. See [response encryption](https://m7.org/docs/api/sso.user.m7.org/response-encryption.md). Responses are
non-cacheable (`Cache-Control: no-store`).

Missing `orgs` returns HTTP 403 `insufficient_scope` and a required-scope
challenge. Invalid/inactive tokens, ineligible subjects or sender-binding
failures follow the [UserInfo error contract](https://m7.org/docs/api/sso.user.m7.org/userinfo.md#request), including
`invalid_token` and `invalid_dpop_proof`. Operational lookup/signing failures use
a safe `server_error`; do not turn them into empty authorization facts. The
current SSO profile does not issue DPoP nonces, while SDK reports preserve a nonce
challenge if an upstream deployment supplies one for explicit caller recovery.

## Integration and rollout

Use [current-source SDK acquisition](https://m7.org/docs/sdk/m7-identity/cli/organization-access.md)
or the [website integration](https://m7.org/docs/sdk/m7-identity/website/organization-access.md).
The SDK performs strict nested validation and exact subject binding. Seeing an
org or a tag does not grant bucket, blog or other product access: consuming
services define scope semantics and authorize resources separately. Reacquire
facts when needed instead of persisting a membership snapshot as authority.

Both public discovery documents advertised `orgs`, `m7_orgs_endpoint` and
`m7_mtls_orgs_endpoint` with identical metadata at 2026-09-27 02:11 UTC.
Current-source projection and validation checks passed locally; live acceptance
of the membership/application projection remains unverified here.
Install API User's existing membership/tag storage before enabling this reader;
SSO installs no org tables. See
[upgrade order](https://m7.org/docs/api/api.user.m7.org/organization-upgrade.md).
