In this document we describe how to work with JWK using github.com/lestrrat-go/jwx/jwk
- Terminology
- Parsing
- Construction
- Setting values to fields
- Fetching JWK Sets
- Converting a jwk.Key to a raw key
Used to describe a JWK key, possibly of typeRSA, ECDSA, OKP, or Symmetric.
A "jwk" resource on the web can either contain a single JWK or an array of multiple JWKs. The latter is called a JWK Set.
It is impossible to know what the resource contains beforehand, so functions like jwk.Parse()
and jwk.ReadFile()
returns a jwk.Set
by default.
Used to describe the underlying raw key that a JWK represents. For example, an RSA JWK can represent rsa.PrivateKey/rsa.PublicKey, ECDSA JWK can represent ecdsa.PrivateKey/ecdsa.PublicKey, and so forth
If you have a key set, or are unsure if the source is a set or a single key, you should use jwk.Parse()
keyset, _ := jwk.Parse(src)
If you are sure that the source only contains a single key, you can use jwk.ParseKey()
key, _ := jwk.ParseKey(src)
Sometimes keys come in ASN.1 DER PEM format. To parse these files, use the jwk.WithPEM()
option.
keyset, _ := jwk.Parse(srcSet, jwk.WithPEM(true))
key, _ := jwk.ParseKey(src, jwk.WithPEM(true))
To parse keys stored in a file, jwk.ReadFile()
can be used.
keyset, _ := jwk.ReadFile(filename)
jwk.ReadFile()
accepts the same options as jwk.Parse()
, therefore you can read a PEM-encoded file via the following incantation:
keyset, _ := jwk.ReadFile(filename, jwk.WithPEM(true))
To parse keys stored in a remote location pointed by a HTTP(s) URL, use jwk.Fetch()
keyset, _ := jwk.Fetch(ctx, url)
If you are going to be using this key repeatedly in a long running process, consider using jwk.AutoRefresh
described elsewhere in this document.
Users can create a new key from scratch using jwk.New()
.
jwk.New()
requires the raw key as its argument.
There are other ways to creating keys from a raw key, but they require knowing its type in advance.
Use jwk.New()
when you have a key type which you do not know its underlying type in advance.
It automatically creates the appropriate underlying key based on the given argument type.
Argument Type | Key Type | Note |
---|---|---|
[]byte | Symmetric Key | |
ecdsa.PrivateKey | ECDSA Private Key | Argument may also be a pointer |
ecdsa.PubliKey | ECDSA Public Key | Argument may also be a pointer |
rsa.PrivateKey | RSA Private Key | Argument may also be a pointer |
rsa.PubliKey | RSA Public Key | Argument may also be a pointer |
x25519.PrivateKey | OKP Private Key | |
x25519.PubliKey | OKP Public Key |
One common mistake we see is users using jwk.New()
to construct a key from a []byte variable containing the raw JSON format JWK.
// THIS IS WRONG!
buf, _ := ioutil.ReadFile(`key.json`) // os.ReadFile in go 1.16+
key, _ := jwk.New(buf) // ALWAYS creates a symmetric key
jwk.New()
is used to create a new key from a known, raw key type. To process a yet-to-be-parsed
JWK, use jwk.Parse()
or jwk.ReadFile()
// Parse a buffer containing a JSON JWK
buf, _ := ioutil.ReadFile(`key.json`) // os.ReadFile in go 1.16+
key, _ := jwk.Parse(buf)
// Read a file, parse it as JSON
key, _ := jwk.ParseFile(`key.json`)
Each of Symmetric, RSA, ECDSA, OKP key types have corresponding constructors to create an empty instance. These keys are completely empty, so if you tried using them without initialization, it will not work.
key := jwk.NewSymmetricKey()
key := jwk.NewECDSAPrivateKey()
key := jwk.NewECDSAPublicKey()
key := jwk.NewRSAPrivateKey()
key := jwk.NewRSAPublicKey()
key := jwk.NewOKPPrivateKey()
key := jwk.NewOKPPublicKey()
For advanced users: Once you obtain these "empty" objects, you can use json.Unmarshal()
to parse the JWK.
// OK
key := jwk.NewRSAPrivateKey()
if err := json.Unmarshal(src, key); err != nil {
return errors.New(`failed to unmarshal RSA key`)
}
// NOT OK
var key jwk.Key // we can't do this because we don't know where to store the data
if err := json.Unmarshal(src, &key); err !+ nil {
...
}
jwk.New()
already does this, but if for some reason you would like to initialize an already existing jwk.Key
, you can use the jwk.FromRaw()
method.
privkey, err := rsa.GenerateKey(...)
key := jwk.NewRSAPrivateKey()
err := key.FromRaw(privkey)
Using jwk.New()
or jwk.FromRaw()
allows you to populate the fields that are required to do perform the computations, but there are other fields that you may want to populate in a key. These fields can all be set using the jwk.Set()
method.
The jwk.Set()
method takes the name of the key, and a value to be associated with it. Some predefined keys have specific types (in which type checks are enforced), and others not.
jwk.Set()
may not alter the Key Type (kty
) field of a key.
the jwk
package defines field key names for predefined keys as constants so you won't ever have to bang your head againt the wall after finding out that you have a typo.
key.Set(jwk.KeyIDKey, `my-awesome-key`)
key.Set(`my-custom-field`, `unbelievable-value`)
To fetch a JWK Set once, use jwk.Fetch()
.
set, err := jwk.Fetch(ctx, url, options...)
Sometimes you need to fetch a remote JWK, and use it mltiple times in a long-running process. For example, you may act as an itermediary to some other service, and you may need to verify incoming JWT tokens against the tokens in said other service.
Normally, you should be able to simply fetch the JWK using jwk.Fetch()
, but keys are usually expired and rotated due to security reasons.
In such cases you would need to refetch the JWK periodically, which is a pain.
github.com/lestrrat-go/jwx/jwk
provides the jwk.AutoRefresh
tool to do this for you.
First, set up the jwk.AutoRefresh
object.
You need to pass it a context.Context
object to control the lifecycle of the background fetching goroutine.
ar := jwk.NewAutoRefresh(ctx)
Next you need to tell jwk.AutoRefresh
which URLs to keep updating. For this, we use the Configure()
method. jwk.AutoRefresh
will use the information found in the HTTP headers (Cache-Control
and Expires
) or the default interval to determine when to fetch the key next time.
ar.Configure(`https://example.com/certs/pubkeys.json`)
And lastly, each time you are about to use the key, load it from the jwk.AutoRefresh
object.
keyset, _ := ar.Fetch(ctx, `https://example.com/certs/pubkeys.json`)
The returned keyset
will always be "reasonably" new. It is important that you always call ar.Fetch()
before using the keyset
as this is where the refreshing occurs.
By "reasonably" we mean that we cannot guarantee that the keys will be refreshed immediately after it has been rotated in the remote source. But it should be close enough, and should you need to forcefully refresh the token using the (jwk.AutoRefresh).Refresh()
method.
If re-fetching the keyset fails, a cached version will be returned from the previous successful fetch upon calling (jwk.AutoRefresh).Fetch()
.
If you are fetching JWK Sets from a possibly untrusted source such as the "jku"
field of a JWS message, you may have to perform some sort of
whitelist checking. You can provide a jwk.Whitelist
object to either jwk.Fetch()
or (*jwk.AutoRefresh).Configure()
methods to specify the
use of a whitelist.
Currently the package provides jwk.MapWhitelist
and jwk.RegexpWhitelist
types for simpler cases.
wl := jwk.NewMapWhitelist().
Add(url1).
Add(url2).
Add(url3
wl := jwk.NewRegexpWhitelist().
Add(regexp1).
Add(regexp2).
Add(regexp3)
jwk.Fetch(ctx, url, jwk.WithWhitelist(wl))
// or in a jwk.AutoRefresh object:
ar.Configure(url, jwk.WithWhitelist(wl))
If you would like to implement something more complex, you can provide a function via jwk.WhitelistFunc
or implement you own type of jwk.Whitelist
.
As discussed in Terminology, this package calls the "original" keys (e.g. rsa.PublicKey
, ecdsa.PrivateKey
, etc) as "raw" keys. To obtain a raw key from a jwk.Key
object, use the Raw()
method.
key, _ := jwk.ParseKey(src)
var raw interface{}
if err := key.Raw(&raw); err != nil {
...
}
In the above example, raw
contains whatever the jwk.Key
represents.
If key
represents an RSA key, it will contain either a rsa.PublicKey
or rsa.PrivateKey
. If it represents an ECDSA key, an ecdsa.PublicKey
, or ecdsa.PrivateKey
, etc.
If the only operation that you are performing is to grab the raw key out of a JSON JWK, use jwk.ParseRawKey
var raw interface{}
if err := jwk.ParseRawKey(src, &raw); err != nil {
...
}