CLI and background-process integration
cli-php is the dedicated M7 Identity command-line adapter. It supplies the
m7-identity executable over the storage-neutral OAuth and identity protocol
operations in token-php; it is not a second protocol implementation. The CLI
package is available as the stable 0.1.1 artifact and requires the stable
token-php >=0.1.2 <0.2.0 package.
The stable executable supports these command families:
| Command | Supported CLI responsibility |
|---|---|
app |
Create, inspect, update, copy, and delete protected local OAuth app records. |
profile |
Manage named grant profiles beneath an app. |
password |
Store or import an optional OAuth client password without treating it as an M7 account password. |
token |
Import, inspect, promote, and delete protected complete token-bundle generations. |
client-credentials |
Acquire and optionally persist an application access-token bundle through token-php. |
device-code |
Start or resume device authorization, show the approval coordinates, poll on the authority-directed schedule, and optionally persist the resulting bundle. |
exchange |
Exchange a complete refresh package and persist the active or pending replacement when an app is selected. |
ack |
Acknowledge a retained pending package and promote the exact stored generation only after authority success. |
ensure |
Reuse or renew a managed app token and durably apply any required bundle transition. |
request |
Ensure a managed token or accept protected explicit credentials, then send one authenticated HTTPS resource request. This is the authenticated-request command. |
The app, profile, password, and token commands operate on the protected
app -> grant -> named-profile keychain beneath ~/.m7/identity. An app name is
a store-wide unique local alias for a registered client ID; it is not scoped to
one grant or profile. app list is the safe alias-to-client-ID lookup surface,
while operational commands select the explicit app alias. A whole-app copy may
intentionally give the same client ID another alias, so a client ID is not
assumed to identify one unique local app.
Ordinary selection defaults to the profile named default. App and profile
copy require explicit --src and --dst, deep-copy every protected file in
scope—including passwords and other secrets, active tokens, pending packages,
and history—and atomically refuse existing destinations. Copy does not print
credentials, hard-link protected files, or alter authority state. Keychain
directories use mode 0700; records, locks, password files, and token bundles
use mode 0600.
For repeatable registration synchronization, app upsert and profile upsert create missing records or replace their complete editable definitions.
They never recursively merge stale fields. Existing passwords, token
generations, history, and creation timestamps remain untouched; rebinding an
existing local app name to a different client ID is refused.
The client-credentials command consumes an app and its default or named client-credentials profile:
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity client-credentials \
--app user.m7.org-cc \
--interactive
Resolution is environment, command option, selected profile, then prompt or default. Profile configuration preserves absent versus explicit-null fields; null suppresses an optional prompt, while a required null fails. An optional OAuth client password can be stored per profile through a hidden prompt or imported JSON, and client credentials consumes it automatically. A successful app-selected acquisition stores the complete response under that profile before output and archives the previous active generation.
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity password set \
--app user.m7.org-cc \
--grant client-credentials \
--password
Protected file and stdin imports avoid argv exposure. Inline --json import is
also supported when the operator accepts shell-history and process-listing
exposure. The password command stores an OAuth client credential, never an M7
user's account password, and never reveals it in status or mutation output.
This public guide covers client credentials, device authorization,
authenticated requests, managed ensure, profile/password/token setup, copying,
protected bundle handling, strict rotation, regular ACK, and fault-tolerant ACK
supersession. Each command also includes concise examples in its built-in
--help output.
Capabilities
The stable token-php 0.1.2 release provides:
- strict compact JWT parsing;
- trusted-certificate resolution and RSA signature verification;
- issuer, time, audience, claims, and configured-scope validation;
- optional DPoP proof validation for a known request context;
- portable cache interfaces for certificates and successful validation;
- OAuth client-credentials access-token acquisition for confidential clients;
- OAuth device authorization and one-attempt token polling for public or confidential clients;
- complete M7 refresh-package exchange and two-phase ACK activation for user-authorized clients;
- storage-neutral access-token ensure across client credentials and refresh;
- schema-neutral authenticated HTTPS resource requests using complete Bearer/DPoP bundles and optional fingerprint binding;
- OAuth token introspection for current authority state; and
- OpenID Connect UserInfo retrieval with optional exact subject binding.
UserInfo accepts application/json or verified RS256/RS512
application/jwt. Signed responses are checked through the configured
issuer's discovery document and JWKS before claims are returned.
The root M7IdentitySDK facade exposes validate(), clientCredentials(),
deviceAuthorization(), deviceToken(), refreshToken(),
acknowledgeToken(), ensureAccessToken(), request(), introspect(), and
userinfo() for
these primary operations. Granular parser, resolver, verifier, policy,
audience, scope, transport, and report types remain available for advanced use.
Capability boundary
The cli-php adapter owns environment, option, and profile resolution;
controlling-terminal prompts; the device polling loop and schedule; stdout,
stderr, output formats, and exit status; and its protected app/profile/password
and token-bundle keychain. Its protocol commands then call the corresponding
M7IdentitySDK operations in token-php.
token-php remains storage-neutral. It does not read CLI environment
variables, prompt, render command output, choose a keychain or database, or run
the device polling schedule. It performs one protocol attempt per call,
validates the response, and returns reports and complete bundles for its caller
to persist. Even its ACK-aware ensure operation accepts a caller-owned durable
checkpoint callback rather than selecting storage itself. The library owns
endpoint normalization, reusable client authentication, request construction,
bounded HTTPS transport, and protocol-response validation.
Neither the CLI nor the token library currently supplies authorization-code acquisition or outbound DPoP proof generation. A DPoP-required caller must create a fresh proof outside these packages and provide it for every applicable request or device polling attempt.
Supply tokens through the consuming process's established protected channel.
Prefer not to place access tokens, refresh credentials, client secrets, or
DPoP proofs in command arguments, shell history, logs, URLs, or world-readable
files. The explicit password import --json escape hatch is available when
the operator accepts its argv exposure; token import --json has the same
exposure for complete bundles. Neither is used implicitly.
The CLI also has a local protected token store beneath
<app>/<grant>/<profile>/tokens/. Import a complete mode-0600 bundle with
m7-identity token import --app APP --grant GRANT --file PATH; omission of
--profile selects default. Normal imports become active and archive the
preceding generation. ACK envelopes remain pending. token status returns
metadata only, and token promote --id ID performs only the local commit after
a separately successful authority ACK. App-selected client credentials and
device acquisition save automatically. Managed exchange performs active or
pending rotation, and managed ACK promotes the exact acknowledged pending
generation.
Protected file and bounded-stdin inputs avoid plaintext credential arguments. Credential-bearing output goes only to stdout when explicitly requested; diagnostics go to stderr, and status/mutation summaries disclose metadata only. Do not redirect output onto its current input file because the shell truncates the destination before the command can validate or persist a replacement.
OAuth acquisition and refresh lifecycle
The released protocol and CLI layers support these grant/authentication combinations:
| Operation | Client and bootstrap | Supported client authentication |
|---|---|---|
| Client credentials | Confidential machine client acting as itself; no user or prior bundle is involved. | client_secret_basic or client_secret_post; public-client none is rejected. |
| Device authorization and polling | Required bootstrap for a native user client. The person authenticates in the M7 browser page; the CLI never collects the account password. | none for a public native client, or client_secret_basic/client_secret_post for a confidential device client. |
| Refresh-package exchange | Requires the complete bundle established by device authorization. It cannot bootstrap a native client from no state. | none, client_secret_basic, or client_secret_post, matching the registered device client. |
| Token acknowledgement | Applies only to a retained pending ACK package. The activation credential in that package authorizes ACK; a DPoP-bound lineage also requires a fresh caller-generated proof. | No separate client password is added to the ACK request. |
A complete native token bundle keeps the access token and type together with
the refresh token and its binding_chain and binding_link; pending ACK
packages also retain activation data and the complete active predecessor.
Persist and transition the whole bundle. A bare access token is insufficient
for refresh, ACK, or durable recovery.
Refresh exchange accepts these lifecycle modes:
| Mode | Lifecycle |
|---|---|
static |
Issue a new access token without rotating the refresh credential. |
strict_rotation |
Return an active replacement and immediately retire the prior refresh credential; persist the replacement atomically because a lost response can strand the client. |
grace_rotation |
Return an active replacement while the predecessor remains usable only for the authority's bounded grace/recovery window. |
ack |
Return one pending successor while retaining the predecessor as active. A second refresh from that predecessor is serialized as refresh_in_progress until the pending package is acknowledged or expires. |
ack_supersede_pending |
Normally begin from the retained active package, atomically disable any older never-active pending child, and return the newest pending successor. Persist and acknowledge only that newest package. |
Both ACK modes require the caller or CLI to durably store the complete pending
successor before acknowledgement and to promote it only after ACK succeeds.
ack_supersede_pending is a normal fault-tolerant lifecycle, not a recovery-only
operation; it can start with no pending package or replace an older
unacknowledged child while the active predecessor remains retained.
Acquire a machine token from the CLI
From the SDK source tree, run the interactive command:
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity client-credentials --interactive
The client secret is read with terminal echo disabled. Environment values take precedence over command options, and options are fallback values only:
defined environment variable > matching CLI option > selected profile > prompt/default
For the client password, an explicit hidden --password prompt sits above the
stored profile password and below environment/command-file sources.
When --app is selected, the complete successful response is stored under
that app's selected client-credentials profile before stdout. This also
applies to --output token; only the display is shortened. The previous
active generation moves to history. Calls using raw client-ID configuration
without an app remain stateless, and an overridden client ID cannot differ
from a selected app.
For an unattended process, prefer a mode-0600 secret file:
export M7_CLIENT_CREDENTIALS_CLIENT_ID='YOUR_REGISTERED_CLIENT_ID'
export M7_CLIENT_CREDENTIALS_CLIENT_SECRET_FILE='/run/secrets/m7-client-secret'
export M7_CLIENT_CREDENTIALS_AUD='https://api.user.m7.org/'
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity client-credentials --output token
There is deliberately no plaintext --client-secret option. Run the command
with --help for every mapping and its exact precedence and file rules. The
sections below cover managed setup, secrets, storage, named profiles, stateless
calls, library use, and complete copy/paste workflows.
Authorize a user device from the CLI
For a registered public device client, upsert a native app and device-code profile, then run it by local app name:
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity app upsert user.m7.org-device \
--client-id YOUR_REGISTERED_DEVICE_CLIENT_ID \
--type native \
--authority consumer
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity profile upsert \
--app user.m7.org-device \
--grant device-code \
--config-json '{"auth_method":"none","aud":["https://user.m7.org"],"scope":"openid profile offline_access","refresh_mode":"strict_rotation"}'
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity device-code \
--app user.m7.org-device
These setup commands are repeatable. Upsert creates missing records or replaces their complete editable definitions without merging old fields. Stored passwords, active/pending token generations, history, and creation timestamps are preserved.
The complete successful bundle is committed to the selected profile before stdout. Machine apps are rejected for device code. The raw stateless form is:
export M7_DEVICE_CODE_AUTH_METHOD='none'
export M7_DEVICE_CODE_CLIENT_ID='YOUR_REGISTERED_DEVICE_CLIENT_ID'
export M7_DEVICE_CODE_SCOPE='openid profile offline_access'
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity device-code
The command displays the authority approval URL and user code, then polls at the returned interval. Approval and account-password entry happen only in the browser at M7; the CLI never collects the password. Basic/Post confidential device clients use the same protected client-secret source as client credentials.
The following sections cover registration, acquisition, storage verification, strict rotation, regular ACK, fault-tolerant ACK supersession, protected resume/import, DPoP boundaries, and direct library use.
Use --start-only to return the authorization JSON without polling. That JSON
contains a sensitive device_code. Resume through
M7_DEVICE_CODE_DEVICE_CODE_FILE or --device-code-file, using a regular
non-symlink file with no group/other permissions. There is no plaintext
device-code argument.
Exchange the returned refresh package
Managed strict rotation reads and atomically replaces the active bundle:
M7_EXCHANGE_REFRESH_MODE=strict_rotation \
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity exchange \
--app user.m7.org-device
Managed ACK rotation retains the provisional successor as pending and leaves active unchanged until the matching ACK promotes it:
M7_EXCHANGE_REFRESH_MODE=ack \
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity exchange \
--app user.m7.org-device
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity ack \
--app user.m7.org-device
Use --id GENERATION when multiple pending generations make selection
ambiguous. M7_EXCHANGE_REFRESH_MODE=ack_supersede_pending can begin from the
active package and replaces any older pending child at Identity. If the old
pending generation is retained locally, exchange stores the new generation
before removing it. A failed exchange or ACK leaves recoverable local state
unchanged.
For stateless operation, the device result is already the bundle used for refresh. Protect its refresh token and binding values as one unit:
chmod 0600 /run/secrets/m7-refresh-bundle.json
export M7_EXCHANGE_AUTH_METHOD='none'
export M7_EXCHANGE_CLIENT_ID='YOUR_REGISTERED_DEVICE_CLIENT_ID'
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity exchange \
--bundle-file /run/secrets/m7-refresh-bundle.json
Use --bundle-stdin for explicitly piped JSON, or --interactive to select a
protected file or enter the three required refresh fields with terminal echo
disabled. No refresh or binding value is accepted as a command argument.
The stateless successful output is the complete replacement bundle; the caller
must persist it safely before retiring the predecessor. A boolean bundle
fingerprint: true means a binding exists but is not the fingerprint value;
supply that protected value separately. Stateless ACK and supersede continue
to accept protected bundle files or explicitly selected stdin.
The native client registration must authorize refresh_token as well as the
standard device-code grant URN.
Redirect output only to a different protected file, never onto the current
input path, because shell redirection truncates the destination first.
Reuse or renew a managed token
After client credentials or device code has established a profile, use the managed ensure command for ordinary token consumption:
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity ensure \
--app user.m7.org-cc
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity ensure \
--app user.m7.org-device \
--minimum-ttl 300
The default output is the usable access token. Internally, a fresh bundle is
reused without a write; a stale machine token runs client credentials, and a
stale native token exchanges the complete refresh package. Machine profiles
can bootstrap without a bundle. Native profiles cannot: run device-code
first. Ensure never starts browser authorization automatically.
ACK mode durably stores pending state before acknowledgement and promotes only
after success. A failed ACK remains recoverable; --id selects among pending
generations. Regular ack serializes one pending child, while
ack_supersede_pending lets Identity replace an older pending child when
refreshing from the retained active package.
PHP integrations call ensureAccessToken() directly and persist
report->bundle() in their own keychain, database, or secrets store. The
library accepts null only for client_credentials; exchange requires a
device-primed bundle. Its result statuses and ACK checkpoint behavior follow
the storage-neutral contract described here.
The library orchestrator accepts caller-owned current state and returns the
complete resulting bundle without selecting storage. For ACK modes, it calls a
caller-owned durable checkpoint with the pending successor before sending ACK;
without that checkpoint it returns pending_ack and does not acknowledge.
m7-identity ensure is the separate adapter that resolves the app and profile,
loads keychain state, supplies that checkpoint, and atomically stores or
promotes replacements.
Send an authenticated resource request
Ensure and use the bundle stored beneath a machine or native app:
./vendor/m7-identity-cli-php-0.1.1/bin/m7-identity request \
--app user.m7.org-device \
--url https://api.user.m7.org/api/v2/session/me \
--response json \
--query include=groups
The profile defaults to default; the app type selects client-credentials
acquisition or device-bundle exchange. A token with at least 60 seconds left is
reused. Otherwise the replacement is stored before the API request. Use
--minimum-ttl to change that threshold and --refresh-mode to override a
native profile's rotation mode. A profile without a configured mode uses
ack_supersede_pending. Native apps must first be primed with device-code.
The method defaults to GET without JSON and automatically becomes POST when
--json or --json-file supplies data. An explicit method wins. Requests
default to JSON Accept, and body-bearing requests default to JSON Content-Type.
Repeat --query and --header for ordinary request inputs.
--response raw is the default and emits the response body without JSON
validation. Use --response json when JSON is required. A malformed, empty, or
incorrectly typed 2xx response then exits 1; its original body is still
written to stdout and the validation reason is written to stderr.
The underlying ResourceRequestReport always exposes the raw body(), HTTP
status, and response headers. In JSON mode, hasJson() distinguishes a decoded
JSON null from no decoded value, json() returns the decoded representation,
and toArray() carries both raw and decoded views. A missing or non-JSON
response media type, or malformed JSON under a JSON media type, fails the
report even when HTTP returned 2xx; the raw body remains available for
diagnosis.
For stateless use, select a protected complete bundle with --bundle PATH or
--bundle-stdin, or use a mode-0600 access-token file or the value-less hidden
--token prompt. A fingerprint and fresh caller-created DPoP proof likewise
come from protected files or hidden prompts. Refresh-only binding_chain and
binding_link values are never sent to the API.
Explicit bundle and manual-token sources are literal: they are not refreshed
or stored. After managed preflight, the command sends exactly one bounded HTTPS
request. It does not follow redirects, generate a DPoP proof, or blindly retry
an API 401. The request lifecycle above applies to every input source and
environment override.
Acquire a machine token from PHP
Use the library operation when a confidential application needs to act as itself:
$identity = new M7\Identity\M7IdentitySDK();
$report = $identity->clientCredentials([
'endpoint' => 'consumer',
'auth' => [
'method' => 'client_secret_basic',
'client_id' => $clientId,
'client_secret' => $clientSecret,
],
'aud' => 'https://api.user.m7.org/',
'scope' => 'profile.read',
'access_expires' => 900,
]);
if (!$report->ok()) {
throw new RuntimeException($report->reason());
}
$accessToken = $report->accessToken();
client_secret_post is also supported. Public-client method none is not,
because this grant represents a confidential application. A returned token has
been acquired, not validated; call validate() or introspect() separately
when that assurance is required.
The library itself does not read environment variables, prompt for secrets, or
print the token. Those behaviors stay in cli-php.
Requirements
Both PHP packages require PHP 8.1 or newer and OpenSSL. cURL is recommended for token acquisition, authenticated resource requests, live certificate resolution, introspection, and UserInfo; supported PHP streams are the fallback transport.
Install
Follow the CLI and background-process installation guide to distinguish the published token-library release from current CLI source use.
Operation model
Use validate() when the process has a serialized token and must verify it
locally against explicit policy. Use clientCredentials() when a confidential
machine client needs an application-principal access token. Use
deviceAuthorization() plus repeated deviceToken() attempts when a person
must approve a public or confidential device client. Use request() to call
an application resource with a complete active token bundle. Use
introspect() when current authority state is required. Use userinfo() when
the process is authorized to retrieve the live scoped identity projection.
Keep these decisions separate:
| Operation | A successful result means |
|---|---|
validate() |
The supplied token passed the configured local cryptographic and claims policy. |
clientCredentials() |
The configured authority returned a well-formed access-token-only client-credentials envelope. The token has not thereby passed local validation or live introspection. |
deviceAuthorization() |
The authority returned a validated device code, user code, verification URL, expiry, and polling interval. No token has been issued to the caller yet. |
deviceToken() |
One poll returned either a validated user token bundle or a classified pending/slow-down/terminal report. It does not sleep or validate returned JWTs cryptographically. |
refreshToken() |
One complete predecessor refresh package produced a validated replacement package. The operation did not persist or cryptographically validate its JWTs. |
acknowledgeToken() |
One retained pending package produced a matching active successor without changing caller storage. |
request() |
One bounded authenticated HTTPS resource request returned a 2xx response and, when requested, a valid JSON representation. It did not refresh, retry, interpret the API schema, or change storage. |
introspect() |
The authority returned a valid introspection response; active() is true only when it explicitly declared the token active. |
userinfo() |
The protected resource returned a valid identity claims object with a nonempty subject, plus any requested subject binding passed. |
A transport failure, malformed response, authorization failure, or explicit inactive result fails the corresponding live operation closed.
Security
Treat parsed claims as untrusted until validation succeeds. An unvalidated issuer or key identifier may help select a candidate certificate cache entry, but it must never establish trust by itself.
When DPoP validation is enabled, the consuming process owns atomic replay-key claiming and retention. A successful SDK proof result identifies the replay key and expiry boundary; it does not reserve the key in application storage.