Guides for Better Auth, Spring Boot, Go and .NET
Docs version v1 (main)Nauthera is pre-1.0, in a design-partner programUpdated
Each guide takes one framework from an empty project to working sign-in or token
validation against Nauthera, with the OIDCClient resource that
registers its client. They assume an issuer at https://auth.example.com and the
operator running in the cluster, with the app's namespace in
operator. (for an organization's issuer, in its
spec.namespaces; see
Which organization a resource configures).
For the protocol underneath, step by step with curl, see
Connect an Application.
| Guide | Your application | What it registers |
|---|---|---|
| Better Auth | A TypeScript web app that signs users in | A private_key_jwt client with the authorization code grant |
| Spring Boot | A REST API in Java or Kotlin that accepts bearer tokens | A client with a generated secret for each calling service |
| Go | A REST API on net/http, and a Go service that calls it | A private_key_jwt client for each calling service |
| .NET | A REST API on ASP.NET Core, and a .NET service that calls it | A private_key_jwt client for each calling service |
The setup these guides use
The Better Auth, Go and .NET guides set Nauthera up as strictly as it and each library allow. The Spring Boot guide is the simpler bearer-token setup: a client secret, and an API that refuses DPoP-bound tokens.
| Threat | Measure |
|---|---|
| A leaked client credential | No client secrets. Every client authenticates with private_key_jwt: a signed assertion that is valid for seconds and accepted once, by a key that never leaves the client. Nauthera stores only the URL of the public key. |
| A stolen access token | The Go and .NET APIs accept only DPoP-bound tokens (RFC 9449). The token names a key of the caller's (cnf.jkt), and each request carries a fresh proof signed with that key, bound to the method, the URL and the token. A token without the key is useless. |
| A token for one API used at another | Each token is issued for one API through resource (RFC 8707), and each API checks aud. |
| An ID token used as an access token | APIs accept only typ: at+jwt. |
| Algorithm confusion | Every party pins ES256: client assertions, DPoP proofs, and the APIs' check of token signatures. Nauthera signs access tokens with keys.default_alg, which is ES256 whenever an EC key is configured; keep it that way. |
| A token that outlives its revocation | Access tokens live five minutes. Revocation does not reach access tokens, so their lifetime is the window. |
The Go and .NET APIs refuse bearer tokens altogether, so every caller must be able to request DPoP-bound tokens. Better Auth cannot, which is why its guide covers sign-in only.
Two kinds of application
An application that signs users in is an OAuth client. It sends the browser to
Nauthera, receives an authorization code and exchanges it for tokens. It needs an
OIDCClient with redirectUris.
An API is a resource server. It never talks to the token endpoint: it validates the
access tokens and DPoP proofs its callers send, locally, against the issuer's JWKS. The
API itself needs no OIDCClient. Its callers do, for example a service that gets a token
for itself with the client_credentials grant.
An API is named by a URI of your choice, for example https:. A
caller asks for a token for it with the resource parameter, and the token's aud is
then that URI. Without resource, aud is the caller's client_id, which an API cannot
know in advance, so every caller of the APIs in these guides sends resource.
The server normalizes the URI before it becomes aud (RFC 3986 §6.2.2–§6.2.3): scheme and
host in lower case, a default port dropped, and an empty path written as /. A token for
resource=https: therefore carries aud:.
Configure each API with the normalized form, as these guides do.
Scopes such as orders:read need no declaration on the server. List them in the caller's
spec.scopes, request them, and check them in the API.
Keys instead of secrets
A private_key_jwt client has two key pairs, each with its own job:
| Key | Signs | Public half |
|---|---|---|
| Client authentication key | The assertion sent to /token | Published at the client's jwksUri, which Nauthera fetches |
| DPoP key | The proof sent with each token request and each API call | Travels inside each proof; its thumbprint is stamped into the token |
Create the client authentication key, keep the private half in a Kubernetes Secret, and mount it into the pod as a read-only file:
openssl genpkey -algorithm EC -pkeyopt ec_paramgen_curve:P-256 -out client-key.pem
kubectl -n billing create secret generic billing-worker-client-key --from-file=client-key.pem
rm client-key.pemPublish the public half as a JWK set on a public https URL, for example a route of the
application behind its ingress, and register that URL as spec.jwksUri. Nauthera fetches
it from the outside: an http URL, or one that resolves to a loopback, private,
link-local or shared address, is refused, and redirects are not followed. It keeps a
client's key set for ten minutes and fetches it again, at most once a minute, when an
assertion names a key the set does not hold. To rotate, publish the new key next to the
old one, wait a minute, switch signing, then remove the old key.
The DPoP key can live in memory and be generated when the process starts: a token is bound to the key that requested it, and a restarted process simply requests a new token.
There is no secret block in these OIDCClients (a generated secret is refused for
private_key_jwt), so the operator writes no Secret for them.
Local development
The Quick Start stack serves https://localhost:8443. It cannot
fetch a jwksUri on your machine, because loopback and private addresses are refused. So
locally, register each client with a generated secret (secret.generate: true on its
OIDCClient, read from the Secret the operator writes) and keep everything else, DPoP
included. Each guide shows the switch.
Each runtime keeps its own trust store, so each guide also says how to make it trust the chart's local CA.