1# Working with JWK
2
3In this document we describe how to work with JWK using `github.com/lestrrat-go/jwx/jwk`
4
5* [Terminology](#terminology)
6 * [JWK / Key](#jwk--key)
7 * [JWK Set / Set](#jwk-set--set)
8 * [Raw Key](#raw-key)
9* [Parsing](#parsing)
10 * [Parse a set](#parse-a-set)
11 * [Parse a key](#parse-a-key)
12 * [Parse a key or set in PEM format](#parse-a-key-or-a-set-in-pem-format)
13 * [Parse a key from a file](#parse-a-key-from-a-file)
14 * [Parse a key from a remote resource](#parse-a-key-from-a-remote-resource)
15* [Construction](#construction)
16 * [Using jwk.New()](#using-jwknew)
17 * [Construct a specific key type from scratch](#construct-a-specific-key-type-from-scratch)
18 * [Construct a specific key type from a raw key](#construct-a-specific-key-type-from-a-raw-key)
19* [Setting values to fields](#setting-values-to-fields)
20* [Fetching JWK Sets](#fetching-jwk-sets)
21 * [Fetching a JWK Set once](#fetching-a-jwk-set-once)
22 * [Auto-refreshing remote keys](#auto-refreshing-remote-keys)
23 * [Using Whitelists](#using-whitelists)
24* [Converting a jwk.Key to a raw key](#converting-a-jwkkey-to-a-raw-key)
25
26---
27
28# Terminology
29
30## JWK / Key
31
32Used to describe a JWK key, possibly of typeRSA, ECDSA, OKP, or Symmetric.
33
34## JWK Set / Set
35
36A "jwk" resource on the web can either contain a single JWK or an array of multiple JWKs.
37The latter is called a JWK Set.
38
39It is impossible to know what the resource contains beforehand, so functions like [`jwk.Parse()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Parse)
40and [`jwk.ReadFile()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#ReadFile) returns a [`jwk.Set`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Set) by default.
41
42## Raw Key
43
44Used to describe the underlying raw key that a JWK represents. For example, an RSA JWK can
45represent rsa.PrivateKey/rsa.PublicKey, ECDSA JWK can represent ecdsa.PrivateKey/ecdsa.PublicKey,
46and so forth
47
48# Parsing
49
50## Parse a set
51
52If you have a key set, or are unsure if the source is a set or a single key, you should use [`jwk.Parse()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Parse)
53
54```go
55keyset, _ := jwk.Parse(src)
56```
57
58## Parse a key
59
60If you are sure that the source only contains a single key, you can use [`jwk.ParseKey()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#ParseKey)
61
62```go
63key, _ := jwk.ParseKey(src)
64```
65
66## Parse a key or a set in PEM format
67
68Sometimes keys come in ASN.1 DER PEM format. To parse these files, use the [`jwk.WithPEM()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#WithPEM) option.
69
70```go
71keyset, _ := jwk.Parse(srcSet, jwk.WithPEM(true))
72
73key, _ := jwk.ParseKey(src, jwk.WithPEM(true))
74```
75
76## Parse a key from a file
77
78To parse keys stored in a file, [`jwk.ReadFile()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#ReadFile) can be used.
79
80```go
81keyset, _ := jwk.ReadFile(filename)
82```
83
84`jwk.ReadFile()` accepts the same options as [`jwk.Parse()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Parse), therefore you can read a PEM-encoded file via the following incantation:
85
86```go
87keyset, _ := jwk.ReadFile(filename, jwk.WithPEM(true))
88```
89
90## Parse a key from a remote resource
91
92To parse keys stored in a remote location pointed by a HTTP(s) URL, use [`jwk.Fetch()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Fetch)
93
94```go
95keyset, _ := jwk.Fetch(ctx, url)
96```
97
98If you are going to be using this key repeatedly in a long running process, consider using [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#AutoRefresh) described elsewhere in this document.
99
100# Construction
101
102## Using jwk.New()
103
104Users can create a new key from scratch using [`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New).
105
106[`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) requires the raw key as its argument.
107There are other ways to creating keys from a raw key, but they require knowing its type in advance.
108Use [`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) when you have a key type which you do not know its underlying type in advance.
109
110It automatically creates the appropriate underlying key based on the given argument type.
111
112| Argument Type | Key Type | Note |
113|---------------|----------|------|
114| []byte | Symmetric Key | |
115| ecdsa.PrivateKey | ECDSA Private Key | Argument may also be a pointer |
116| ecdsa.PubliKey | ECDSA Public Key | Argument may also be a pointer |
117| rsa.PrivateKey | RSA Private Key | Argument may also be a pointer |
118| rsa.PubliKey | RSA Public Key | Argument may also be a pointer |
119| x25519.PrivateKey | OKP Private Key | |
120| x25519.PubliKey | OKP Public Key | |
121
122One common mistake we see is users using [`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) to construct a key from a []byte variable containing the raw JSON format JWK.
123
124```go
125// THIS IS WRONG!
126buf, _ := ioutil.ReadFile(`key.json`) // os.ReadFile in go 1.16+
127key, _ := jwk.New(buf) // ALWAYS creates a symmetric key
128```
129
130[`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) is used to create a new key from a known, *raw key* type. To process a yet-to-be-parsed
131JWK, use [`jwk.Parse()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Parse) or [`jwk.ReadFile()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#ReadFile)
132
133```go
134// Parse a buffer containing a JSON JWK
135buf, _ := ioutil.ReadFile(`key.json`) // os.ReadFile in go 1.16+
136key, _ := jwk.Parse(buf)
137```
138
139```go
140// Read a file, parse it as JSON
141key, _ := jwk.ParseFile(`key.json`)
142```
143
144## Construct a specific key type from scratch
145
146Each of Symmetric, RSA, ECDSA, OKP key types have corresponding constructors to create an empty instance.
147These keys are completely empty, so if you tried using them without initialization, it will not work.
148
149```go
150key := jwk.NewSymmetricKey()
151key := jwk.NewECDSAPrivateKey()
152key := jwk.NewECDSAPublicKey()
153key := jwk.NewRSAPrivateKey()
154key := jwk.NewRSAPublicKey()
155key := jwk.NewOKPPrivateKey()
156key := jwk.NewOKPPublicKey()
157```
158
159For advanced users: Once you obtain these "empty" objects, you *can* use `json.Unmarshal()` to parse the JWK.
160
161```
162// OK
163key := jwk.NewRSAPrivateKey()
164if err := json.Unmarshal(src, key); err != nil {
165 return errors.New(`failed to unmarshal RSA key`)
166}
167
168// NOT OK
169var key jwk.Key // we can't do this because we don't know where to store the data
170if err := json.Unmarshal(src, &key); err !+ nil {
171 ...
172}
173```
174
175## Construct a specific key type from a raw key
176
177[`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) already does this, but if for some reason you would like to initialize an already existing [`jwk.Key`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Key), you can use the [`jwk.FromRaw()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#FromRaw) method.
178
179```go
180privkey, err := rsa.GenerateKey(...)
181
182key := jwk.NewRSAPrivateKey()
183err := key.FromRaw(privkey)
184```
185
186## Setting values to fields
187
188Using [`jwk.New()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#New) or [`jwk.FromRaw()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/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()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Set) method.
189
190The [`jwk.Set()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/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.
191
192[`jwk.Set()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Set) may not alter the Key Type (`kty`) field of a key.
193
194the `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.
195
196```go
197key.Set(jwk.KeyIDKey, `my-awesome-key`)
198key.Set(`my-custom-field`, `unbelievable-value`)
199```
200# Fetching JWK Sets
201
202## Fetching a JWK Set once
203
204To fetch a JWK Set once, use `jwk.Fetch()`.
205
206```go
207set, err := jwk.Fetch(ctx, url, options...)
208```
209
210## Auto-refreshing remote keys
211
212Sometimes you need to fetch a remote JWK, and use it mltiple times in a long-running process.
213For 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.
214
215Normally, you should be able to simply fetch the JWK using [`jwk.Fetch()`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Fetch), but keys are usually expired and rotated due to security reasons.
216In such cases you would need to refetch the JWK periodically, which is a pain.
217
218`github.com/lestrrat-go/jwx/jwk` provides the [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#AutoRefresh) tool to do this for you.
219
220First, set up the [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#AutoRefresh) object.
221You need to pass it a `context.Context` object to control the lifecycle of the background fetching goroutine.
222
223```go
224ar := jwk.NewAutoRefresh(ctx)
225```
226
227Next you need to tell [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#AutoRefresh) which URLs to keep updating. For this, we use the `Configure()` method. [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/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.
228
229```go
230ar.Configure(`https://example.com/certs/pubkeys.json`)
231```
232
233And lastly, each time you are about to use the key, load it from the [`jwk.AutoRefresh`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#AutoRefresh) object.
234
235```go
236keyset, _ := ar.Fetch(ctx, `https://example.com/certs/pubkeys.json`)
237```
238
239The 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.
240
241By "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.
242
243If re-fetching the keyset fails, a cached version will be returned from the previous successful fetch upon calling `(jwk.AutoRefresh).Fetch()`.
244
245## Using Whitelists
246
247If 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
248whitelist checking. You can provide a `jwk.Whitelist` object to either `jwk.Fetch()` or `(*jwk.AutoRefresh).Configure()` methods to specify the
249use of a whitelist.
250
251Currently the package provides `jwk.MapWhitelist` and `jwk.RegexpWhitelist` types for simpler cases.
252
253```go
254wl := jwk.NewMapWhitelist().
255 Add(url1).
256 Add(url2).
257 Add(url3
258
259wl := jwk.NewRegexpWhitelist().
260 Add(regexp1).
261 Add(regexp2).
262 Add(regexp3)
263
264jwk.Fetch(ctx, url, jwk.WithWhitelist(wl))
265
266// or in a jwk.AutoRefresh object:
267ar.Configure(url, jwk.WithWhitelist(wl))
268```
269
270If you would like to implement something more complex, you can provide a function via `jwk.WhitelistFunc` or implement you own type of `jwk.Whitelist`.
271
272# Converting a jwk.Key to a raw key
273
274As discussed in [Terminology](#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`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Key) object, use the [`Raw()`](https://github.com/github.com/lestrrat-go/jwx/jwk#Raw) method.
275
276```go
277key, _ := jwk.ParseKey(src)
278
279var raw interface{}
280if err := key.Raw(&raw); err != nil {
281 ...
282}
283```
284
285In the above example, `raw` contains whatever the [`jwk.Key`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#Key) represents.
286If `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.
287
288If the only operation that you are performing is to grab the raw key out of a JSON JWK, use [`jwk.ParseRawKey`](https://pkg.go.dev/github.com/lestrrat-go/jwx/jwk#ParseRawKey)
289
290```go
291var raw interface{}
292if err := jwk.ParseRawKey(src, &raw); err != nil {
293 ...
294}
295```
View as plain text