# 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](https://m7.org/docs/sdk/m7-identity/cli/organization-access.md#installation-and-release-boundary).

## Operation and permission

```php
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](https://m7.org/docs/api/sso.user.m7.org/userinfo-orgs.md).

## 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](https://m7.org/docs/api/api.user.m7.org/org.md#organization-discovery-policy).

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:

```php
$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](https://m7.org/docs/sdk/m7-identity/response-encryption.md) 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](https://m7.org/docs/sdk/m7-identity/cli/installation.md) 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:

```bash
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](https://m7.org/docs/api/api.user.m7.org/organization-upgrade.md).
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](https://m7.org/docs/sdk/m7-identity/cli/account-emails.md) use an API User bundle and a distinct
permission. Standard UserInfo primary email remains `openid email`.
For browser-backed callers, see [website integration](https://m7.org/docs/sdk/m7-identity/website/organization-access.md).
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.
