Framework Guides
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. 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, and a Java service that calls it | A private_key_jwt client 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 guides set Nauthera up as strictly as it and each library allow:
| 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 | Every API accepts 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 yet, so their lifetime is the window. |
The 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://orders.example.com/. 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://orders.example.com therefore carries aud: "https://orders.example.com/".
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, so to rotate, publish the new key next to the old one,
wait ten minutes, 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:8080. It cannot
fetch a jwksUri on your machine, because loopback and private addresses are refused. So
locally, register each client with a generated secret (nauthera-cli create client
prints it) 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 local certificate.