lestrrat-go/jwx v4: a full JOSE toolkit for Go, and what it costs to adopt
Complete implementation of JWx (Javascript Object Signing and Encryption/JOSE) technologies for Go. #golang #jwt #jws #jwk #jwe
At a glance
- What is it?
- lestrrat-go/jwx implements JWA, JWK, JWS, JWE and JWT in one Go module with a deliberately uniform API. The v4 line also raises the toolchain floor to Go 1.26 and pulls in encoding/json/v2, which is the part most teams will feel first.
- Who is it for?
- Adopt lestrrat-go/jwx if you need JWS with multiple signatures, JWE with multiple recipients, or post-quantum algorithms such as ML-KEM and ML-DSA, and you can move the service to Go 1.26. Do not adopt it for a single signed session cookie where a lighter JWT library already works, and do not start on v3 unless you have read MIGRATION.md and Changes-v4.md.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 1, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What lestrrat-go/jwx covers that a JWT-only library does not
Most Go teams arrive at JOSE through one door: a signed token in an Authorization header. That door is narrow. lestrrat-go/jwx is built for the rooms behind it. The README describes coverage of JWA, JWE, JWK, JWS and JWT, and is explicit that this is "not just JWT + minimum tool set". The concrete extras are the ones that decide an adoption: JWS messages carrying multiple signatures in both compact and JSON serialization, JWS with a detached payload, JWS with an unencoded payload per RFC 7797, and JWE messages with multiple recipients in compact and JSON serialization.
That matters when you are the party producing or consuming those structures rather than a single token. An API gateway that re-signs a payload and forwards the original signature alongside its own needs multi-signature JWS. A service that encrypts to several recipients at once, each with a different key, needs multi-recipient JWE. A protocol that must sign bytes exactly as they appear on the wire, with no base64 wrapping, needs the RFC 7797 path. These are not edge cases in OIDC or in payment and document signing flows, and they are where a minimal library stops.
The second thing the README stresses is API uniformity. The naming convention is symmetric: jws.Parse, jws.Verify, jws.Sign, and jwe.Parse, jwe.Encrypt, jwe.Decrypt. Required arguments are positional, and everything optional arrives through WithXXXX() style options. Once you have learned one package, the shape of the others is predictable. That is a real maintenance argument, because the alternative is remembering a different call convention for each of the five specifications.
The library also states that most operations accept either a JWK or a raw key such as *rsa.PrivateKey or *ecdsa.PrivateKey. In practice that removes a conversion layer: code that already holds a crypto/rsa key does not have to wrap it before signing.
How the packages fit together: keys, signatures, tokens, encryption
The module splits along the JOSE specifications rather than along use cases. Each package maps to one standard, and the README gives the table: jwa implements RFC 7518, jwk implements RFC 7517 with RFC 7638 thumbprints and RFC 8037 CFRG curves, jws implements RFC 7515 plus RFC 7797, and jwe implements RFC 7516 plus the HPKE draft. jwt sits on top of jws for the token layer.
That layout determines the data flow. A key enters as a JWK through jwk.ParseKey, or as a raw Go key. The algorithm is selected separately through the jwa package, for example jwa.RS256() or jwa.RSA_OAEP_256(), and passed into a WithKey option alongside the key material. Signing and verification live in jws for arbitrary bytes and in jwt for claims. Encryption lives in jwe. Nothing is implicit: the algorithm is never inferred from the key type, which is why the option carries both.
The README's own synopsis walks this path end to end. It parses a JWK, derives the public key with jwk.PublicKeyOf, builds a token with jwt.NewBuilder() using Issuer and IssuedAt, signs it with jwt.Sign and jwt.WithKey(jwa.RS256(), privkey), then verifies with jwt.Parse. It then shows the same token being pulled straight out of an *http.Request with jwt.ParseRequest, which is the piece that saves boilerplate in a middleware. The JWE half of the example encrypts a payload with jwe.Encrypt and jwe.WithKey(jwa.RSA_OAEP_256(), jwkRSAPublicKey) and decrypts it with the private key, and the JWS half signs and verifies raw bytes with jws.Sign and jws.Verify.
Two architectural details are worth calling out. The README describes an extension module architecture, with opt-in features documented in docs/10-extensions.md, and jwkfetch is named as an extension that keeps a JWKS up to date. That is a deliberate split: the core module stays at the specification level, and stateful conveniences such as remote key-set refresh live outside it. The second is the companion tooling. The repository carries a .claude-plugin directory and a bundled jwx-dev-v4 skill that the README says is scoped to v4 only and intended for developers using the library, not for working on it.
Installing lestrrat-go/jwx v4 and signing your first token
The install is a single go get against the v4 module path. The README gives the command, and the import paths in the synopsis confirm the /v4 suffix is part of the module path.
go get github.com/lestrrat-go/jwx/v4Before that command works, two requirements apply. The README states Go 1.26 or later, and the go.mod file confirms it with a go 1.26.0 directive. The second requirement is the one that catches people: v4 uses encoding/json/v2, which on Go 1.26 sits behind GOEXPERIMENT=jsonv2 and becomes part of the standard library from Go 1.27 on. The README is direct about the consequence: on Go 1.27 leave GOEXPERIMENT unset, because naming an experiment the toolchain already ships rebuilds the standard library under a non-default configuration for no benefit.
The repository's Makefile handles this rather than hardcoding a version. It probes the toolchain with go list encoding/json/v2 and rewrites GOEXPERIMENT accordingly, preserving any experiments other than jsonv2 and clearing the variable for the probe itself so a recursive make re-probes honestly. If you build through make, that logic is already applied. If you build with plain go build, you set it yourself on Go 1.26.
GOEXPERIMENT=jsonv2 go build ./...A first real use is a signed token and its verification. The README's synopsis shows the sequence: build claims with jwt.NewBuilder(), sign with jwt.Sign and jwt.WithKey(jwa.RS256(), privkey), then verify with jwt.Parse and jwt.WithKey(jwa.RS256(), pubkey). Note that the algorithm has to be supplied on both sides. If you already have a JWK, jwk.ParseKey turns it into key material, and jwk.PublicKeyOf derives the public half. For an HTTP handler, jwt.ParseRequest reads the token from the request, which is the same verification step with the extraction done for you.
The Go 1.26 floor and the v3 to v4 migration are the real adoption costs
The limitation that will stop more teams than any cryptographic detail is the toolchain requirement. Go 1.26 or later is not a suggestion in the README, it is what the module declares, and the encoding/json/v2 dependency means the build configuration is not the default one on that release. A team on an older Go, or one that pins its toolchain for reproducibility, cannot adopt v4 without moving. There is no stated fallback to the older JSON implementation.
The second cost is the migration itself. The repository ships MIGRATION.md as a step-by-step guide with before and after code examples, and Changes-v4.md for the complete list of breaking changes and new features. Those files exist precisely because v3 code does not simply recompile against v4. If you are on github.com/lestrrat-go/jwx/v3, the README points you at both documents before anything else. v3 is not abandoned: the release list shows v3.3.0 published on the same day as v4.5.0, so staying on v3 is a supported position rather than a dead end.
The third limitation is scope. This is a JOSE library, not an identity platform. The README's feature table mentions OIDC only indirectly, through the topics and the extension architecture. There is no discovery document handling, no token endpoint client, and no session management in what is described here. If what you actually need is an OIDC client, the module gives you the JWT and JWK primitives to build one, and jwkfetch to keep the key set current, but the provider interaction is yours to write.
A fourth point is the documentation surface. The README points to the API reference on pkg.go.dev, a docs directory, and runnable examples in a separate repository. The how-to material lives in ./docs, and the extension behaviour is described in docs/10-extensions.md rather than in the README. Anyone evaluating this should read those files rather than judging from the synopsis, which covers the happy path only.
How lestrrat-go/jwx differs from golang-jwt and go-jose
The closest comparison in the related searches is golang-jwt, and the difference is the layer each one occupies. golang-jwt is a JWT library: you get token parsing, claims, and signing algorithms, and that is the whole surface. lestrrat-go/jwx implements the JOSE specifications underneath, so JWK handling, JWS serialization variants, and JWE encryption are first-class rather than absent. If your requirement is signing and verifying a bearer token, golang-jwt is the smaller dependency and the shorter path. If your requirement includes encrypting a payload to multiple recipients or verifying a message that carries several signatures, golang-jwt does not have the machinery.
go-jose is the nearer peer, because it also covers JWS, JWE and JWK. The difference the README makes visible is API philosophy. go-jose and lestrrat-go/jwx both implement the same RFCs, so interoperability is not the question; the question is how the call sites read. lestrrat-go/jwx states its convention explicitly: everything symmetric, required arguments positional, optional behaviour through WithXXXX() options, and the same verb names across packages. Whether that is better is a matter of taste, but it is a stated design commitment rather than an accident, and it is the reason the five packages feel like one library.
Two other names in the related searches sit at different levels. Square/go-jose is the historical import path for the go-jose project, so a search for it is usually a search for the same library under an old module path. Micahparks/jwkset and the broader JWKS question point at key-set management, which in this project is an extension rather than core: jwkfetch is named in the README as the way to keep a JWKS up to date. If remote key rotation is your main problem, compare at that layer, not at the token layer.
The distinguishing feature in the README's table is post-quantum cryptography. ML-KEM, ML-DSA and HPKE are listed as supported. That is a concrete reason to choose this module over a JWT-only alternative, and it is also a reason to check the JWA package tables for the exact algorithm identifiers before committing, since the README names the families rather than the individual constants.
Editorial conclusion
Adopt lestrrat-go/jwx if you need JWS with multiple signatures, JWE with multiple recipients, or post-quantum algorithms such as ML-KEM and ML-DSA, and you can move the service to Go 1.26. Do not adopt it for a single signed session cookie where a lighter JWT library already works, and do not start on v3 unless you have read MIGRATION.md and Changes-v4.md. Verify two things first: that your build sets GOEXPERIMENT=jsonv2 on Go 1.26 and leaves it unset on Go 1.27, and that the algorithms you plan to use are listed in the JWA tables rather than assumed.
Frequently asked questions
Which Go version does lestrrat-go/jwx v4 require?
The README states Go 1.26 or later, and go.mod declares go 1.26.0. On Go 1.26 you must also set GOEXPERIMENT=jsonv2, because v4 uses encoding/json/v2; on Go 1.27 that experiment is part of the standard library and the README says to leave GOEXPERIMENT unset.
How do I install lestrrat-go/jwx v4?
The README gives a single command, go get github.com/lestrrat-go/jwx/v4, and the import paths in the synopsis carry the /v4 suffix. The module has no separate installer or CLI.
Can I migrate from lestrrat-go/jwx v3 to v4 without code changes?
No. The repository ships MIGRATION.md as a step-by-step guide with before and after code examples, and Changes-v4.md for the full list of breaking changes and new features. The README points v3 users at both documents.
Does lestrrat-go/jwx support post-quantum algorithms?
The README's feature table lists ML-KEM, ML-DSA and HPKE as supported. The jwe package is documented as implementing RFC 7516 together with the HPKE encryption draft.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/lestrrat-go-jwx)