Skip to main content

jwx-guide-v4

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.

跳到安装

来源信息

仓库
lestrrat-go/jwx
最近来源活动
2026年9月3日 09:54
检测到的 SKILL.md 语言
英语
星标
2,426
分支
195

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 |
在 GitHub 查看
这个 SKILL.md 很大,SkillsMP 这里只预览前一段内容。 在 GitHub 查看