OAuth client management
Use this API family to create and manage OAuth application registrations, set their policy, and manage application-to-application connections. For standard OAuth and OpenID Connect authorization, token, device-authorization, and registration protocol endpoints, use the M7 SSO API documentation.
Access and identifiers
Personal applications belong to the signed-in consumer user. Organization
applications belong to an organization where the signed-in consumer user has
active Management-group owner access. Admins and members cannot view or
manage organization apps or credentials, including assets they created. Creator
uid attribution never substitutes for the current stored owner role. See the
organization matrix. Organization application tokens do not manage
OAuth client records through this family. See the
authorization guide for the complete principal rules.
OAuth-app routes use two identifiers:
| Identifier | Used by | Meaning |
|---|---|---|
id |
Most api.user application routes |
The OAuth application record UUID. |
client_id |
OAuth protocol requests and /org/manage/add with type: "app" |
The public OAuth client identifier. |
Do not substitute client_id for id on /oauth/clients/view, update,
set_auth, set_knobs, set_groups, set_tags, tag-management, or lifecycle
routes.
Ownership and directory context
org selects who manages the registration; tenant selects its user directory.
Insert, registration search, authorized search, connected search, and connected
insert accept both fields.
| Space | org |
tenant |
Management authority |
|---|---|---|---|
| Personal consumer | Nil UUID | Nil UUID | Authenticated user matches uid |
| Organization consumer | Organization UUID | Nil UUID | Active Management owner of org |
| Organization tenant | Organization UUID | Same organization UUID | Active Management owner of org |
The nil UUID is 00000000-0000-0000-0000-000000000000, not SQL NULL.
The API normalizes tenant first: omitted, JSON null, empty or whitespace-only
strings select nil. Nonblank values must be dashed UUID strings; surrounding
whitespace is trimmed and letters are uppercased. Numbers, booleans, arrays,
objects and malformed UUIDs are rejected.
| Input | Resolved space |
|---|---|
| Both omitted | Personal (org nil, tenant nil) |
org omitted, non-nil tenant |
Legacy organization tenant: org inherits tenant |
Explicit organization org, omitted/null/blank/nil tenant |
Organization consumer |
Explicit organization org, matching non-nil tenant |
Organization tenant |
Explicit null/blank/nil org, non-nil tenant |
Rejected; explicit nil does not inherit the tenant |
Non-nil tenant different from org |
Rejected |
Explicit org uses the same null/blank/UUID normalization as tenant. Only an
omitted org inherits the normalized tenant. A non-nil owner must be active,
unarchived and accessible through the caller's stored Management owner role;
invalid or inaccessible scope never falls back to personal ownership.
Registration search enforces the exact (org, tenant) pair. Personal results
also require the authenticated uid; a caller-supplied uid cannot broaden
the search. Authorized views match the provider registration's pair; Connected
views match the consumer registration's pair. Selecting a record or connection
by ID does not escape that scope or the stored-owner check.
User M7 provides separate Consumer Apps and Tenant Apps, each with Registrations, Authorized, and Connected views. Personal registration uses these shared endpoints with both coordinates nil. A nil tenant alone does not distinguish personal registrations from organization consumer registrations.
Both fields are fixed at insertion. Ordinary updates ignore supplied org,
tenant, or creator uid, preserving the stored scope. A non-nil tenant
currently must equal org; multiple independent tenants per organization remain
future work. Existing databases must already carry the correct ownership values;
request compatibility does not backfill stored registrations. See
ownership upgrade prerequisites.
Organization consumer applications use the consumer master directory. Their
owning organization does not change SSO login mode, root-session matching,
refresh realm, or token tenant claims. The existing token claim named org
continues to describe the identity tenant; it does not carry registration
ownership. Personal machine-owner proxy privilege requires both fields to be
nil. Same-site connection approval compares the owning org, or uid for
personal registrations. User-token issuance checks an organization registration
against its active owning org and active human Management presence, rather than
the original creator's user account. Personal registrations retain the active
user-owner check.
Use /oauth/clients/tenants to list accessible personal and organization
directories. Use /oauth/clients/tenant-info with an optional tenant to
inspect the directory; a nil tenant returns the consumer directory for both
personal and organization consumer applications.
Application lifecycle
An application registration is created as a draft. A typical setup sequence is:
- Create the record with
/oauth/clients/insert. - Make it confidential, when needed, with
/oauth/clients/set_auth. - Configure policy with
/oauth/clients/set_knobs. - Activate it with
/oauth/clients/update_status. - For tenant applications, configure registration and login groups with
/oauth/clients/set_groups. - Optionally organize the application with a scoped tag pool and
/oauth/clients/set_tags.
Core application routes
| Route | Required fields | Behavior |
|---|---|---|
/oauth/clients/insert |
None strictly; send name and usually registration fields |
Creates a draft record and generates its client_id. |
/oauth/clients/view |
id |
Returns one application, including config, knobs, resolved groups, and assigned tags. |
/oauth/clients/export |
id |
Returns non-secret, paste-ready M7 Identity CLI exports for the grants registered to the application. Installation uses app/profile upserts so existing keychain secrets and tokens survive metadata updates; ordinary execution uses m7-identity ensure. Authorization is informational and contains no script; unsupported grants are marked unavailable so clients can omit them. |
/oauth/clients/search |
None | Lists applications in the selected org and tenant context. Supports shared pagination. |
/oauth/clients/update |
id |
Partial update of registration metadata. Cannot change org, tenant, uid, client auth, or knobs. |
/oauth/clients/delete |
id |
First call archives the record (ARCHIVE); an archived record with the required lock confirmation is permanently deleted (DELETE). |
/oauth/clients/recover |
id |
Restores an archived record. |
/oauth/clients/update_status |
id, status |
Sets lifecycle status: draft, active, or disabled. |
/oauth/clients/set_auth |
id, token_endpoint_auth_method |
Sets the token-endpoint authentication method. |
/oauth/clients/set_knobs |
id |
Partially updates token and hosted-application policy fields. |
/oauth/clients/set_principal_list |
id, list, initial_status; optional reject_disabled_members, require_active_machine_membership, enrich_token_with_role, enrich_token_with_data, manage_members, acl |
Stores the application's list binding, enrollment/rejection settings, independent token role/data enrichment switches (both default off), opt-in confidential application membership management, and ordered inbound machine ACL. SSO evaluates that ACL at the receiving app's /introspect/acl call; rule expiry extends the shared membership lease, bounded by the inbound token expiry. See principal lists. |
/oauth/clients/set_groups |
id, register and/or login |
Sets organization-application registration and login groups. |
/oauth/clients/set_tags |
id, tag_ids |
Replaces the application’s tag assignments in its derived tag scope. |
/oauth/clients/tags/search |
id |
Lists the tag pool available to the application. |
/oauth/clients/tags/insert |
id, name |
Creates a tag in the application’s derived tag scope. |
/oauth/clients/tags/update |
id, tag_id |
Updates a tag in the application’s derived tag scope. |
/oauth/clients/tags/delete |
id, tag_id |
Deletes an unused tag from the application’s derived tag scope. |
/oauth/clients/tags/merge |
id, source_tag_id, target_tag_id |
Merges two tags in the application’s derived tag scope. |
/oauth/clients/set_config_lock |
id, config_lock_hash_new |
Sets, replaces, or clears a configuration lock. |
/oauth/clients/tenants |
None | Lists accessible tenant contexts. |
/oauth/clients/tenant-info |
None | Optional tenant; omitting it selects the personal context. |
The export response groups commands under exports.<grant>.sections. Each
section has a stable key, a display title, and a separately copyable
script. Client credentials separates setup from the ordinary token ensure.
Device code exposes three steps when the application also offers
refresh_token: install or update, one-time device authorization, and ensure.
The ensure snippet lists refresh-mode choices while keeping the lower-level
exchange and ACK tools out of the normal workflow.
Create an application
insert creates a registration, not a ready-to-use OAuth client. The API:
- generates the public
client_id; callers cannot choose it; - sets
statustodraft; - sets
token_endpoint_auth_methodtonone; - ignores policy-knob fields (use
set_knobsinstead); - fixes the tenant at creation time; and
- defaults an empty
grant_typeslist to["authorization_code"]and enables PKCE by default.
Use name for the application name. These list fields accept JSON arrays or
newline-separated strings: redirect_uris, post_logout_redirect_uris,
grant_types, ip_whitelist, response_types, and response_modes.
audiences is accepted as an alias for allowed_audiences.
curl -sS https://api.user.m7.org/api/v2/oauth/clients/insert \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"name": "Example App",
"id_token_signed_response_alg": "RS512",
"redirect_uris": ["https://app.example.com/callback"],
"grant_types": ["authorization_code"],
"org": "11111111-1111-4111-8111-111111111111",
"tenant": "00000000-0000-0000-0000-000000000000"
}'
{
"status": 1,
"comment": "INSERT",
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"client_id": "PUBLIC_CLIENT_ID",
"name": "Example App",
"status": "draft",
"token_endpoint_auth_method": "none",
"require_pkce": true
}
}
View an application
curl -sS https://api.user.m7.org/api/v2/oauth/clients/view \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{ "id": "22222222-2222-4222-8222-222222222222" }'
Logout notification settings
Use POST /api/v2/oauth/clients/set_knobs for these optional receiver fields.
It requires the application record id, existing ownership/tenant access and
configuration-lock confirmation when the record is locked. This is a partial
policy update; omitted fields preserve their current values.
| Field | Accepted value |
|---|---|
frontchannel_logout_uri |
HTTPS URL whose scheme, host and port match a registered sign-in redirect URI |
backchannel_logout_uri |
HTTPS URL; a different host and port are allowed |
frontchannel_logout_session_required |
Boolean receiver requirement, retained for standard registration compatibility |
backchannel_logout_session_required |
Boolean receiver requirement, retained for standard registration compatibility |
URLs allow static query parameters, are limited to 2,048 characters and cannot contain credentials or fragments. Null or blank clears a URL and its associated requirement. Explicitly requiring a session ID without a URL is rejected. A false requirement is stored sparsely; it does not disable delivery.
M7 always includes sid in both logout channels and iss in front-channel
requests, even when the requirement flags are false or absent. User.M7 exposes
only the URL fields in Client → Logout when editing an existing application;
that tab has its own Update Logout Settings action. It is absent during
creation. post_logout_redirect_uris remains a General registration field and
uses the ordinary update route. It controls the final browser return, not
notification delivery.
curl -sS https://api.user.m7.org/api/v2/oauth/clients/set_knobs \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"frontchannel_logout_uri": "https://app.example.com/logout/front",
"backchannel_logout_uri": "https://app.example.com/logout/back"
}'
For a locked record, also supply the existing configuration-lock confirmation
as described below. The settings are readable through the existing client
config and knobs projections. Saving them sends no logout notification.
The provider uses current registration for applications recorded as participants
in the selected root login. See
SSO logout notifications
for trigger scope, delivery, receiver validation and the receipt-only SDK back
receiver. SSO Dynamic Registration accepts these fields at creation through
its separate standard metadata contract.
Registration fields
Use /oauth/clients/update for partial changes to main registration metadata.
The endpoint accepts registration fields such as name, issuer,
application_type (web, spa, native, or machine), redirect_uris,
post_logout_redirect_uris, grant_types, response_types, response_modes,
allowed_audiences, allowed_scopes, ip_whitelist, require_pkce,
dpop_policy, id_token_signed_response_alg, initiate_login_uri,
default_max_age, notes, and expires.
update cannot change org, tenant, uid, client_secret,
token_endpoint_auth_method, or policy knobs. Use set_auth and set_knobs
for those operations.
dpop_policy accepts disabled, optional, or required. The SSO
token endpoint applies the current value to authorization-code, device-code,
refresh-token, and client-credentials issuance. disabled blocks DPoP and
prevents new DPoP and fingerprint binding, optional binds supplied material,
and required requires a valid DPoP proof on every token request while leaving
fingerprint binding optional. The setting controls customer application tokens
and does not alter SSO's internal root-session protection. Changing it does not
migrate an existing refresh lineage; incompatible packages may fail and require
the user to authorize again.
id_token_signed_response_alg is the stored ID-token signing preference.
Registration accepts RS256, RS384, RS512, PS256, PS384, PS512,
ES256, ES384, ES512, ES256K, Ed25519, Ed448, ML-DSA-44,
ML-DSA-65, and ML-DSA-87. These 15 asymmetric profiles are available for
Identity issuance when their signing keys and backend are configured. The API
also accepts HS256, HS384, and HS512 for client-secret HMAC issuance.
Save the selection, then set/reset the secret through set_auth. Missing,
hashed, unreadable or undersized secrets fail issuance; saving registration
alone does not establish signing readiness or verifier compatibility.
Algorithm spelling is trimmed and normalized to the canonical names above.
Omission when creating a new client stores the OIDC default, RS256; M7
clients which prefer RS512 select it explicitly. Dynamic Registration returns
the effective stored value. Existing registrations are not rewritten, and
legacy rows which genuinely lack a stored value retain the historical RS512
read fallback.
Use the live SSO discovery document and your verifier's supported algorithms
when choosing a profile. The updated M7 Identity SDK source can delegate
additional verification profiles to the optional m7crypto extension; existing
immutable download ZIPs do not gain that support automatically. ML-DSA device
and process-side token handling is distinct from the current Web SDK's
full-token cookies, whose browser size limits make this session path unsuitable
for general ML-DSA use. See
SSO signing profiles and SDK compatibility.
During consumer and tenant issuance, API User reloads the authenticated
client_id, resolves this value from the authoritative client row, and
forwards it to Identity. Identity applies the preference to application token
issuance, refresh/access exchange, and the provider-backed /sign path used
for OIDC ID tokens.
Configure authorization response modes
The app-management API accepts response_modes on insert and
update, and includes the stored list in insert, update, view, and
search results. This list controls the modes the client may request. SSO
currently advertises all ten names below; the application selects one
response_mode per authorization request. This is independent of
response_types, which remains ["code"], and of token-endpoint grants.
The registration inventory is query, form_post, fragment, query.jwt,
form_post.jwt, fragment.jwt, jwt, web_message, web_message.opener,
and web_message.parent.
- Accepts a JSON array of strings or newline-separated text. Entries are trimmed, lowercased and deduplicated; stored results are sorted by mode name.
- Omitting the field on
updatepreserves the current nonempty list. It does not mean the caller edited or cleared that field. - Sending
[]or empty/whitespace-only text sets the list to["query"]. Omitting it oninsert, or on an update whose stored list is empty, also saves["query"]. A supplied nonempty list takes precedence. viewandsearchbackfill an empty stored list with["query"]before returning it. This saves the default, including for existing applications; reading an already configured list leaves it unchanged. Search only backfills accessible applications returned in the current result/page.- Null, booleans, objects, nested arrays, non-string array entries, blank array entries and unknown mode names are rejected before saving. Commas and spaces are not list separators.
- The existing owner/organization access checks and configuration lock apply to explicit updates. Authorized reads may fill the compatibility default for a locked legacy application without lock confirmation; they do not change its other settings.
Replace the configured list with:
{
"id": "22222222-2222-4222-8222-222222222222",
"response_modes": ["query", "form_post"]
}
Submit that body to /api/v2/oauth/clients/update. For a locked application,
also supply the existing config_lock_hash confirmation. Runtime enforcement
requires the selected mode to be in this list and in SSO discovery. Omission
on an authorization request selects query; it does not choose the first
entry. A client allowing only form_post must request it explicitly. See
SSO response modes
for delivery, JARM verification, and popup/iframe prerequisites.
Make an application confidential
Use set_auth to create or change client authentication. The currently
supported methods are:
noneclient_secret_postclient_secret_basicclient_secret_jwtprivate_key_jwttls_client_authself_signed_tls_client_auth
For client_secret_post, client_secret_basic, and client_secret_jwt, also
send a caller-chosen client_secret. Basic and POST secrets are stored as
one-way password hashes unless HS token signing is selected. Secret-JWT and
HS256/384/512 token signing need the original key, so those values are stored
in a versioned AES-256-GCM envelope in the main client record. Management
responses reduce every stored secret to a configured/not-configured boolean;
they never return a usable plaintext secret, password hash, or encrypted
envelope. Generate the secret in your application, store it securely when you
make this call, and use that same value at the SSO endpoint.
private_key_jwt requires exactly one public key source. It and mTLS can store
an additional secret for HMAC without changing their authentication method.
The shared API storage also retains support for none plus an HMAC secret;
this is not an eligible SSO configuration. User.M7 prevents that combination
using saved settings, and the SSO guard rejects it. That eligibility update
is implemented and locally tested; production verification remains pending.
The Auth page collects a secret for Basic, POST, secret-JWT or the saved HMAC
signing selection, and keeps existing invalid selections visible for repair.
Set a secret for HMAC signing
For a client that can protect its credentials, first configure and save its
confidential-client authentication. Then save HS256/384/512 in
id_token_signed_response_alg and call set_auth
with the record id, its token_endpoint_auth_method, client_secret and
any required method-specific key/certificate metadata. Existing ownership and
configuration-lock checks still apply. The signing choice comes from the saved
client, not an algorithm supplied in the Auth request.
Use generated original secrets of at least 32, 48 or 64 bytes for HS256, HS384 or HS512 respectively. Keep the original value: it serves both HMAC and secret-based authentication when both apply. An existing Basic/POST hash cannot be recovered for signing. Save the algorithm, then set/reset the secret; issuance fails until a valid recoverable secret is configured.
An existing none/HMAC record needs explicit repair: a public client must
save an asymmetric algorithm; only a deployment that can protect its credentials should
configure confidential authentication. Save the asymmetric algorithm before
switching an HMAC client to none. Direct API management can still store the
incompatible combination, but SSO returns invalid_client_metadata at Dynamic
Registration or unauthorized_client at PAR, device authorization and /token
in the eligibility update. No automatic algorithm change or revocation occurs.
A secret stored under none neither changes authentication nor permits
confidential-only introspection. Management reads never reveal the usable
secret. See SSO HMAC setup
for consumption and acceptance boundaries. For mTLS subject selectors,
certificate JWKs and token binding, use the same SSO registration reference.
The following table covers shared Auth fields.
| Field | Required | Notes |
|---|---|---|
id |
Yes | OAuth application record UUID. |
token_endpoint_auth_method |
Yes | none, client_secret_post, client_secret_basic, client_secret_jwt, private_key_jwt, tls_client_auth, or self_signed_tls_client_auth. |
client_secret |
Required for a client-secret method or HS token signing | Original secret, encrypted for secret-JWT or HS signing; otherwise hashed for Basic/POST. Management reads return presence only. |
token_endpoint_auth_signing_alg |
JWT methods | HS256, HS384, or HS512 for client_secret_jwt; RS256, RS384, RS512, or ES256 for private_key_jwt. Omission selects HS256 or RS256, respectively. |
jwks_uri |
Private-key JWT | Public HTTPS JWK Set URL. Mutually exclusive with jwks. |
jwks |
Private-key JWT | Inline JWK Set with a key compatible with the selected algorithm. Mutually exclusive with jwks_uri. |
API User and SSO, plus ID.M7 for HMAC token signing, must receive the same
M7_OAUTH_CLIENT_SECRET_JWT_KEYS environment value before enabling
client_secret_jwt or HMAC token issuance. It is a comma-separated key ring of 64-character
hexadecimal AES keys. New secrets use the first key; all listed keys remain
available for decryption during rotation. The singular
M7_OAUTH_CLIENT_SECRET_JWT_KEY is accepted as a one-key fallback. Existing
legacy rows containing one-way password hashes cannot authenticate as
secret-JWT clients or supply HMAC signing material and must be issued a new client secret after this key ring
is configured.
curl -sS https://api.user.m7.org/api/v2/oauth/clients/set_auth \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"token_endpoint_auth_method": "client_secret_post",
"client_secret": "STORE_THIS_SECRET_SECURELY"
}'
The success comment is SET_AUTH. Changing to a client-secret method makes
the application confidential.
{
"status": 1,
"comment": "SET_AUTH",
"data": {
"id": "22222222-2222-4222-8222-222222222222",
"client_id": "PUBLIC_CLIENT_ID",
"token_endpoint_auth_method": "client_secret_post"
}
}
Activate an application
curl -sS https://api.user.m7.org/api/v2/oauth/clients/update_status \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"status": "active"
}'
The success comment is UPDATE_STATUS.
Response encryption
Use POST /api/v2/oauth/clients/set_knobs to configure outbound ID-token, JARM
and signed-UserInfo encryption for an application you may manage. Supply the
record id, an accepted management access token and the current
config_lock_hash when locked. Ownership, tenant and configuration-lock checks
apply before changes are saved. The response uses the normal SET_KNOBS envelope.
| Field | Type | Contract |
|---|---|---|
response_encryption_jwks |
Object or JSON object string | Public RSA JWKS, stored as an object; mutually exclusive with the URL source |
response_encryption_jwks_uri |
String | Public DNS HTTPS JWKS URL on port 443; mutually exclusive with inline keys |
id_token_encrypted_response_alg / id_token_encrypted_response_enc |
Strings | Set the pair to RSA-OAEP-256 / A256GCM for ID tokens |
authorization_encrypted_response_alg / authorization_encrypted_response_enc |
Strings | Same required pair for JARM |
userinfo_encrypted_response_alg / userinfo_encrypted_response_enc |
Strings | Same required pair for signed UserInfo |
Omitted patch fields preserve stored values. With both algorithm fields absent,
that artifact is off; both explicit supported values require encryption. Clear
both with null or empty strings to turn an artifact off, retaining the shared
keys and other policies. Clearing only half a pair fails. A source may be saved
with all policies off, but removing the last source while any policy is required
fails. To switch sources, set the new one and explicitly clear the old one.
Validation applies to the merged configuration, including later unrelated knob
edits; no silent fallback is applied.
UserInfo requires saved userinfo_signed_response_alg=RS256 or RS512; one
API patch may set signing and encryption together. Disabling signing while
retaining UserInfo encryption fails. JARM requires the app to allow a JWT mode
and the authorization request to select it explicitly. Authentication, JAR and
response-encryption key sources are independent. The application retains its
private decryption key; never submit private or symmetric material here.
Inline JWKS accepts only a keys array with 1–32 public RSA keys, at most 64 KiB.
Every key needs a unique nonempty kid of at most 256 bytes, canonical public
n/e, and a 2048–8192-bit modulus. Optional use is enc, alg is
RSA-OAEP-256, and key_ops permits only encrypt and/or wrapKey. Unknown,
private, symmetric, embedded-certificate and key-URL members are rejected.
Remote URLs cannot contain credentials, fragments, IP literals or local
hostnames. Saving validates syntax without fetching the URL. Runtime key
availability, key selection and safe HTTPS retrieval are described in the
SSO encryption guide.
When multiple valid recipients exist, SSO selects the bytewise smallest kid;
retain earlier private keys while outstanding responses can still arrive.
Example patch, after configuring the receiving application:
{
"id": "22222222-2222-4222-8222-222222222222",
"response_encryption_jwks": null,
"response_encryption_jwks_uri": "https://app.example.com/.well-known/encryption-jwks.json",
"id_token_encrypted_response_alg": "RSA-OAEP-256",
"id_token_encrypted_response_enc": "A256GCM",
"authorization_encrypted_response_alg": "RSA-OAEP-256",
"authorization_encrypted_response_enc": "A256GCM",
"userinfo_signed_response_alg": "RS256",
"userinfo_encrypted_response_alg": "RSA-OAEP-256",
"userinfo_encrypted_response_enc": "A256GCM"
}
Saved fields appear in data.config, data.knobs and the top-level editor
projection data.<field>. Unset fields are absent from the sparse maps and null
in that editor projection. View/search and save responses use the same shape.
Dynamic Registration uses standard jwks / jwks_uri names; these management
source names belong to this API and are not extra DCR fields.
The application settings panel
exposes Knobs → Response Encryption. The provider enforces these policies;
older panel versions may still show inactive-preview copy. Public-client none
can use encryption, but the PHP web session/profile bridge's introspection path
requires confidential client authentication. Encryption does not grant scopes,
change token authentication or replace resource authorization.
Policy knobs and configuration locks
Use /oauth/clients/set_knobs with the application record id and only the
values to change. It supports token TTL, claim, and inheritable
tenant_policy_* application-policy fields from organization policy knobs.
Browser-direct authentication, registration and email-sign-in allowances can
be overridden here. The org-only multiple-identity, two-factor and email-two-factor
switches are rejected at application level, including false/null values; see
tenant policy for the exact field matrix.
null or "" removes an optional application override.
require_auth_time is a sparse OIDC policy override available through this
endpoint. Absence is the standard false default, true requires every ID
token for the client to carry the immutable authentication-event time, and
submitting false removes the override. It does not itself demand a new login;
use default_max_age or an authorization request's max_age parameter for
freshness. User M7 exposes it under Token Policy → General.
userinfo_signed_response_alg is a sparse OIDC response-format preference.
When absent, the UserInfo endpoint returns ordinary application/json.
RS256 and RS512 request a signed application/jwt response. This setting
is applied by SSO User after it validates the access token and resolves the
authoritative client registration.
authorization_signed_response_alg is the separate JARM signing preference.
It accepts exactly RS256 or RS512. Omission on set_knobs preserves the
current setting; null, "", or "default" removes the override, so the
effective algorithm is RS256. Responses expose the stored override in
config and knobs; an absent override means the default applies. Changing
this setting does not enable a response mode or change ID-token, UserInfo,
JAR, or client-authentication signing policy. The separate
response-encryption policy selects recipient keys and
requires encryption without changing the inner signing algorithm.
To configure RS512 for a client that already allows a JARM mode:
{
"id": "22222222-2222-4222-8222-222222222222",
"authorization_signed_response_alg": "RS512"
}
Send this body to POST /api/v2/oauth/clients/set_knobs, with the management
access token and any required configuration-lock confirmation. Allow the
desired JARM mode separately through update, and configure the relying
client to expect the same signing algorithm.
Application policy values override organization values when set. view,
search, and set_knobs return policy data in both config and the filtered
knobs map.
curl -sS https://api.user.m7.org/api/v2/oauth/clients/set_knobs \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"access_token_ttl_default": 900,
"access_token_ttl_mutable": true
}'
The success comment is SET_KNOBS.
A configuration-locked application requires its current config_lock_hash on
protected configuration changes, including update, set_auth, set_knobs,
and set_groups. To set or clear the lock, send config_lock_hash_new to
/oauth/clients/set_config_lock; send null to clear it. If a lock already
exists, also send the current config_lock_hash to confirm the operation.
Registration and login groups
/oauth/clients/set_groups applies only to a tenant application. Organization
consumer applications do not acquire tenant registration or login groups.
| Field | Required | Meaning |
|---|---|---|
id |
Yes | OAuth application record UUID. |
register |
One of register or login |
Group UUIDs or slugs that control the registration group list. |
login |
One of register or login |
Group UUIDs or slugs that control the login group list. |
Omit one list to leave it unchanged. Send null or [] to clear a list.
Groups must be active, unarchived, unexpired, and in the application's tenant.
The response returns resolved records in data.groups.register and
data.groups.login.
Group restrictions fail closed over their lifecycle. If a configured group is later disabled, archived, expired, or missing, registration, login, and refresh admission are denied until the group is restored or an administrator explicitly changes the application's group configuration. Only an explicitly empty group list means unrestricted.
curl -sS https://api.user.m7.org/api/v2/oauth/clients/set_groups \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"register": ["33333333-3333-4333-8333-333333333333"],
"login": ["operators"]
}'
The success comment is SET_GROUPS.
Application tags
Tags are organizational metadata, not authorization policy. Personal
applications share a tag pool scoped to the owning user; organization
applications share a pool scoped to their owning org, independently of tenant. The API always derives that
scope from an accessible OAuth application record:
| Application ownership | Generic tag scope | Assignment asset |
|---|---|---|
| Personal | user + uid |
oauth_client + id |
| Organization consumer or tenant | organization + org |
oauth_client + id |
Callers cannot submit arbitrary scope coordinates. This keeps a personal or organization pool reusable across its applications while preventing a tag ID from another scope from being assigned, updated, deleted, or merged.
The service stores definitions in tag_names and assignments in
tag_junction; oauth_clients is unchanged. Both tables are ensured lazily
when the OAuth-client tag service is first used.
Create a tag by identifying any accessible application in the intended scope:
curl -sS https://api.user.m7.org/api/v2/oauth/clients/tags/insert \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"name": "Production",
"description": "Production-facing applications"
}'
List the pool with /oauth/clients/tags/search. It accepts id plus optional
exact tag_id, slug, or name filters and the standard limit,
page_number, offset, or cursor position fields. The response contains
scope, tags, limit, offset, has_more, next_offset, and
next_cursor.
Replace one application’s complete tag selection with set_tags:
curl -sS https://api.user.m7.org/api/v2/oauth/clients/set_tags \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "22222222-2222-4222-8222-222222222222",
"tag_ids": ["66666666-6666-4666-8666-666666666666"]
}'
Send an empty tag_ids array to clear the application. Tag deletion fails
while any asset still uses the tag. Merge moves assignments to the target,
removes duplicate assignments, and deletes the source tag. Permanently
deleting an OAuth application clears its assignments but preserves the shared
tag definitions.
Application connections
Connection policy and the scoped RFC 8693 runtime are deployed. Consumer
exchange, delegated strict refresh and permission withdrawal passed live
acceptance on 2026-09-20. The public exchange endpoint is SSO /token;
API.User manages the registrations, connection records and approvals it uses.
A connection joins a consumer application to a provider application. The three route families expose the same connection record from different perspectives:
| Family | Perspective |
|---|---|
/oauth/clients/connections/* |
Shared connection record; callers with consumer or provider access can use it. |
/oauth/clients/connected/* |
Consumer-side view and management. |
/oauth/clients/authorized/* |
Provider-side view and status decisions. |
Admission and exchange policy
The provider configures incoming relationships with /oauth/clients/set_knobs
or User.M7's Knobs → Connections panel:
connection_policy_availability:closed(default),unlisted, ordiscoverable. Closed prevents new connections; existing approvals remain.connection_policy_approval:manual(default),auto, orsame_site_auto. Same-site means the same registered owner: the same personal user or the same organization. Two organizations never match merely because a person manages both.connection_policy_other_siteschoosesmanualorrejectoutside that owner boundary.connection_policy_token_exchange_enabled: whether this provider accepts exchange; absent means false.connection_policy_token_exchange_approval:manual(default) requires provider-ownedtoken_exchange_approvedon each connection;connection_policyuses the active connection's approval.connection_policy_token_exchange_scopes: exchange scope ceiling. Missing or empty permits nothing; it never grants scopes absent from the user's token.connection_policy_token_exchange_ttl_max: optional positive whole-second cap on both access and refresh lifetimes.
The fields use the existing partial configuration store. An omitted field
preserves its value; null/empty clears an override. SSO /register accepts the
same metadata using its stricter JSON creation contract. The requesting client
also needs the canonical token-exchange grant and confidential authentication;
offline issuance additionally requires the refresh grant and explicitly
requested, authorized offline_access.
User.M7's Authorized Applications review exposes the per-connection exchange
approval. Provider managers can submit token_exchange_approved: true or
false to /oauth/clients/authorized/update (or the shared update route).
Consumers cannot grant this approval. Consumer changes to scopes or expiry
reset the relationship to pending and clear exchange approval; notes-only
changes preserve it. Unchanged scope/expiry fields are omitted from the write.
Connection decisions, scope replacement and exchange approval are stored in
one transaction, serialized with exchange issuance on the connection row.
Live policy is checked on both exchange and delegated refresh. The connection verification endpoint also rechecks exchange approval, target and scopes for delegated access tokens. Resource servers must still validate their expected audience and enforce token sender constraints. A resource that only validates JWTs offline will not observe policy withdrawal until expiry or a separate revocation check. See the SSO exchange contract for request fields and the supported local-user, single-target profile.
Configure a caller and provider for exchange
- Register the calling app with confidential authentication and
urn:ietf:params:oauth:grant-type:token-exchangeingrant_types. Addrefresh_tokenif it will request authorizedoffline_access. - Configure the receiving provider's admission and approval policy, enable incoming token exchange and set its allowed exchange scopes. A provider flag alone does not enable the caller's grant or create a connection.
- Create the consumer → provider connection with the exact resource audience registered for both apps and the permitted scopes. Obtain active connection approval and, when configured as manual, separate provider exchange approval.
- Acquire an original M7 user access token for the calling app. Exchange it at
SSO
/tokenfor that one audience. Client-credentials tokens cannot be used as exchange subjects. Requestoffline_accessexplicitly only when a refresh package is needed; an access-only request remains access-only.
The original user remains sub; the caller remains client_id and becomes
act.sub. The receiving app's public OAuth client UUID is stamped in
m7.target_client_id and appended to the resource aud. It is not the
provider_cid database-record UUID or the owner's UUID. /register and
set_knobs configure policy; callers cannot supply custom target claims to
override the issuer. See token claims
and the current-source PHP/CLI exchange guide.
Setting availability to closed stops new connections but leaves existing
approvals intact. To withdraw an existing delegation, change its active status
or withdraw the applicable exchange approval. New exchange and delegated
refresh then fail; the source login remains independently usable. Resources
that need immediate policy enforcement must perform applicable live checks.
Shared connection routes
| Route | Required fields | Notes |
|---|---|---|
/oauth/clients/connections/insert |
consumer_cid, audience |
Provider is resolved from provider_cid or provider_client_id. Admission policy selects pending, automatic activation, or rejection; consumer and provider must differ. |
/oauth/clients/connections/search |
None | Filters: id, consumer_cid, provider_cid, provider_client_id, status, archived, side (consumer or provider), and optional org/tenant scope on an accessible side. With neither scope field, accessible sides are not restricted to one pair. status: "all" removes the status filter. |
/oauth/clients/connections/view |
id |
Returns one connection. |
/oauth/clients/connections/update |
id |
Cannot change provider, consumer, or audience. Provider access permits exchange approval; consumer permission edits require renewed approval. An empty patch fails with nothing to update. |
/oauth/clients/connections/update_status |
id, status |
Provider-side decision. Allowed statuses: pending, active, paused, declined, revoked. |
/oauth/clients/connections/delete |
id |
Optional archived. |
/oauth/clients/connections/recover |
id |
Restores an archived connection. |
/oauth/clients/connections/verify |
token, plus provider_cid or provider_client_id |
No bearer token required. Verifies the token against the current provider connection. |
Create a connection
curl -sS https://api.user.m7.org/api/v2/oauth/clients/connections/insert \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"consumer_cid": "44444444-4444-4444-8444-444444444444",
"audience": "https://api.example.m7.org",
"provider_client_id": "PROVIDER_PUBLIC_CLIENT_ID"
}'
The response uses comment: "INSERT" and returns the connection in data.
Its initial status follows the provider's admission policy.
Decide a connection request
curl -sS https://api.user.m7.org/api/v2/oauth/clients/connections/update_status \
-H 'Authorization: Bearer ACCESS_TOKEN' \
-H 'Content-Type: application/json' \
-d '{
"id": "55555555-5555-4555-8555-555555555555",
"status": "active"
}'
This returns comment: "UPDATE_STATUS".
Verify a connection token
/oauth/clients/connections/verify is the one documented route in this service
that does not use the caller's Bearer token. Send the token to inspect in the
JSON body together with either the provider application record ID
(provider_cid) or its public OAuth identifier (provider_client_id).
curl -sS https://api.user.m7.org/api/v2/oauth/clients/connections/verify \
-H 'Content-Type: application/json' \
-d '{
"token": "ACCESS_TOKEN_TO_VERIFY",
"provider_client_id": "PROVIDER_PUBLIC_CLIENT_ID"
}'
A completed check returns comment: "VERIFY". Inspect data.allowed and
data.token_valid; a cryptographically valid token can still have
allowed: false when the consumer, audience, connection status, or approved
scope relationship does not match.
Consumer and provider route families
/oauth/clients/connected/* is the consumer-side surface:
| Route | Required fields | Notes |
|---|---|---|
/oauth/clients/connected/search |
None | Optional id, consumer_cid, status, archived, org, tenant, and shared pagination. status: "all" removes the status filter. |
/oauth/clients/connected/discover |
None | Authenticated POST. Returns only public client_id and name for active, unexpired apps explicitly marked discoverable. Unlisted apps can still be entered by ID. |
/oauth/clients/connected/insert |
consumer_cid, audience |
Optional org, tenant select the consumer registration scope; provider is resolved from provider_cid or provider_client_id. Applies the provider's admission policy. |
/oauth/clients/connected/view |
id |
Returns one consumer-side connection. |
/oauth/clients/connected/update |
id |
Updates notes, scopes or expiry. A permission change resets status to pending and clears exchange approval. Provider, consumer and audience are immutable. |
/oauth/clients/authorized/* is the provider-side surface:
| Route | Required fields | Notes |
|---|---|---|
/oauth/clients/authorized/search |
None | Optional id, provider_cid, provider_client_id, status, archived, org, tenant, and shared pagination. status: "all" removes the status filter. |
/oauth/clients/authorized/view |
id |
Returns one provider-side connection. |
/oauth/clients/authorized/update |
id |
Updates notes, scopes, expiry and token_exchange_approved. Provider, consumer and audience are immutable; use update_status for status decisions. |
/oauth/clients/authorized/update_status |
id, status |
Provider-side status decision. Use pending, active, paused, declined, or revoked. |
Search responses
/oauth/clients/search, connection searches, and related issued-record
searches use the shared pagination contract. Registration
search always returns records in data.items with page metadata, even without
explicit pagination fields. Authorized/Connected list searches can return a
paged envelope when pagination is requested; ID lookups and an empty accessible
registration set still return arrays. Shared /connections/search returns an
array and does not apply pagination.