Command line tool jwx and libraries implementing various JWx technologies
Why?
My goal was to write a server that heavily uses JWK and JWT. At first glance
the libraries that already exist seemed sufficient, but soon I realized that
- To completely implement the protocols, I needed the entire JWT, JWK, JWS, JWE (and JWA, by necessity).
- Most of the libraries that existed only deal with a subset of the various JWx specifications that were necessary to implement their specific needs
For example, a certain library looks like it had most of JWS, JWE, JWK covered, but then it lacked the ability to include private claims in its JWT responses. Another library had support of all the private claims, but completely lacked in its flexibility to generate various different response formats.
Because I was writing the server side (and the client side for testing), I needed the entire JOSE toolset to properly implement my server, and they needed to be flexible enough to fulfill the entire spec that I was writing.
So here's github.com/lestrrat-go/jwx
. This library is extensible, customizable, and hopefully well organized to the point that it is easy for you to slice and dice it.
Backwards Compatibility Notice
Uh, why are you using such an ancient version? You know that repository is archived for a reason, yeah? Please use the new version.
Pre-1.0.0 users
The API has been reworked quite substantially between pre- and post 1.0.0 releases. Please check out the Changes file (or the diff, if you are into that sort of thing)
v1.0.x users
The API has gone under some changes for v1.1.0. If you are upgrading, you might want to read the relevant parts in the Changes file.
Since v1.1.1 we have a command line tool jwx
(*). With jwx
you can create JWKs (from PEM files, even), sign and verify JWS message, encrypt and decrypt JWE messages, etc.
(*) Okay, it existed since a long time ago, but it was never useful.
Installation
go install github.com/lestrrat-go/jwx/cmd/jwx
Packages
JWA
Package github.com/lestrrat-go/jwx/jwa defines the various algorithm described in RFC7518
JWT
Package github.com/lestrrat-go/jwx/jwt implements JSON Web Tokens as described in RFC7519.
- Convenience methods for oft-used keys ("aud", "sub", "iss", etc)
- Ability to Get/Set arbitrary keys
- Conversion to and from JSON
- Generate signed tokens
- Verify signed tokens
- Extra support for OpenID tokens via github.com/lestrrat-go/jwx/jwt/openid
Examples are located in the examples directory (jwt_example_test.go)
JWK
Package github.com/lestrrat-go/jwx/jwk implements JWK as described in RFC7517
- Parse and work with RSA/EC/Symmetric/OKP JWK types
- Convert to and from JSON
- Convert to and from raw key types (e.g. *rsa.PrivateKey)
- Ability to keep a JWKS fresh.
- Add arbitrary fields in the JWK object
Examples are located in the examples directory (jwk_example_test.go)
Supported key types:
kty |
Curve |
Go Key Type |
RSA |
N/A |
rsa.PrivateKey / rsa.PublicKey (2) |
EC |
P-256 P-384 P-521 |
ecdsa.PrivateKey / ecdsa.PublicKey (2) |
oct |
N/A |
[]byte |
OKP |
Ed25519 (1) |
ed25519.PrivateKey / ed25519.PublicKey (2) |
|
X25519 (1) |
(jwx/)x25519.PrivateKey / x25519.PublicKey (2) |
- Note 1: Experimental
- Note 2: Either value or pointers accepted (e.g. rsa.PrivateKey or *rsa.PrivateKey)
JWS
Package github.com/lestrrat-go/jwx/jws implements JWS as described in RFC7515
- Parse and generate compact or JSON serializations
- Sign and verify arbitrary payload
- Use any of the keys supported in github.com/lestrrat-go/jwx/jwk
- Add arbitrary fields in the JWS object
- Ability to add/replace existing signature methods
Examples are located in the examples directory (jws_example_test.go)
Supported signature algorithms:
Algorithm |
Supported? |
Constant in jwa |
HMAC using SHA-256 |
YES |
jwa.HS256 |
HMAC using SHA-384 |
YES |
jwa.HS384 |
HMAC using SHA-512 |
YES |
jwa.HS512 |
RSASSA-PKCS-v1.5 using SHA-256 |
YES |
jwa.RS256 |
RSASSA-PKCS-v1.5 using SHA-384 |
YES |
jwa.RS384 |
RSASSA-PKCS-v1.5 using SHA-512 |
YES |
jwa.RS512 |
ECDSA using P-256 and SHA-256 |
YES |
jwa.ES256 |
ECDSA using P-384 and SHA-384 |
YES |
jwa.ES384 |
ECDSA using P-521 and SHA-512 |
YES |
jwa.ES512 |
RSASSA-PSS using SHA256 and MGF1-SHA256 |
YES |
jwa.PS256 |
RSASSA-PSS using SHA384 and MGF1-SHA384 |
YES |
jwa.PS384 |
RSASSA-PSS using SHA512 and MGF1-SHA512 |
YES |
jwa.PS512 |
EdDSA (1) |
YES |
jwa.EdDSA |
JWE
Package github.com/lestrrast-go/jwx/jwe implements JWE as described in RFC7516
- Encrypt and Decrypt arbitrary data
- Content compression and decompression
- Add arbitrary fields in the JWE header object
Examples are located in the examples directory (jwe_example_test.go)
Supported key encryption algorithm:
Algorithm |
Supported? |
Constant in jwa |
RSA-PKCS1v1.5 |
YES |
jwa.RSA1_5 |
RSA-OAEP-SHA1 |
YES |
jwa.RSA_OAEP |
RSA-OAEP-SHA256 |
YES |
jwa.RSA_OAEP_256 |
AES key wrap (128) |
YES |
jwa.A128KW |
AES key wrap (192) |
YES |
jwa.A192KW |
AES key wrap (256) |
YES |
jwa.A256KW |
Direct encryption |
YES (1) |
jwa.DIRECT |
ECDH-ES |
YES (1) |
jwa.ECDH_ES |
ECDH-ES + AES key wrap (128) |
YES |
jwa.ECDH_ES_A128KW |
ECDH-ES + AES key wrap (192) |
YES |
jwa.ECDH_ES_A192KW |
ECDH-ES + AES key wrap (256) |
YES |
jwa.ECDH_ES_A256KW |
AES-GCM key wrap (128) |
YES |
jwa.A128GCMKW |
AES-GCM key wrap (192) |
YES |
jwa.A192GCMKW |
AES-GCM key wrap (256) |
YES |
jwa.A256GCMKW |
PBES2 + HMAC-SHA256 + AES key wrap (128) |
YES |
jwa.PBES2_HS256_A128KW |
PBES2 + HMAC-SHA384 + AES key wrap (192) |
YES |
jwa.PBES2_HS384_A192KW |
PBES2 + HMAC-SHA512 + AES key wrap (256) |
YES |
jwa.PBES2_HS512_A256KW |
- Note 1: Single-recipient only
Supported content encryption algorithm:
Algorithm |
Supported? |
Constant in jwa |
AES-CBC + HMAC-SHA256 (128) |
YES |
jwa.A128CBC_HS256 |
AES-CBC + HMAC-SHA384 (192) |
YES |
jwa.A192CBC_HS384 |
AES-CBC + HMAC-SHA512 (256) |
YES |
jwa.A256CBC_HS512 |
AES-GCM (128) |
YES |
jwa.A128GCM |
AES-GCM (192) |
YES |
jwa.A192GCM |
AES-GCM (256) |
YES |
jwa.A256GCM |
Global Settings
Switching to a faster JSON library
By default we use the standard library's encoding/json
for all of our JSON needs.
However, if performance for parsing/serializing JSON is really important to you, you might want to enable github.com/goccy/go-json by enabling the jwx_goccy
tag.
% go build -tags jwx_goccy ...
github.com/goccy/go-json is disabled by default because it uses some really advanced black magic, and I really do not feel like debugging it IF it breaks. Please note that that's a big "if".
As of github.com/goccy/go-json@v0.3.3 I haven't see any problems, and I would say that it is mostly stable.
However, it is a depdenency that you can go without, and I won't be of much help if it breaks -- therefore it is not the default.
If you know what you are doing, I highly recommend enabling this module -- all you need to do is to enable this tag.
Disable the tag if you feel like it's not worth the hassle.
And when you do enable github.com/goccy/go-json and you encounter some mysterious error, I also trust that you know to file an issue to github.com/goccy/go-json and NOT to this library.
Using json.Number
If you want to parse numbers in the incoming JSON objects as json.Number
instead of floats, you can use the following call to globally affect the behavior of JSON parsing.
func init() {
jwx.DecoderSettings(jwx.WithUseNumber(true))
}
Do be aware that this has global effect. All code that calls in to encoding/json
within jwx
will use your settings.
Decode private fields to objects
Packages within github.com/lestrrat-go/jwx
parses known fields into pre-defined types,
but for everything else (usually called private fields/headers/claims) are decoded into
wharever "encoding/json".Unmarshal
deems appropriate.
For example, JSON objects are converted to map[string]interface{}
, JSON arrays into
[]interface{}
, and so on.
Sometimes you know beforehand that it makes sense for certain fields to be decoded into
proper objects instead of generic maps or arrays. When you encounter this, you can use
the RegisterCustomField()
method in each of jwe
, jwk
, jws
, and jwt
packages.
func init() {
jwt.RegisterCustomField(`x-foo-bar`, mypkg.FooBar{})
}
This tells the decoder that when it encounters a JWT token with the field named
"x-foo-bar"
, it should be decoded to an instance of mypkg.FooBar
. Then you can
access this value by using Get()
v, _ := token.Get(`x-foo-bar`)
foobar := v.(mypkg.FooBar)
Do be aware that this has global effect. In the above example, all JWT tokens containing
the "x-foo-bar"
key will decode in the same way. If you need this behavior from
jwe
, jwk
, or jws
packages, you need to do the same thing for each package.
Contributions
Issues
For bug reports and feature requests, please try to follow the issue templates as much as possible.
For either bug reports or feature requests, failing tests are even better.
Pull Requests
Please make sure to include tests that excercise the changes you made.
Discussions / Usage
Please try discussions first.
Credits
- Work on this library was generously sponsored by HDE Inc (https://www.hde.co.jp)
- Lots of code, especially JWE was taken from go-jose library (https://github.com/square/go-jose)
- Lots of individual contributors have helped this project over the years. Thank each and everyone of you very much.
FAQ
I get a "no Go files in ..." error
You are using Go in GOPATH mode. Short answer: use Go modules.
A slightly more elaborate version of the answer can be found in github.com/lestrrat-go/backoff FAQ
And no, I do not intend to support GOPATH mode as of 2021. There are ways to manually workaround it, but do not expect this library to do that for you.
Why don't you automatically infer the algorithm for jws.Verify
?
Please read https://auth0.com/blog/critical-vulnerabilities-in-json-web-token-libraries/. Despite this article's publish date, the original had been published sometime around 2015. It's a well known problem with JWS libraries.
Why did you change the API?
Presumably you are asking this because your code broke when we bumped the version and broke backwards compatibility. Then the short answer is: "You wouldn't have had to worry about it if you were properly using go.mod"
The longer answer is as follows: From time to time, we introduce API changes, because we learn of mistakes in our old ways.
Maybe we used the wrong terminology. Maybe we made public something that should have been internal. Maybe we intended an API to be used one way, but it was confusing.
So then we introduce API changes. Sorry if breaks your builds, but it's done because we deem it necessary.
You should also know that we do not introduce API changes between micro versions.
And on top of that, Go provides extremely good support for idempodent builds via Go modules.
If you are in an environment where API changes disrupts your environment, you should definitely migrade to using Go modules now.