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 }