Repository: lestrrat-go/jwx
Pull request #748 (2022-05-23 12:51:51)

merge develop/v2

codecov-commenter: # [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
> Merging [#748](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (e033e1c) into [v2](https://codecov.io/gh/lestrrat-go/jwx/commit/43ca1406d71955b098e833da5fc170ddef88e51a?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (43ca140) will **decrease** coverage by `0.09%`.
> The diff coverage is `65.21%`.

```diff
@@            Coverage Diff             @@
##               v2     #748      +/-   ##
==========================================
- Coverage   71.78%   71.69%   -0.10%     
==========================================
  Files          90       89       -1     
  Lines        9662     9856     +194     
==========================================
+ Hits         6936     7066     +130     
- Misses       1928     1988      +60     
- Partials      798      802       +4     
```


| [Impacted Files](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Coverage Δ | |
|---|---|---|
| [internal/ecutil/ecutil.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvZWN1dGlsL2VjdXRpbC5nbw==) | `84.61% <ø> (ø)` | |
| [internal/iter/mapiter.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvaXRlci9tYXBpdGVyLmdv) | `75.00% <ø> (ø)` | |
| [internal/json/json.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvanNvbi9qc29uLmdv) | `66.66% <ø> (ø)` | |
| [internal/pool/pool.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvcG9vbC9wb29sLmdv) | `100.00% <ø> (ø)` | |
| [jwe/compress.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL2NvbXByZXNzLmdv) | `0.00% <0.00%> (ø)` | |
| [jwe/internal/cipher/cipher.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL2ludGVybmFsL2NpcGhlci9jaXBoZXIuZ28=) | `62.88% <0.00%> (+2.49%)` | :arrow_up: |
| [jwk/fetch.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL2ZldGNoLmdv) | `73.07% <ø> (ø)` | |
| [jws/message.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andzL21lc3NhZ2UuZ28=) | `81.49% <ø> (ø)` | |
| [jwt/http.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L2h0dHAuZ28=) | `87.64% <ø> (ø)` | |
| [jwt/openid/token\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L29wZW5pZC90b2tlbl9nZW4uZ28=) | `66.35% <ø> (+0.17%)` | :arrow_up: |
| ... and [34 more](https://codecov.io/gh/lestrrat-go/jwx/pull/748/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Last update [23a2852...e033e1c](https://codecov.io/gh/lestrrat-go/jwx/pull/748?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index dd3addb8..3083d468 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -48,7 +48,7 @@ jobs:
       - name: Test with coverage
         run: make cover-${{ matrix.go_tags }}
       - name: Upload code coverage to codecov
-        if: matrix.go == '1.17.x'
+        if: matrix.go == '1.18'
         uses: codecov/codecov-action@v1
         with:
           file: ./coverage.out
diff --git a/Changes b/Changes
index caada4b6..7f7e98e6 100644
--- a/Changes
+++ b/Changes
@@ -4,6 +4,20 @@ Changes
 v2 has many incompatibilities with v1. To see the full list of differences between
 v1 and v2, please read the Changes-v2.md file (https://github.com/lestrrat-go/jwx/blob/develop/v2/Changes-v2.md)
 
+v2.0.2 - 23 May 2022
+[Bug Fixes][Security]
+  * [jwe] An old bug from at least 7 years ago existed in handling AES-CBC unpadding,
+    where the unpad operation might remove more bytes than necessary (#744)
+    This affects all jwx code that is available before v2.0.2 and v1.2.25.
+
+[New Features]
+  * [jwt] RFC3339 timestamps are also accepted for Numeric Date types in JWT tokens.
+    This allows users to parse servers that errnously use RFC3339 timestamps in
+    some pre-defined fields. You can change this behavior by setting 
+    `jwt.WithNumericDateParsePedantic` to `false`
+  * [jwt] `jwt.WithNumericDateParsePedantic` has been added. This is a global
+    option that is set using `jwt.Settings`
+
 v2.0.1 - 06 May 2022
   * [jwk] `jwk.Set` had erronously been documented as not returning an error
     when the same key already exists in the set. This is a behavior change
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index c0e3bed1..98c7ef78 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -941,21 +941,34 @@ func ExampleJWT_SerializeJWS() {
     return
   }
 
-  key, err := jwk.FromRaw([]byte(`abracadavra`))
+  rawKey := []byte(`abracadavra`)
+  jwkKey, err := jwk.FromRaw(rawKey)
   if err != nil {
     fmt.Printf("failed to create symmetric key: %s\n", err)
     return
   }
 
-  serialized, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))
-  if err != nil {
-    fmt.Printf("failed to sign token: %s\n", err)
-    return
+  // This example shows you two ways to passing keys to
+  // jwt.Sign()
+  //
+  // * The first key is the "raw" key.
+  // * The second one is a jwk.Key that represents the raw key.
+  //
+  // If this were using RSA/ECDSA keys, you would be using
+  // *rsa.PrivateKey/*ecdsa.PrivateKey as the raw key.
+  for _, key := range []interface{}{rawKey, jwkKey} {
+    serialized, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))
+    if err != nil {
+      fmt.Printf("failed to sign token: %s\n", err)
+      return
+    }
+
+    fmt.Printf("%s\n", serialized)
   }
 
-  fmt.Printf("%s\n", serialized)
   // OUTPUT:
   // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIzMzQzMTIwMCwiaXNzIjoiZ2l0aHViLmNvbS9sZXN0cnJhdC1nby9qd3gifQ.rTlpyVnHFWosNud7seqlsvhM8UoXUIAKfdWHySFO5Ro
+  // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIzMzQzMTIwMCwiaXNzIjoiZ2l0aHViLmNvbS9sZXN0cnJhdC1nby9qd3gifQ.rTlpyVnHFWosNud7seqlsvhM8UoXUIAKfdWHySFO5Ro
 }
 ```
 source: [examples/jwt_serialize_jws_example_test.go](https://github.com/lestrrat-go/jwx/blob/v2/examples/jwt_serialize_jws_example_test.go)
diff --git a/docs/04-jwk.md b/docs/04-jwk.md
index 5d6ebe63..7ffaddec 100644
--- a/docs/04-jwk.md
+++ b/docs/04-jwk.md
@@ -31,7 +31,7 @@ In this document we describe how to work with JWK using `github.com/lestrrat-go/
 
 ## JWK / Key
 
-Used to describe a JWK key, possibly of typeRSA, ECDSA, OKP, or Symmetric.
+Used to describe a JWK key, possibly of type RSA, ECDSA, OKP, or Symmetric.
 
 ## JWK Set / Set
 
@@ -47,6 +47,18 @@ Used to describe the underlying raw key that a JWK represents. For example, an R
 represent rsa.PrivateKey/rsa.PublicKey, ECDSA JWK can represent ecdsa.PrivateKey/ecdsa.PublicKey,
 and so forth
 
+---
+
+The table below shows the matrix of key types and their respective `jwk.Key` and "raw" types.
+If given anything else, `jwk.FromRaw` will return an error.
+
+|           | `jwk.Key` Type                               | Raw Key Type                              |
+|-----------|----------------------------------------------|-------------------------------------------|
+| RSA       | `jwk.RSAPublicKey` / `jwk.RSAPrivateKey`     | `*rsa.PublicKey` / `*rsa.PublicKey`       |
+| ECDSA     | `jwk.ECDSAPublicKey` / `jwk.ECDSAPrivateKey` | `*ecdsa.PublicKey` / `*ecdsa.PublicKey`   |
+| OKP       | `jwk.OKPPublicKey` / `jwk.OKPPrivateKey`     | `ed25519.PublicKey` / `ed25519.PublicKey` |
+| Symmetric | `jwk.SymmetricKey`                           | []byte                                    |
+
 # Parsing
 
 ## Parse a set
@@ -473,7 +485,7 @@ func ExampleJWK_FromRaw() {
   {
     raw, err := rsa.GenerateKey(rand.Reader, 2048)
     if err != nil {
-      fmt.Printf("failed to generate new RSA privatre key: %s\n", err)
+      fmt.Printf("failed to generate new RSA private key: %s\n", err)
       return
     }
 
@@ -494,7 +506,7 @@ func ExampleJWK_FromRaw() {
   {
     raw, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
     if err != nil {
-      fmt.Printf("failed to generate new ECDSA privatre key: %s\n", err)
+      fmt.Printf("failed to generate new ECDSA private key: %s\n", err)
       return
     }
 
diff --git a/jwe/internal/aescbc/aescbc.go b/jwe/internal/aescbc/aescbc.go
index 500325fc..d38245ff 100644
--- a/jwe/internal/aescbc/aescbc.go
+++ b/jwe/internal/aescbc/aescbc.go
@@ -33,23 +33,36 @@ func pad(buf []byte, n int) []byte {
 func unpad(buf []byte, n int) ([]byte, error) {
 	lbuf := len(buf)
 	rem := lbuf % n
+
+	// First, `buf` must be a multiple of `n`
 	if rem != 0 {
 		return nil, fmt.Errorf("input buffer must be multiple of block size %d", n)
 	}
 
-	count := 0
+	// Find the last byte, which is the encoded padding
+	// i.e. 0x1 == 1 byte worth of padding
 	last := buf[lbuf-1]
-	for i := lbuf - 1; i >= 0; i-- {
-		if buf[i] != last {
-			break
-		}
-		count++
+
+	// This is the number of padding bytes that we expect
+	expected := int(last)
+
+	if expected == 0 || /* we _have_ to have padding here. therefore, 0x0 is not an option */
+		expected > n || /* we also must make sure that we don't go over the block size (n) */
+		expected > lbuf /* finally, it can't be more than the buffer itself. unlikely, but could happen */ {
+		return nil, fmt.Errorf(`invalid padding byte at the end of buffer`)
 	}
-	if count != int(last) {
-		return nil, fmt.Errorf(`invalid padding`)
+
+	// start i = 1 because we have already established that expected == int(last) where
+	// last = buf[lbuf-1].
+	//
+	// we also don't check against lbuf-i in range, because we have established expected <= lbuf
+	for i := 1; i < expected; i++ {
+		if buf[lbuf-i] != last {
+			return nil, fmt.Errorf(`invalid padding`)
+		}
 	}
 
-	return buf[:lbuf-int(last)], nil
+	return buf[:lbuf-expected], nil
 }
 
 type Hmac struct {
diff --git a/jwt/internal/types/date.go b/jwt/internal/types/date.go
index 79f965f9..0878397f 100644
--- a/jwt/internal/types/date.go
+++ b/jwt/internal/types/date.go
@@ -14,6 +14,7 @@ const (
 	MaxPrecision     uint32 = 9 // nanosecond level
 )
 
+var Pedantic uint32
 var ParsePrecision = DefaultPrecision
 var FormatPrecision = DefaultPrecision
 
@@ -52,6 +53,27 @@ func intToTime(v interface{}, t *time.Time) bool {
 
 func parseNumericString(x string) (time.Time, error) {
 	var t time.Time // empty time for empty return value
+
+	// Only check for the escape hatch if it's the pedantic
+	// flag is off
+	if Pedantic != 1 {
+		// This is an escape hatch for non-conformant providers
+		// that gives us RFC3339 instead of epoch time
+		for _, r := range x {
+			// 0x30 = '0', 0x39 = '9', 0x2E = '.'
+			if (r >= 0x30 && r <= 0x39) || r == 0x2E {
+				continue
+			}
+
+			// if it got here, then it probably isn't epoch time
+			tv, err := time.Parse(time.RFC3339, x)
+			if err != nil {
+				return t, fmt.Errorf(`value is not number of seconds since the epoch, and attempt to parse it as RFC3339 timestamp failed: %w`, err)
+			}
+			return tv, nil
+		}
+	}
+
 	var fractional string
 	whole := x
 	if i := strings.IndexRune(x, '.'); i > 0 {
diff --git a/jwt/jwt.go b/jwt/jwt.go
index 101fa5fc..5846a8ff 100644
--- a/jwt/jwt.go
+++ b/jwt/jwt.go
@@ -18,6 +18,7 @@ import (
 // Settings controls global settings that are specific to JWTs.
 func Settings(options ...GlobalOption) {
 	var flattenAudienceBool bool
+	var parsePedantic bool
 	var parsePrecision = types.MaxPrecision + 1  // illegal value, so we can detect nothing was set
 	var formatPrecision = types.MaxPrecision + 1 // illegal value, so we can detect nothing was set
 
@@ -26,6 +27,8 @@ func Settings(options ...GlobalOption) {
 		switch option.Ident() {
 		case identFlattenAudience{}:
 			flattenAudienceBool = option.Value().(bool)
+		case identNumericDateParsePedantic{}:
+			parsePedantic = option.Value().(bool)
 		case identNumericDateParsePrecision{}:
 			v := option.Value().(int)
 			// only accept this value if it's in our desired range
@@ -55,6 +58,17 @@ func Settings(options ...GlobalOption) {
 		}
 	}
 
+	{
+		v := atomic.LoadUint32(&types.Pedantic)
+		if (v == 1) != parsePedantic {
+			var newVal uint32
+			if parsePedantic {
+				newVal = 1
+			}
+			atomic.CompareAndSwapUint32(&types.Pedantic, v, newVal)
+		}
+	}
+
 	{
 		v := atomic.LoadUint32(&json.FlattenAudience)
 		if (v == 1) != flattenAudienceBool {
diff --git a/jwt/options.yaml b/jwt/options.yaml
index 303caa9e..2684ade2 100644
--- a/jwt/options.yaml
+++ b/jwt/options.yaml
@@ -186,4 +186,18 @@ options:
       WithNumericDateFormatPrecision sets the precision up to which the
       library uses to format fractional dates found in the numeric date
       fields. Default is 0 (second, no fractionals), max is 9 (nanosecond)
+  - ident: NumericDateParsePedantic
+    interface: GlobalOption
+    argument_type: bool
+    comment: |
+      WithNumericDateParsePedantic specifies if the parser should behave
+      in a pedantic manner when parsing numeric dates. Normally this library
+      attempts to interpret timestamps as a numeric value representing
+      number of seconds (with an optional fractional part), but if that fails
+      it tries to parse using a RFC3339 parser. This allows us to parse
+      payloads from non-comforming servers.
+      
+      However, when you set WithNumericDateParePedantic to `true`, the
+      RFC3339 parser is not tried, and we expect a numeric value strictly 
+
 
diff --git a/jwt/options_gen.go b/jwt/options_gen.go
index 41c48ba6..98cccf36 100644
--- a/jwt/options_gen.go
+++ b/jwt/options_gen.go
@@ -132,6 +132,7 @@ type identFormKey struct{}
 type identHeaderKey struct{}
 type identKeyProvider struct{}
 type identNumericDateFormatPrecision struct{}
+type identNumericDateParsePedantic struct{}
 type identNumericDateParsePrecision struct{}
 type identPedantic struct{}
 type identSignOption struct{}
@@ -180,6 +181,10 @@ func (identNumericDateFormatPrecision) String() string {
 	return "WithNumericDateFormatPrecision"
 }
 
+func (identNumericDateParsePedantic) String() string {
+	return "WithNumericDateParsePedantic"
+}
+
 func (identNumericDateParsePrecision) String() string {
 	return "WithNumericDateParsePrecision"
 }
@@ -285,6 +290,19 @@ func WithNumericDateFormatPrecision(v int) GlobalOption {
 	return &globalOption{option.New(identNumericDateFormatPrecision{}, v)}
 }
 
+// WithNumericDateParsePedantic specifies if the parser should behave
+// in a pedantic manner when parsing numeric dates. Normally this library
+// attempts to interpret timestamps as a numeric value representing
+// number of seconds (with an optional fractional part), but if that fails
+// it tries to parse using a RFC3339 parser. This allows us to parse
+// payloads from non-comforming servers.
+//
+// However, when you set WithNumericDateParePedantic to `true`, the
+// RFC3339 parser is not tried, and we expect a numeric value strictly
+func WithNumericDateParsePedantic(v bool) GlobalOption {
+	return &globalOption{option.New(identNumericDateParsePedantic{}, v)}
+}
+
 // WithNumericDateParsePrecision sets the precision up to which the
 // library uses to parse fractional dates found in the numeric date
 // fields. Default is 0 (second, no fractionals), max is 9 (nanosecond)


Test patch:
diff --git a/examples/jwk_from_raw_example_test.go b/examples/jwk_from_raw_example_test.go
index fe7179b2..d0b9e427 100644
--- a/examples/jwk_from_raw_example_test.go
+++ b/examples/jwk_from_raw_example_test.go
@@ -50,7 +50,7 @@ func ExampleJWK_FromRaw() {
 	{
 		raw, err := rsa.GenerateKey(rand.Reader, 2048)
 		if err != nil {
-			fmt.Printf("failed to generate new RSA privatre key: %s\n", err)
+			fmt.Printf("failed to generate new RSA private key: %s\n", err)
 			return
 		}
 
@@ -71,7 +71,7 @@ func ExampleJWK_FromRaw() {
 	{
 		raw, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
 		if err != nil {
-			fmt.Printf("failed to generate new ECDSA privatre key: %s\n", err)
+			fmt.Printf("failed to generate new ECDSA private key: %s\n", err)
 			return
 		}
 
diff --git a/examples/jwt_get_claims_example_test.go b/examples/jwt_get_claims_example_test.go
new file mode 100644
index 00000000..94992187
--- /dev/null
+++ b/examples/jwt_get_claims_example_test.go
@@ -0,0 +1,67 @@
+package examples_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/lestrrat-go/jwx/v2/jwt"
+)
+
+func ExampleJWT_GetClaims() {
+	tok, err := jwt.NewBuilder().
+		IssuedAt(time.Now()).
+		Issuer(`github.com/lestrrat-go/jwx`).
+		Subject(`example`).
+		Claim(`claim1`, `value1`).
+		Claim(`claim2`, `2022-05-16T07:35:56+00:00`).
+		Build()
+	if err != nil {
+		fmt.Printf("failed to build token: %s\n", err)
+		return
+	}
+
+	// Pre-defined fields have typed accessors.
+	var _ time.Time = tok.IssuedAt()
+	var _ string = tok.Issuer()
+	var _ string = tok.Subject()
+
+	var v interface{}
+	var ok bool
+
+	// But you can also get them via the generic `.Get()` method.
+	// However, v is of type interface{}, so you might need to
+	// use a type switch to properly use its value.
+	//
+	// For the key name you could also use jwt.IssuedAtKey constant
+	v, ok = tok.Get(`iat`)
+
+	// Private claims
+	v, ok = tok.Get(`claim1`)
+	v, ok = tok.Get(`claim2`)
+
+	// However, it is possible to globally specify that a private
+	// claim should be parsed into a custom type.
+	// In the sample below `claim2` is to be an instance of time.Time
+	jwt.RegisterCustomField(`claim2`, time.Time{})
+
+	tok = jwt.New()
+	if err := json.Unmarshal([]byte(`{"claim2":"2022-05-16T07:35:56+00:00"}`), tok); err != nil {
+		fmt.Printf(`failed to parse token: %s`, err)
+		return
+	}
+	v, ok = tok.Get(`claim2`)
+	if !ok {
+		fmt.Printf(`failed to get private claim "claim2"`)
+		return
+	}
+	if _, ok := v.(time.Time); !ok {
+		fmt.Printf(`claim2 expected to be time.Time, but got %T`, v)
+		return
+	}
+
+	_ = v
+	_ = ok
+
+	// OUTPUT:
+}
diff --git a/examples/jwt_serialize_jws_example_test.go b/examples/jwt_serialize_jws_example_test.go
index 36d75cb6..aa3fffb5 100644
--- a/examples/jwt_serialize_jws_example_test.go
+++ b/examples/jwt_serialize_jws_example_test.go
@@ -19,19 +19,32 @@ func ExampleJWT_SerializeJWS() {
 		return
 	}
 
-	key, err := jwk.FromRaw([]byte(`abracadavra`))
+	rawKey := []byte(`abracadavra`)
+	jwkKey, err := jwk.FromRaw(rawKey)
 	if err != nil {
 		fmt.Printf("failed to create symmetric key: %s\n", err)
 		return
 	}
 
-	serialized, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))
-	if err != nil {
-		fmt.Printf("failed to sign token: %s\n", err)
-		return
+	// This example shows you two ways to passing keys to
+	// jwt.Sign()
+	//
+	// * The first key is the "raw" key.
+	// * The second one is a jwk.Key that represents the raw key.
+	//
+	// If this were using RSA/ECDSA keys, you would be using
+	// *rsa.PrivateKey/*ecdsa.PrivateKey as the raw key.
+	for _, key := range []interface{}{rawKey, jwkKey} {
+		serialized, err := jwt.Sign(tok, jwt.WithKey(jwa.HS256, key))
+		if err != nil {
+			fmt.Printf("failed to sign token: %s\n", err)
+			return
+		}
+
+		fmt.Printf("%s\n", serialized)
 	}
 
-	fmt.Printf("%s\n", serialized)
 	// OUTPUT:
 	// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIzMzQzMTIwMCwiaXNzIjoiZ2l0aHViLmNvbS9sZXN0cnJhdC1nby9qd3gifQ.rTlpyVnHFWosNud7seqlsvhM8UoXUIAKfdWHySFO5Ro
+	// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjIzMzQzMTIwMCwiaXNzIjoiZ2l0aHViLmNvbS9sZXN0cnJhdC1nby9qd3gifQ.rTlpyVnHFWosNud7seqlsvhM8UoXUIAKfdWHySFO5Ro
 }
diff --git a/jwt/openid/openid_test.go b/jwt/openid/openid_test.go
index 78a0af4c..15aba8e2 100644
--- a/jwt/openid/openid_test.go
+++ b/jwt/openid/openid_test.go
@@ -15,6 +15,7 @@ import (
 	"github.com/lestrrat-go/jwx/v2/jwt/internal/types"
 	"github.com/lestrrat-go/jwx/v2/jwt/openid"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
 )
 
 const aLongLongTimeAgo = 233431200
@@ -676,3 +677,32 @@ func TestKeys(t *testing.T) {
 	at.Equal(`website`, openid.WebsiteKey)
 	at.Equal(`zoneinfo`, openid.ZoneinfoKey)
 }
+
+func TestGH734(t *testing.T) {
+	const src = `{
+    "nickname": "miniscruff",
+    "updated_at": "2022-05-06T04:57:24.367Z",
+    "email_verified": true
+  }`
+
+	expected, _ := time.Parse(time.RFC3339, "2022-05-06T04:57:24.367Z")
+	for _, pedantic := range []bool{true, false} {
+		t.Run(fmt.Sprintf("pedantic=%t", pedantic), func(t *testing.T) {
+			jwt.Settings(jwt.WithNumericDateParsePedantic(pedantic))
+			tok := openid.New()
+			_, err := jwt.Parse(
+				[]byte(src),
+				jwt.WithToken(tok),
+				jwt.WithVerify(false),
+				jwt.WithValidate(false),
+			)
+			if pedantic {
+				require.Error(t, err, `jwt.Parse should fail for pedantic parser`)
+			} else {
+				require.NoError(t, err, `jwt.Parse should succeed`)
+				require.Equal(t, expected, tok.UpdatedAt(), `updated_at should match`)
+			}
+		})
+	}
+	jwt.Settings(jwt.WithNumericDateParsePedantic(false))
+}
diff --git a/jwt/options_gen_test.go b/jwt/options_gen_test.go
index ba07af85..060436af 100644
--- a/jwt/options_gen_test.go
+++ b/jwt/options_gen_test.go
@@ -19,6 +19,7 @@ func TestOptionIdent(t *testing.T) {
 	require.Equal(t, "WithHeaderKey", identHeaderKey{}.String())
 	require.Equal(t, "WithKeyProvider", identKeyProvider{}.String())
 	require.Equal(t, "WithNumericDateFormatPrecision", identNumericDateFormatPrecision{}.String())
+	require.Equal(t, "WithNumericDateParsePedantic", identNumericDateParsePedantic{}.String())
 	require.Equal(t, "WithNumericDateParsePrecision", identNumericDateParsePrecision{}.String())
 	require.Equal(t, "WithPedantic", identPedantic{}.String())
 	require.Equal(t, "WithSignOption", identSignOption{}.String())