Acquire organization memberships and apps in a PHP process
Availability: verified Token/PHP 0.1.4 ZIP. Its immutable archive includes
the Organizations and Emails classes. This is PHP library
functionality for commands/workers/backend code, not a new m7-identity
executable subcommand. See installation.
Operation and permission
The prepared Token/PHP 0.1.5-rc.1 candidate preserves optional canonical
principal_id on org rows and allowed client.org descriptors after response
verification. This addition is
not part of the immutable 0.1.4 package described above; use an SDK build containing
the updated parser when consuming it. A supplied value must be a non-nil UUID;
duplicate org principal IDs are rejected. Older responses without it remain valid,
and the SDK never derives missing values.
Native id remains the identifier for ordinary API User/SSO calls. Canonical
principal_id is currently for principal lists, rosters, and external ownership
references. It conveys no membership, lease renewal, delegation, or authority to
act for the org. Consumers still enforce their own resource policy.
use M7\Identity\M7IdentitySDK;
use M7\Identity\Token\Organizations\ManagedOrganizationsReport;
// Load the installed Token/PHP autoloader first.
$sdk = new M7IdentitySDK();
$options = [
'issuer' => 'https://sso.user.m7.org',
'client_id' => $verifiedClientId,
'expected_sub' => $verifiedSubject,
];
$report = $sdk->managedOrganizations($accessToken, $options);
if (!$report->ok()) {
throw new RuntimeException($report->reason() ?? 'Organization request failed');
}
$organizations = $report->organizations();
Inputs are an already acquired active access token (string or
M7\Identity\Token\Token) and trusted options. Current SSO policy permits
consumer users, including approved Device callers, ordinary client-credentials
applications, and validated personal machine-owner proxies. SSO rejects tenant
member subjects. Register and explicitly request orgs; consumer authorization
and Device authorization require the hosted permission step. This operation
does not acquire the token or permission. An orgs-only token works here, while
standard UserInfo requires openid.
| Caller | Trusted expected_sub |
Management membership used by SSO |
|---|---|---|
| Consumer user, including Device authorization | Verified consumer user UUID | That user's direct membership. |
| Ordinary client-credentials application | Verified public Client ID | The application's own direct membership. |
| Validated personal owner proxy | Verified proxy public Client ID | Its current registered owner's direct user membership. |
Direct /userinfo/orgs transport and this response-subject binding are already
supported by the immutable Token/PHP 0.1.4 package. Hosted issuance/consent and
proxy eligibility are SSO responsibilities, not new SDK grant handling. The
canonical principal_id projection and receiver_auth path still require
updated source or the prepared 0.1.5-rc.1 candidate.
For an ordinary application token, set trusted expected_sub to its verified public
client_id, rather than its creator's user UUID. Add the app itself to an org's
Management group: its own membership supplies access, role, public data and tags.
Owning the registration does not create membership, and creator permissions are
never inherited. Ordinary application org access requires a live human Management member
in the org, matching API User's rule.
See the SSO endpoint contract.
Consumer Device tokens
A registered Device client can explicitly request orgs. After authentication,
SSO requires Allow on the hosted consent page unless an existing active
grant already covers all requested scopes and audiences. Authentication alone
leaves token pickup at authorization_pending; Deny returns access_denied.
The approved scope ceiling remains bound through pickup and refresh. See
hosted Device consent.
For an already configured native app, CLI/PHP 0.1.3 with Token/PHP 0.1.4 can request the scope during acquisition:
M7_DEVICE_CODE_SCOPE='openid profile orgs offline_access' \
./vendor/m7-identity-cli-php-0.1.3/bin/m7-identity device-code \
--app YOUR_DEVICE_APP_ALIAS
Register every requested scope and keep the app's selected authentication and
sender-binding policy. After approval, call managedOrganizations() with the
active token, registered Device Client ID and verified consumer UUID as
expected_sub. This helper owns neither polling nor token storage; the
Device acquisition workflow
owns those steps. Create a fresh proof for the org endpoint when required.
Device consent grants scope, not Management membership, owner disclosure or
product permissions.
Validated personal owner proxies
A personal admin proxy is an explicit SSO policy exception to ordinary apps'
non-inheritance of creator permissions. Its token must carry the issuer's
signed boolean machine_owner_proxy: true. SSO rechecks the live, unexpired
personal admin registration, client_credentials grant, nil organization and
tenant, and its current active owner. It resolves that owner from the trusted
registration, never a request or owner claim; nested act and m7_delegation
credentials are rejected.
The proxy token and response sub still identify the machine's public Client
ID. Set expected_sub to that verified ID, not the owner's user UUID:
$options = [
'issuer' => 'https://sso.user.m7.org',
'client_id' => $verifiedProxyClientId,
'expected_sub' => $verifiedProxyClientId,
];
$endpoint = $sdk->managedOrganizationsEndpoint($options);
$options['endpoint'] = $endpoint;
// Add a fresh POST proof for this endpoint/token when the proxy is bound.
$report = $sdk->managedOrganizations($proxyAccessToken, $options);
if (!$report->ok()) {
throw new RuntimeException($report->reason() ?? 'Organization request failed');
}
$organizations = $report->organizations();
Only SSO's internal Management lookup switches to the owner's direct user membership. Its role, public data and tags supply the returned facts; the machine's membership, root role or a transitive relationship cannot replace that membership. The SDK neither substitutes the owner as response subject nor performs SSO's registration/owner eligibility checks locally. Direct lookup uses the calling proxy registration for disclosure. Candidate receiver lookup uses the authenticated receiving registration; allowing the proxy does not allow a separate recipient.
The receiving service's owner-proxy ACL remains a separate gate requiring existing active memberships for both proxy and owner. Org lookup enrolls or renews neither membership and grants no resource permission. The immutable 0.1.4 SDK accepts the direct lookup's machine subject and ordinary org projection; canonical references and receiver authentication retain their candidate boundaries. Verify installed SSO policy and availability against the org endpoint contract.
Owner-controlled organization disclosure
SSO filters live Management memberships under each org owner's discovery policy
before returning membership pub, tags or owned app inventories. The requesting
registration row UUID and its current owning org come from the validated token's
client_id, never caller-supplied org/tenant selectors. The requester may be
personal or org-owned; owning a consumer app does not switch its identity directory.
Only stored organization owners may read/write this policy. The ordered app/org
ACL uses the first matching rule across both target types. App rules bind a
registration UUID; org rules bind the requester's owning org UUID. With no match,
same-org requesters use same_org_discoverable; all others use discoverable.
Both switches default off. A matching deny is authoritative; owner/admin/member
roles cannot bypass it. An allow permits disclosure of existing membership facts,
not membership, app management or resource access.
An org owner can opt in the requesting app in User M7 Access → Policy using its public Client ID, or allow its owning org. API User resolves and stores the registration UUID; recreating a Client ID does not transfer an old rule to the replacement registration. See policy editing and target binding.
A successful orgs: [] can mean no eligible memberships or none allowed for this
requesting app. Policy/storage failures produce failed reports, not successful
empty results. Default-off rollout requires owner opt-in even for existing orgs.
The SDK cannot override disclosure policy, and imported local org identities do
not bypass a fresh policy check.
Trusted endpoint and proof creation
managedOrganizationsEndpoint(options): string resolves the HTTPS URI before
the caller creates a proof. managedOrganizations(token, options) returns
ManagedOrganizationsReport. Both require nonempty trusted client_id and
expected_sub; never derive trust policy from an unvalidated token or request.
| Option | Contract |
|---|---|
issuer |
Exact HTTPS issuer, default https://sso.user.m7.org. |
endpoint |
Optional trusted absolute HTTPS URI, without credentials/query/fragment. Omit for discovery. |
method |
POST default; GET also supported for ordinary org lookup. Receiver-authenticated lookup requires POST. |
receiver_auth |
Prepared 0.1.5-rc.1 candidate: confidential receiving-app credentials for /introspect/orgs; separate from caller token and client_id. |
dpop |
Only proof: a caller-created fresh proof for the exact URI/method/token. |
client_certificate |
Existing UserInfo certificate/key configuration for mTLS. |
response_encryption, recipient, leeway |
Existing UserInfo encryption/verifier policy. |
connect_timeout, timeout, max_response_bytes |
Positive bounds; defaults 5 seconds, 10 seconds, 65,536 bytes. |
Unknown options and downstream proof-relay inputs fail before network access.
Discovery fetches the issuer's OpenID document, requires exact issuer equality,
and selects m7_orgs_endpoint; with a certificate it selects the top-level
m7_mtls_orgs_endpoint. Discovered endpoints must match issuer host and port;
canonical M7 consumer mTLS uses the dedicated mtls-sso.user.m7.org host.
An unsafe/missing endpoint fails before the token is sent. An explicit endpoint
is trusted deployment configuration, not request-selected discovery bypass.
For a DPoP-bound token:
$endpoint = $sdk->managedOrganizationsEndpoint($options);
$options['endpoint'] = $endpoint; // Fix the resolved target for this call.
$options['method'] = 'POST';
$options['dpop'] = ['proof' => $freshOrgsProof];
$report = $sdk->managedOrganizations($accessToken, $options);
Create $freshOrgsProof after resolution, with the original bound key,
exact endpoint, POST, fresh jti and ath for this access token. Proofs for
standard UserInfo or a consuming API are different. Caller code owns proof
generation and explicit nonce recovery: inspect error()/dpopNonce(), create
a new proof carrying the challenge nonce, then deliberately make another call.
The SDK neither retries nor creates proofs. Current SSO has no nonce policy;
reports still preserve upstream challenges. Endpoint resolution throws on failure;
retrieval returns a failed report for discovery/upstream failure. Invalid trusted
configuration throws before transport.
Receive a caller's organization facts
The prepared Token/PHP 0.1.5-rc.1 candidate adds a receiver-authenticated form of
managedOrganizations(). It is absent from the immutable 0.1.4 ZIP. A receiving
PHP service can ask SSO for the inbound caller's organization projection using
its own confidential credentials; this does not make the receiver the subject.
$options = [
'issuer' => 'https://sso.user.m7.org',
'client_id' => $verifiedCallerClientId,
'expected_sub' => $verifiedCallerSubject,
'receiver_auth' => [
'method' => 'client_secret_basic',
'client_id' => $receiverClientId,
'client_secret' => $receiverClientSecret,
],
];
$endpoint = $sdk->managedOrganizationsEndpoint($options);
$options['endpoint'] = $endpoint;
// A bound caller supplies a fresh POST proof for this endpoint and token.
$report = $sdk->managedOrganizations($inboundToken, $options);
if (!$report->ok()) {
throw new RuntimeException($report->reason() ?? 'Organization lookup failed');
}
$organizations = $report->organizations();
client_id identifies the verified caller application; expected_sub binds
its verified user or application subject. receiver_auth.client_id identifies
the receiving OAuth application. The SDK POSTs the exact inbound token as
token and caller application as caller_client_id, with Basic/Post or
tls_client_auth/self_signed_tls_client_auth receiver authentication. No
Bearer authorization header substitutes for those receiver credentials.
Omitting endpoint selects the trusted issuer's /introspect/orgs path;
consumer mTLS selects https://mtls-sso.user.m7.org/introspect/orgs. Explicit
configuration must use the same trusted host, port and exact path. GET,
none, untrusted destinations and a separate top-level client_certificate
are rejected before transport; certificate custody belongs inside
receiver_auth. Resolve this different endpoint before producing a fresh
proof. The SDK forwards caller dpop.proof but does not create it or retry.
Response acceptance binds receiver_client_id exactly to the authenticated
receiver, sub to expected_sub, and signed-response audience to the receiver.
It then applies the normal org projection checks, including canonical
principal reference and duplicate checks. A missing/foreign receiver binding,
malformed response, scope denial or failed transport yields a failed report
with no accepted organizations. Successful empty results remain valid.
The receiving service still verifies the inbound credential and its sender proof, and enforces scope, disclosure and resource policy. This operation does not persist organization results or authorize access to product data. Keep receiver secrets in server configuration. See the SSO organization introspection contract for server prerequisites and availability; local candidate tests establish SDK transport and response handling only.
Accepted authorization facts
The client first accepts HTTP 200 JSON or verifies the signed/encrypted UserInfo
response policy and exact expected_sub. Signed JWT verification checks trusted
discovery/JWKS, RS256/RS512 key/signature, issuer, client-ID audience, time and
subject; encryption authenticates/decrypts before those checks. See
response encryption for recipient runtime and policy.
The report's client() is separate from organizations(). For a personal
consumer app it is ['org' => null, 'tenant' => ['id' => '00000000-0000-0000-0000-000000000000', 'slug' => 'm7-identity-consumer']].
An org-owned app may disclose org: {id, slug} and a tenant identity or null,
according to its owning org's policy. A policy-hidden client is null. The SDK
validates the personal marker and strips extra client fields; orgs can still
be populated when client() is null or identifies a personal app.
Each org has UUID id, nonempty name/slug; current orgs additionally have
access_level and management_groups. Each Management entry carries UUID
group id, nonempty name, UUID membership_id, role, object/array/null
pub, and a tags list. Each tag has UUID id, nonempty name/slug, and
object/array/null definition data. Role/access values must be exactly
owner, admin, or member. Nil/malformed UUIDs and duplicate org/group/
membership/tag IDs (case-insensitive in their collections) fail closed.
Missing pub, invalid/scalar data, partial access fields or an empty supplied
management group list fail without exposing partial organizations.
Each current org also carries consumer and tenant app lists. Each descriptor
has UUID id, nonempty public client_id and nonempty name; client_id need
not be a UUID. These are the returned org's owned active, unarchived, unexpired
registrations, split by nil/non-nil tenant directory. Both lists can be empty.
Personal apps are excluded. The SDK strips extra app fields and rejects partial
list pairs, non-list values, malformed descriptors and duplicate app UUIDs or
public client IDs across either list. App UUID comparison is case-insensitive;
public Client ID comparison is exact and case-sensitive. Older responses omitting both app lists
remain valid; the SDK preserves their absence.
Use the lists to identify related apps by org ownership. They convey no app membership or grant to call another app. A human or app caller's Management membership still determines which orgs are returned and its access facts.
The current provider returns one implicit Management group per org. The SDK
validates collections without promising a new group-creation API. Assignments
and pub belong to the caller's specific membership, not the global principal;
tag data belongs to the individual definition. Whitelisted projection excludes
other memberships, pri, owner/principal IDs, timestamps and internal metadata.
Service-defined contents inside pub and tag data remain intact. Associative
PHP JSON decoding turns an empty object into []; null remains null and no
assigned tags gives []. There are no membership expiry fields.
Original identity-only {id,name,slug} rows remain valid. The client does not
synthesize missing access fields. If either access field is present, both must
be valid; legacy tags without data retain its absence. Missing authorization
facts grant nothing. Consumers define their own role/tag/data meanings and
check resource authorization. A membership snapshot is not durable authority.
Reports and lifecycle ownership
ManagedOrganizationsReport exposes ok(), reason(), httpStatus(),
subject(), client(), organizations(), response(), error(), errorDescription(), headers(),
header(name), dpopNonce() and toArray(). A successful empty list is valid.
Every failed report has organizations(): [] and subject(): null; OAuth errors
and nonce challenges remain available. Reports contain no token/proof credentials.
Do not substitute empty permissions for transport, storage or validation failure.
In current source, response() exposes the complete decoded SSO payload only
after all UserInfo and organization checks pass; it returns [] on failure.
Transparent proxies can use it to retain extensions and field absence under
SSO's disclosure policy. The existing client(), organizations() and toArray()
projections retain their whitelist. This addition is not yet a published artifact.
The operation owns no session, refresh, token acquisition, proof creation, retries or organization-result persistence/cache. Separate optional SDK validation and certificate caches retain their own policies; this statement concerns the org operation. Reacquire live caller facts when a service needs them.
Installation and release boundary
Use the base PHP installation to checksum and extract the Token/PHP 0.1.4 ZIP. Its facade constructs org/email clients, and the archive includes all three Organizations and both Emails classes. Clean extraction checks verify the default facade and these operations. Older versioned ZIPs remain immutable; install 0.1.4 for this API.
For direct PHP use, load packages/token-php/autoload.php from the SDK checkout.
For shared platform startup, configure the roots as deployment paths:
export M7_LIB_ROOT="/opt/m7/m7-php-lib"
export M7_PLATFORM_ROOT="/opt/m7/m7-php-platform"
export M7_IDENTITY_TOKEN_ROOT="/opt/m7/m7-identity-sdk"
The platform bootstrap at require/startup/v1/bootstrap.php requires the
library namespace.php, platform require/startup/, and SDK
packages/token-php/autoload.php; it maps lib\, platform\ and loads
both Identity namespaces through the token autoloader. Set these values in the
actual PHP worker/CLI environment and preserve project-specific startup config.
The standalone CLI executable uses the same M7_IDENTITY_TOKEN_ROOT name for
the Token/PHP package directory containing autoload.php, a different
layout: do not copy that CLI value into platform startup. Use separate process
configuration or the bootstrap's explicit identityTokenRoot input.
Token/PHP declares PHP 8.1+ and OpenSSL. The checks here ran on PHP 8.4; PHP 8.1 was not rerun for this documentation check. cURL is preferred with the supported streams fallback; optional encrypted responses require the separate documented native runtime. Installing org acquisition alone does not require local SQL or native crypto.
For the complete API User storage → SSO → SDK → platform → API BigFS → BigFS
sequence, see upgrade order.
The platform Organization::available() carries live facts; import() and
refresh() pass only org ID/name/slug to local reference storage. No caller
scope/tag/pub authorization snapshot is persisted by that import boundary.
Related operations and validation
Account emails use an API User bundle and a distinct
permission. Standard UserInfo primary email remains openid email.
For browser-backed callers, see website integration.
Current-source ManagedOrganizationsTest, account-email tests, shared UserInfo
and encryption tests verify the projection, trust and failure boundaries;
synthetic/local tests and source extraction do not establish deployment.