Clients: How Applications Are Registered
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
An OAuth client belongs to exactly one organization, 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 manifest, reconciled by the operator | Application teams on Kubernetes |
gRPC AdminService. | 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, and installing and connecting the
operator in 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:
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. with the clients operation, and organizations too to
use --org. Without them it speaks plaintext gRPC, which the server accepts only with
server. 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?.
POST /register implements RFC 7591. Each registration must present an initial
access token: a bearer token for the target organization that carries the
nauthera. scope. The response includes a registration_
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 |
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,profileandemail.offline_accessis deliberately excluded, so a self-registered client gets no refresh tokens and has to come back through/authorizewith a live session. Anynauthera.*scope is refused outright. - Grant types:
authorization_codeandrefresh_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. 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 withnauthera.among its scopes, and have someone sign in to it.clients: register - A
client_credentialstoken always carriesorg: the organization the server homed the client in when it was first stored. Register a confidential client with theclient_credentialsgrant andnauthera.among its scopes, for exampleclients: register nauthera-cli create client registrar --grant-types client_. Every label undercredentials --scopes nauthera. clients: register 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.. 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.
PUTreplaces the whole configuration: a field left out is removed, not kept. - Unknown clients look like bad tokens. An unknown
client_idanswers401, never404, 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,PUTandDELETEon/register/{client_id}for one of them answer401, the same as an unknownclient_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_valuesandinitiate_login_uri.
Viewing clients
The admin API lists and reads an organization's clients, and the admin dashboard shows the same on its clients pages.
| Method | Path | Returns | Scope |
|---|---|---|---|
GET | / | 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 | / | 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 | / | 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_ are refused. |
| Grant types | authorization_code, refresh_token, client_credentials, urn:. 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_). |
| 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_) 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. |
| Identity providers | The organization's identity providers 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 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. 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. | Dynamic client registration (an initial access token). Not an admin scope. |
Admin scopes also need a matching RBAC role. See Admin API & Users.