# Guides for Better Auth, Spring Boot, Go and .NET

> Set up Nauthera with Better Auth, Spring Boot, Go and ASP.NET Core: private_key_jwt and DPoP-bound tokens where the library allows, and each app's OIDCClient.

Each guide takes one framework from an empty project to working sign-in or token
validation against Nauthera, with the [`OIDCClient`](https://www.nauthera.io/docs/v1/oidc-client) resource that
registers its client. They assume an issuer at `https://auth.example.com` and the
[operator](https://www.nauthera.io/docs/v1/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](https://www.nauthera.io/docs/v1/operator#which-organization-a-resource-configures)).
For the protocol underneath, step by step with `curl`, see
[Connect an Application](https://www.nauthera.io/docs/v1/integration).

| Guide | Your application | What it registers |
|---|---|---|
| [Better Auth](https://www.nauthera.io/docs/v1/guides/better-auth) | A TypeScript web app that signs users in | A `private_key_jwt` client with the authorization code grant |
| [Spring Boot](https://www.nauthera.io/docs/v1/guides/spring-boot) | A REST API in Java or Kotlin that accepts bearer tokens | A client with a generated secret for each calling service |
| [Go](https://www.nauthera.io/docs/v1/guides/go) | A REST API on `net/http`, and a Go service that calls it | A `private_key_jwt` client for each calling service |
| [.NET](https://www.nauthera.io/docs/v1/guides/dotnet) | 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://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:

```bash
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 `OIDCClient`s (a generated secret is refused for
`private_key_jwt`), so the operator writes no Secret for them.

## Local development

The [Quick Start](https://www.nauthera.io/docs/v1/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.
