Skip to content

The v1 docs follow nauthera-server's main branch and its 0.x releases on the way to 1.0, so fields and APIs can still change.

v1

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.

PathWho uses it
OIDCClient manifest, reconciled by the operatorApplication teams on Kubernetes
gRPC AdminService.UpsertOIDCClientPlatform 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
FlagEffect
--publicPublic client: no secret, PKCE required
--jwks-uriprivate_key_jwt client, verified against the keys at this https URL
--org / --issuerHome the client in another organization
--frontchannel-logout-uriRegister a front-channel logout URI
--identity-providerOffer 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-fileA 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?.

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.

MethodPath
POST/registerRegister 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, 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.

ResponseCause
401No token, an invalid token, or a token without an org claim (invalid_token)
403The 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 shows the same on its clients pages.

MethodPathReturnsScope
GET/v1/admin/orgs/{slug}/clientsOne page of the organization's clients, searchable by client ID or display-name prefix (q) and filterable by type, auth_method, grant_type and sourcenauthera.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 labelsnauthera.clients:read
GET/v1/admin/orgs/{slug}/clients/{id}/usageDistinct users who obtained a token for the client in the last 24 hours, the last 30 days and since counting begannauthera.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

SettingValues
Token endpoint authenticationnone, 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 typesauthorization_code, refresh_token, client_credentials, urn:ietf:params:oauth:grant-type:device_code. implicit and password are refused.
Response typescode
PKCE policyrequired_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 signingES256 or RS256 (id_token_signed_response_alg).
LifetimesA 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 tokensIssued 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 providersThe 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 policyNarrows 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.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.

ScopeGrants
nauthera.orgs:read, nauthera.orgs:writeThe 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:writeThe users admin API
nauthera.audit:readThe audit log API
nauthera.clients:readListing and reading clients
nauthera.sessions:read, nauthera.sessions:writeListing and ending sessions
nauthera.insights:readAggregates: an organization's snapshot, client usage counts and audit event counts over time
nauthera.threats:readThe threat analytics
nauthera.roles:read, nauthera.roles:writeReading a user's roles, and granting and revoking them
nauthera.brokers:writeInviting, accepting, limiting and ending broker memberships
nauthera.domains:writeClaiming, verifying and removing email domains
nauthera.clients:registerDynamic client registration (an initial access token). Not an admin scope.

Admin scopes also need a matching RBAC role. See Admin API & Users.