lestrrat-go / jwx

Compare 3b7a676 ... +55 ... 20c50b0

Coverage Reach
jwk/rsa_gen.go jwk/ecdsa_gen.go jwk/okp_gen.go jwk/jwk.go jwk/symmetric_gen.go jwk/refresh.go jwk/set.go jwk/ecdsa.go jwk/rsa.go jwk/okp.go jwk/certchain.go jwk/key_ops.go jwk/symmetric.go jwk/option.go jwk/usage.go jwk/whitelist.go jwk/io.go jwk/es256k.go jwt/openid/token_gen.go jwt/openid/address.go jwt/openid/birthdate.go jwt/openid/builder_gen.go jwt/openid/openid.go jwt/token_gen.go jwt/jwt.go jwt/validate.go jwt/http.go jwt/serialize.go jwt/internal/types/date.go jwt/internal/types/string.go jwt/options.go jwt/builder_gen.go jwt/io.go jwe/internal/keyenc/keyenc.go jwe/internal/aescbc/aescbc.go jwe/internal/cipher/cipher.go jwe/internal/keygen/keygen.go jwe/internal/concatkdf/concatkdf.go jwe/internal/content_crypt/content_crypt.go jwe/headers_gen.go jwe/message.go jwe/jwe.go jwe/decrypt.go jwe/encrypt.go jwe/headers.go jwe/serializer.go jwe/compress.go jwe/options.go jwe/io.go jwe/interface.go jws/jws.go jws/headers_gen.go jws/message.go jws/ecdsa.go jws/rsa.go jws/hmac.go jws/eddsa.go jws/headers.go jws/verifier.go jws/signer.go jws/option.go jws/io.go jws/es256k.go internal/keyconv/keyconv.go internal/json/json.go internal/json/registry.go internal/json/stdlib.go internal/json/goccy.go internal/ecutil/ecutil.go internal/base64/base64.go internal/pool/pool.go internal/iter/mapiter.go jwa/key_encryption_gen.go jwa/key_type_gen.go jwa/signature_gen.go jwa/content_encryption_gen.go jwa/compression_gen.go jwa/elliptic_gen.go jwa/secp2561k.go x25519/x25519.go format.go cmd/jwx/jwx.go options.go formatkind_string_gen.go

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.

Showing 22 of 71 files from the diff.
Newly tracked file
jws/es256k.go created.
Other files ignored by Codecov
jwk/jwk_test.go has changed.
Makefile has changed.
go.sum has changed.
cmd/jwx/go.mod has changed.
docs/99-faq.md has changed.
cmd/jwx/go.sum has changed.
examples/go.sum has changed.
go.mod has changed.
docs/04-jwk.md has changed.
jwe/jwe_test.go has changed.
jws/interface.go has changed.
docs/02-jws.md has changed.
docs/01-jwt.md has changed.
jwk/interface.go has changed.
jwt/jwt_test.go has changed.
Changes has changed.
README.md has changed.
jws/jws_test.go has changed.
examples/go.mod has changed.

@@ -29,6 +29,7 @@
Loading
29 29
	errSink      chan AutoRefreshError
30 30
	cache        map[string]Set
31 31
	configureCh  chan struct{}
32 +
	removeCh     chan removeReq
32 33
	fetching     map[string]chan struct{}
33 34
	muErrSink    sync.Mutex
34 35
	muCache      sync.RWMutex
@@ -86,7 +87,8 @@
Loading
86 87
	lastRefresh time.Time
87 88
	nextRefresh time.Time
88 89
89 -
	wl Whitelist
90 +
	wl           Whitelist
91 +
	parseOptions []ParseOption
