OIDCClient
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/crds/oidcclient-crd.yaml in nauthera-server |
Example
apiVersion: nauthera.io/v1alpha1
kind: OIDCClient
metadata:
name: example-webapp
namespace: nauthera
spec:
clientId: example-webapp
issuer: https://auth.example.com
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 the access and ID token lifetimes. It accepts and stores a refresh token
lifetime but does not apply it: a refresh token always lives for issuer.refresh_token_ttl,
so refreshTokenTtlSeconds has no effect.
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.io/secret-fingerprint 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 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.
Spec
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 operator surfaces it as a NotReady condition.
One of: ES256, RS256.
spec.idTokenTtlSeconds
Type: integer | Optional
ID token lifetime in seconds.
spec.issuer
Type: string | Optional
Issuer URL this client belongs to. Must be an https URL in production (http allowed for localhost development).
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.pkcePolicy
Type: string | Optional
PKCE enforcement. required_s256 mandates code_challenge with S256; disabled rejects PKCE; optional permits but does not require it.
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 with INVALID_ARGUMENT and the operator surfaces it as a NotReady condition on the OIDCClient.
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.
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.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. |
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.io/retained-from), so deleting a namespace cannot take a live client or its credential with it.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. |
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. |
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). |