# Dynamic client registration

```text
POST https://sso.user.m7.org/register
Content-Type: application/json
Authorization: Bearer ACCESS_TOKEN
```

Dynamic registration creates an active personal OAuth client for the M7
principal represented by the access token, unless the request explicitly
selects an organization that principal may manage. It is suitable for
developer tooling and self-service application setup. First complete a user
sign-in to obtain that access token, then call `/register`. Use the
[M7 User API](https://m7.org/docs/api/api.user.m7.org/oauth-clients)
to manage a registered client's later configuration, status, or deletion.

For app-owned tenant sign-in screens, register an organization-owned application
then configure the explicit [browser-direct allowances](https://m7.org/docs/api/api.user.m7.org/tenant-policy)
through API User/User M7. Follow [browser-direct setup](https://m7.org/docs/api/sso.user.m7.org/browser-direct.md) for
callback origins and mandatory proofs; these policy switches are not additional
Dynamic Registration metadata advertised by this page.

The endpoint uses the JSON request/response format defined by
[OAuth Dynamic Client Registration (RFC 7591)](https://www.rfc-editor.org/rfc/rfc7591.html#section-3.1)
and [OpenID Connect Registration](https://openid.net/specs/openid-connect-registration-1_0.html).
M7-specific metadata is identified below; `response_modes` is an optional M7
extension, distinct from the standard `response_types` registration field.

The request must be JSON and must use an active M7 access token. The current
registration contract uses `Authorization: Bearer ACCESS_TOKEN`; requests
without a valid token and any binding material required by that credential
fail with `invalid_token`. The standard `dpop_bound_access_tokens` client
metadata controls the new client's DPoP requirement independently of the
credential used to authorize registration. See the
[project-specific DPoP status](https://m7.org/docs/api/sso.user.m7.org/authorization.md#dpop-status).

## Minimal public client

The minimal request creates a public web client enabled for authorization code
flow:

```bash
curl --fail-with-body --silent --show-error \
  --request POST 'https://sso.user.m7.org/register' \
  --header 'Authorization: Bearer ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "client_name": "Example App",
    "redirect_uris": ["https://app.example.com/oauth/callback"],
    "token_endpoint_auth_method": "none"
  }'
```

M7 requires at least one HTTPS `redirect_uris` entry. Redirect URIs are exact
values, not patterns. The registration endpoint does not permit HTTP loopback
or custom-scheme exceptions; use a registered HTTPS redirect URI.

## Request metadata

| Field | Required | Rules |
| --- | --- | --- |
| `redirect_uris` | Yes | Array of one or more unique HTTPS URLs. |
| `client_name` | No | Display name. Defaults from the first redirect URI host when omitted. |
| `application_type` | No | `web` (default), `native`, or `spa`. |
| `response_types` | No | Omit or set to `['code']`; no other response type is supported. |
| `response_modes` | No | M7 extension: JSON array of authorization response-mode names. Omission or `[]` registers `["query"]`. See [Response modes](https://m7.org/docs/api/sso.user.m7.org/registration.md#response-modes-m7-extension). |
| `authorization_signed_response_alg` | No | Standard JARM metadata: exactly `RS256` or `RS512`; omission defaults to RS256. The effective value is returned. It is independent of ID-token, UserInfo and client-authentication algorithms. |
| `grant_types` | No | Defaults to `['authorization_code']`. Allowed values: `authorization_code`, `refresh_token`, `client_credentials`, `urn:ietf:params:oauth:grant-type:device_code`, and `urn:ietf:params:oauth:grant-type:token-exchange`. See [connection configuration](https://m7.org/docs/api/sso.user.m7.org/registration.md#connection-configuration-m7-extensions) for exchange prerequisites and deployment status. |
| `scope` | No | Space-delimited string of allowed scopes. Scope names are case-sensitive; malformed or non-string values are rejected. |
| `tenant` | No | Organization UUID. Omit it for personal ownership. When supplied, the authenticated principal must be allowed to manage that active, non-archived organization. |
| `token_endpoint_auth_method` | No | `client_secret_basic` (default), `none`, `client_secret_post`, `client_secret_jwt`, `private_key_jwt`, `tls_client_auth`, or `self_signed_tls_client_auth`. Request `none` explicitly for a public client. |
| `client_secret` | No | Optional caller-selected secret for a secret-based method or HS256/384/512 token signing. M7 generates one when omitted; HMAC needs at least 32/48/64 bytes respectively. |
| `post_logout_redirect_uris` | No | Array of exact HTTPS URLs allowed for end-session return. |
| `frontchannel_logout_uri` | No | Optional [logout notification](https://m7.org/docs/api/sso.user.m7.org/logout-notifications.md) endpoint. HTTPS URL, at most 2048 characters, without credentials or a fragment; scheme, host and port must match a registered redirect URI. |
| `frontchannel_logout_session_required` | No | JSON boolean; defaults to `false`. `true` requires a configured front-channel URL. |
| `backchannel_logout_uri` | No | Optional [logout notification](https://m7.org/docs/api/sso.user.m7.org/logout-notifications.md) endpoint. HTTPS URL, at most 2048 characters, without credentials or a fragment. A separate host/port and query parameters are permitted. |
| `backchannel_logout_session_required` | No | JSON boolean; defaults to `false`. `true` requires a configured back-channel URL. |
| `initiate_login_uri` | No | Application's HTTPS login-start URL. M7 can use it after an expired PAR request. |
| `default_max_age` | No | Non-negative integer default authentication age in seconds. |
| `require_auth_time` | No | Boolean; defaults to `false`. When true, every ID token for the client must include the standard `auth_time` claim. This does not itself force reauthentication. |
| `id_token_signed_response_alg` | No | Available for issuance: `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, `ES512`, `ES256K`, `Ed25519`, `Ed448`, `ML-DSA-44`, `ML-DSA-65`, `ML-DSA-87`. Omission defaults to `RS256`; spelling is normalized and the effective value is returned. `HS256`, `HS384`, and `HS512` are also supported with a recoverable client secret; see [HMAC setup](https://m7.org/docs/api/sso.user.m7.org/registration.md#hmac-token-signing). |
| `userinfo_signed_response_alg` | No | `RS256` or `RS512`. When present, `/userinfo` returns a signed JWT using that algorithm. Omission retains ordinary JSON. |
| `dpop_bound_access_tokens` | No | Boolean; defaults to `false`. `true` requires a valid DPoP proof on every token request. `false` or omission still permits a client to request an opportunistically DPoP-bound token by sending a valid proof. |
| `token_endpoint_auth_signing_alg` | Conditional | Signing algorithm for `client_secret_jwt` or `private_key_jwt`. Omission selects `HS256` or `RS256`, respectively. |
| `jwks_uri` | Conditional | Public HTTPS JWKS URL for key-based authentication or the response-encryption policy below; mutually exclusive with `jwks`. |
| `jwks` | Conditional | Inline public JWK Set for key-based authentication or the response-encryption policy below; mutually exclusive with `jwks_uri`. |
| `tls_client_auth_subject_dn` | Conditional | Exact certificate subject DN selector for `tls_client_auth`; use exactly one subject-DN or SAN selector. |
| `tls_client_auth_san_dns` | Conditional | Exact DNS SAN selector for `tls_client_auth`. |
| `tls_client_auth_san_uri` | Conditional | Exact URI SAN selector for `tls_client_auth`. |
| `tls_client_auth_san_ip` | Conditional | Exact IP-address SAN selector for `tls_client_auth`. |
| `tls_client_auth_san_email` | Conditional | Exact email SAN selector for `tls_client_auth`. |
| `tls_client_certificate_bound_access_tokens` | Conditional | Boolean valid only with an mTLS authentication method; defaults to `false`. When true, issued access and refresh tokens are bound to the presented leaf certificate. |

Select an ID-token algorithm that is advertised by the live discovery document
and supported by the relying party's verifier. Both discovery documents
advertise all 18 profiles: 15 asymmetric plus HS256/384/512. This does not
upgrade existing SDKs. Updated M7 token-verifier source can use the optional
`m7crypto` backend, but current Web SDK full-token cookies are unsuitable for
general ML-DSA use; check [SDK/key and storage compatibility](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#signing-profiles)
before changing an existing web app. The signing choice is independent of
`userinfo_signed_response_alg`, client-authentication assertions, and JAR.

Registration does not grant a resource audience. Before the client can obtain
tokens, an administrator must configure at least one exact
`allowed_audiences` value for it. When several audiences are configured, each
grant may request any nonempty subset; omission selects the complete configured
set where the grant permits omission.

Organization ownership is authorized again during draft creation,
authentication setup, and activation. Registration fails closed if the
organization is missing, inactive, archived, or no longer manageable by the
authenticated principal. The successful response includes `tenant` for an
organization-owned client and omits it for a personal registration.

Dynamic registration supports only `web`, `native`, and `spa` application
types. It cannot create an `application_type=machine` client; create and
manage machine applications through the M7 User API instead.

The registration request also accepts these optional client metadata fields:
`client_uri`, `logo_uri`, `policy_uri`, `tos_uri`, `contacts`, `subject_type`,
`sector_identifier_uri`, `default_acr_values`, `software_id`, and
`software_version`.

The following deferred protocol metadata is deliberately rejected with
`invalid_client_metadata` before client creation or modification:

- `request_object_signing_alg`, `request_object_encryption_alg`, and
  `request_object_encryption_enc`; and
- `request_uris`.

Signed JAR is supported separately at `/authorize` for clients with an enabled
JAR Request Policy, configured through the M7 account application's OAuth
client settings. Dynamic Registration does not configure that policy: the
standard Request Object metadata listed above remains rejected. JAR signing
keys are distinct from this endpoint's token-authentication `jwks` or
`jwks_uri`. See [Signed Request Objects
(JAR)](./authorization-requests.md#signed-request-objects-jar).

An M7-issued PAR `request_uri` remains valid at `/authorize` independently of
JAR policy.

URL-bearing metadata must be valid URLs. `jwks_uri` and the URL configuration
fields that represent security metadata must use HTTPS.

Authentication metadata is validated as one closed combination before client
creation. `client_secret_jwt` accepts only `HS256`, `HS384`, or `HS512` and
requires a usable secret. `private_key_jwt` accepts only `RS256`, `RS384`,
`RS512`, or `ES256` and requires exactly one of `jwks_uri` or `jwks`.
JWT authentication fields on `none`, Basic, or POST registrations, incompatible
algorithms and incomplete key sources are rejected with
`invalid_client_metadata` without leaving an active client. A `client_secret`
on private-key JWT or mTLS is permitted when HS token signing is selected;
it serves HMAC and does not change the authentication method. SSO rejects
HS token signing with `none` as `invalid_client_metadata` before creating a
registration in the locally tested eligibility update described below;
production verification of that update remains pending.

`tls_client_auth` requires exactly one of the five subject-DN/SAN selector
fields. `self_signed_tls_client_auth` requires exactly one of `jwks_uri` or
`jwks`, and each usable key must represent a self-signed certificate through
its public `x5c` certificate chain. Private JWK members are rejected. The
certificate-bound-token boolean is valid only with one of these two methods.
After registration, use the mTLS operation aliases published by discovery;
the issuer and browser authorization endpoint do not change.

Before registering `client_secret_jwt`, configure the same
`M7_OAUTH_CLIENT_SECRET_JWT_KEYS` value on API User and SSO. HMAC token signing
also requires the same ring on ID.M7. New secrets are
encrypted with the first 64-character hexadecimal key; remaining keys permit
rolling decryption. A legacy password-hash row is deliberately not accepted as
an HMAC key and requires secret reissue.

Logout notification metadata configures [logout delivery](https://m7.org/docs/api/sso.user.m7.org/logout-notifications.md)
for participating applications. Delivery is deployed and advertised; saving a URL
alone does not send notifications or invalidate application sessions. M7 always
sends `sid` on both channels and `iss` on the front channel, even when the standard
session-required booleans are false or absent. They express receiver requirements,
not a send/omit option.

User.M7 shows the URL fields under Client → Logout only when editing an existing
application, with a separate save action and no session-ID checkboxes. These
receivers are independent of `post_logout_redirect_uris`, which remain under
General. Dynamic Registration can supply the notification fields at creation.

Blank or null notification URLs remain unconfigured. False session requirements
use sparse storage; supplied flags must be JSON booleans. For each configured
channel, the registration response returns the stored URL and effective boolean.
Unconfigured channels are omitted. Stored values are checked after policy setup
and before/after activation; a mismatch fails registration and triggers cleanup.

## HMAC token signing

Select HS256, HS384 or HS512 in `id_token_signed_response_alg`. In M7 this
preference also controls application access and refresh token signing. When
editing an existing app, first configure and save confidential-client
authentication on the Auth page. Then save the HMAC algorithm and set/reset
its secret on the Auth page or through API User's
`POST /api/v2/oauth/clients/set_auth`.
Dynamic Registration instead accepts a suitable supplied secret or generates
one, returning it once in the successful response.

Use randomly generated original secrets of at least 32 bytes for HS256,
48 for HS384 or 64 for HS512. The same value serves HMAC and Basic, POST or
secret-JWT authentication when both apply. With private-key JWT or mTLS, the
stored HMAC secret does not change the authentication method. Public clients
using `none` must choose an asymmetric signing algorithm. A browser or native
application does not become confidential merely by embedding a static secret.

The 2026-09-16 eligibility restriction is implemented and locally tested;
production deployment verification remains pending. User.M7 prevents the
`none`/HMAC combination using saved settings, while keeping an existing invalid
selection visible for repair. Dynamic Registration returns HTTP 400
`invalid_client_metadata`; supplying a secret does not make `none` eligible.
SSO returns HTTP 400 with
`unauthorized_client` at PAR, device authorization and `/token`, including
code pickup and refresh, before consuming grant artifacts. Existing invalid
records require an explicit configuration repair; there is no automatic
algorithm change or credential revocation. To change an HMAC client to
`none`, first choose and save an asymmetric algorithm. Shared secret storage,
signing services and the advertised algorithm inventory remain available.

Direct API User management retains the underlying storage support and can
still save this combination; it does not make the configuration eligible at
SSO. For an existing public client, save an asymmetric algorithm and start a
new sign-in where a new signing lineage is needed. Configure confidential
authentication only when the actual client can protect its credentials;
changing the method label alone does not establish confidentiality. This
restriction does not revoke previously issued credentials or replace their
normal expiry and revocation controls.

HMAC requires recoverable encrypted storage. Existing Basic/POST password
hashes cannot be converted into signing keys. Missing, empty, hashed,
unreadable or undersized secrets fail before issuance; save a suitable secret
before retrying. M7 never downgrades to another algorithm. A pre-existing
personal asymmetric refresh lineage cannot switch families in place; start
a new sign-in after changing it to HMAC. Root credentials remain asymmetric.

HMAC has no public certificate or PEM/JWK/JWKS lookup. Never expose the secret
to browser JavaScript. A secret holder can both verify and create MACs, so
local MAC validation alone does not prove provider issuance or active state.
M7 additionally checks the issued token's recorded hash; resource consumers
must retain authoritative issued-token/state checks, such as applicable
introspection, plus claims, scope, token-class and sender constraints.
Retain an old secret while accepting old tokens after rotation. ID tokens
are relying-party assertions, not API credentials; refresh tokens remain
opaque client state.

HS256/384/512 passed client-credentials, consumer device-code/strict-refresh
and browser authorization-code/PAR login, profile and strict-refresh campaigns
in September 2026. Browser tests included PKCE and DPoP. Basic/Post
introspection, revocation and temporary-client cleanup passed. The historical
`none` device case verified signing mechanics; the eligibility restriction
above now excludes that combination and preserves confidential-client HMAC.
This does not claim every authentication method, refresh mode or live secret
rotation, nor formal OpenID conformance.

The Web SDK uses authenticated SSO introspection and passed without m7crypto.
Current Token/PHP source supports local HMAC through explicit `hmac_secret`
and the native extension; existing immutable SDK ZIPs are unchanged. See
[SDK HMAC guidance](https://m7.org/sdk/m7-identity#hmac-in-current-source).
Both live discovery documents advertise these HMAC algorithms with the
15 asymmetric profiles, verified at 2026-09-15 21:49:24 UTC. See
[HMAC and discovery](https://m7.org/docs/api/sso.user.m7.org/discovery-and-keys.md#hmac-and-discovery);
clients must follow live metadata for automatic negotiation.
UserInfo, JARM, JAR, DPoP and client-authentication assertions retain their
separate algorithm lists.

## Response modes (M7 extension)

Register one or more modes through the same JSON request:

```json
{
  "client_name": "Example App",
  "redirect_uris": ["https://app.example.com/oauth/callback"],
  "response_types": ["code"],
  "response_modes": ["query", "form_post"],
  "token_endpoint_auth_method": "none"
}
```

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`. Names are trimmed, lowercased and deduplicated.

- Omission or an empty JSON array registers `query`.
- A nonempty list replaces that default; `query` is not added automatically
  to a list such as `["form_post"]`.
- The field must be a JSON array of strings. Null, strings (including
  newline-separated text), objects, nested arrays, non-string entries, blank
  names and unknown mode names produce `invalid_client_metadata` before
  creating a client.
- The successful response includes the modes actually saved by API.USER,
  in stored lexical order. The endpoint verifies that this set matches the
  normalized request before activation and again after activation. Missing,
  malformed or changed backend metadata produces `server_error` and invokes
  registration cleanup.

All ten modes are implemented and advertised by live discovery. Register only
the modes the application can securely receive; each authorization request
selects one permitted mode. `response_mode` (singular) belongs to an individual
authorization request; `response_modes` (plural) is this M7 registration extension. Other
providers are not required to understand this extension.

For signed JARM, register a JWT mode and set
`authorization_signed_response_alg` to RS256 or RS512. The default is RS256
even when the field is omitted. Unsupported algorithms, `none`, null and
malformed values are rejected as `invalid_client_metadata`.
JARM encryption metadata is configured through the
[response-encryption policy](https://m7.org/docs/api/sso.user.m7.org/registration.md#response-encryption-policy).
Registered response encryption is deployed; see the scoped live evidence in
[response encryption](https://m7.org/docs/api/sso.user.m7.org/response-encryption.md). See
[response delivery and validation](https://m7.org/docs/api/sso.user.m7.org/response-modes.md).

Web Message registration does not grant iframe permission. The embedding
origin also needs the provider's explicit framing allowance, and the client
must satisfy the [browser requirements](https://m7.org/docs/api/sso.user.m7.org/response-modes.md#iframe-prerequisites-and-current-limits).

## Response-encryption policy

Registration accepts and forwards the following standard metadata to API User.
SSO enforces required encryption for ID tokens, JARM and signed UserInfo.
The inline-key RS256 browser path passed live login, refresh, UserInfo and
cancellation acceptance on 2026-09-16. Confirm receiving SDK support before
enabling a required policy. See
[response encryption](https://m7.org/docs/api/sso.user.m7.org/response-encryption.md) for key selection and rollout.

| Response | Algorithm field | Content-encryption field |
| --- | --- | --- |
| ID token | `id_token_encrypted_response_alg` | `id_token_encrypted_response_enc` |
| JARM | `authorization_encrypted_response_alg` | `authorization_encrypted_response_enc` |
| Signed UserInfo | `userinfo_encrypted_response_alg` | `userinfo_encrypted_response_enc` |

For each selected response, set the algorithm to exactly `RSA-OAEP-256` and
the content-encryption value to exactly `A256GCM`. Omit both fields to leave
that response's policy unset. Nulls, empty strings, partial pairs and other
algorithms produce HTTP 400 `invalid_client_metadata` before client creation.
OIDC and JARM default an omitted content-encryption value to `A128CBC-HS256`;
this profile does not implement that default and requires explicit `A256GCM`.
The inner signing algorithm remains independent. UserInfo encryption also
requires `userinfo_signed_response_alg` to be `RS256` or `RS512`.

Supply exactly one standard public source, `jwks` or `jwks_uri`. For example,
merge this metadata with the usual registration request:

```json
{
  "jwks_uri": "https://app.example.com/.well-known/jwks.json",
  "id_token_encrypted_response_alg": "RSA-OAEP-256",
  "id_token_encrypted_response_enc": "A256GCM"
}
```

Encryption may be registered with any supported authentication method,
including `none`. Inline encryption keys must be public RSA keys with distinct
nonempty `kid` values, 2048–8192-bit moduli and compatible `use`, `alg` and
`key_ops`. Private/symmetric material and encryption-key URL/certificate
members are rejected. The inline set is limited to 32 keys and 64 KiB.
For `private_key_jwt` or `self_signed_tls_client_auth`, a combined inline set
must identify every key with `use`: `sig` for authentication, `enc` for
encryption. Each subset must satisfy its existing validator. Signing keys in
an encryption registration currently require one of those key-based auth
methods. A source without any encryption policy retains the existing
authentication-only registration rules.

Encryption `jwks_uri` must use a public DNS HTTPS hostname on port 443 with no
credentials, fragment, IP literal or local hostname. Registration checks URL
syntax without fetching it. At issuance, SSO fetches through its pinned HTTPS
boundary, validates the document and selects the encryption subset.

SSO forwards the public encryption source into API User's separate
`response_encryption_jwks` or `response_encryption_jwks_uri` configuration and
sends the six policy fields unchanged through `oauth.clients.set_knobs`.
Authentication retains its own key configuration. Successful registration
returns the standard six fields and `jwks` or `jwks_uri`; a mixed inline set
is reconstructed from both stored purposes. Internal source aliases are not
part of the public registration contract. Missing or changed backend settings
produce HTTP 502 `server_error` and trigger cleanup, with checks before and
after activation.

Configure the receiving application with the matching private key and required
policy before enabling encryption. Registering a public key does not supply a
private key or decryption implementation to your application.
The field names follow [OIDC Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html#ClientMetadata)
and [JARM client metadata](https://openid.net/specs/oauth-v2-jarm-final.html#section-3).

## Connection configuration (M7 extensions)

The following optional top-level fields store the same settings as User.M7's
Knobs → Connections panel. Registration stores these settings; the token
endpoint enforces them with the live connection and source authorization.

These settings control incoming connections and exchange policy for the
registered provider. `closed` rejects new connections without revoking existing
ones; `unlisted` permits requests by known client ID; `discoverable` also lists
the app's public name and client ID. Approval can be manual, automatic, or
automatic for the same registered owner. For organization apps, the owner is
the organization itself; the managing users never define the same-site boundary.
Personal apps match by their personal owner. Missing settings default to closed,
manual approval and exchange disabled.

| Field | JSON type and accepted values |
| --- | --- |
| `connection_policy_availability` | String: `closed`, `unlisted`, or `discoverable`. |
| `connection_policy_approval` | String: `manual`, `same_site_auto`, or `auto`. |
| `connection_policy_other_sites` | String: `manual` or `reject`; fallback for different owners under `same_site_auto`. |
| `connection_policy_token_exchange_enabled` | Boolean. Explicit `false` is stored and returned. |
| `connection_policy_token_exchange_approval` | String: `manual` or `connection_policy`; records separate approval or inclusion in connection approval. |
| `connection_policy_token_exchange_scopes` | Array of up to 100 scope strings, at most 256 bytes each, using OAuth scope-token characters. Duplicates are removed. `[]` leaves the setting absent. |
| `connection_policy_token_exchange_ttl_max` | Integer from 1 to 2147483647 seconds. |

Omit a field to leave it unconfigured. Null, empty strings, string booleans,
string TTLs and scope objects are rejected with `invalid_client_metadata`
before client creation. Preferences may be stored while exchange is disabled.
The scope ceiling is separate from the client's `scope` registration metadata;
neither it nor an absent ceiling grants resource authority.

```json
{
  "client_name": "Example connection configuration",
  "redirect_uris": ["https://app.example.com/oauth/callback"],
  "token_endpoint_auth_method": "client_secret_basic",
  "grant_types": [
    "authorization_code",
    "urn:ietf:params:oauth:grant-type:token-exchange"
  ],
  "connection_policy_availability": "unlisted",
  "connection_policy_approval": "manual",
  "connection_policy_other_sites": "reject",
  "connection_policy_token_exchange_enabled": true,
  "connection_policy_token_exchange_approval": "manual",
  "connection_policy_token_exchange_scopes": ["files:read"],
  "connection_policy_token_exchange_ttl_max": 600
}
```

The canonical Token Exchange URI is accepted in the standard `grant_types`
field. It permits the requesting app to use the grant; the provider's exchange
policy flag controls incoming exchange and does not add a grant. `/token`
implements the [local-user, single-target exchange profile](https://m7.org/docs/api/sso.user.m7.org/token.md#token-exchange-rfc-8693),
and both live discovery documents advertise the canonical grant. The consumer
exchange and refresh path passed scoped live acceptance on 2026-09-20.
In `manual` exchange-approval mode, the
provider must also approve exchange on the particular connection. In
`connection_policy` mode, the active connection supplies that approval.
Neither mode expands the user's signed scope authority. An absent exchange
scope ceiling permits nothing. The exchange TTL cap limits both access and
refresh lifetimes; refresh still requires explicit authorized `offline_access`.
Existing HTTPS redirect-URI and application-type requirements still apply,
including registrations containing only the exchange grant.

Successful responses include these fields from the saved configuration and
the saved grant list. SSO checks storage after saving policy, after client
authentication setup and after activation. Missing or changed values cause
HTTP 502 `server_error` and trigger deletion of the incomplete client, so a
backend that drops the new fields cannot report successful registration.

## Confidential-client examples

### Client secret

```json
{
  "client_name": "Example server app",
  "redirect_uris": ["https://app.example.com/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "client_secret_basic"
}
```

M7 returns `client_secret` once in the successful registration response if the
selected authentication method or HMAC token-signing choice needs one. Store it immediately in a
secret manager; do not expect the plaintext secret to be retrievable later.

### Private-key JWT

```json
{
  "client_name": "Example private-key client",
  "redirect_uris": ["https://app.example.com/oauth/callback"],
  "token_endpoint_auth_method": "private_key_jwt",
  "token_endpoint_auth_signing_alg": "ES256",
  "jwks_uri": "https://app.example.com/.well-known/jwks.json"
}
```

For `private_key_jwt`, M7 selects the assertion key by `kid` from either the
registered inline JWKS or the registered HTTPS JWKS URL. The assertion must
use a configured supported signing algorithm and satisfy the token-endpoint
client assertion checks described in [Token endpoint](https://m7.org/docs/api/sso.user.m7.org/token.md#client-authentication).

## Successful response

M7 responds with HTTP 201, standard registration metadata and the M7
`response_modes` extension:

```json
{
  "client_id": "CLIENT_ID",
  "client_id_issued_at": 1786309200,
  "application_type": "web",
  "client_name": "Example App",
  "redirect_uris": ["https://app.example.com/oauth/callback"],
  "response_types": ["code"],
  "response_modes": ["query"],
  "grant_types": ["authorization_code"],
  "token_endpoint_auth_method": "none",
  "dpop_bound_access_tokens": false
}
```

The response includes the submitted optional metadata that M7 accepted. A
secret-based or HMAC-signing registration additionally includes `client_secret` and
`client_secret_expires_at` (currently `0` for no fixed expiry).

## Common errors

| Error | Typical cause |
| --- | --- |
| `invalid_token` | Missing, expired, revoked, or incorrectly bound bearer token. |
| `invalid_client_metadata` | Body is not a JSON object, has unsupported or malformed metadata (including `response_modes`), contains a malformed `tenant`, or selects an organization the principal cannot currently manage. |
| `invalid_redirect_uri` | Missing, malformed, non-HTTPS, or duplicate-invalid redirect URI data. |
| `server_error` | Registration could not reach or complete its management backend. |

## Register consumer organization access

Register `orgs` in the consumer client's permitted scope set, request it
explicitly and obtain hosted consent before calling
[consumer org discovery](https://m7.org/docs/api/sso.user.m7.org/userinfo-orgs.md). Registration alone does not add
the scope to existing tokens. The same registered UserInfo signing/encryption
policy governs `/userinfo/orgs`; there is no per-request format override or
separate org response-policy flag. Tenant `groups` and primary-email `email`
remain separate permissions. Device authorization requires hosted
[device consent](https://m7.org/docs/api/sso.user.m7.org/device-authorization.md#user-approval) before granting `orgs`.
