github.com/lestrrat-go/jwx/v2@v2.0.21/jwt/validate_test.go (about)

     1  package jwt_test
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"log"
     7  	"testing"
     8  	"time"
     9  
    10  	"github.com/lestrrat-go/jwx/v2/internal/json"
    11  	"github.com/lestrrat-go/jwx/v2/jwt"
    12  	"github.com/stretchr/testify/assert"
    13  	"github.com/stretchr/testify/require"
    14  )
    15  
    16  func TestGHIssue10(t *testing.T) {
    17  	t.Parallel()
    18  
    19  	// Simple string claims
    20  	testcases := []struct {
    21  		ClaimName  string
    22  		ClaimValue string
    23  		OptionFunc func(string) jwt.ValidateOption
    24  		BuildFunc  func(v string) (jwt.Token, error)
    25  	}{
    26  		{
    27  			ClaimName:  jwt.JwtIDKey,
    28  			ClaimValue: `my-sepcial-key`,
    29  			OptionFunc: jwt.WithJwtID,
    30  			BuildFunc: func(v string) (jwt.Token, error) {
    31  				return jwt.NewBuilder().
    32  					JwtID(v).
    33  					Build()
    34  			},
    35  		},
    36  		{
    37  			ClaimName:  jwt.SubjectKey,
    38  			ClaimValue: `very important subject`,
    39  			OptionFunc: jwt.WithSubject,
    40  			BuildFunc: func(v string) (jwt.Token, error) {
    41  				return jwt.NewBuilder().
    42  					Subject(v).
    43  					Build()
    44  			},
    45  		},
    46  	}
    47  	for _, tc := range testcases {
    48  		tc := tc
    49  		t.Run(tc.ClaimName, func(t *testing.T) {
    50  			t.Parallel()
    51  			t1, err := tc.BuildFunc(tc.ClaimValue)
    52  			if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
    53  				return
    54  			}
    55  
    56  			// This should succeed, because validation option (tc.OptionFunc)
    57  			// is not provided in the optional parameters
    58  			if !assert.NoError(t, jwt.Validate(t1), "t1.Validate should succeed") {
    59  				return
    60  			}
    61  
    62  			// This should succeed, because the option is provided with same value
    63  			if !assert.NoError(t, jwt.Validate(t1, tc.OptionFunc(tc.ClaimValue)), "t1.Validate should succeed") {
    64  				return
    65  			}
    66  
    67  			if !assert.Error(t, jwt.Validate(t1, jwt.WithIssuer("poop")), "t1.Validate should fail") {
    68  				return
    69  			}
    70  		})
    71  	}
    72  	t.Run(jwt.IssuerKey, func(t *testing.T) {
    73  		t.Parallel()
    74  		t1, err := jwt.NewBuilder().
    75  			Issuer("github.com/lestrrat-go/jwx/v2").
    76  			Build()
    77  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
    78  			return
    79  		}
    80  
    81  		// This should succeed, because WithIssuer is not provided in the
    82  		// optional parameters
    83  		if !assert.NoError(t, jwt.Validate(t1), "jwt.Validate should succeed") {
    84  			return
    85  		}
    86  
    87  		// This should succeed, because WithIssuer is provided with same value
    88  		if !assert.NoError(t, jwt.Validate(t1, jwt.WithIssuer(t1.Issuer())), "jwt.Validate should succeed") {
    89  			return
    90  		}
    91  
    92  		err = jwt.Validate(t1, jwt.WithIssuer("poop"))
    93  		if !assert.Error(t, err, "jwt.Validate should fail") {
    94  			return
    95  		}
    96  
    97  		if !assert.ErrorIs(t, err, jwt.ErrInvalidIssuer(), "error should be jwt.ErrInvalidIssuer") {
    98  			return
    99  		}
   100  
   101  		if !assert.True(t, jwt.IsValidationError(err), "error should be a validation error") {
   102  			return
   103  		}
   104  	})
   105  	t.Run(jwt.IssuedAtKey, func(t *testing.T) {
   106  		t.Parallel()
   107  		tm := time.Now()
   108  		t1, err := jwt.NewBuilder().
   109  			Claim(jwt.IssuedAtKey, tm).
   110  			Build()
   111  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   112  			return
   113  		}
   114  
   115  		testcases := []struct {
   116  			Name    string
   117  			Options []jwt.ValidateOption
   118  			Error   bool
   119  		}{
   120  			{
   121  				Name:  `clock is set to before iat`,
   122  				Error: true,
   123  				Options: []jwt.ValidateOption{
   124  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Hour) })),
   125  				},
   126  			},
   127  			{
   128  				// This works because the sub-second difference is rounded
   129  				Name: `clock is set to some sub-seconds before iat`,
   130  				Options: []jwt.ValidateOption{
   131  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
   132  				},
   133  			},
   134  			{
   135  				Name:  `clock is set to some sub-seconds before iat (trunc = 0)`,
   136  				Error: true,
   137  				Options: []jwt.ValidateOption{
   138  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
   139  					jwt.WithTruncation(0),
   140  				},
   141  			},
   142  		}
   143  
   144  		for _, tc := range testcases {
   145  			tc := tc
   146  			t.Run(tc.Name, func(t *testing.T) {
   147  				log.Printf("%s", tc.Name)
   148  				err := jwt.Validate(t1, tc.Options...)
   149  				if !tc.Error {
   150  					assert.NoError(t, err, `jwt.Validate should succeed`)
   151  					return
   152  				}
   153  
   154  				if !assert.Error(t, err, `jwt.Validate should fail`) {
   155  					return
   156  				}
   157  
   158  				if !assert.True(t, errors.Is(err, jwt.ErrInvalidIssuedAt()), `error should be jwt.ErrInvalidIssuedAt`) {
   159  					return
   160  				}
   161  
   162  				if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be not ErrNotYetValid`) {
   163  					return
   164  				}
   165  
   166  				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
   167  					return
   168  				}
   169  			})
   170  		}
   171  	})
   172  	t.Run(jwt.AudienceKey, func(t *testing.T) {
   173  		t.Parallel()
   174  		t1, err := jwt.NewBuilder().
   175  			Claim(jwt.AudienceKey, []string{"foo", "bar", "baz"}).
   176  			Build()
   177  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   178  			return
   179  		}
   180  
   181  		// This should succeed, because WithAudience is not provided in the
   182  		// optional parameters
   183  		t.Run("`aud` check disabled", func(t *testing.T) {
   184  			t.Parallel()
   185  			if !assert.NoError(t, jwt.Validate(t1), `jwt.Validate should succeed`) {
   186  				return
   187  			}
   188  		})
   189  
   190  		// This should succeed, because WithAudience is provided, and its
   191  		// value matches one of the audience values
   192  		t.Run("`aud` contains `baz`", func(t *testing.T) {
   193  			t.Parallel()
   194  			if !assert.NoError(t, jwt.Validate(t1, jwt.WithAudience("baz")), "jwt.Validate should succeed") {
   195  				return
   196  			}
   197  		})
   198  
   199  		t.Run("check `aud` contains `poop`", func(t *testing.T) {
   200  			t.Parallel()
   201  			err := jwt.Validate(t1, jwt.WithAudience("poop"))
   202  			if !assert.Error(t, err, "token.Validate should fail") {
   203  				return
   204  			}
   205  			if !assert.ErrorIs(t, err, jwt.ErrInvalidAudience(), `error should be ErrInvalidAudience`) {
   206  				return
   207  			}
   208  			if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
   209  				return
   210  			}
   211  		})
   212  	})
   213  	t.Run(jwt.SubjectKey, func(t *testing.T) {
   214  		t.Parallel()
   215  		t1, err := jwt.NewBuilder().
   216  			Claim(jwt.SubjectKey, "github.com/lestrrat-go/jwx/v2").
   217  			Build()
   218  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   219  			return
   220  		}
   221  
   222  		// This should succeed, because WithSubject is not provided in the
   223  		// optional parameters
   224  		if !assert.NoError(t, jwt.Validate(t1), "token.Validate should succeed") {
   225  			return
   226  		}
   227  
   228  		// This should succeed, because WithSubject is provided with same value
   229  		if !assert.NoError(t, jwt.Validate(t1, jwt.WithSubject(t1.Subject())), "token.Validate should succeed") {
   230  			return
   231  		}
   232  
   233  		if !assert.Error(t, jwt.Validate(t1, jwt.WithSubject("poop")), "token.Validate should fail") {
   234  			return
   235  		}
   236  	})
   237  	t.Run(jwt.NotBeforeKey, func(t *testing.T) {
   238  		t.Parallel()
   239  
   240  		// NotBefore is set to future date
   241  		tm := time.Now().Add(72 * time.Hour)
   242  
   243  		t1, err := jwt.NewBuilder().
   244  			Claim(jwt.NotBeforeKey, tm).
   245  			Build()
   246  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   247  			return
   248  		}
   249  
   250  		testcases := []struct {
   251  			Name    string
   252  			Options []jwt.ValidateOption
   253  			Error   bool
   254  		}{
   255  			{ // This should fail, because nbf is the future
   256  				Name:  `'nbf' is less than current time`,
   257  				Error: true,
   258  			},
   259  			{ // This should succeed, because we have given reaaaaaaly big skew
   260  				Name: `skew is large enough`,
   261  				Options: []jwt.ValidateOption{
   262  					jwt.WithAcceptableSkew(73 * time.Hour),
   263  				},
   264  			},
   265  			{ // This should succeed, because we have given a time
   266  				// that is well enough into the future
   267  				Name: `clock is set to some time after in nbf`,
   268  				Options: []jwt.ValidateOption{
   269  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Hour) })),
   270  				},
   271  			},
   272  			{ // This should succeed, the time == NotBefore time
   273  				// Note, this could fail if you are returning a monotonic clock
   274  				// and we didn't do something about it
   275  				Name: `clock is set to the same time as nbf`,
   276  				Options: []jwt.ValidateOption{
   277  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm })),
   278  				},
   279  			},
   280  			{
   281  				Name:  `clock is set to some subseconds before nbf`,
   282  				Error: true,
   283  				Options: []jwt.ValidateOption{
   284  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
   285  					jwt.WithTruncation(0),
   286  				},
   287  			},
   288  			{
   289  				Name: `clock is set to some subseconds before nbf (but truncation = default)`,
   290  				Options: []jwt.ValidateOption{
   291  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
   292  				},
   293  			},
   294  			{
   295  				Name: `clock is set to some subseconds after nbf`,
   296  				Options: []jwt.ValidateOption{
   297  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
   298  					jwt.WithTruncation(0),
   299  				},
   300  			},
   301  		}
   302  		for _, tc := range testcases {
   303  			tc := tc
   304  			t.Run(tc.Name, func(t *testing.T) {
   305  				err := jwt.Validate(t1, tc.Options...)
   306  				if !tc.Error {
   307  					assert.NoError(t, err, "token.Validate should succeed")
   308  					return
   309  				}
   310  
   311  				if !assert.Error(t, err, "token.Validate should fail") {
   312  					return
   313  				}
   314  				if !assert.True(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should be ErrTokenNotYetValid`) {
   315  					return
   316  				}
   317  				if !assert.False(t, errors.Is(err, jwt.ErrTokenExpired()), `error should not be ErrTokenExpierd`) {
   318  					return
   319  				}
   320  				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
   321  					return
   322  				}
   323  			})
   324  		}
   325  	})
   326  	t.Run(jwt.ExpirationKey, func(t *testing.T) {
   327  		t.Parallel()
   328  
   329  		tm := time.Now()
   330  		t1, err := jwt.NewBuilder().
   331  			// issuedat = 1 Hr before current time
   332  			Claim(jwt.IssuedAtKey, tm.Add(-1*time.Hour)).
   333  			// valid for 2 minutes only from IssuedAt
   334  			Claim(jwt.ExpirationKey, tm).
   335  			Build()
   336  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   337  			return
   338  		}
   339  
   340  		testcases := []struct {
   341  			Name    string
   342  			Options []jwt.ValidateOption
   343  			Error   bool
   344  		}{
   345  			{
   346  				Name:  `clock is not modified (exp < now)`,
   347  				Error: true,
   348  			},
   349  			{
   350  				Name: `clock is set to some time before exp`,
   351  				Options: []jwt.ValidateOption{
   352  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Hour) })),
   353  				},
   354  			},
   355  			{ // This should fail, the time == Expiration.
   356  				// Note, this could fail if you are returning a monotonic clock
   357  				// and we didn't do something about it
   358  				Name:  `clock is set to same time as exp`,
   359  				Error: true,
   360  				Options: []jwt.ValidateOption{
   361  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm })),
   362  				},
   363  			},
   364  			{
   365  				Name:  `clock is set to some subseconds after exp`,
   366  				Error: true,
   367  				Options: []jwt.ValidateOption{
   368  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
   369  					jwt.WithTruncation(0),
   370  				},
   371  			},
   372  			{
   373  				Name:  `clock is set to some subseconds after exp (but truncation = default)`,
   374  				Error: true,
   375  				Options: []jwt.ValidateOption{
   376  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(time.Millisecond) })),
   377  				},
   378  			},
   379  			{
   380  				Name: `clock is set to some subseconds before exp`,
   381  				Options: []jwt.ValidateOption{
   382  					jwt.WithClock(jwt.ClockFunc(func() time.Time { return tm.Add(-1 * time.Millisecond) })),
   383  					jwt.WithTruncation(0),
   384  				},
   385  			},
   386  		}
   387  
   388  		for _, tc := range testcases {
   389  			tc := tc
   390  			t.Run(tc.Name, func(t *testing.T) {
   391  				err := jwt.Validate(t1, tc.Options...)
   392  				if !tc.Error {
   393  					assert.NoError(t, err, `jwt.Validate should succeed`)
   394  					return
   395  				}
   396  
   397  				require.Error(t, err, `jwt.Validate should fail`)
   398  				if !assert.False(t, errors.Is(err, jwt.ErrTokenNotYetValid()), `error should not be ErrTokenNotYetValid`) {
   399  					return
   400  				}
   401  				if !assert.True(t, errors.Is(err, jwt.ErrTokenExpired()), `error should be ErrTokenExpierd`) {
   402  					return
   403  				}
   404  				if !assert.True(t, jwt.IsValidationError(err), `error should be a validation error`) {
   405  					return
   406  				}
   407  			})
   408  		}
   409  	})
   410  	t.Run("Unix zero times", func(t *testing.T) {
   411  		t.Parallel()
   412  		tm := time.Unix(0, 0)
   413  		t1, err := jwt.NewBuilder().
   414  			Claim(jwt.NotBeforeKey, tm).
   415  			Claim(jwt.IssuedAtKey, tm).
   416  			Claim(jwt.ExpirationKey, tm).
   417  			Build()
   418  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   419  			return
   420  		}
   421  
   422  		// This should pass because the unix zero times should be ignored
   423  		if assert.NoError(t, jwt.Validate(t1), "token.Validate should pass") {
   424  			return
   425  		}
   426  	})
   427  	t.Run("Go zero times", func(t *testing.T) {
   428  		t.Parallel()
   429  		tm := time.Time{}
   430  		t1, err := jwt.NewBuilder().
   431  			Claim(jwt.NotBeforeKey, tm).
   432  			Claim(jwt.IssuedAtKey, tm).
   433  			Claim(jwt.ExpirationKey, tm).
   434  			Build()
   435  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   436  			return
   437  		}
   438  
   439  		// This should pass because the go zero times should be ignored
   440  		if assert.NoError(t, jwt.Validate(t1), "token.Validate should pass") {
   441  			return
   442  		}
   443  	})
   444  	t.Run("Parse and validate", func(t *testing.T) {
   445  		t.Parallel()
   446  		tm := time.Now()
   447  		t1, err := jwt.NewBuilder().
   448  			// issuedat = 1 Hr before current time
   449  			Claim(jwt.IssuedAtKey, tm.Add(-1*time.Hour)).
   450  			// valid for 2 minutes only from IssuedAt
   451  			Claim(jwt.ExpirationKey, tm.Add(-58*time.Minute)).
   452  			Build()
   453  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   454  			return
   455  		}
   456  
   457  		buf, err := json.Marshal(t1)
   458  		if !assert.NoError(t, err, `json.Marshal should succeed`) {
   459  			return
   460  		}
   461  
   462  		_, err = jwt.Parse(buf, jwt.WithVerify(false), jwt.WithValidate(true))
   463  		// This should fail, because exp is set in the past
   464  		if !assert.Error(t, err, "jwt.Parse should fail") {
   465  			return
   466  		}
   467  
   468  		_, err = jwt.Parse(buf, jwt.WithVerify(false), jwt.WithValidate(true), jwt.WithAcceptableSkew(time.Hour))
   469  		// This should succeed, because we have given big skew
   470  		// that is well enough to get us accepted
   471  		if !assert.NoError(t, err, "jwt.Parse should succeed (1)") {
   472  			return
   473  		}
   474  
   475  		// This should succeed, because we have given a time
   476  		// that is well enough into the past
   477  		clock := jwt.ClockFunc(func() time.Time {
   478  			return tm.Add(-59 * time.Minute)
   479  		})
   480  		_, err = jwt.Parse(buf, jwt.WithVerify(false), jwt.WithValidate(true), jwt.WithClock(clock))
   481  		if !assert.NoError(t, err, "jwt.Parse should succeed (2)") {
   482  			return
   483  		}
   484  	})
   485  	t.Run("any claim value", func(t *testing.T) {
   486  		t.Parallel()
   487  		t1, err := jwt.NewBuilder().
   488  			Claim("email", "email@example.com").
   489  			Build()
   490  		if !assert.NoError(t, err, `jwt.NewBuilder should succeed`) {
   491  			return
   492  		}
   493  
   494  		// This should succeed, because WithClaimValue("email", "xxx") is not provided in the
   495  		// optional parameters
   496  		if !assert.NoError(t, jwt.Validate(t1), "t1.Validate should succeed") {
   497  			return
   498  		}
   499  
   500  		// This should succeed, because WithClaimValue is provided with same value
   501  		if !assert.NoError(t, jwt.Validate(t1, jwt.WithClaimValue("email", "email@example.com")), "t1.Validate should succeed") {
   502  			return
   503  		}
   504  
   505  		if !assert.Error(t, jwt.Validate(t1, jwt.WithClaimValue("email", "poop")), "t1.Validate should fail") {
   506  			return
   507  		}
   508  		if !assert.Error(t, jwt.Validate(t1, jwt.WithClaimValue("xxxx", "email@example.com")), "t1.Validate should fail") {
   509  			return
   510  		}
   511  		if !assert.Error(t, jwt.Validate(t1, jwt.WithClaimValue("xxxx", "")), "t1.Validate should fail") {
   512  			return
   513  		}
   514  	})
   515  }
   516  
   517  func TestClaimValidator(t *testing.T) {
   518  	t.Parallel()
   519  	const myClaim = "my-claim"
   520  	err0 := errors.New(myClaim + " does not exist")
   521  	v := jwt.ValidatorFunc(func(_ context.Context, tok jwt.Token) jwt.ValidationError {
   522  		_, ok := tok.Get(myClaim)
   523  		if !ok {
   524  			return jwt.NewValidationError(err0)
   525  		}
   526  		return nil
   527  	})
   528  
   529  	testcases := []struct {
   530  		Name      string
   531  		MakeToken func() jwt.Token
   532  		Error     error
   533  	}{
   534  		{
   535  			Name: "Successful validation",
   536  			MakeToken: func() jwt.Token {
   537  				t1 := jwt.New()
   538  				_ = t1.Set(myClaim, map[string]interface{}{"k": "v"})
   539  				return t1
   540  			},
   541  		},
   542  		{
   543  			Name: "Target claim does not exist",
   544  			MakeToken: func() jwt.Token {
   545  				t1 := jwt.New()
   546  				_ = t1.Set("other-claim", map[string]interface{}{"k": "v"})
   547  				return t1
   548  			},
   549  			Error: err0,
   550  		},
   551  	}
   552  	for _, tc := range testcases {
   553  		tc := tc
   554  		t.Run(tc.Name, func(t *testing.T) {
   555  			t.Parallel()
   556  			t1 := tc.MakeToken()
   557  			if err := tc.Error; err != nil {
   558  				if !assert.ErrorIs(t, jwt.Validate(t1, jwt.WithValidator(v)), err) {
   559  					return
   560  				}
   561  				return
   562  			}
   563  
   564  			if !assert.NoError(t, jwt.Validate(t1, jwt.WithValidator(v))) {
   565  				return
   566  			}
   567  		})
   568  	}
   569  }