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:

  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.
/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.

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

{
  "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:

  • 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 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, 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 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 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.