Authorize an inbound machine at a PHP service

Token/PHP 0.1.4 supports two online operations. introspect() asks an authorization server for standard RFC 7662 token activity. M7-specific introspectAcl() asks SSO for the receiving application's current principal-list decision on an inbound machine token. The latter returns a receiver-list principal. Ordinary-machine checks can enroll that machine; personal admin owner proxies require two existing memberships as described below. The live Token/PHP 0.1.4 ZIP and its integrity sidecars were verified on 2026-09-30; its clean-extraction conformance suite exercises this ACL operation.

Set up the receiving application

Bind the receiving OAuth application to an active personal or organization principal list through API User. Choose the initial Active, Pending, or Disabled membership status, optional rejection switches, and ordered app/org ACL rules. App rules match the issuing machine's registered app; org rules match its current owning organization. The first matching rule selects the policy status, otherwise the initial status applies. The list's consumer or tenant human pool is fixed; machine principals can be members of either kind of list. List membership is separate from organization Management membership.

Register the exact resource audience accepted by the receiving application. The machine app obtains a client-credentials access token with that audience and sends it to the receiving resource. SSO requires at least one value in the token's signed aud to match one of the authenticated receiver's current allowed_audiences rows. The receiver's public Client ID need not appear in the token's aud. The receiver authenticates to SSO with its own confidential client credentials; callers cannot select another receiver's policy by a form field. Resolve m7_acl_introspection_endpoint from trusted SSO discovery, or the top-level mTLS ACL extension for certificate client authentication.

Check the request

$report = $identity->introspectAcl($inboundToken, [
    'auth' => [
        'method' => 'client_secret_basic',
        'client_id' => $receiverClientId,
        'client_secret' => $receiverClientSecret,
    ],
]);

if (!$report->allowed()) {
    throw new RuntimeException('Inbound machine access denied');
}
$principal = $report->principal(); // receiver list: id, status, list
$confirmation = $report->confirmation(); // cnf when sender-bound

The SDK sends the raw inbound token to SSO's POST /introspect/acl as the token form field and returns an AclIntrospectionReport. ok() means a valid M7 ACL response was received; tokenActive() reports verified activity; allowed() is true only for an active token and active effective ACL status. Pending, disabled, expired, unbound, malformed, and failed requests deny. For an ordinary machine, principal() identifies the machine in the receiving list, independently of any m7.principal claim in the inbound token. An active result carries the current stored role and data even if the receiving application's two token-enrichment switches are off. Pending or denied results omit both. The list's selectable roles and default govern assignment separately; removing a choice does not erase an existing member's stored role from this live result. An inbound token's own role and data, if enriched at issuance, remain a snapshot.

On a verified qualifying ordinary-machine check, SSO can create the stable principal and list membership. A rule's optional token_expires_after_seconds proposes a sliding deadline for principal_lists.expires, bounded by the inbound token's signed expiry. Later qualifying checks extend the stored expiry only when the new deadline is later; a disabled member is not reactivated. This is shared list state and can be seen by other applications using the list. The decision's cache deadline is also token-bounded. Neither deadline changes or revokes a token already issued. Call the ACL endpoint on every protected request when policy removal must take effect immediately.

If confirmation() contains jkt, validate the DPoP proof on the inbound resource request: signature, method, URL, access-token hash, freshness, and replay. If it contains x5t#S256, compare the certificate presented to the resource's trusted TLS boundary. Check both when both are present, and reject unsupported confirmation shapes. SSO cannot verify this proof because it does not receive the resource request. The resource also enforces its own route, scope, object ownership, and business permissions after the ACL decision.

Personal admin owner proxies

A signed boolean machine_owner_proxy: true marks a personal admin owner proxy. SSO verifies the exact token, current scope/audience policy and active personal admin registration with the client_credentials grant. It resolves the current registered owner account and requires both the proxy app and owner user to have existing active, unexpired memberships in the same receiving consumer list. Both principal records and the owner account must also be active. This path never enrolls either actor or extends a lease.

An allowed response keeps the standard principal shape but returns the owner user's global principal ID, role, and data. The proxy membership only gates access; its role contributes no permission. The additional binding is available through the existing report accessors:

$principal = $report->principal();
$proxy = $report->value('owner_proxy');
$traceId = $report->value('trace_id');
Binding field Meaning
client_id Proxy app's public OAuth Client ID.
principal_id Proxy app's global list-principal ID.
owner_uid Current registered owner's consumer-account UID.
owner_name Owner account name, or null.

The SDK validates the base ACL response shape and retains the binding; it does not validate the additional owner binding or rewrite the token's machine subject. Direct consumers must interpret and validate the binding before assigning the effective user. Match its client_id to the validated token's machine identity (id, sub, and client_id). Use this binding only for a verified signed boolean machine_owner_proxy: true; reject missing, malformed, or unexpected bindings and require non-nil UUIDs for the proxy principal and owner UID. If the token carries a signed m7.principal.id, match that ID to owner_proxy.principal_id; the returned principal.id belongs to the user. Updated shared M7Roster performs these checks and supplies kind=user, id=principal.id, and rid=owner_proxy.owner_uid to the receiving app.

A failed membership gate returns an active-token authorization denial with allowed: false, policy_status: null, and no principal or owner binding. An optional trace_id is a support reference. Token/profile rejection returns token_active: false. Ordinary-machine responses omit owner_proxy. The owner-proxy decision expiry is bounded by token expiry and both membership leases; its membership status/expiry fields describe the owner membership.

This response handling uses the existing Token/PHP 0.1.4 report accessors; server acceptance follows SSO's current owner-proxy ACL contract. The resource still validates inbound sender proof and its own permissions. Current M7Roster refuses sender-bound tokens until inbound proof enforcement is connected.

Serverless entry for a consumer service

M7 can provide identity, list enrollment, policy decisions, and a shared expiring membership record, reducing account and principal administration a consumer service must build. The receiving service still handles trusted credential configuration, inbound sender proof, request authorization, resource state, and its own permission model. Mail currently has an isolated ACL test route; that route does not replace Mail's internal authorization or establish universal production enforcement.

See API User principal lists for list and app configuration and SSO authorization for the endpoint response and failure contract.