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

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' => $verifiedConsumerSubject,
];
$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 consumer or ordinary client-credentials access token (string or M7\Identity\Token\Token) and trusted options. Register and explicitly request orgs; consumer authorization also requires hosted consent. This operation does not acquire the token or permission. An orgs-only token works here, while standard UserInfo requires openid. Tenant member tokens and personal machine-owner proxies are ineligible. Device authorization currently rejects orgs.

For an 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. Application org access requires a live human Management member in the org, matching API User's rule. See the SSO 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.
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.

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(), 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.

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.

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.