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

## 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](https://m7.org/docs/api/api.user.m7.org/org.md#organization-roles). Organization application tokens do not manage
OAuth client records through this family. See the
[authorization guide](https://m7.org/docs/api/api.user.m7.org/authorization.md) 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](https://m7.org/docs/api/api.user.m7.org/organization-upgrade.md#oauth-ownership-storage-prerequisite).

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:

1. Create the record with `/oauth/clients/insert`.
2. Make it confidential, when needed, with `/oauth/clients/set_auth`.
3. Configure policy with `/oauth/clients/set_knobs`.
4. Activate it with `/oauth/clients/update_status`.
5. For tenant applications, configure registration and login groups with
   `/oauth/clients/set_groups`.
6. 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](https://m7.org/docs/api/api.user.m7.org/principal-lists.md). |
| `/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 `status` to `draft`;
- sets `token_endpoint_auth_method` to `none`;
- ignores policy-knob fields (use `set_knobs` instead);
- fixes the tenant at creation time; and
- defaults an empty `grant_types` list 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`.

```bash
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"
  }'
```

```json
{
  "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

```bash
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.

```bash
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](https://m7.org/docs/api/sso.user.m7.org/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](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys#signing-profiles).

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 `update` preserves 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 on `insert`, or on an update whose stored list is empty, also
  saves `["query"]`. A supplied nonempty list takes precedence.
- `view` and `search` backfill 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:

```json
{
  "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](https://m7.org/docs/api/sso.user.m7.org/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:

- `none`
- `client_secret_post`
- `client_secret_basic`
- `client_secret_jwt`
- `private_key_jwt`
- `tls_client_auth`
- `self_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](https://m7.org/docs/api/sso.user.m7.org/registration#hmac-token-signing)
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.

```bash
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.

```json
{
  "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

```bash
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](https://m7.org/docs/api/sso.user.m7.org/response-encryption).
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:

```json
{
  "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](https://user.m7.org/members/settings/app-registration)
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](https://m7.org/docs/api/api.user.m7.org/org.md#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](https://m7.org/docs/api/api.user.m7.org/tenant-policy.md) 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](https://m7.org/docs/api/api.user.m7.org/oauth-clients.md#response-encryption) selects recipient keys and
requires encryption without changing the inner signing algorithm.

To configure RS512 for a client that already allows a JARM mode:

```json
{
  "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.

```bash
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.

```bash
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:

```bash
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`:

```bash
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`, or
  `discoverable`. Closed prevents new connections; existing approvals remain.
- `connection_policy_approval`: `manual` (default), `auto`, or `same_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_sites` chooses `manual` or `reject` outside
  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-owned `token_exchange_approved` on each connection;
  `connection_policy` uses 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](https://m7.org/docs/api/sso.user.m7.org/token#token-exchange-rfc-8693)
for request fields and the supported local-user, single-target profile.

### Configure a caller and provider for exchange

1. Register the calling app with confidential authentication and
   `urn:ietf:params:oauth:grant-type:token-exchange` in `grant_types`. Add
   `refresh_token` if it will request authorized `offline_access`.
2. 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.
3. 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.
4. Acquire an original M7 user access token for the calling app. Exchange it at
   SSO `/token` for that one audience. Client-credentials tokens cannot be used
   as exchange subjects. Request `offline_access` explicitly 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](https://m7.org/docs/api/sso.user.m7.org/token-formats#requesting-client-and-receiving-application)
and the [current-source PHP/CLI exchange guide](https://m7.org/docs/sdk/m7-identity/cli/token-exchange).

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

```bash
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

```bash
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`).

```bash
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](https://m7.org/docs/api/api.user.m7.org/README.md#pagination). 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.
