- name
- jwx-guide-v4
- description
- Guide for developing Go applications with github.com/lestrrat-go/jwx v4 — parse/sign JWTs, work with JWS/JWE/JWK, pick algorithms, and avoid the common footguns. For developers using jwx, not for developing the library itself.
# jwx-guide-v4
This skill helps you assist Go developers who are **using** `github.com/lestrrat-go/jwx/v4` in their own projects. It is scoped to **v4** only. There is no equivalent skill for v3 or v2; for those, work from the version's own `docs/` directory and pkg.go.dev. Do not apply v4 rules to a v3 or v2 codebase — the `jwa` identifiers are constants there, not functions, and the `jwk` and error APIs differ.
## Where to look things up
You will not have this repository checked out. To verify an API claim before answering, use these sources, in order of preference:
1. **Go module cache** — if the user's project depends on jwx, the source is on disk:
```bash
echo "$(go env GOMODCACHE)/github.com/lestrrat-go/jwx/v4@$(go list -m -f '{{.Version}}' github.com/lestrrat-go/jwx/v4)"
```
This directory contains the full source tree including `docs/`, `jwt/`, `jws/`, `jwe/`, `jwk/`, `jwa/`.
2. **pkg.go.dev** — canonical API reference for any exported symbol:
- `https://pkg.go.dev/github.com/lestrrat-go/jwx/v4`
- `https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwt` (etc. per subpackage)
3. **GitHub** — narrative documentation lives at the repo:
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/01-jwt.md`
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/02-jws.md`
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/03-jwe.md`
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/04-jwk.md`
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/10-extensions.md`
- `https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/99-faq.md`
4. **Examples repo** — runnable usage patterns. The repo's `README.md` is a topical index that maps "what do I want to do" → "which `*_example_test.go` file." Fetch the README first when looking for an example by topic; then fetch the linked file:
- `https://github.com/jwx-go/examples/blob/develop/v4/README.md`
When the user's question goes beyond the patterns in this skill (custom claim types, JWE recipients with per-recipient headers, nested JWS+JWE serializers, custom key providers, base64 backend swap, performance tuning), fetch from these sources rather than guessing.
## Prerequisites
- **Go 1.26+** is required. v4 uses generics features and stdlib additions (`errors.AsType[T]`) that are new in 1.26.
- **`GOEXPERIMENT=jsonv2`** must be set on Go 1.26 for every `go build`/`go test`/`go run`, because v4 depends on `encoding/json/v2`. Without it → builds fail with `build constraints exclude all Go files`. NEVER set it on Go 1.27+ → `encoding/json/v2` is in the standard library there, and naming the experiment rebuilds the standard library under a non-default configuration.
Sub-package map:
| Package | Role |
|---------|------|
| `jwa` | Algorithm identifiers as **functions**: `jwa.RS256()`, `jwa.ES256()`, `jwa.HS256()`, `jwa.A256GCM()`, `jwa.RSA_OAEP_256()`, `jwa.EdDSAEd25519()`, etc. |
| `jwk` | JSON Web Keys: parsing, generating, import/export between `jwk.Key` and `crypto.*` keys, key sets. |
| `jws` | Sign and verify arbitrary payloads (compact or JSON serialization). |
| `jwe` | Encrypt and decrypt arbitrary payloads. |
| `jwt` | JWT tokens — claims, signing, verification + validation. Wraps `jws`. |
| `jwt/openid` | OpenID Connect ID-token-flavored claims. |
## Critical rules (read first)
1. **`jwt.Parse` verifies AND validates by default.** A bare `jwt.Parse(data)` errors because no key was supplied. To intentionally skip both, use `jwt.ParseInsecure`. To verify but skip claim validation, pass `jwt.WithValidate(false)`. This is deliberately asymmetric vs. `jws.Parse`/`jwe.Parse` (which only parse). Do not "correct" it.
2. **Always pin the algorithm on the verify side.** `jwt.WithKey(jwa.RS256(), key)`, `jws.WithKey(jwa.ES256(), key)`. Never trust the `alg` from the incoming header alone.
3. **Never use `jwt.ParseInsecure` for tokens received from the network.** It is for testing or for extracting claims from a token whose origin is already trusted by other means.
4. **`jwa` algorithms are functions in v4, not constants.** Write `jwa.RS256()`, not `jwa.RS256`. This trips up users migrating from v2/v3.
5. **`kid` matching is enforced when verifying with a JWK Set.** Override the requirement with `jwt.WithKeySet(set, jws.WithRequireKid(false))` only when you understand the consequences.
6. **`jku` (key URL in the JWS header) is attacker-controlled.** Use `jwt.WithVerifyAuto` only with a `jwkfetch.Client` configured with a `jwkfetch.NewMapWhitelist()` of allowed URLs.
7. **HMAC keys are `[]byte`, not `string`.** Pass `[]byte("secret")`, or better, a `jwk.Key` imported from those bytes.
8. **`jwk.Import` and `jwk.Export` require explicit type parameters.** `jwk.Import[jwk.Key](raw)`, `jwk.Export[*rsa.PublicKey](key)`. Their type argument is not inferable from the call, so bare `jwk.Import(raw)` does **not** compile.
9. **`jwk.ParseKey` is not generic.** `jwk.ParseKey(data)` returns `(jwk.Key, error)`. Use `jwk.ParseKeyAs[jwk.RSAPublicKey](data)` when a concrete JWK type is required.
## Verifying a JWT (the 90% case)
```go
import (
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwt"
)
tok, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256(), publicKey))
if err != nil {
// signature failed, claim validation failed, or parse failed
return err
}
// tok is verified + validated; safe to read claims
```
`publicKey` may be:
- a `*rsa.PublicKey` / `*ecdsa.PublicKey` / `ed25519.PublicKey` from `crypto/*`
- `[]byte` for HMAC algorithms
- a `jwk.Key`
To validate against expected claim values, pass `jwt.WithIssuer(...)`, `jwt.WithAudience(...)`, `jwt.WithSubject(...)`, `jwt.WithJwtID(...)`. To extend the default clock skew window: `jwt.WithAcceptableSkew(30 * time.Second)`.
### Verifying with a JWK Set (kid-based key selection)
```go
set, err := jwk.Parse(jwksBytes)
if err != nil { return err }
tok, err := jwt.Parse(raw, jwt.WithKeySet(set))
```
If the JWS header has a `kid`, the matching key is selected from the set. The algorithm comes from each key's `alg` field. To opt out of kid-required matching: `jwt.WithKeySet(set, jws.WithRequireKid(false))`.
**A key with no `alg` field is skipped, not guessed at.** Inference from the key type is opt-in via `jwt.WithKeySet(set, jws.WithInferAlgorithmFromKey(true))`, and it is a fallback, not a default. It tries every algorithm compatible with the key type, so it is slower and weaker than an explicit `alg`; combined with `jws.WithRequireKid(false)` against a large JWKS it also multiplies out to `N_keys × N_algs_per_keytype` verification attempts. The right fix is almost always to add `alg` to the keys in the JWKS. If a user reports that verification against a JWKS silently finds no usable key, check for missing `alg` fields first.
### Verifying via JWKS endpoint
HTTP fetching is **not** in the core `jwk` package in v4. It lives in a companion module:
```go
import "github.com/jwx-go/jwkfetch/v4"
client := jwkfetch.NewClient()
set, err := client.Fetch(ctx, "https://issuer.example/jwks.json")
// then: jwt.Parse(raw, jwt.WithKeySet(set))
```
For repeated fetches with background refresh, use `jwkfetch.NewCache`. For `jku`-driven verification, build a `jwkfetch.Client` with a `jwkfetch.NewMapWhitelist()` of allowed URLs and pass it to `jwt.WithVerifyAuto(client)`.
## Signing a JWT
```go
tok, err := jwt.NewBuilder().
Issuer("https://issuer.example").
Audience([]string{"https://api.example"}).
Subject("user-123").
IssuedAt(time.Now()).
Expiration(time.Now().Add(15 * time.Minute)).
Claim("scope", "read:things").
Build()
if err != nil { return err }
signed, err := jwt.Sign(tok, jwt.WithKey(jwa.RS256(), privateKey))
```
`privateKey` may be a `*rsa.PrivateKey`/`*ecdsa.PrivateKey`/`ed25519.PrivateKey`/HMAC `[]byte`, or a `jwk.Key`.
Match algorithm to key type:
| Family | Use when |
|--------|----------|
| `HS256` / `HS384` / `HS512` | Shared-secret (HMAC). Same secret signs and verifies. |
| `RS256` / `RS384` / `RS512` | RSA, widest interop. |
| `PS256` / `PS384` / `PS512` | RSA-PSS, prefer over `RS*` for new systems. |
| `ES256` / `ES384` / `ES512` | ECDSA, smaller signatures than RSA. |
| `Ed25519` (`jwa.EdDSAEd25519()`) | Ed25519, fastest verify; preferred for new systems where supported. |
| `EdDSA` (`jwa.EdDSA()`) | The pre-RFC-9864 polymorphic identifier. **Deprecated.** Use it only to interoperate with a producer or consumer that still emits or expects `alg: EdDSA`. |
| `none` | **Never.** jwx refuses by default. |
## JWK basics
`jwk.Import` and `jwk.Export` require the type parameter. The parsers do not: `jwk.ParseKey` and `jwk.Parse` are non-generic, and `jwk.ParseKeyAs[T]` is the typed variant.
```go
// Parse a single JWK (returns jwk.Key):
key, err := jwk.ParseKey(jwkBytes)
// Parse a single JWK with a concrete type (fails if not that type):
rsaKey, err := jwk.ParseKeyAs[jwk.RSAPublicKey](jwkBytes)
// Parse a JWK Set (returns jwk.Set, not generic):
set, err := jwk.Parse(jwksBytes)
// Wrap an existing crypto.* key as a jwk.Key:
key, err := jwk.Import[jwk.Key](rsaPrivKey)
// Or with a concrete jwk type:
typed, err := jwk.Import[jwk.RSAPrivateKey](rsaPrivKey)
// Export a jwk.Key back to a crypto.* key:
raw, err := jwk.Export[*rsa.PublicKey](key)
```
`jwk.Import[jwk.Key]` is the default. Use a concrete type parameter (`jwk.RSAPrivateKey`, `jwk.ECDSAPublicKey`, `jwk.SymmetricKey`, `jwk.OKPPublicKey`, etc.) only when you want compile-time guarantees and acceptance of failure when the input is anything else.
### Generating keys
```go
import (
"crypto/rand"
"crypto/rsa"
"github.com/lestrrat-go/jwx/v4/jwa"
"github.com/lestrrat-go/jwx/v4/jwk"
)
raw, _ := rsa.GenerateKey(rand.Reader, 2048)
key, _ := jwk.Import[jwk.Key](raw)
key.Set(jwk.KeyIDKey, "2025-q1")
key.Set(jwk.AlgorithmKey, jwa.RS256())
pubKey, _ := jwk.PublicKeyOf(key)
```
`jwk.KeyIDKey` and `jwk.AlgorithmKey` are string constants for the standard JWK fields `kid` and `alg`.
## JWS (signing arbitrary payloads)
```go
sig, err := jws.Sign(payload, jws.WithKey(jwa.ES256(), privateKey))
payload, err := jws.Verify(sig, jws.WithKey(jwa.ES256(), publicKey))
```
`jws.Parse` only parses the structure — it does **not** verify. Use `jws.Verify` (which returns the verified payload) for verification.
### The protected `alg` must match the verifying algorithm exactly
`jws.Verify` rejects a message whose protected header advertises one algorithm while it is verified under another. The comparison is plain string equality with no aliasing, and it applies to every key source (`jws.WithKey`, `jws.WithKeySet`, `jws.WithVerifyAuto`, custom `jws.WithKeyProvider`). The check only fires when the protected header actually carries an `alg`.
The practical consequence involves EdDSA. Per RFC 9864, `EdDSA`, `Ed25519` and `Ed448` are three distinct `alg` values, so a token whose header says `alg: Ed25519` does **not** verify under `jws.WithKey(jwa.EdDSA(), key)`, and vice versa. Match the identifier the producer actually emitted.
`jws.WithSkipAlgorithmMatch(true)` bypasses the check. It exists for interop with non-conforming producers, and it weakens a real safety guard, so treat it the way you treat `jws.WithRequireKid(false)`.
## JWE (encrypting payloads)
```go
enc, err := jwe.Encrypt(
payload,
jwe.WithKey(jwa.RSA_OAEP_256(), recipientPublicKey),
jwe.WithContentEncryption(jwa.A256GCM()),
)
plain, err := jwe.Decrypt(enc, jwe.WithKey(jwa.RSA_OAEP_256(), recipientPrivateKey))
```
`jwe.WithKey(alg, key)` is the same on both encrypt and decrypt sides — this symmetry is intentional.
## Companion modules
Beyond the core `github.com/lestrrat-go/jwx/v4` module, the project ships companion modules under `github.com/jwx-go`. The agent should know **what's available and when to reach for each one** — depth lives in each module's godoc.
For algorithm and HPKE modules: **import for side effects** (`import _ "..."`). They register themselves in `init()` and panic at import time if registration fails (intentional — surfaces problems early). The one case that used to panic in normal use no longer does: see the ML-DSA note below.
### Signature algorithms (extension)
| Module | What it enables | When to use |
Auf GitHub ansehen