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

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.mainOrganizationNamespaces (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.

GuideYour applicationWhat it registers
Better AuthA TypeScript web app that signs users inA private_key_jwt client with the authorization code grant
Spring BootA REST API in Java or Kotlin that accepts bearer tokensA client with a generated secret for each calling service
GoA REST API on net/http, and a Go service that calls itA private_key_jwt client for each calling service
.NETA REST API on ASP.NET Core, and a .NET service that calls itA 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.

ThreatMeasure
A leaked client credentialNo 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 tokenThe 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 anotherEach token is issued for one API through resource (RFC 8707), and each API checks aud.
An ID token used as an access tokenAPIs accept only typ: at+jwt.
Algorithm confusionEvery 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 revocationAccess 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://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:

KeySignsPublic half
Client authentication keyThe assertion sent to /tokenPublished at the client's jwksUri, which Nauthera fetches
DPoP keyThe proof sent with each token request and each API callTravels 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.pem

Publish 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.