90 92
}
91 93
92 94
type resetTimerReq struct {
@@ -114,6 +116,7 @@
Loading
114 116
	af := &AutoRefresh{
115 117
		cache:        make(map[string]Set),
116 118
		configureCh:  make(chan struct{}),
119 +
		removeCh:     make(chan removeReq),
117 120
		fetching:     make(map[string]chan struct{}),
118 121
		registry:     make(map[string]*target),
119 122
		resetTimerCh: make(chan *resetTimerReq),
@@ -132,6 +135,19 @@
Loading
132 135
	return nil, false
133 136
}
134 137
138 +
type removeReq struct {
139 +
	replyCh chan error
140 +
	url     string
141 +
}
142 +
143 +
// Remove removes `url` from the list of urls being watched by jwk.AutoRefresh.
144 +
// If the url is not already registered, returns an error.
145 +
func (af *AutoRefresh) Remove(url string) error {
146 +
	ch := make(chan error)
147 +
	af.removeCh <- removeReq{replyCh: ch, url: url}
148 +
	return <-ch
149 +
}
150 +
135 151
// Configure registers the url to be controlled by AutoRefresh, and also
136 152
// sets any options associated to it.
137 153
//
@@ -153,9 +169,15 @@
Loading
153 169
	var hasRefreshInterval bool
154 170
	var refreshInterval time.Duration
155 171
	var wl Whitelist
172 +
	var parseOptions []ParseOption
156 173
	minRefreshInterval := time.Hour
157 174
	bo := backoff.Null()
158 175
	for _, option := range options {
176 +
		if v, ok := option.(ParseOption); ok {
177 +
			parseOptions = append(parseOptions, v)
178 +
			continue
179 +
		}
180 +
159 181
		//nolint:forcetypeassert
160 182
		switch option.Ident() {
161 183
		case identFetchBackoff{}:
@@ -172,39 +194,34 @@
Loading
172 194
		}
173 195
	}
174 196
175 -
	var doReconfigure bool
176 197
	af.muRegistry.Lock()
177 198
	t, ok := af.registry[url]
178 199
	if ok {
179 200
		if t.httpcl != httpcl {
180 201
			t.httpcl = httpcl
181 -
			doReconfigure = true
182 202
		}
183 203
184 204
		if t.minRefreshInterval != minRefreshInterval {
185 205
			t.minRefreshInterval = minRefreshInterval
186 -
			doReconfigure = true
187 206
		}
188 207
189 208
		if t.refreshInterval != nil {
190 209
			if !hasRefreshInterval {
191 210
				t.refreshInterval = nil
192 -
				doReconfigure = true
193 211
			} else if *t.refreshInterval != refreshInterval {
194 212
				*t.refreshInterval = refreshInterval
195 -
				doReconfigure = true
196 213
			}
197 214
		} else {
198 215
			if hasRefreshInterval {
199 216
				t.refreshInterval = &refreshInterval
200 -
				doReconfigure = true
201 217
			}
202 218
		}
203 219
204 220
		if t.wl != wl {
205 221
			t.wl = wl
206 -
			doReconfigure = true
207 222
		}
223 +
224 +
		t.parseOptions = parseOptions
208 225
	} else {
209 226
		t = &target{
210 227
			backoff:            bo,
@@ -215,23 +232,21 @@
Loading
215 232
			// This is a placeholder timer so we can call Reset() on it later
216 233
			// Make it sufficiently in the future so that we don't have bogus
217 234
			// events firing
218 -
			timer: time.NewTimer(24 * time.Hour),
219 -
			wl:    wl,
235 +
			timer:        time.NewTimer(24 * time.Hour),
236 +
			wl:           wl,
237 +
			parseOptions: parseOptions,
220 238
		}
221 239
		if hasRefreshInterval {
222 240
			t.refreshInterval = &refreshInterval
223 241
		}
224 242
225 243
		// Record this in the registry
226 244
		af.registry[url] = t
227 -
		doReconfigure = true
228 245
	}
229 246
	af.muRegistry.Unlock()
230 247
231 -
	if doReconfigure {
232 -
		// Tell the backend to reconfigure itself
233 -
		af.configureCh <- struct{}{}
234 -
	}
248 +
	// Tell the backend to reconfigure itself
249 +
	af.configureCh <- struct{}{}
235 250
}
236 251
237 252
func (af *AutoRefresh) releaseFetching(url string) {
@@ -346,21 +361,32 @@
Loading
346 361
	// in a very fast iteration, but we assume here that refreshes happen
347 362
	// seldom enough that being able to call one `select{}` with multiple
348 363
	// targets / channels outweighs the speed penalty of using reflect.
349 -
	baseSelcases := []reflect.SelectCase{
350 -
		{
351 -
			Dir:  reflect.SelectRecv,
352 -
			Chan: reflect.ValueOf(ctx.Done()),
353 -
		},
354 -
		{
355 -
			Dir:  reflect.SelectRecv,
356 -
			Chan: reflect.ValueOf(af.configureCh),
357 -
		},
358 -
		{
359 -
			Dir:  reflect.SelectRecv,
360 -
			Chan: reflect.ValueOf(af.resetTimerCh),
361 -
		},
364 +
	//
365 +
	const (
366 +
		ctxDoneIdx = iota
367 +
		configureIdx
368 +
		resetTimerIdx
369 +
		removeIdx
370 +
		baseSelcasesLen
371 +
	)
372 +
373 +
	baseSelcases := make([]reflect.SelectCase, baseSelcasesLen)
374 +
	baseSelcases[ctxDoneIdx] = reflect.SelectCase{
375 +
		Dir:  reflect.SelectRecv,
376 +
		Chan: reflect.ValueOf(ctx.Done()),
377 +
	}
378 +
	baseSelcases[configureIdx] = reflect.SelectCase{
379 +
		Dir:  reflect.SelectRecv,
380 +
		Chan: reflect.ValueOf(af.configureCh),
381 +
	}
382 +
	baseSelcases[resetTimerIdx] = reflect.SelectCase{
383 +
		Dir:  reflect.SelectRecv,
384 +
		Chan: reflect.ValueOf(af.resetTimerCh),
385 +
	}
386 +
	baseSelcases[removeIdx] = reflect.SelectCase{
387 +
		Dir:  reflect.SelectRecv,
388 +
		Chan: reflect.ValueOf(af.removeCh),
362 389
	}
363 -
	baseidx := len(baseSelcases)
364 390
365 391
	var targets []*target
366 392
	var selcases []reflect.SelectCase
@@ -376,7 +402,7 @@
Loading
376 402
		}
377 403
378 404
		if cap(selcases) < len(af.registry) {
379 -
			selcases = make([]reflect.SelectCase, 0, len(af.registry)+baseidx)
405 +
			selcases = make([]reflect.SelectCase, 0, len(af.registry)+baseSelcasesLen)
380 406
		} else {
381 407
			selcases = selcases[:0]
382 408
		}
@@ -393,15 +419,15 @@
Loading
393 419
394 420
		chosen, recv, recvOK := reflect.Select(selcases)
395 421
		switch chosen {
396 -
		case 0:
422 +
		case ctxDoneIdx:
397 423
			// <-ctx.Done(). Just bail out of this loop
398 424
			return
399 -
		case 1:
425 +
		case configureIdx:
400 426
			// <-configureCh. rebuild the select list from the registry.
401 427
			// since we're rebuilding everything for each iteration,
402 428
			// we just need to start the loop all over again
403 429
			continue
404 -
		case 2:
430 +
		case resetTimerIdx:
405 431
			// <-resetTimerCh. interrupt polling, and reset the timer on
406 432
			// a single target. this needs to be handled inside this select
407 433
			if !recvOK {
@@ -418,14 +444,28 @@
Loading
418 444
				}
419 445
			}
420 446
			t.timer.Reset(d)
447 +
		case removeIdx:
448 +
			// <-removeCh. remove the URL from future fetching
449 +
			//nolint:forcetypeassert
450 +
			req := recv.Interface().(removeReq)
451 +
			replyCh := req.replyCh
452 +
			url := req.url
453 +
			af.muRegistry.Lock()
454 +
			if _, ok := af.registry[url]; !ok {
455 +
				replyCh <- errors.Errorf(`invalid url %q (not registered)`, url)
456 +
			} else {
457 +
				delete(af.registry, url)
458 +
				replyCh <- nil
459 +
			}
460 +
			af.muRegistry.Unlock()
421 461
		default:
422 462
			// Do not fire a refresh in case the channel was closed.
423 463
			if !recvOK {
424 464
				continue
425 465
			}
426 466
427 467
			// Time to refresh a target
428 -
			t := targets[chosen-baseidx]
468 +
			t := targets[chosen-baseSelcasesLen]
429 469
430 470
			// Check if there are other goroutines still doing the refresh asynchronously.
431 471
			// This could happen if the refreshing goroutine is stuck on a backoff
@@ -457,24 +497,24 @@
Loading
457 497
458 498
	// In case the refresh fails due to errors in fetching/parsing the JWKS,
459 499
	// we want to retry. Create a backoff object,
460 -
461 -
	options := []FetchOption{WithHTTPClient(t.httpcl)}
500 +
	parseOptions := t.parseOptions
501 +
	fetchOptions := []FetchOption{WithHTTPClient(t.httpcl)}
462 502
	if enableBackoff {
463 -
		options = append(options, WithFetchBackoff(t.backoff))
503 +
		fetchOptions = append(fetchOptions, WithFetchBackoff(t.backoff))
464 504
	}
465 505
	if t.wl != nil {
466 -
		options = append(options, WithFetchWhitelist(t.wl))
506 +
		fetchOptions = append(fetchOptions, WithFetchWhitelist(t.wl))
467 507
	}
468 508
469 -
	res, err := fetch(ctx, url, options...)
509 +
	res, err := fetch(ctx, url, fetchOptions...)
470 510
	if err == nil {
471 511
		if res.StatusCode != http.StatusOK {
472 512
			// now, can there be a remote resource that responds with a status code
473 513
			// other than 200 and still be valid...? naaaaaaahhhhhh....
474 514
			err = errors.Errorf(`bad response status code (%d)`, res.StatusCode)
475 515
		} else {
476 516
			defer res.Body.Close()
477 -
			keyset, parseErr := ParseReader(res.Body)
517 +
			keyset, parseErr := ParseReader(res.Body, parseOptions...)
478 518
			if parseErr == nil {
479 519
				// Got a new key set. replace the keyset in the target
480 520
				af.muCache.Lock()

@@ -63,11 +63,27 @@
Loading
63 63
	return &verifyOption{option.New(identMessage{}, m)}
64 64
}
65 65
66 -
// WithDetachedPayload can be used to verify a JWS message with a
67 -
// detached payload. If you have to verify using this option, you should
68 -
// know exactly how and why this works.
69 -
func WithDetachedPayload(v []byte) VerifyOption {
70 -
	return &verifyOption{option.New(identDetachedPayload{}, v)}
66 +
type SignVerifyOption interface {
67 +
	SignOption
68 +
	VerifyOption
69 +
}
70 +
71 +
type signVerifyOption struct {
72 +
	Option
73 +
}
74 +
75 +
func (*signVerifyOption) signOption()   {}
76 +
func (*signVerifyOption) verifyOption() {}
77 +
78 +
// WithDetachedPayload can be used to both sign or verify a JWS message with a
79 +
// detached payload.
80 +
//
81 +
// When this option is used for `jws.Sign()`, the first parameter (normally the payload)
82 +
// must be set to `nil`.
83 +
//
84 +
// If you have to verify using this option, you should know exactly how and why this works.
85 +
func WithDetachedPayload(v []byte) SignVerifyOption {
86 +
	return &signVerifyOption{option.New(identDetachedPayload{}, v)}
71 87
}
72 88
73 89
// WithFetchWhitelist specifies the whitelist object to be passed

@@ -41,7 +41,7 @@
Loading
41 41
	x509URL                *string           // https://tools.ietf.org/html/rfc7515#section-4.1.5
42 42
	privateParams          map[string]interface{}
43 43
	mu                     *sync.RWMutex
44 -
	dc                     DecodeCtx
44 +
	dc                     json.DecodeCtx
45 45
}
46 46
47 47
func NewSymmetricKey() SymmetricKey {
@@ -340,13 +340,13 @@
Loading
340 340
	return cloneKey(k)
341 341
}
342 342
343 -
func (k *symmetricKey) DecodeCtx() DecodeCtx {
343 +
func (k *symmetricKey) DecodeCtx() json.DecodeCtx {
344 344
	k.mu.RLock()
345 345
	defer k.mu.RUnlock()
346 346
	return k.dc
347 347
}
348 348
349 -
func (k *symmetricKey) SetDecodeCtx(dc DecodeCtx) {
349 +
func (k *symmetricKey) SetDecodeCtx(dc json.DecodeCtx) {
350 350
	k.mu.Lock()
351 351
	defer k.mu.Unlock()
352 352
	k.dc = dc

@@ -41,15 +41,46 @@
Loading
41 41
// jwt.Token needs to handle private claims, and this really does not
42 42
// work well when it is embedded in other structure
43 43
type Token interface {
44 +
45 +
	// Audience returns the value for "aud" field of the token
44 46
	Audience() []string
47 +
48 +
	// Expiration returns the value for "exp" field of the token
45 49
	Expiration() time.Time
50 +
51 +
	// IssuedAt returns the value for "iat" field of the token
46 52
	IssuedAt() time.Time
53 +
54 +
	// Issuer returns the value for "iss" field of the token
47 55
	Issuer() string
56 +
57 +
	// JwtID returns the value for "jti" field of the token
48 58
	JwtID() string
59 +
60 +
	// NotBefore returns the value for "nbf" field of the token
49 61
	NotBefore() time.Time
62 +
63 +
	// Subject returns the value for "sub" field of the token
50 64
	Subject() string
65 +
66 +
	// PrivateClaims return the entire set of fields (claims) in the token
67 +
	// *other* than the pre-defined fields such as `iss`, `nbf`, `iat`, etc.
51 68
	PrivateClaims() map[string]interface{}
69 +
70 +
	// Get returns the value of the corresponding field in the token, such as
71 +
	// `nbf`, `exp`, `iat`, and other user-defined fields. If the field does not
72 +
	// exist in the token, the second return value will be `false`
73 +
	//
74 +
	// If you need to access fields like `alg`, `kid`, `jku`, etc, you need
75 +
	// to access the corresponding fields in the JWS/JWE message. For this,
76 +
	// you will need to access them by directly parsing the payload using
77 +
	// `jws.Parse` and `jwe.Parse`
52 78
	Get(string) (interface{}, bool)
79 +
80 +
	// Set assigns a value to the corresponding field in the token. Some
81 +
	// pre-defined fields such as `nbf`, `iat`, `iss` need their values to
82 +
	// be of a specific type. See the other getter methods in this interface
83 +
	// for the types of each of these fields
53 84
	Set(string, interface{}) error
54 85
	Remove(string) error
55 86
	Clone() (Token, error)

@@ -114,8 +114,20 @@
Loading
114 114
	}
115 115
116 116
	var pubkey rsa.PublicKey
117 -
	if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
118 -
		return errors.Wrapf(err, `failed to retrieve rsa.PublicKey out of %T`, key)
117 +
	if cs, ok := key.(crypto.Signer); ok {
118 +
		cpub := cs.Public()
119 +
		switch cpub := cpub.(type) {
120 +
		case rsa.PublicKey:
121 +
			pubkey = cpub
122 +
		case *rsa.PublicKey:
123 +
			pubkey = *cpub
124 +
		default:
125 +
			return errors.Errorf(`failed to retrieve rsa.PublicKey out of crypto.Signer %T`, key)
126 +
		}
127 +
	} else {
128 +
		if err := keyconv.RSAPublicKey(&pubkey, key); err != nil {
129 +
			return errors.Wrapf(err, `failed to retrieve rsa.PublicKey out of %T`, key)
130 +
		}
119 131
	}
120 132
121 133
	h := rv.hash.New()

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Click to load this diff.
Loading diff...

Learn more Showing 6 files with coverage changes found.

New file jws/es256k.go
New
Loading file...
Changes in jwk/rsa_gen.go
-1
-1
+2
Loading file...
Changes in jwt/jwt.go
+4
Loading file...
Changes in jwk/jwk.go
-1
-2
+3
Loading file...
Changes in jwk/refresh.go
-3
+3
Loading file...
Changes in jwe/encrypt.go
-1
+1
Loading file...

57 Commits

+5
-1
-4
+12
+21
-2
-7
-6
+1
+5
+6
-1
-5
Hiding 1 contexual commits
-6
+1
+5
+6
-1
-5
-5
+1
+4
-1
+1
+6
-1
-5
+42
+8
+7
+27
+11
+12
-1
Hiding 2 contexual commits
-8
+2
+6
+8
-2
-6
+18
+16
+2
-5
+1
+4
+5
-1
-4
+1 Files
+1
+1
+24
+18
+1
+5
+6
-1
-5
Files Coverage
internal 80.92%
jwa 100.00%
jwe -0.05% 64.16%
jwk 0.61% 73.89%
jws -1.21% 74.78%
jwt 74.56%
cmd/jwx/jwx.go 100.00%
format.go 84.62%
formatkind_string_gen.go 100.00%
options.go 66.67%
x25519/x25519.go 82.35%
Project Totals (84 files) 72.84%
Loading