OIDCClient
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
An OIDCClient declares one OAuth 2.0 / OpenID Connect client. The
operator sends its spec to the server's gRPC AdminService
(UpsertOIDCClient), writes the server's answer to status, and delivers a generated
client secret into a Kubernetes Secret.
| Property | Value |
|---|---|
| API group / version | nauthera.io/v1alpha1 |
| Kind | OIDCClient |
| Plural / short name | oidcclients / oidcc |
| Scope | Namespaced |
| Definition | deploy/ in nauthera-server |
Example
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: example-webapp
namespace: nauthera # a namespace the organization admits
spec:
clientId: example-webapp
displayName: Example Web App
redirectUris:
- https://app.example.com/callback
- https://app.example.com/silent-renew
postLogoutRedirectUris:
- https://app.example.com/
grantTypes:
- authorization_code
- refresh_token
responseTypes:
- code
scopes:
- openid
- profile
- email
- offline_access
tokenEndpointAuthMethod: client_secret_basic
pkcePolicy: required_s256
allowRefresh: true
accessTokenTtlSeconds: 900 # 15m
idTokenTtlSeconds: 900 # 15m
deletionPolicy: Retain
secret:
generate: trueThe server applies all three token lifetimes. refreshTokenTtlSeconds may not exceed the
deployment's issuer.refresh_token_ttl (a longer one is refused), and a client that sets
none gets that lifetime.
The spec below comes from the resource definition, and two of its field descriptions are
wrong. pkcePolicy: disabled does not reject PKCE. It behaves like optional: a
code_challenge is accepted and verified if sent, but not required. A public client
always needs PKCE, whatever its policy says. The idTokenSignedResponseAlg and
pkcePolicy descriptions also say the operator surfaces a refused apply as a NotReady
condition. There is no such condition: a refusal is the Ready condition with status
False and reason Refused, and phase: Error (see Status).
How secrets work
The resource never holds a client secret. When secret.generate is true, the server
generates a high-entropy secret and stores only its SHA-256 digest. It 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 without one
(a public client made confidential, for example) also mints and returns one. Re-applying a
spec never rotates the secret. Every other response carries only a non-reversible
fingerprint (status.secretFingerprint).
The operator writes the plaintext into a Secret in the resource's namespace, named by
spec.secret.secretName or, by default, after the resource. The Secret holds the keys
client-id, client-secret and issuer, carries the fingerprint in its
nauthera. annotation, and is recorded in status.secretRef. It is
owned by the resource, and the operator never writes over a Secret it does not own; that
case is reported as SecretUndeliverable.
To rotate the secret, delete the Secret. The operator mints a new secret, writes it again
and records a SecretRotated event with both fingerprints. Workloads that read the old
secret must reload it.
A secret you supply
spec.secret.secretRef takes the secret from a Secret you hold instead, such as one a
SealedSecret or the External
Secrets operator produces, or one an application outside the cluster also uses:
spec:
tokenEndpointAuthMethod: client_secret_basic
secret:
secretRef:
name: storefront-oidc
key: client-secret # the defaultThe operator reads the value and sends it; the server stores only its digest and reports
its fingerprint in status.secretFingerprint, as for a minted one. The operator never
writes or owns that Secret. Editing it sends the new value, which replaces the old one at
once, with no overlap. The value must be at least 43 printable ASCII characters with no
whitespace, drawn at random (openssl rand -base64 48 | tr -d '\n'); the server refuses a
shorter one, and a value with leading or trailing whitespace is SecretPadded. A missing
Secret or key is SecretNotFound or SecretKeyMissing, and nothing is sent until it
appears. secretRef excludes generate and secretName, and is refused for none and
private_key_jwt. It needs a server that serves control-plane contract 8; an older one
leaves the resource Unsupported.
A private_key_jwt client authenticates with an RFC 7523 §2.2 assertion signed by a key
it publishes at jwksUri. For that method jwksUri is required, and secret.generate
is refused.
Second factor and identity providers
spec.secondFactor is the client's second-factor policy. It narrows its organization's
policy, which narrows the deployment's, and can never loosen it: it can remove methods,
require a factor, shorten rememberForSeconds, spare in fewer zones, always challenge,
or add rules that narrow it for sign-ins outside or inside named network zones, from a
new network or from a new browser. Passkey sign-in with no password is open only where
the deployment, the organization and the client all set passwordless: true. A policy
that states more than the levels above allow is refused, and the resource reports
Refused naming the field and the bound. See
Multi-Factor Authentication.
secondFactor:
required: true
methods: [passkey]
rules:
- name: off-site
outsideZones: [office]
require: truespec.identityProviders lists the identity providers
in the client's organization that its users may use, by slug. A provider the client does
not list is not offered to it, and none is listed by default. allowSignIn lets a user
whose account at the provider is already known, or links to an existing account, sign in
through it. allowSignUp lets an account at the provider that no user holds create one,
and still needs the deployment's auth.. See
Social sign-in.
identityProviders:
- slug: google
allowSignIn: true
allowSignUp: trueSpec
spec. accessTokenTtlSeconds
Type: integer | Optional
Access token lifetime in seconds.
spec.allowRefresh
Type: boolean | Optional
Whether refresh tokens may be issued to this client.
Defaults to false.
spec.clientId
Type: string | Required
Stable, unique OAuth2 client_id. Immutable.
spec.deletionPolicy
Type: string | Optional
What deleting this resource does to the client it registered. Retain leaves the client registered and the Secret holding its credential in place, so deleting a namespace cannot take a live client with it. Delete removes the client from the server, and the Secret goes with the resource. A removal that fails keeps the resource, in phase Deleting, until it succeeds.
Defaults to "Retain".
One of: Retain, Delete.
spec.displayName
Type: string | Optional
Human-readable name shown on consent screens.
spec. frontchannelLogoutSessionRequired
Type: boolean | Optional
The client requires the iss and sid query parameters on the front-channel logout request. It is SKIPPED when no sid is available, rather than sent a request it cannot act on.
spec. frontchannelLogoutUri
Type: string | Optional
RP URL the provider renders in an iframe to log this client out (OIDC Front-Channel Logout 1.0 §2). Validated as a redirection target: the browser fetches it, not the server.
spec.grantTypes
Type: []string | Optional
OAuth2 grant types this client may use.
spec. idTokenSignedResponseAlg
Type: string | Optional
JWS algorithm this client's ID tokens are signed with (RFC 7591 id_token_signed_response_alg). Omit to accept the algorithm the deployment signs with.
An organization held to FAPI 2.0 accepts only the algorithms that profile permits (§5.4.1), which this build narrows to ES256. That cannot be expressed here — the org's FAPI flag is server state, not part of this resource — so the server refuses the apply, and the resource reports Ready False with reason Refused and phase Error.
One of: ES256, RS256.
spec.idTokenTtlSeconds
Type: integer | Optional
ID token lifetime in seconds.
spec.identityProviders
Type: []object | Optional
The identity providers in this client's organization its users may sign in or sign up with, each named by its IdentityProvider's slug. A provider not listed is not offered to this client.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
allowSignIn | boolean | No | false | Whether a user whose account at the provider is already known, or links to an existing account, may sign in through it. |
allowSignUp | boolean | No | false | Whether an account at the provider that no user holds may create one. The organization's own sign-up setting still applies. |
slug | string | Yes | — | The slug of an IdentityProvider, or of an LDAPDirectory, in the client's organization. |
spec.jwksUri
Type: string | Optional
HTTPS URL where this client publishes its own public keys (RFC 7591 jwks_uri). Required for tokenEndpointAuthMethod 'private_key_jwt', where it IS the credential: the RFC 7523 client assertion is verified against the keys served here. Also used to verify an RFC 9101 signed request object.
The server FETCHES this URL, which makes it a server-side request-forgery surface rather than a displayed link. It must be https and must not name a loopback, private, link-local or shared-address-space host; the server re-checks the resolved address at dial time and never follows redirects.
spec.ldapDirectories
Type: []object | Optional
The LDAP directories in this client's organization whose users sign in with their directory password, each named by its LDAPDirectory's slug and tried in order. An identifier no listed directory holds signs in with a local password.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
allowSignIn | boolean | No | false | Whether a user whose account at the provider is already known, or links to an existing account, may sign in through it. |
allowSignUp | boolean | No | false | Whether an account at the provider that no user holds may create one. The organization's own sign-up setting still applies. |
slug | string | Yes | — | The slug of an IdentityProvider, or of an LDAPDirectory, in the client's organization. |
spec.organization
Type: string | Optional
The slug of the organization this client belongs to. Needed only where more than one organization admits the namespace in its spec.namespaces; otherwise the one that does is taken. The main organization is "default". Immutable.
spec.pkcePolicy
Type: string | Optional
PKCE enforcement. required_s256 mandates code_challenge with S256. optional and disabled both permit it without requiring it, and a challenge that is sent is verified. A public client must use PKCE whatever the policy.
If the organization this client is homed at runs in FAPI mode, only required_s256 is accepted for a client registering the authorization_code grant (FAPI 2.0 Security Profile 5.3.2.2). That cannot be expressed as a CEL rule here — the org's FAPI flag is server state, not part of this resource — so the server refuses the apply, and the resource reports Ready False with reason Refused and phase Error.
Defaults to "required_s256".
One of: required_s256, optional, disabled.
spec. postLogoutRedirectUris
Type: []string | Optional
Allowed post_logout_redirect_uri values.
spec.redirectUris
Type: []string | Optional
Allowed redirect_uri values (exact match).
spec. refreshTokenTtlSeconds
Type: integer | Optional
Refresh token lifetime in seconds. The server refuses a value longer than its issuer.refresh_token_ttl.
spec.responseTypes
Type: []string | Optional
OAuth2/OIDC response types. Only the authorization code response is served; the implicit and hybrid flows are not implemented, so no other value is accepted.
spec.scopes
Type: []string | Optional
Scopes the client is permitted to request.
spec.secondFactor
Type: object | Optional
The client's second-factor policy, narrowing its organization's. Passwordless sign-in stays closed for a client that does not open it, whatever its organization says.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
alwaysChallenge | boolean | No | — | Spares no sign-in, on a remembered browser or in a zone. |
methods | []string | No | — | The methods a user may enroll and answer with. Empty inherits the level above. |
passwordless | boolean | No | — | Opens sign-in with a discoverable passkey and no password. It is open only where the deployment, the organization and the client all open it. |
rememberForSeconds | integer | No | — | Spares a user who holds a permitted factor the challenge on a browser that passed a second factor for them within this many seconds, while their password and factors are the ones it passed under. Zero inherits the level above's window. |
required | boolean | No | — | Sends a user who holds no permitted factor through enrollment before a session is issued. |
rules | []object | No | — | Narrow this level's policy for the sign-ins that meet them. A rule only ever narrows its own level. |
spareZones | []string | No | — | Spares such a user the challenge from these of the deployment's network zones. Empty inherits the level above's. |
spec.secret
Type: object | Optional
Client secret handling. The secret value itself is never placed in this resource; setting generate=true asks the server to mint one and return it exactly once.
Defaults to {"generate":false}.
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
generate | boolean | No | false | Ask the server to generate a client secret. |
secretName | string | No | — | The Secret in this namespace the generated secret is written to, under the keys client-id, client-secret and issuer. Defaults to the resource's own name. The Secret is owned by this resource and is never written over one that is not; deleting it makes the operator mint a new secret and write it again. |
secretRef | object | No | — | The Secret in this namespace, and its key, holding a client secret the deployer chose, in place of one the server generates. The operator reads it and sends it to the server, which stores only its hash; it never writes or owns the Secret. Editing the Secret sends the new secret. It must be at least 43 printable ASCII characters with no whitespace, drawn at random. |
spec. tokenEndpointAuthMethod
Type: string | Optional
Client authentication method at the token endpoint.
Defaults to "client_secret_basic".
One of: none, client_secret_basic, client_secret_post, private_key_jwt.
spec. tokenEndpointAuthSigningAlg
Type: string | Optional
JWS algorithm this client signs its private_key_jwt assertions with (RFC 7591 token_endpoint_auth_signing_alg). Omit to accept any algorithm the server supports.
It is enforced, not recorded: an assertion signed with another algorithm is refused. Pinning it is what stops a second key - one added to the published JWK set by someone else - from being usable against an unchanged registration.
One of: ES256, RS256.
Deleting a resource
spec.deletionPolicy decides what deleting the resource does, and defaults to Retain:
Retainkeeps the client registered. The Secret is released from the resource (its owner reference is removed and it is annotatednauthera.), so deleting a namespace cannot take a live client or its credential with it.io/ retained-from Deleteremoves the client from the server, and the Secret is deleted with the resource.
A finalizer (nauthera.io/oidcclient) holds the resource until the policy has been
carried out. A removal the server does not take keeps the resource in phase Deleting
with reason ClientNotRemoved, and is retried. Each outcome is recorded as a Removed or
Retained event.
Status
The operator writes status from the server's answer. clientId, issuer, etag,
registeredAt and observedGeneration come from the server and are not changed by a
refusal. lastReconciledTime moves on every reconcile.
phase is one of Pending, Ready, Error or Deleting. The Ready condition's
reason says why:
| Reason | Phase | Meaning |
|---|---|---|
Registered | Ready | The server holds the current generation. |
Refused | Error | The server refused the spec. The message gives its objections field by field. |
Unreachable | Pending | The server did not answer. The message names the endpoint and the cause. |
Unsupported | Pending | The server serves a control-plane contract outside the operator's range. Nothing is written. |
Drifted | Error | The client was changed or removed outside the operator. Nothing is written over the change. |
NamespaceNotCovered | Error | The resource's namespace is not in the operator's --namespaces. Nothing is sent to the server. |
NamespaceNotAdmitted | Error | No organization admits the resource's namespace, or the one spec.organization names does not. Nothing is sent to the server. |
OrganizationAmbiguous | Error | Several organizations admit the namespace and spec.organization names none of them. Nothing is sent to the server. |
OrganizationNotReady | Pending | The admitting organization has no issuer yet, because the server has not created it. The manifest is sent once it has one. |
OrganizationChanged | Error | The server holds the client in one organization and the namespace now configures another. Delete and recreate the resource to move it. |
CredentialUnavailable | Pending | The operator's client certificate or key is missing, unreadable or expired. |
TrustAnchorUnavailable | Pending | The CA file is missing or holds no certificate. |
SecretUndeliverable | Error or Pending | The server holds the secret, but the Secret could not be written. |
ClientNotRemoved | Deleting | Under deletionPolicy: Delete, the server has not removed the client yet. |
kubectl get oidcclients shows the phase and the Ready condition's status and reason;
-o wide adds the message. To write the declared state back over a drifted client,
annotate the resource nauthera.io/restore. See Operator for
what each reason asks you to check.
| Field | Type | Description |
|---|---|---|
clientId | string | The resolved client_id registered server-side. |
conditions | []object | Standard Kubernetes conditions (Ready, Synced, ...). |
etag | string | The server's revision of the client as the operator last wrote it. The operator compares it against the revision the server holds to tell its own change from one made elsewhere; a client changed elsewhere is reported as drifted and not written over until the manifest changes or the resource is annotated nauthera.io/restore. |
issuer | string | The issuer the server homed the client at, after its default was applied: the organization whose rules the client is held to. |
lastReconciledTime | string | When the operator last reconciled the resource, whatever the outcome. A stale value means the operator is not running, not the leader, or cannot see the resource. |
observedGeneration | integer | .metadata.generation last reconciled by the server. |
organization | string | The slug of the organization the server holds the client in. Once set, a namespace that comes to configure another organization is refused rather than moving the client. |
phase | string | High-level lifecycle phase. |
registeredAt | string | When the client was first registered server-side. |
secretFingerprint | string | A non-reversible fingerprint of the client secret the server holds, as the server reports it; never the secret. The Secret written for it carries the same value in its nauthera.io/secret-fingerprint annotation, so the two can be told to match. |
secretRef | object | Reference to the Kubernetes Secret holding the one-time generated client secret (populated only when spec.secret.generate was true). |