Repository: lestrrat-go/jwx
Pull request #501 (2021-11-22 12:45:17)

[jwt] Introduce Builder pattern for JWT package
refs #500

lestrrat: ATM I do not like the name NewWith, nor the WithClaim option, but it's a start.

Also, currently there's a problem with providing an option-based interface to jwt/openid package. I don't know what the best approach is yet.

Random ideas

Builder. Easier programmatic generation. Slightly ugly

```go
tok, err := jwt.Builder().
   Claim(foo, fooValue).
   Claim(bar, barValue).
   Build()
```

public option identifier functions so jwt/openid can figure out private options:

```go
package jwt

func IsFooOption(o Option) bool { ... }

package openid

for _, option := range options {
   switch {
   case jwt.IsFooOption(option):
      ....
    }
}
```

Other random API ideas

```go
jwt.Contruct(jwt.Claim(foo, fooValue), jwt.Claim(bar, barValue))
```

codecov-commenter: # [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/501?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
> Merging [#501](https://codecov.io/gh/lestrrat-go/jwx/pull/501?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (5691157) into [main](https://codecov.io/gh/lestrrat-go/jwx/commit/e371adc10f659fa05b064274ff549a8e0353d2b6?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (e371adc) will **decrease** coverage by `0.10%`.
> The diff coverage is `38.88%`.

[![Impacted file tree graph](https://codecov.io/gh/lestrrat-go/jwx/pull/501/graphs/tree.svg?width=650&height=150&src=pr&token=kU2RSDosPt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)](https://codecov.io/gh/lestrrat-go/jwx/pull/501?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)

```diff
@@            Coverage Diff             @@
##             main     #501      +/-   ##
==========================================
- Coverage   72.58%   72.48%   -0.11%     
==========================================
  Files          80       81       +1     
  Lines        9231     9237       +6     
==========================================
- Hits         6700     6695       -5     
- Misses       1727     1737      +10     
- Partials      804      805       +1     
```


| [Impacted Files](https://codecov.io/gh/lestrrat-go/jwx/pull/501?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Coverage Δ | |
|---|---|---|
| [jwt/openid/builder\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/501/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L29wZW5pZC9idWlsZGVyX2dlbi5nbw==) | `0.00% <0.00%> (ø)` | |
| [jwt/options.go](https://codecov.io/gh/lestrrat-go/jwx/pull/501/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L29wdGlvbnMuZ28=) | `76.92% <33.33%> (-3.08%)` | :arrow_down: |
| [jwt/builder\_gen.go](https://codecov.io/gh/lestrrat-go/jwx/pull/501/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L2J1aWxkZXJfZ2VuLmdv) | `71.42% <71.42%> (ø)` | |
| [jwt/jwt.go](https://codecov.io/gh/lestrrat-go/jwx/pull/501/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-and0L2p3dC5nbw==) | `75.00% <100.00%> (ø)` | |
| [internal/json/goccy.go](https://codecov.io/gh/lestrrat-go/jwx/pull/501/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvanNvbi9nb2NjeS5nbw==) | | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/501?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/501?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Last update [e371adc...5691157](https://codecov.io/gh/lestrrat-go/jwx/pull/501?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).

lestrrat: Hmmm, ATM I like the Builder pattern.

```go
tok, err := jwt.NewBuilder().
  Claim(...).
  Claim(...).
  Build()
```

Patch:
diff --git a/Changes b/Changes
index 30cb97de..289d500f 100644
--- a/Changes
+++ b/Changes
@@ -1,6 +1,14 @@
 Changes
 =======
 
+v1.2.13 [UNRELEASED]
+[New Features]
+  * `jwt` package now has a `Builder` that may make it easier to programmatically
+    create a JWT for some users. 
+  * `jwt` errors now can be distinguished between validation errors and others.
+    Use `jwt.IsValidationError()` to check if it's a validation error, and then
+    use `errors.Is()` to check if it's one of the known (oft-used) errors
+
 v1.2.12 01 Dec 2021
 [New Features]
   * `jwk.Set` can now parse private parameters. For example, after parsing
diff --git a/docs/01-jwt.md b/docs/01-jwt.md
index 18474cce..4c8ef053 100644
--- a/docs/01-jwt.md
+++ b/docs/01-jwt.md
@@ -9,6 +9,7 @@ In this document we describe how to work with JWT using `github.com/lestrrat-go/
   * [Parse a JWT](#parse-a-jwt)
   * [Parse a JWT from file](#parse-a-jwt-from-file)
   * [Parse a JWT from a *http.Request](#parse-a-jwt-from-a-httprequest)
+* [Programmatically Creating a JWT](#programmatically-creating-a-jwt)
 * [Verification](#jwt-verification)
   * [Parse and Verify a JWT (with a single key)](#parse-and-verify-a-jwt-with-single-key)
   * [Parse and Verify a JWT (with a key set, matching "kid")](#parse-and-verify-a-jwt-with-a-key-set-matching-kid)
@@ -71,6 +72,59 @@ token, err := jwt.ParseRequest(req, jwt.WithHeaderKey("Authorization"), jwt.With
 // Looks under "Authorization" header and "access_token" form field
 token, err := jwt.ParseRequest(req, jwt.WithFormKey("access_token"))
 ```
+
+# Programmatically Creating a JWT
+
+## Using `jwt.New`
+
+The most straight
+
+```go
+token := jwt.New()
+_ = token.Set(name, value)
+```
+
+If repeatedly checking for errors in `Set()` sounds like too much trouble, consider using the builder.
+
+## Using the Builder
+
+Since v1.2.12, the `jwt` package comes with a builder, which you can use to initialize a JWT token in (almost) one go:
+
+```go
+token, err := jwt.NewBuilder().
+  Claim(name1, value1).
+  Claim(name2, value2).
+  ...
+  Build()
+```
+
+For known fields, you can use the special methods:
+
+```go
+token, err := jwt.NewBuilder().
+  IssuedAt(time.Now()).
+  Audience("me").
+  Issuer("foobar").
+  Build()
+```
+
+One caveat that you should be aware about is that all calls to set a claim in the builder performs an _overwriting_
+operation. For example, specifying `Audience` multiple times will only overwrite the previous value. If you have fields
+that require a list of string, you should use `[]string` as the value
+
+```go
+// WRONG. The result will be "aud": "bar", not "aud": ["foo", "bar"]
+_, _ = jwt.NewBuilder().
+  Audience("foo").
+  Audience("bar).
+  Build
+
+// CORRECT.
+_, _ = jwt.NewBuilder().
+  Audience([]string{"foo", "bar"}).
+  Build()
+```
+
 # JWT Verification
 
 ## Parse and Verify a JWT (with single key)
diff --git a/jwt/builder_gen.go b/jwt/builder_gen.go
new file mode 100644
index 00000000..4ce5a42c
--- /dev/null
+++ b/jwt/builder_gen.go
@@ -0,0 +1,71 @@
+// This file is auto-generated by jwt/internal/cmd/gentoken/main.go. DO NOT EDIT
+
+package jwt
+
+import (
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Builder is a convenience wrapper around the New() constructor
+// and the Set() methods to assign values to Token claims.
+// Users can successively call Claim() on the Builder, and have it
+// construct the Token when Build() is called. This alleviates the
+// need for the user to check for the return value of every single
+// Set() method call.
+// Note that each call to Claim() overwrites the value set from the
+// previous call.
+type Builder struct {
+	claims []*ClaimPair
+}
+
+func NewBuilder() *Builder {
+	return &Builder{}
+}
+
+func (b *Builder) Claim(name string, value interface{}) *Builder {
+	b.claims = append(b.claims, &ClaimPair{Key: name, Value: value})
+	return b
+}
+
+func (b *Builder) Audience(v []string) *Builder {
+	return b.Claim(AudienceKey, v)
+}
+
+func (b *Builder) Expiration(v time.Time) *Builder {
+	return b.Claim(ExpirationKey, v)
+}
+
+func (b *Builder) IssuedAt(v time.Time) *Builder {
+	return b.Claim(IssuedAtKey, v)
+}
+
+func (b *Builder) Issuer(v string) *Builder {
+	return b.Claim(IssuerKey, v)
+}
+
+func (b *Builder) JwtID(v string) *Builder {
+	return b.Claim(JwtIDKey, v)
+}
+
+func (b *Builder) NotBefore(v time.Time) *Builder {
+	return b.Claim(NotBeforeKey, v)
+}
+
+func (b *Builder) Subject(v string) *Builder {
+	return b.Claim(SubjectKey, v)
+}
+
+// Build creates a new token based on the claims that the builder has received
+// so far. If a claim cannot be set, then the method returns a nil Token with
+// a en error as a second return value
+func (b *Builder) Build() (Token, error) {
+	tok := New()
+	for _, claim := range b.claims {
+		if err := tok.Set(claim.Key.(string), claim.Value); err != nil {
+			return nil, errors.Wrapf(err, `failed to set claim %q`, claim.Key.(string))
+		}
+	}
+	return tok, nil
+}
diff --git a/jwt/internal/cmd/gentoken/main.go b/jwt/internal/cmd/gentoken/main.go
index 3826cf76..9885de67 100644
--- a/jwt/internal/cmd/gentoken/main.go
+++ b/jwt/internal/cmd/gentoken/main.go
@@ -6,6 +6,7 @@ import (
 	"fmt"
 	"log"
 	"os"
+	"path/filepath"
 	"strconv"
 	"strings"
 
@@ -55,6 +56,12 @@ func _main() error {
 		}
 	}
 
+	for _, object := range def.Objects {
+		if err := genBuilder(object); err != nil {
+			return errors.Wrapf(err, `failed to generate builder for package %q`, objectPackage(object))
+		}
+	}
+
 	return nil
 }
 
@@ -618,3 +625,69 @@ func generateToken(obj *codegen.Object) error {
 	}
 	return nil
 }
+
+func genBuilder(obj *codegen.Object) error {
+	var buf bytes.Buffer
+	pkg := objectPackage(obj)
+	o := codegen.NewOutput(&buf)
+	o.L("// This file is auto-generated by jwt/internal/cmd/gentoken/main.go. DO NOT EDIT")
+	o.LL("package %s", pkg)
+
+	o.LL("// Builder is a convenience wrapper around the New() constructor")
+	o.L("// and the Set() methods to assign values to Token claims.")
+	o.L("// Users can successively call Claim() on the Builder, and have it")
+	o.L("// construct the Token when Build() is called. This alleviates the")
+	o.L("// need for the user to check for the return value of every single")
+	o.L("// Set() method call.")
+	o.L("// Note that each call to Claim() overwrites the value set from the")
+	o.L("// previous call.")
+	o.L("type Builder struct {")
+	o.L("claims []*ClaimPair")
+	o.L("}")
+
+	o.LL("func NewBuilder() *Builder {")
+	o.L("return &Builder{}")
+	o.L("}")
+
+	o.LL("func (b *Builder) Claim(name string, value interface{}) *Builder {")
+	o.L("b.claims = append(b.claims, &ClaimPair{Key: name, Value: value})")
+	o.L("return b")
+	o.L("}")
+
+	for _, f := range obj.Fields() {
+		ftyp := f.Type()
+		if ftyp == "types.NumericDate" {
+			ftyp = "time.Time"
+		} else if ftyp == "types.StringList" {
+			ftyp = "[]string"
+		}
+		o.LL("func (b *Builder) %s(v %s) *Builder {", f.Name(true), ftyp)
+		o.L("return b.Claim(%sKey, v)", f.Name(true))
+		o.L("}")
+	}
+
+	o.LL("// Build creates a new token based on the claims that the builder has received")
+	o.L("// so far. If a claim cannot be set, then the method returns a nil Token with")
+	o.L("// a en error as a second return value")
+	o.L("func (b *Builder) Build() (Token, error) {")
+	o.L("tok := New()")
+	o.L("for _, claim := range b.claims {")
+	o.L("if err := tok.Set(claim.Key.(string), claim.Value); err != nil {")
+	o.L("return nil, errors.Wrapf(err, `failed to set claim %%q`, claim.Key.(string))")
+	o.L("}")
+	o.L("}")
+	o.L("return tok, nil")
+	o.L("}")
+
+	fn := "builder_gen.go"
+	if pkg != "jwt" {
+		fn = filepath.Join(pkg, fn)
+	}
+	if err := o.WriteFile(fn, codegen.WithFormatCode(true)); err != nil {
+		if cfe, ok := err.(codegen.CodeFormatError); ok {
+			fmt.Fprint(os.Stderr, cfe.Source())
+		}
+		return errors.Wrapf(err, `failed to write to %s`, fn)
+	}
+	return nil
+}
diff --git a/jwt/jwt.go b/jwt/jwt.go
index 183d7e72..eace4dbe 100644
--- a/jwt/jwt.go
+++ b/jwt/jwt.go
@@ -133,7 +133,7 @@ func parseBytes(data []byte, options ...ParseOption) (Token, error) {
 		case identValidate{}:
 			ctx.validate = o.Value().(bool)
 		case identTypedClaim{}:
-			pair := o.Value().(typedClaimPair)
+			pair := o.Value().(claimPair)
 			if ctx.localReg == nil {
 				ctx.localReg = json.NewRegistry()
 			}
diff --git a/jwt/openid/builder_gen.go b/jwt/openid/builder_gen.go
new file mode 100644
index 00000000..3c08eaf3
--- /dev/null
+++ b/jwt/openid/builder_gen.go
@@ -0,0 +1,147 @@
+// This file is auto-generated by jwt/internal/cmd/gentoken/main.go. DO NOT EDIT
+
+package openid
+
+import (
+	"time"
+
+	"github.com/pkg/errors"
+)
+
+// Builder is a convenience wrapper around the New() constructor
+// and the Set() methods to assign values to Token claims.
+// Users can successively call Claim() on the Builder, and have it
+// construct the Token when Build() is called. This alleviates the
+// need for the user to check for the return value of every single
+// Set() method call.
+// Note that each call to Claim() overwrites the value set from the
+// previous call.
+type Builder struct {
+	claims []*ClaimPair
+}
+
+func NewBuilder() *Builder {
+	return &Builder{}
+}
+
+func (b *Builder) Claim(name string, value interface{}) *Builder {
+	b.claims = append(b.claims, &ClaimPair{Key: name, Value: value})
+	return b
+}
+
+func (b *Builder) Address(v *AddressClaim) *Builder {
+	return b.Claim(AddressKey, v)
+}
+
+func (b *Builder) Audience(v []string) *Builder {
+	return b.Claim(AudienceKey, v)
+}
+
+func (b *Builder) Birthdate(v *BirthdateClaim) *Builder {
+	return b.Claim(BirthdateKey, v)
+}
+
+func (b *Builder) Email(v string) *Builder {
+	return b.Claim(EmailKey, v)
+}
+
+func (b *Builder) EmailVerified(v bool) *Builder {
+	return b.Claim(EmailVerifiedKey, v)
+}
+
+func (b *Builder) Expiration(v time.Time) *Builder {
+	return b.Claim(ExpirationKey, v)
+}
+
+func (b *Builder) FamilyName(v string) *Builder {
+	return b.Claim(FamilyNameKey, v)
+}
+
+func (b *Builder) Gender(v string) *Builder {
+	return b.Claim(GenderKey, v)
+}
+
+func (b *Builder) GivenName(v string) *Builder {
+	return b.Claim(GivenNameKey, v)
+}
+
+func (b *Builder) IssuedAt(v time.Time) *Builder {
+	return b.Claim(IssuedAtKey, v)
+}
+
+func (b *Builder) Issuer(v string) *Builder {
+	return b.Claim(IssuerKey, v)
+}
+
+func (b *Builder) JwtID(v string) *Builder {
+	return b.Claim(JwtIDKey, v)
+}
+
+func (b *Builder) Locale(v string) *Builder {
+	return b.Claim(LocaleKey, v)
+}
+
+func (b *Builder) MiddleName(v string) *Builder {
+	return b.Claim(MiddleNameKey, v)
+}
+
+func (b *Builder) Name(v string) *Builder {
+	return b.Claim(NameKey, v)
+}
+
+func (b *Builder) Nickname(v string) *Builder {
+	return b.Claim(NicknameKey, v)
+}
+
+func (b *Builder) NotBefore(v time.Time) *Builder {
+	return b.Claim(NotBeforeKey, v)
+}
+
+func (b *Builder) PhoneNumber(v string) *Builder {
+	return b.Claim(PhoneNumberKey, v)
+}
+
+func (b *Builder) PhoneNumberVerified(v bool) *Builder {
+	return b.Claim(PhoneNumberVerifiedKey, v)
+}
+
+func (b *Builder) Picture(v string) *Builder {
+	return b.Claim(PictureKey, v)
+}
+
+func (b *Builder) PreferredUsername(v string) *Builder {
+	return b.Claim(PreferredUsernameKey, v)
+}
+
+func (b *Builder) Profile(v string) *Builder {
+	return b.Claim(ProfileKey, v)
+}
+
+func (b *Builder) Subject(v string) *Builder {
+	return b.Claim(SubjectKey, v)
+}
+
+func (b *Builder) UpdatedAt(v time.Time) *Builder {
+	return b.Claim(UpdatedAtKey, v)
+}
+
+func (b *Builder) Website(v string) *Builder {
+	return b.Claim(WebsiteKey, v)
+}
+
+func (b *Builder) Zoneinfo(v string) *Builder {
+	return b.Claim(ZoneinfoKey, v)
+}
+
+// Build creates a new token based on the claims that the builder has received
+// so far. If a claim cannot be set, then the method returns a nil Token with
+// a en error as a second return value
+func (b *Builder) Build() (Token, error) {
+	tok := New()
+	for _, claim := range b.claims {
+		if err := tok.Set(claim.Key.(string), claim.Value); err != nil {
+			return nil, errors.Wrapf(err, `failed to set claim %q`, claim.Key.(string))
+		}
+	}
+	return tok, nil
+}
diff --git a/jwt/options.go b/jwt/options.go
index 80d21807..e51c68eb 100644
--- a/jwt/options.go
+++ b/jwt/options.go
@@ -13,6 +13,17 @@ import (
 
 type Option = option.Interface
 
+type ConstructorOption interface {
+	Option
+	constructorOption()
+}
+
+type constructorOption struct {
+	Option
+}
+
+func (*constructorOption) constructorOption() {}
+
 // GlobalOption describes an Option that can be passed to `Settings()`.
 type GlobalOption interface {
 	Option
@@ -109,6 +120,7 @@ func newValidateOption(n interface{}, v interface{}) ValidateOption {
 func (*validateOption) validateOption() {}
 
 type identAcceptableSkew struct{}
+type identConstructorClaim struct{}
 type identClock struct{}
 type identContext struct{}
 type identDecrypt struct{}
@@ -266,7 +278,7 @@ func WithAudience(s string) ValidateOption {
 	return WithValidator(ClaimContainsString(AudienceKey, s))
 }
 
-// WithClaimValue specifies that expected any claim value.
+// WithClaimValue specifies the expected value for a given claim
 func WithClaimValue(name string, v interface{}) ValidateOption {
 	return WithValidator(ClaimValueIs(name, v))
 }
@@ -300,7 +312,7 @@ func WithFlattenAudience(v bool) GlobalOption {
 	return &globalOption{option.New(identFlattenAudience{}, v)}
 }
 
-type typedClaimPair struct {
+type claimPair struct {
 	Name  string
 	Value interface{}
 }
@@ -329,7 +341,7 @@ type typedClaimPair struct {
 // `openid.New()` will respect this option, if you provide your own custom
 // token type, it will need to implement the TokenWithDecodeCtx interface.
 func WithTypedClaim(name string, object interface{}) ParseOption {
-	return newParseOption(identTypedClaim{}, typedClaimPair{Name: name, Value: object})
+	return newParseOption(identTypedClaim{}, claimPair{Name: name, Value: object})
 }
 
 // WithRequiredClaim specifies that the claim identified the given name
@@ -478,3 +490,7 @@ func WithKeySetProvider(p KeySetProvider) ParseOption {
 func WithContext(ctx context.Context) ValidateOption {
 	return newValidateOption(identContext{}, ctx)
 }
+
+func WithClaim(name string, v interface{}) ConstructorOption {
+	return &constructorOption{option.New(identConstructorClaim{}, &claimPair{Name: name, Value: v})}
+}


Test patch:
diff --git a/jwt/jwt_test.go b/jwt/jwt_test.go
index 6b793fd6..54b6adef 100644
--- a/jwt/jwt_test.go
+++ b/jwt/jwt_test.go
@@ -472,13 +472,13 @@ func TestValidateClaims(t *testing.T) {
 	t.Run("Empty fields", func(t *testing.T) {
 		token := jwt.New()
 
-		if !assert.Error(t, jwt.Validate(token, jwt.WithIssuer("foo")), `token.Validate shold fail`) {
+		if !assert.Error(t, jwt.Validate(token, jwt.WithIssuer("foo")), `token.Validate should fail`) {
 			return
 		}
-		if !assert.Error(t, jwt.Validate(token, jwt.WithJwtID("foo")), `token.Validate shold fail`) {
+		if !assert.Error(t, jwt.Validate(token, jwt.WithJwtID("foo")), `token.Validate should fail`) {
 			return
 		}
-		if !assert.Error(t, jwt.Validate(token, jwt.WithSubject("foo")), `token.Validate shold fail`) {
+		if !assert.Error(t, jwt.Validate(token, jwt.WithSubject("foo")), `token.Validate should fail`) {
 			return
 		}
 	})
@@ -1010,8 +1010,12 @@ func TestGHIssue368(t *testing.T) {
 				}
 			})
 			t.Run("Multiple Keys", func(t *testing.T) {
-				tok := jwt.New()
-				_ = tok.Set(jwt.AudienceKey, []string{"hello", "world"})
+				tok, err := jwt.NewBuilder().
+					Audience([]string{"hello", "world"}).
+					Build()
+				if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+					return
+				}
 
 				buf, err := json.MarshalIndent(tok, "", "  ")
 				if !assert.NoError(t, err, `json.MarshalIndent should succeed`) {
@@ -1040,8 +1044,12 @@ func TestGH375(t *testing.T) {
 	}
 	key.Set(jwk.KeyIDKey, `test`)
 
-	token := jwt.New()
-	token.Set(jwt.IssuerKey, `foobar`)
+	token, err := jwt.NewBuilder().
+		Issuer(`foobar`).
+		Build()
+	if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+		return
+	}
 
 	signAlg := jwa.RS512
 	signed, err := jwt.Sign(token, signAlg, key)
@@ -1161,9 +1169,13 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("exp - iat < WithMaxDelta(10 secs)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set(jwt.IssuedAtKey, now)
-		tok.Set(jwt.ExpirationKey, now.Add(5*time.Second))
+		tok, err := jwt.NewBuilder().
+			IssuedAt(now).
+			Expiration(now.Add(5 * time.Second)).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.Error(t, jwt.Validate(tok, jwt.WithMaxDelta(2*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey)), `jwt.Validate should fail`) {
 			return
@@ -1175,9 +1187,13 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("iat - exp (5 secs) < WithMinDelta(10 secs)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set(jwt.ExpirationKey, now.Add(5*time.Second))
-		tok.Set(jwt.IssuedAtKey, now)
+		tok, err := jwt.NewBuilder().
+			IssuedAt(now).
+			Expiration(now.Add(5 * time.Second)).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.Error(t, jwt.Validate(tok, jwt.WithMinDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey)), `jwt.Validate should fail`) {
 			return
@@ -1185,9 +1201,13 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("iat - exp (5 secs) > WithMinDelta(10 secs)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set(jwt.ExpirationKey, now.Add(5*time.Second))
-		tok.Set(jwt.IssuedAtKey, now)
+		tok, err := jwt.NewBuilder().
+			IssuedAt(now).
+			Expiration(now.Add(5 * time.Second)).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.NoError(t, jwt.Validate(tok, jwt.WithMinDelta(10*time.Second, jwt.ExpirationKey, jwt.IssuedAtKey), jwt.WithAcceptableSkew(5*time.Second)), `jwt.Validate should succeed`) {
 			return
@@ -1195,8 +1215,12 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("now - iat < WithMaxDelta(10 secs)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set(jwt.IssuedAtKey, now)
+		tok, err := jwt.NewBuilder().
+			IssuedAt(now).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.NoError(t, jwt.Validate(tok, jwt.WithMaxDelta(10*time.Second, "", jwt.IssuedAtKey), jwt.WithClock(jwt.ClockFunc(func() time.Time { return now.Add(5 * time.Second) }))), `jwt.Validate should succeed`) {
 			return
@@ -1204,9 +1228,13 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("invalid claim name (c1)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set("foo", now)
-		tok.Set(jwt.ExpirationKey, now.Add(5*time.Second))
+		tok, err := jwt.NewBuilder().
+			Claim("foo", now).
+			Expiration(now.Add(5 * time.Second)).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.Error(t, jwt.Validate(tok, jwt.WithMinDelta(10*time.Second, jwt.ExpirationKey, "foo"), jwt.WithAcceptableSkew(5*time.Second)), `jwt.Validate should fail`) {
 			return
@@ -1214,9 +1242,13 @@ func TestGH393(t *testing.T) {
 	})
 	t.Run("invalid claim name (c2)", func(t *testing.T) {
 		now := time.Now()
-		tok := jwt.New()
-		tok.Set(jwt.IssuedAtKey, now)
-		tok.Set("foo", now.Add(5*time.Second))
+		tok, err := jwt.NewBuilder().
+			Claim("foo", now.Add(5*time.Second)).
+			IssuedAt(now).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 
 		if !assert.Error(t, jwt.Validate(tok, jwt.WithMinDelta(10*time.Second, "foo", jwt.IssuedAtKey), jwt.WithAcceptableSkew(5*time.Second)), `jwt.Validate should fail`) {
 			return
@@ -1233,8 +1265,12 @@ func TestGH393(t *testing.T) {
 		}
 	})
 	t.Run(`WithRequiredClaim succeeds for existing claim`, func(t *testing.T) {
-		tok := jwt.New()
-		tok.Set(`foo`, 1)
+		tok, err := jwt.NewBuilder().
+			Claim(`foo`, 1).
+			Build()
+		if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+			return
+		}
 		if !assert.NoError(t, jwt.Validate(tok, jwt.WithRequiredClaim("foo")), `jwt.Validate should fail`) {
 			return
 		}
@@ -1247,8 +1283,12 @@ func TestNested(t *testing.T) {
 		return
 	}
 
-	token := jwt.New()
-	token.Set(jwt.IssuerKey, `https://github.com/lestrrat-go/jwx`)
+	token, err := jwt.NewBuilder().
+		Issuer(`https://github.com/lestrrat-go/jwx`).
+		Build()
+	if !assert.NoError(t, err, `jwt.Builder should succeed`) {
+		return
+	}
 
 	serialized, err := jwt.NewSerializer().
 		Sign(jwa.RS256, key).
diff --git a/jwt/validate_test.go b/jwt/validate_test.go
index 55735dfd..3ca246a6 100644
--- a/jwt/validate_test.go
+++ b/jwt/validate_test.go
@@ -13,26 +13,70 @@ import (
 
 func TestGHIssue10(t *testing.T) {
 	t.Parallel()
-	t.Run(jwt.IssuerKey, func(t *testing.T) {
-		t.Parallel()
-		t1 := jwt.New()
-		t1.Set(jwt.IssuerKey, "github.com/lestrrat-go/jwx")
 
-		// This should succeed, because WithIssuer is not provided in the
-		// optional parameters
-		if !assert.NoError(t, jwt.Validate(t1), "t1.Validate should succeed") {
-			return
-		}
+	// Simple string claims
+	testcases := []struct {
+		ClaimName  string
+		ClaimValue string
+		OptionFunc func(string) jwt.ValidateOption
+		BuildFunc  func(v string) (jwt.Token, error)
+	}{
+		{
+			ClaimName:  jwt.IssuerKey,
+			ClaimValue: `github.com/lestrrat-go/jwx`,
+			OptionFunc: jwt.WithIssuer,
+			BuildFunc: func(v string) (jwt.Token, error) {
+				return jwt.NewBuilder().
+					Issuer(v).
+					Build()
+			},
+		},
+		{
+			ClaimName:  jwt.JwtIDKey,
+			ClaimValue: `my-sepcial-key`,
+			OptionFunc: jwt.WithJwtID,
+			BuildFunc: func(v string) (jwt.Token, error) {
+				return jwt.NewBuilder().
+					JwtID(v).
+					Build()
+			},
+		},
+		{
+			ClaimName:  jwt.SubjectKey,
+			ClaimValue: `very important subject`,
+			OptionFunc: jwt.WithSubject,
+			BuildFunc: func(v string) (jwt.Token, error) {
+				return jwt.NewBuilder().
+					Subject(v).
+					Build()
+			},
+		},
+	}
+	for _, tc := range testcases {
+		tc := tc
+		t.Run(tc.ClaimName, func(t *testing.T) {
+			t.Parallel()
+			t1, err := tc.BuildFunc(tc.ClaimValue)
+			if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+				return
+			}
 
-		// This should succeed, because WithIssuer is provided with same value
-		if !assert.NoError(t, jwt.Validate(t1, jwt.WithIssuer(t1.Issuer())), "t1.Validate should succeed") {
-			return
-		}
+			// This should succeed, because validation option (tc.OptionFunc)
+			// is not provided in the optional parameters
+			if !assert.NoError(t, jwt.Validate(t1), "t1.Validate should succeed") {
+				return
+			}
 
-		if !assert.Error(t, jwt.Validate(t1, jwt.WithIssuer("poop")), "t1.Validate should fail") {
-			return
-		}
-	})
+			// This should succeed, because the option is provided with same value
+			if !assert.NoError(t, jwt.Validate(t1, tc.OptionFunc(tc.ClaimValue)), "t1.Validate should succeed") {
+				return
+			}
+
+			if !assert.Error(t, jwt.Validate(t1, jwt.WithIssuer("poop")), "t1.Validate should fail") {
+				return
+			}
+		})
+	}
 	t.Run(jwt.IssuedAtKey, func(t *testing.T) {
 		t.Parallel()
 		t1 := jwt.New()
@@ -59,10 +103,11 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run(jwt.AudienceKey, func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-		err := t1.Set(jwt.AudienceKey, []string{"foo", "bar", "baz"})
-		if err != nil {
-			t.Fatalf("Failed to set audience claim: %s", err.Error())
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.AudienceKey, []string{"foo", "bar", "baz"}).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
 		}
 
 		// This should succeed, because WithAudience is not provided in the
@@ -99,8 +144,12 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run(jwt.SubjectKey, func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-		t1.Set(jwt.SubjectKey, "github.com/lestrrat-go/jwx")
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.SubjectKey, "github.com/lestrrat-go/jwx").
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		// This should succeed, because WithSubject is not provided in the
 		// optional parameters
@@ -119,11 +168,16 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run(jwt.NotBeforeKey, func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
 
 		// NotBefore is set to future date
 		tm := time.Now().Add(72 * time.Hour)
-		t1.Set(jwt.NotBeforeKey, tm)
+
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.NotBeforeKey, tm).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		testcases := []struct {
 			Name    string
@@ -182,14 +236,17 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run(jwt.ExpirationKey, func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
 
-		// issuedat = 1 Hr before current time
 		tm := time.Now()
-		t1.Set(jwt.IssuedAtKey, tm.Add(-1*time.Hour))
-
-		// valid for 2 minutes only from IssuedAt
-		t1.Set(jwt.ExpirationKey, tm.Add(-58*time.Minute))
+		t1, err := jwt.NewBuilder().
+			// issuedat = 1 Hr before current time
+			Claim(jwt.IssuedAtKey, tm.Add(-1*time.Hour)).
+			// valid for 2 minutes only from IssuedAt
+			Claim(jwt.ExpirationKey, tm.Add(-58*time.Minute)).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		// This should fail, because exp is set in the past
 		t.Run("exp set in the past", func(t *testing.T) {
@@ -231,13 +288,15 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run("Unix zero times", func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-
 		tm := time.Unix(0, 0)
-
-		t1.Set(jwt.NotBeforeKey, tm)
-		t1.Set(jwt.IssuedAtKey, tm)
-		t1.Set(jwt.ExpirationKey, tm)
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.NotBeforeKey, tm).
+			Claim(jwt.IssuedAtKey, tm).
+			Claim(jwt.ExpirationKey, tm).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		// This should pass because the unix zero times should be ignored
 		if assert.NoError(t, jwt.Validate(t1), "token.Validate should pass") {
@@ -246,13 +305,15 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run("Go zero times", func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-
 		tm := time.Time{}
-
-		t1.Set(jwt.NotBeforeKey, tm)
-		t1.Set(jwt.IssuedAtKey, tm)
-		t1.Set(jwt.ExpirationKey, tm)
+		t1, err := jwt.NewBuilder().
+			Claim(jwt.NotBeforeKey, tm).
+			Claim(jwt.IssuedAtKey, tm).
+			Claim(jwt.ExpirationKey, tm).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		// This should pass because the go zero times should be ignored
 		if assert.NoError(t, jwt.Validate(t1), "token.Validate should pass") {
@@ -261,14 +322,16 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run("Parse and validate", func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-
-		// issuedat = 1 Hr before current time
 		tm := time.Now()
-		t1.Set(jwt.IssuedAtKey, tm.Add(-1*time.Hour))
-
-		// valid for 2 minutes only from IssuedAt
-		t1.Set(jwt.ExpirationKey, tm.Add(-58*time.Minute))
+		t1, err := jwt.NewBuilder().
+			// issuedat = 1 Hr before current time
+			Claim(jwt.IssuedAtKey, tm.Add(-1*time.Hour)).
+			// valid for 2 minutes only from IssuedAt
+			Claim(jwt.ExpirationKey, tm.Add(-58*time.Minute)).
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		buf, err := json.Marshal(t1)
 		if !assert.NoError(t, err, `json.Marshal should succeed`) {
@@ -300,8 +363,12 @@ func TestGHIssue10(t *testing.T) {
 	})
 	t.Run("any claim value", func(t *testing.T) {
 		t.Parallel()
-		t1 := jwt.New()
-		t1.Set("email", "email@example.com")
+		t1, err := jwt.NewBuilder().
+			Claim("email", "email@example.com").
+			Build()
+		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
+			return
+		}
 
 		// This should succeed, because WithClaimValue("email", "xxx") is not provided in the
 		// optional parameters