Organization memberships and applications
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.
Validated personal machine-owner proxies can discover their current registered
owner's direct Management memberships. Tenant member tokens 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 requires the same permission through its hosted
device consent page after authentication.
See the normative provider profile.
Request and trust
Resolve m7_orgs_endpoint from trusted exact-issuer discovery. For an unbound
token:
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 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.
Current projection
{
"sub": "22222222-2222-4222-8222-222222222222",
"client": null,
"orgs": [
{
"id": "11111111-1111-4111-8111-111111111111",
"principal_id": "9D91E1C7-700E-55C0-91ED-EA9C7B1F7D98",
"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.
id remains the native organization UUID used by API User and SSO. principal_id
is the organization's stable canonical M7 reference for principal lists, rosters,
and external resource ownership. It does not replace the native org ID in ordinary
org, directory, session, or token operations. The example principal ID is illustrative;
consumers must use the verified provider value rather than derive their own.
The shared org service registers this reference at creation and ensures it again
when an authorized org projection is exported, covering older organizations.
Both paths use the existing native org UUID as source, source_kind=org, and a
nil UUID tenant in the central registry. Repeated ensures preserve the same ID
and existing registry metadata. Registration does not create list or Management
membership, renew a lease, grant delegation, or authorize a user or app to act for
the org. Existing disclosure and access checks still apply. A failed ensure fails
the response through its existing operational error contract.
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 separate
client_org_userinfo_disclosure setting controls
whether client contains org: {id, slug, principal_id} 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 exact modes are always, acl, and never (the default). always
permits an otherwise eligible registration summary; never hides it; acl
applies the owner’s ordered discovery policy to the calling registration, or
the authenticated recipient on /introspect/orgs. This setting is independent
of both the membership-list disclosure policy and client_org_release, which
controls newly issued token ownership descriptors. An allowed client summary
does not add the caller to an organization.
The owning-org reference is ensured only after disclosure is allowed. Personal
consumer markers and tenant directory descriptors retain their native ID shapes.
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.
Personal owner proxies are a separate, explicit case. The token must carry the
issuer-controlled boolean machine_owner_proxy: true, retain the machine's
public client_id as its subject, and pass live token and registration checks.
The registration must be active and unexpired, permit client_credentials, have
admin_application: yes, and have nil org and tenant. SSO resolves the active
owner from that registration's current uid, never an owner claim or request
parameter. The validated effective user and registered owner must agree.
Nested act or m7_delegation credentials are rejected.
The proxy's response sub remains the machine's public Client ID. Only the
internal Management lookup uses the owner's user UUID; the owner's direct
membership supplies the role, pub and tags. Missing owner membership is not
replaced by the machine's membership, root role, or transitive delegation.
Disclosure remains independent: direct /userinfo/orgs calls use the calling
registration, while recipient-authenticated /introspect/orgs calls use the
receiving service's trusted registration. Allowing the calling proxy in the
disclosure policy does not allow an unrelated receiving service. The receiving
service's separate owner-proxy ACL
still requires existing memberships for both the machine and its owner; this
org lookup neither enrolls nor renews 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.idand niltenant.tenant:oauth_clients.org = org.idand non-niltenant.
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. 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, 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.
Receiving-service organization lookup
Current source provides POST https://sso.user.m7.org/introspect/orgs for a
confidential receiving service. Deployment and live acceptance of this route
remain unverified here. It returns the same caller-bound organization facts
as /userinfo/orgs, using the receiving registration for disclosure decisions
and response signing/encryption. Caller-oriented discovery cannot establish
disclosure permission to a different service.
Authenticate the recipient with its configured confidential method: Basic,
Post, secret JWT, private-key JWT, PKI mTLS, or self-signed mTLS. The
introspection client-authentication contract
applies, including assertion audience/time/replay checks and IP policy.
Public none clients cannot call this route. Both the recipient and its
owning organization, when present, must remain active.
Send an application/x-www-form-urlencoded body:
| Field | Contract |
|---|---|
token |
Required caller access token, carrying orgs. It identifies the caller, independently of recipient client authentication. |
caller_client_id |
Optional exact-match assertion of the token’s requesting client_id; never selects another caller. |
fingerprint |
Required when the caller token has the corresponding fingerprint binding. |
| Client authentication fields | As required by the recipient’s registered method; body client_id identifies the recipient. |
Example for a confidential recipient and an unbound caller token:
curl --fail-with-body --silent --show-error \
--request POST 'https://sso.user.m7.org/introspect/orgs' \
--user 'RECEIVER_CLIENT_ID:RECEIVER_CLIENT_SECRET' \
--data-urlencode 'token=CALLER_ACCESS_TOKEN'
The recipient authentication header does not authenticate the caller token.
Caller DPoP still needs a fresh matching-key proof for POST and this exact
endpoint URL, including the token’s ath. A resource-request proof cannot be
replayed here. Certificate-bound caller tokens require their bound certificate
on the actual verified TLS connection; recipient client authentication does
not remove that requirement. Tokens carrying both bindings require both.
SSO checks the caller token’s live status and exact signed resource audience
against the recipient’s registered resource audiences. The receiver Client ID
need not appear in aud; an arbitrary submitted audience grants no access.
The caller must retain its own qualifying direct Management membership (or
the registered owner’s membership for a validated personal owner proxy). Each
organization must separately allow disclosure to the recipient registration.
HTTP 200 returns sub, client, orgs, and receiver_client_id, which must
equal the authenticated recipient’s public Client ID. sub remains the
caller subject. The client summary describes the caller registration, filtered
by recipient disclosure; it is not the recipient registration. The recipient’s
registered UserInfo policy determines JSON, signed JWT, or encrypted signed
JWT delivery. Verify recipient audience and exact caller subject after any
decryption and signature verification. Treat the result as fresh facts, then
apply local resource and action authorization.
Missing token or invalid caller assertion returns HTTP 400 invalid_request;
caller mismatch, missing orgs, or recipient-audience mismatch returns HTTP
403 insufficient_scope. Inactive/ineligible callers and sender-binding
failures use the existing token/DPoP errors. Invalid client authentication,
recipient eligibility, IP policy, method, lookup, and response-policy failures
retain their existing OAuth error contracts. Errors are never empty successful
facts. Owner denial can legitimately produce orgs: [].
This route is separate from standard token introspection and the receiver’s principal-list ACL decision. No new discovery extension or live mTLS alias is asserted for this route; verify listener availability and the installed SDK version separately.
Integration and rollout
Use current-source SDK acquisition
or the website integration.
Published Token/PHP 0.1.4 supports basic Organizations and Emails lookups.
Canonical principal_id handling and receiving-service organization lookup
require updated SDK source or the locally built and verified 0.1.5-rc.1
candidate. That candidate is not a public release; verify the installed version
and endpoint availability. The updated 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.