# Clients: How Applications Are Registered

> How applications are registered with nauthera-server: OIDCClient manifests, the gRPC AdminService and dynamic client registration, and the settings that matter.

An OAuth client belongs to exactly one [organization](https://www.nauthera.io/docs/v1/organizations), decided
when it is first registered and never moved by a later write. There are three ways to
register one.

| Path | Who uses it |
|---|---|
| [`OIDCClient`](https://www.nauthera.io/docs/v1/oidc-client) manifest, reconciled by the [operator](https://www.nauthera.io/docs/v1/operator) | Application teams on Kubernetes |
| gRPC `AdminService.UpsertOIDCClient` | Platform automation and `nauthera-cli` |
| Dynamic client registration (`/register`) | Applications holding an initial access token |

The server also provisions each organization's own admin console and machine clients.
The admin API reports which of these a client came from as its `source`: `operator`
(the control plane, which the operator and `nauthera-cli` use), `dynamic` or
`provisioner`.

## With an OIDCClient manifest

The operator sends an `OIDCClient`'s spec to the server over the gRPC control plane,
writes the server's answer to the resource's `status`, and delivers a minted client
secret into a Kubernetes `Secret`. `spec.deletionPolicy`
decides what deleting the manifest does: under `Retain`, the default, the client stays
registered and its Secret is released; under `Delete` the client is removed. The
operator also notices when a client it manages was changed outside it. The resource is
described in [OIDCClient](https://www.nauthera.io/docs/v1/oidc-client), and installing and connecting the
operator in [Operator](https://www.nauthera.io/docs/v1/operator).

## Through the AdminService

`UpsertOIDCClient` is a declarative, idempotent full replace with an `etag` for
optimistic concurrency. When the spec asks for a secret (`secret.generate`), the server
mints one, stores only its SHA-256 digest, and returns the plaintext **once**, in the
response to the upsert that minted it. That is normally the upsert that created the
client. An update that asks for a secret for a client that has none (a public client
made confidential, for example) also mints one. Re-applying a spec never rotates the
secret.

On a local stack, `nauthera-cli` drives the same RPC:

```bash
go run ./cmd/nauthera-cli create client my-app \
  --redirect-uri https://app.example.com/callback \
  --post-logout-redirect-uri https://app.example.com/ \
  --scopes openid,profile,email,offline_access \
  --grant-types authorization_code,refresh_token \
  --pkce required_s256
```

| Flag | Effect |
|---|---|
| `--public` | Public client: no secret, PKCE required |
| `--jwks-uri` | `private_key_jwt` client, verified against the keys at this https URL |
| `--org` / `--issuer` | Home the client in another organization |
| `--frontchannel-logout-uri` | Register a front-channel logout URI |
| `--identity-provider` | Offer one of the organization's identity providers on the client's sign-in pages, as `<slug>` (sign-in and sign-up), `<slug>:sign-in` or `<slug>:sign-up`; repeatable |
| `--second-factor-file` | A YAML or JSON file with the client's second-factor policy, which replaces the whole policy |

`nauthera-cli` connects with mutual TLS when given `--tls-ca-file`, `--tls-cert-file`
and `--tls-key-file`; its certificate's identity must be listed in
`server.grpc.tls.allowed_clients` with the `clients` operation, and `organizations` too to
use `--org`. Without them it speaks plaintext gRPC, which the server accepts only with
`server.grpc.allow_insecure: true` on a loopback address.

## Dynamic client registration

Dynamic registration is for clients that are not yours to declare, such as third-party
software registering against your server. For your own applications on Kubernetes, prefer
an `OIDCClient` manifest: see [Operator or dynamic registration?](https://www.nauthera.io/docs/v1/operator-or-dynamic-registration).

`POST /register` implements RFC 7591. Each registration must present an **initial
access token**: a bearer token for the target organization that carries the
`nauthera.clients:register` scope. The response includes a `registration_access_token`
and a `registration_client_uri` for RFC 7592 management. Store the token from the
registration response: read and update responses do not return it again.

| Method | Path | |
|---|---|---|
| `POST` | `/register` | Register a client |
| `GET` | `/register/{client_id}` | Read its configuration |
| `PUT` | `/register/{client_id}` | Replace its configuration |
| `DELETE` | `/register/{client_id}` | Deregister it |

```bash
curl -s https://auth.example.com/register \
  -H "Authorization: Bearer $INITIAL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "client_name": "My App",
        "redirect_uris": ["https://app.example.com/callback"],
        "grant_types": ["authorization_code"],
        "response_types": ["code"],
        "token_endpoint_auth_method": "client_secret_basic",
        "scope": "openid profile email"
      }'
```

The registration policy is fixed in code and cannot be loosened by configuration:

- Initial access token: always required. It is what binds the new client to an
  organization.
- Scopes: only `openid`, `profile` and `email`. `offline_access` is deliberately
  excluded, so a self-registered client gets no refresh tokens and has to come back
  through `/authorize` with a live session. Any `nauthera.*` scope is refused outright.
- Grant types: `authorization_code` and `refresh_token`.
- PKCE: always `required_s256`. No registration field changes it.
- Redirect URIs: at most 8.

Back-channel logout URIs (`backchannel_logout_uri`) can be registered only this way.
A client's display name is checked for characters built to be misread, and suspicious
names are recorded in the audit log.

### Getting an initial access token

An initial access token is an ordinary access token from the organization's `/token`
endpoint that carries the `nauthera.clients:register` scope **and** an `org` claim. The
`org` claim decides which organization the new client belongs to; nothing in the
registration request can change that.

- A token issued to a **user** always carries `org`. Register a client through the gRPC
  AdminService with `nauthera.clients:register` among its scopes, and have someone sign
  in to it.
- A **`client_credentials`** token always carries `org`: the organization the server
  homed the client in when it was first stored. Register a confidential client with the
  `client_credentials` grant and `nauthera.clients:register` among its scopes, for
  example `nauthera-cli create client registrar --grant-types client_credentials
  --scopes nauthera.clients:register`. Every
  label under `nauthera.dev/` is the server's own: the control plane refuses a client
  that sets one, so no writer can name another organization there.

The reserved `{slug}-admin-*` clients do not carry the scope.

| Response | Cause |
|---|---|
| `401` | No token, an invalid token, or a token without an `org` claim (`invalid_token`) |
| `403` | The token lacks `nauthera.clients:register`. The body says `access_denied`; the `WWW-Authenticate` challenge says `insufficient_scope`. |

A DPoP-bound initial access token must be sent with the `DPoP` scheme and a matching
`DPoP` proof. A proof without a current nonce also answers `401`: the body says
`invalid_token`, the challenge says `use_dpop_nonce`, and a `DPoP-Nonce` header carries
the nonce to retry with.

### Managing a registered client

- **Identifiers.** The server assigns the `client_id` (`dcr_` followed by 128 random
  bits) and ignores one sent in the request. The secret is 256 random bits, returned
  once.
- **Read and update** need the registration access token. `PUT` replaces the whole
  configuration: a field left out is removed, not kept.
- **Unknown clients look like bad tokens.** An unknown `client_id` answers `401`, never
  `404`, so the endpoint cannot be used to discover client IDs.
- **Clients registered through the AdminService** cannot be changed over HTTP. They have no
  registration access token, so `GET`, `PUT` and `DELETE` on `/register/{client_id}` for one
  of them answer `401`, the same as an unknown `client_id`.
- **Deleting** a client makes every later grant fail at once, but access tokens already
  issued stay valid until they expire.
- **Unsupported metadata** is dropped rather than stored, as RFC 7591 §2 requires:
  `jwks`, `sector_identifier_uri`, `subject_type`, `request_object_signing_alg`,
  `userinfo_signed_response_alg`, `default_max_age`, `require_auth_time`,
  `default_acr_values` and `initiate_login_uri`.

## Viewing clients

The admin API lists and reads an organization's clients, and the
[admin dashboard](https://www.nauthera.io/docs/v1/admin-dashboard) shows the same on its clients pages.

| Method | Path | Returns | Scope |
|---|---|---|---|
| `GET` | `/v1/admin/orgs/{slug}/clients` | One page of the organization's clients, searchable by client ID or display-name prefix (`q`) and filterable by `type`, `auth_method`, `grant_type` and `source` | `nauthera.clients:read` |
| `GET` | `/v1/admin/orgs/{slug}/clients/{id}` | One client's registered configuration: redirect and logout URIs, scopes, grant and response types, authentication method, PKCE policy, token lifetimes, signing algorithms, source and labels | `nauthera.clients:read` |
| `GET` | `/v1/admin/orgs/{slug}/clients/{id}/usage` | Distinct users who obtained a token for the client in the last 24 hours, the last 30 days and since counting began | `nauthera.insights:read` |

No secret material is ever returned: the detail says only whether the client holds a
secret (`has_secret`). Registration `contacts` are returned only to a caller with the
`users.pii.read` permission. A client of another organization answers `404`, like one
that does not exist. A display name built to be misread (direction overrides, control
or invisible characters, padding, excessive length, markup) is flagged in
`display_name_markers`.

The usage counts include only grants that act for a user: the authorization code, a
refresh and the device code. A `client_credentials` token counts nobody. Each user counts
once per window, and the window edges are exact to within an hour.

## Client settings

| Setting | Values |
|---|---|
| Token endpoint authentication | `none`, `client_secret_basic`, `client_secret_post`, `private_key_jwt` (ES256 or RS256 assertions). `tls_client_auth` and `self_signed_tls_client_auth` are refused. |
| Grant types | `authorization_code`, `refresh_token`, `client_credentials`, `urn:ietf:params:oauth:grant-type:device_code`. `implicit` and `password` are refused. |
| Response types | `code` |
| PKCE policy | `required_s256` (default), `optional`, `disabled`. `optional` and `disabled` behave the same: a `code_challenge` is accepted and verified if sent, but not required. A public client always needs PKCE, whatever its policy says. Only S256 is accepted. FAPI organizations force `required_s256`. |
| ID token signing | `ES256` or `RS256` (`id_token_signed_response_alg`). |
| Lifetimes | A client registered through the AdminService can set its own access and ID token lifetimes (`access_token_ttl_seconds`, `id_token_ttl_seconds`), which override `issuer.access_token_ttl` and `issuer.id_token_ttl`; `0` means the issuer default. A per-client refresh lifetime (`refresh_token_ttl_seconds`) is applied too, and may not exceed `issuer.refresh_token_ttl`, which is also its default. Dynamically registered clients always use the issuer defaults. |
| Refresh tokens | Issued when the client is allowed to refresh and requests `offline_access`. They rotate on every use; reusing an old one revokes the whole family. See [Tokens & Sender Constraint](https://www.nauthera.io/docs/v1/tokens#refresh-tokens). |
| Identity providers | The organization's [identity providers](https://www.nauthera.io/docs/v1/identity-provider-resource) the client offers on its sign-in pages, each by slug, and whether each may sign users in, sign new users up, or both. A provider not listed is not offered, and its sign-ins are refused for this client. Set through the control plane only. |
| Second-factor policy | Narrows the organization's [second-factor policy](https://www.nauthera.io/docs/v1/mfa#the-policy) for this client. Unset defers to the organization, except that passwordless sign-in stays closed for a client that does not open it. Set through the control plane only. |

### Redirect URIs

Redirect URIs are matched **exactly**, with no wildcards and no prefix matching. Two
relaxations that native apps need are not supported yet: any port on a loopback
redirect and private-use URI schemes.

### Secrets

`UpsertOIDCClient` with `rotate_secret` mints a new secret for an existing client,
replaces the stored digest and returns the plaintext once. The operator does this when
the Secret holding an `OIDCClient`'s secret is deleted. `nauthera-cli` cannot rotate a
secret: delete the client and create it again.

## Scopes

Besides `openid`, `profile`, `email` and `offline_access`, the server reserves the
`nauthera.` prefix for its own resource scopes. Every scope in it except
`nauthera.clients:register` opens the admin API, and only an organization's own reserved
admin console and machine clients may carry one. The control plane refuses them on any
other client, dynamic registration refuses the whole prefix, and the admin API refuses a
token from any other client whatever it carries. At startup the server removes admin
scopes from any other client that still holds them and records each change in the audit
log.

The machine client gets `nauthera.users:read`, `nauthera.users:write` and
`nauthera.audit:read`, and in the main organization `nauthera.orgs:read` and
`nauthera.orgs:write` too. The console gets the same plus the client, session, insights,
threats, role, broker and domain scopes of the dashboard's other pages; a tenant
organization's console also gets `nauthera.orgs:read`.

| Scope | Grants |
|---|---|
| `nauthera.orgs:read`, `nauthera.orgs:write` | The organizations admin API, including the email relay page and its test message, the list of an organization's identity providers, and reading its broker memberships, email domains and the problems operators report for its resources |
| `nauthera.users:read`, `nauthera.users:write` | The users admin API |
| `nauthera.audit:read` | The audit log API |
| `nauthera.clients:read` | Listing and reading clients |
| `nauthera.sessions:read`, `nauthera.sessions:write` | Listing and ending sessions |
| `nauthera.insights:read` | Aggregates: an organization's snapshot, client usage counts and audit event counts over time |
| `nauthera.threats:read` | The threat analytics |
| `nauthera.roles:read`, `nauthera.roles:write` | Reading a user's roles, and granting and revoking them |
| `nauthera.brokers:write` | Inviting, accepting, limiting and ending broker memberships |
| `nauthera.domains:write` | Claiming, verifying and removing email domains |
| `nauthera.clients:register` | Dynamic client registration (an initial access token). Not an admin scope. |

Admin scopes also need a matching RBAC role. See
[Admin API & Users](https://www.nauthera.io/docs/v1/user-management).
