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.

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.