JWT
kataras/jwt
Development guide for the jwt JSON Web Token library for Go (github.com/kataras/jwt).
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.
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install lestrrat-go/jwx jwx-guide-v4 --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .claude/skills && cp -r skills-src/agents/plugin/skills/guide .claude/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .claude/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guideType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install lestrrat-go/jwx jwx-guide-v4 --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .agents/skills && cp -r skills-src/agents/plugin/skills/guide .agents/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .agents/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install lestrrat-go/jwx jwx-guide-v4 --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/agents/plugin/skills/guide .cursor/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .cursor/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/lestrrat-go/jwx.git --path agents/plugin/skills/guide--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install lestrrat-go/jwx jwx-guide-v4 --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/agents/plugin/skills/guide .gemini/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .gemini/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install lestrrat-go/jwx jwx-guide-v4Installs for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .github/skills && cp -r skills-src/agents/plugin/skills/guide .github/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .github/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install lestrrat-go/jwx jwx-guide-v4 --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/lestrrat-go/jwx.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/agents/plugin/skills/guide .opencode/skills/jwx-guide-v4 && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "jwx-guide-v4" agent skill from https://github.com/lestrrat-go/jwx/tree/develop%2Fv4/agents/plugin/skills/guide into .opencode/skills/jwx-guide-v4/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "jwx-guide-v4", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
jwx-guide-v4Guide 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.
Jwx Guide V4 is an agent skill from lestrrat-go/jwx. 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.
Its SKILL.md is about 5.7k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Authentication. It works with GitHub and Go. The repository describes itself as: Complete implementation of JWx (Javascript Object Signing and Encryption/JOSE) technologies for Go. golang jwt jws jwk jwe. The licence is MIT.
4 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit 03115b6. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
goFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
pkg.go.devgithub.comFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Jwx Guide V4 loads about 5.7k tokens when it runs. Until then it costs about 60 tokens; SKILL.md has 2,350 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from lestrrat-go/jwx at commit 03115b6, republished under its MIT licence (© lestrrat-go). 2,350 words, ~5,667 tokens.
.claude/skills/jwx-guide-v4/SKILL.md (or your agent's skills folder).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.
You will not have this repository checked out. To verify an API claim before answering, use these sources, in order of preference:
echo "$(go env GOMODCACHE)/github.com/lestrrat-go/jwx/v4@$(go list -m -f '{{.Version}}' github.com/lestrrat-go/jwx/v4)"docs/, jwt/, jws/, jwe/, jwk/, jwa/.https://pkg.go.dev/github.com/lestrrat-go/jwx/v4https://pkg.go.dev/github.com/lestrrat-go/jwx/v4/jwt (etc. per subpackage)https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/01-jwt.mdhttps://github.com/lestrrat-go/jwx/blob/develop/v4/docs/02-jws.mdhttps://github.com/lestrrat-go/jwx/blob/develop/v4/docs/03-jwe.mdhttps://github.com/lestrrat-go/jwx/blob/develop/v4/docs/04-jwk.mdhttps://github.com/lestrrat-go/jwx/blob/develop/v4/docs/10-extensions.mdhttps://github.com/lestrrat-go/jwx/blob/develop/v4/docs/99-faq.mdREADME.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.mdWhen the user's question goes beyond the patterns in this skill (custom claim types, JWE recipients with per-recipient headers, custom key providers, base64 backend swap, performance tuning), fetch from these sources rather than guessing.
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. |
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.jwt.WithKey(jwa.RS256(), key), jws.WithKey(jwa.ES256(), key). Never trust the alg from the incoming header alone.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.jwa algorithms are functions in v4, not constants. Write jwa.RS256(), not jwa.RS256. This trips up users migrating from v2/v3.kid matching is enforced when verifying with a JWK Set. If a token omits kid and the set contains exactly
one key, use jwt.WithKeySet(set, jws.WithUseDefault(true)). Use jws.WithRequireKid(false) only when verification
must consider multiple keys without matching kid values.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.[]byte, not string. Pass []byte("secret"), or better, a jwk.Key imported from those bytes.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.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.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 claimspublicKey may be:
*rsa.PublicKey / *ecdsa.PublicKey / ed25519.PublicKey from crypto/*[]byte for HMAC algorithmsjwk.KeyTo validate against expected claim values, pass jwt.WithIssuer(...), jwt.WithAudience(...),
jwt.WithSubject(...), and jwt.WithJwtID(...). Validation uses exact timestamps by default. Configure clock skew
tolerance with jwt.WithAcceptableSkew(30 * time.Second).
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. If a token omits kid and the set contains exactly one key, use
jwt.WithKeySet(set, jws.WithUseDefault(true)). Use jws.WithRequireKid(false) only when every key in the set should
be considered without matching kid values.
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. When the protected
header has an alg, inference tries only that algorithm against compatible keys. When the header omits alg,
inference tries every compatible algorithm; combined with jws.WithRequireKid(false), verification can perform
N_keys × N_algs_per_keytype attempts. Keep JWKS inputs bounded. The right fix is almost always to add alg to the
keys in the JWKS. If verification against a JWKS finds no usable key, check for missing alg fields first.
jwk.Parse retains an unparseable JWKS entry as a jwk.UnsupportedKey by default, so one unknown key type does not
make the whole set fail. Use jwk.IsUnsupportedKey when inspecting entries. Pass
jwk.WithStrictKeySetParsing(true) when every entry must parse or the whole operation must fail.
HTTP fetching is not in the core jwk package in v4. It lives in a companion module:
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).
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.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.
// 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.
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.
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.
For RFC 7518 ECDSA signing or verification, pass jws.WithStrictECDSA(true) to reject ES256/P-256, ES384/P-384,
and ES512/P-521 curve mismatches. The check is opt-in. Pass it through JWT signing as
jwt.WithSignOption(jws.WithStrictECDSA(true)), or JWT parsing as
jwt.WithVerifyOption(jws.WithStrictECDSA(true)). This also applies to keys selected from a JWKS.
jws.VerifyCompactFast does not accept options; use jws.Verify for strict ECDSA verification.
alg must match the verifying algorithm exactlyjws.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).
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.
Use jwt.NewSerializer() to sign a token and then encrypt the result. The steps run in the order they are called, so Sign(...) followed by Encrypt(...) produces a JWE whose payload is the signed JWT.
nested, err := jwt.NewSerializer().
Sign(jwt.WithKey(jwa.RS256(), signerPrivateKey)).
Encrypt(
jwt.WithKey(jwa.RSA_OAEP_256(), recipientPublicKey),
jwt.WithEncryptOption(jwe.WithContentEncryption(jwa.A256GCM())),
).
Serialize(tok)jwe options to the encrypt step with jwt.WithEncryptOption, and jws options to the sign step with jwt.WithSignOption. The content encryption defaults to A256GCM when jwe.WithContentEncryption is not given.jwt.Parse does not decrypt. On the receiving side, decrypt the JWE first, then verify and validate the inner JWT:signed, err := jwe.Decrypt(nested, jwe.WithKey(jwa.RSA_OAEP_256(), recipientPrivateKey))
if err != nil { return err }
tok, err := jwt.Parse(signed, jwt.WithKey(jwa.RS256(), signerPublicKey))Serialize returns the JWE in compact form as []byte, and jwe.Decrypt returns the inner signed JWT as []byte.jwt.Parse step is what proves who signed the token, so NEVER skip it or replace it with jwt.ParseInsecure after decrypting.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.
Algorithm and HPKE modules register themselves in init() and panic at import time if registration fails. Import a
module by name when calling its algorithm constructors, such as es256k.ES256K(). Use a blank import only when
registration is the sole reason for the import. The one case that used to panic in normal use no longer does: see the
ML-DSA note below.
| Module | What it enables | When to use |
|---|---|---|
github.com/jwx-go/mldsa/v4 | ML-DSA-44/65/87 (FIPS 204 post-quantum) | Forward-looking post-quantum signing. AKP key type, "alg" field required on keys. |
github.com/jwx-go/ed448/v4 | Ed448, via ed448.EdDSAEd448() | When Ed25519 isn't strong enough or interop requires Ed448. |
github.com/jwx-go/es256k/v4 | ES256K (secp256k1) | Web3/crypto ecosystem interop. Uses ECDSA with the secp256k1 curve. |
github.com/jwx-go/compsig/v4 | ML-DSA composite signatures (PQ + classical) per draft-ietf-jose-pq-composite-sigs | Experimental, draft-spec. Hybrid signing during PQ transition. |
| Module | What it enables | When to use |
|---|---|---|
github.com/jwx-go/x448/v4 | X448 ECDH-ES, HPKE with DHKEM(X448), incl. HPKE-5-KE and HPKE-6-KE | Stronger ECDH than X25519 when required. |
github.com/jwx-go/mlkem/v4 | ML-KEM-768/1024 (post-quantum KEM) per draft-ietf-jose-pqc-kem | Experimental, draft-spec. Post-quantum key encapsulation. |
github.com/jwx-go/reddy-pqchpke/v4 | Hybrid PQ HPKE per draft-reddy-cose-jose-pqc-hybrid-hpke | Highly experimental, pre-WG-adoption. PQ + classical hybrid. |
| Module | What it does | When to use |
|---|---|---|
github.com/jwx-go/jwkfetch/v4 | HTTP JWK Set retrieval — Client (one-shot) and Cache (background-refreshed, backed by httprc) | Always, whenever you fetch JWKS over HTTP. Core jwk has no HTTP fetch implementation; this is the entry point. |
github.com/jwx-go/jwxfilter/v4 | Filter and introspection helpers for jwt.Token, jws.Headers, jwe.Headers, jwk.Key, and openid.Token | Selecting or redacting fields on a token, header, or key. Extracted from core in v4, so a user porting v3 filter code needs this module. |
github.com/jwx-go/asmbase64/v4 | Assembly-optimized base64 backend (via segmentio/asm) | High-throughput JWS verify/decode paths where base64 is hot. Drop-in import. |
github.com/jwx-go/jwxmigrate | Machine-readable v3→v4 migration rules and automated checking | A user porting an app from jwx/v3 to jwx/v4. |
github.com/jwx-go/examples | Runnable usage patterns covering JWT/JWS/JWE/JWK/extensions; README.md is a topical index by package and sub-topic | Pointing the user at canonical example code — fetch the README first to find the right file by topic, then fetch the linked test file. Also importable via go.work in local development. |
https://github.com/lestrrat-go/jwx/blob/develop/v4/docs/10-extensions.mdhttps://pkg.go.dev/github.com/jwx-go/<name>/v4Default jwx supports the common RFC 7518 algorithms (RS*, PS*, ES*, HS*, EdDSA, AGCM, RSA-OAEP-, etc.) out of the
box. Non-default algorithm modules must be added to go.mod and imported so their init() functions run. Use a named
import when calling the module's algorithm constructors. A missing import commonly causes algorithm not registered
errors for ES256K, Ed448, ML-DSA, ML-KEM, and X448. Tooling modules such as jwkfetch, jwxfilter, jwxmigrate, and
examples are ordinary APIs or repositories, not algorithm registrars.
ML-DSA is the one exception, and it depends on the toolchain. From Go 1.27 on, crypto/mldsa is in the standard library, so jwx registers jwa.MLDSA44()/MLDSA65()/MLDSA87() natively and no companion module or side-effect import is needed. On Go 1.26 the algorithms are not registered at all, and github.com/jwx-go/mldsa/v4 is still required.
Keeping the extension imported on Go 1.27 is harmless. From jwx-go/mldsa v4.0.5 on it detects jwx's native registration and bridges filippo.io/mldsa keys onto it instead of registering the algorithms a second time, so code mid-migration keeps working. Only versions before v4.0.5 panic at startup on that combination, because the duplicate registration is rejected. If a user hits that panic, tell them to upgrade the extension, not to drop the import. Canonical owner: docs/10-extensions.md, section "Which implementation you get".
Most JWT errors and selected JWS/JWE/JWK errors are struct types with named fields. Use errors.Is with a zero-value
struct to test a struct error's kind, or Go 1.26's errors.AsType[T] to recover its fields. Other package-level errors
remain sentinel functions such as jws.VerificationError(), jwe.DecryptError(), and jwk.ParseError().
import "errors"
tok, err := jwt.Parse(raw, jwt.WithKey(jwa.RS256(), key))
if err != nil {
if errors.Is(err, jwt.TokenExpiredError{}) {
// exp claim failed
}
if e, ok := errors.AsType[jwt.InvalidAudienceError](err); ok {
log.Printf("audience mismatch: %+v", e)
}
}Common types:
jwt: TokenExpiredError, TokenNotYetValidError, InvalidIssuerError, InvalidAudienceError, MissingRequiredClaimError, ValidationError, ParseErrorjws: VerificationError() factory (signature verification failed)jwe: DecryptError() factory, AlgorithmMismatchErrorjwk: KeyTypeMismatchError, ImportError(), ParseError()Do not match errors by string contents — messages may change.
Standard claims have dedicated typed accessors (returning (value, ok)):
exp, ok := tok.Expiration() // time.Time
iss, ok := tok.Issuer() // string
aud, ok := tok.Audience() // []string
sub, ok := tok.Subject() // string
nbf, ok := tok.NotBefore()
iat, ok := tok.IssuedAt()
jti, ok := tok.JwtID()For private claims, use tok.Field(name) which returns (any, bool). For type-safe access, use jwt.Get[T](tok, name).
When reviewing or writing jwx-using code, watch for these:
jwt.Parse(data) with no key option — errors out; if the intent was to read claims without verifying, that's a security bug unless the source is already trusted, in which case use jwt.ParseInsecure.jwa.RS256 instead of jwa.RS256() — these are functions in v4.jwk.Import(raw) or jwk.Export(key) without the type parameter — won't compile.jwk.ParseKey[jwk.RSAPublicKey](data) — ParseKey is not generic; the typed parser is jwk.ParseKeyAs[T].jwk.Key to a crypto.* type. Use jwk.Export[*rsa.PublicKey](key) instead.jwt.Builder across goroutines — builders aren't safe to share.jws.Sign/jwt.Sign.tok.Get("exp") — the method is tok.Field("exp") (or just tok.Expiration()).jws.WithRequireKid(false).encoding/json directly — always go through jwt.Parse / jws.Verify.© lestrrat-go, MIT. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in agents/plugin/skills/guide of lestrrat-go/jwx.
Open the folder on GitHubat commit 03115b6
Jwx Guide V4 next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Jwx Guide V4 this skilllestrrat-go/jwx | 2.4k | — | ~5.7k | Automated safety check: Pass | MIT | |
| JWTkataras/jwt | 212 | — | ~1.5k | Automated safety check: Pass | MIT | |
| GitHub OAuth Nango IntegrationAgentWorkforce/relay | 869 | 1 repos | ~3.4k | Automated safety check: Pass | Apache-2.0 | |
| Auth Setupbutterbase-ai/butterbase-skills | 534 | — | ~2.2k | Automated safety check: Pass | MIT | |
| Apikerhodgef/apiker | 127 | — | ~1.4k | Automated safety check: Pass | MIT | |
| Golang Swaggercontext-labs/whip | 1.1k | 2 repos | ~2.3k | Automated safety check: Pass | MIT |
kataras/jwt
Development guide for the jwt JSON Web Token library for Go (github.com/kataras/jwt).
AgentWorkforce/relay
A skill your agent uses when implementing GitHub OAuth + GitHub App authentication with Nango - provides two-connection pattern for user login and repo access with webhook handling
butterbase-ai/butterbase-skills
A skill your agent uses when configuring OAuth providers (Google/GitHub/Apple/X/etc.), setting up post-login auth hooks, tuning JWT lifetimes, or generating service API keys
hodgef/apiker
Develop, review, and extend the Apiker library — a framework for building serverless REST APIs on Cloudflare Workers + Durable Objects.
context-labs/whip
Golang OpenAPI/Swagger docs with swaggo/swag — annotations (@Summary, @Param, @Success, @Router, @Security), swag init, framework integrations (gin, echo, fiber, chi), security definitions, struct…
microsoft/apm
Activate when code touches token management, credential resolution, git auth flows, GITHUBAPMPAT, ADOAPMPAT, AuthResolver, HostInfo, AuthContext, or any remote host authentication -- even if 'auth'…
lestrrat-go/jwx
Apply bulk operations across all jwx companion modules. An agent skill from lestrrat-go/jwx.
Categories
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. Jwx Guide V4 is an agent skill from lestrrat-go/jwx.com/lestrrat-go/jwx v4 — parse/sign JWTs, work with JWS/JWE/JWK, pick algorithms, and avoid the common footguns.
Jwx Guide V4 fits situations like: tasks that involve Authentication.
Run `npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a claude-code`. Or copy the skill folder (agents/plugin/skills/guide in lestrrat-go/jwx) into .claude/skills/jwx-guide-v4 in your project. Claude Code loads it when a task matches its description.
Run `npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a codex`. Or copy the skill folder (agents/plugin/skills/guide in lestrrat-go/jwx) into .agents/skills/jwx-guide-v4 in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add lestrrat-go/jwx --skill jwx-guide-v4 -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/jwx-guide-v4, .gemini/skills/jwx-guide-v4, .github/skills/jwx-guide-v4 and .opencode/skills/jwx-guide-v4 in your project.
Going by SKILL.md and its folder, Jwx Guide V4 needs the command-line tools its instructions call (go).
SKILL.md names 2 domains. In commands or code: pkg.go.dev and github.com; the agent is likely to contact these when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Jwx Guide V4 is published under the MIT licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 5.7k tokens (SKILL.md is roughly 23k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Jwx Guide V4: JWT (kataras/jwt, 212 stars), GitHub OAuth Nango Integration (AgentWorkforce/relay, 869 stars), Auth Setup (butterbase-ai/butterbase-skills, 534 stars) and Apiker (hodgef/apiker, 127 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
lestrrat-go (a GitHub organization) maintains it in lestrrat-go/jwx, which has 2,433 GitHub stars. The repository holds 2 skills in this directory. The repository was last updated on October 9, 2026.
Source: lestrrat-go/jwx on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.