@@ -425,3 +425,188 @@ func TestNewNodeJWTAuthenticator(t *testing.T) {
425425 assert .NotNil (t , authenticator .parser )
426426 assert .Equal (t , logger , authenticator .logger )
427427}
428+
429+ func TestNodeJWTAuthenticator_WithoutLeeway_StrictValidation (t * testing.T ) {
430+ t .Run ("token expired by 1 second without leeway should be rejected" , func (t * testing.T ) {
431+ // Given
432+ privateKey , csaPubKey := createValidatorTestKeys ()
433+ mockProvider := & mocks.NodeAuthProvider {}
434+ // No leeway option provided - strict validation
435+ authenticator := NewNodeJWTAuthenticator (mockProvider , createTestLogger ())
436+
437+ testRequest := testRequest {Field : "test-request" }
438+ digest := utils .CalculateRequestDigest (testRequest )
439+
440+ // Create a token that expired 1 second ago
441+ now := time .Now ()
442+ token := jwt .NewWithClaims (jwt .SigningMethodEdDSA , types.NodeJWTClaims {
443+ PublicKey : hex .EncodeToString (csaPubKey ),
444+ Digest : digest ,
445+ RegisteredClaims : jwt.RegisteredClaims {
446+ Issuer : hex .EncodeToString (csaPubKey ),
447+ Subject : hex .EncodeToString (csaPubKey ),
448+ ExpiresAt : jwt .NewNumericDate (now .Add (- 1 * time .Second )), // Expired 1s ago
449+ IssuedAt : jwt .NewNumericDate (now .Add (- 1 * time .Hour )),
450+ },
451+ })
452+
453+ jwtToken , err := token .SignedString (privateKey )
454+ require .NoError (t , err )
455+
456+ // When: Authenticate JWT without leeway
457+ valid , claims , err := authenticator .AuthenticateJWT (context .Background (), jwtToken , testRequest )
458+
459+ // Expect: Should fail as no leeway is configured
460+ require .Error (t , err )
461+ assert .False (t , valid )
462+ assert .NotNil (t , claims )
463+ assert .Contains (t , err .Error (), "token is expired" )
464+ })
465+
466+ t .Run ("token issued 1 second in future without leeway should be rejected" , func (t * testing.T ) {
467+ // Given
468+ privateKey , csaPubKey := createValidatorTestKeys ()
469+ mockProvider := & mocks.NodeAuthProvider {}
470+ // No leeway option provided - strict validation
471+ authenticator := NewNodeJWTAuthenticator (mockProvider , createTestLogger ())
472+
473+ testRequest := testRequest {Field : "test-request" }
474+ digest := utils .CalculateRequestDigest (testRequest )
475+
476+ // Create a token issued 1 second in the future
477+ now := time .Now ()
478+ token := jwt .NewWithClaims (jwt .SigningMethodEdDSA , types.NodeJWTClaims {
479+ PublicKey : hex .EncodeToString (csaPubKey ),
480+ Digest : digest ,
481+ RegisteredClaims : jwt.RegisteredClaims {
482+ Issuer : hex .EncodeToString (csaPubKey ),
483+ Subject : hex .EncodeToString (csaPubKey ),
484+ ExpiresAt : jwt .NewNumericDate (now .Add (workflowJWTExpiration )),
485+ IssuedAt : jwt .NewNumericDate (now .Add (1 * time .Second )), // Issued 1s in future
486+ },
487+ })
488+
489+ jwtToken , err := token .SignedString (privateKey )
490+ require .NoError (t , err )
491+
492+ // When: Authenticate JWT without leeway
493+ valid , claims , err := authenticator .AuthenticateJWT (context .Background (), jwtToken , testRequest )
494+
495+ // Expect: Should fail as no leeway is configured
496+ require .Error (t , err )
497+ assert .False (t , valid )
498+ assert .NotNil (t , claims )
499+ assert .Contains (t , err .Error (), "used before issued" )
500+ })
501+ }
502+
503+ func TestNodeJWTAuthenticator_WithLeeway_CustomDurations (t * testing.T ) {
504+ t .Run ("custom leeway of 10 seconds allows token expired 8 seconds ago" , func (t * testing.T ) {
505+ // Given
506+ privateKey , csaPubKey := createValidatorTestKeys ()
507+ mockProvider := & mocks.NodeAuthProvider {}
508+ mockProvider .On ("IsNodePubKeyTrusted" , mock .Anything , csaPubKey ).Return (true , nil )
509+ // Custom 10 second leeway
510+ authenticator := NewNodeJWTAuthenticator (mockProvider , createTestLogger (), WithLeeway (10 * time .Second ))
511+
512+ testRequest := testRequest {Field : "test-request" }
513+ digest := utils .CalculateRequestDigest (testRequest )
514+
515+ // Create a token that expired 8 seconds ago (within 10s leeway)
516+ now := time .Now ()
517+ token := jwt .NewWithClaims (jwt .SigningMethodEdDSA , types.NodeJWTClaims {
518+ PublicKey : hex .EncodeToString (csaPubKey ),
519+ Digest : digest ,
520+ RegisteredClaims : jwt.RegisteredClaims {
521+ Issuer : hex .EncodeToString (csaPubKey ),
522+ Subject : hex .EncodeToString (csaPubKey ),
523+ ExpiresAt : jwt .NewNumericDate (now .Add (- 8 * time .Second )), // Expired 8s ago
524+ IssuedAt : jwt .NewNumericDate (now .Add (- 1 * time .Hour )),
525+ },
526+ })
527+
528+ jwtToken , err := token .SignedString (privateKey )
529+ require .NoError (t , err )
530+
531+ // When: Authenticate JWT
532+ valid , claims , err := authenticator .AuthenticateJWT (context .Background (), jwtToken , testRequest )
533+
534+ // Expect: Should succeed with 10s leeway
535+ require .NoError (t , err )
536+ assert .True (t , valid )
537+ assert .NotNil (t , claims )
538+ mockProvider .AssertExpectations (t )
539+ })
540+
541+ t .Run ("custom leeway of 2 seconds rejects token expired 3 seconds ago" , func (t * testing.T ) {
542+ // Given
543+ privateKey , csaPubKey := createValidatorTestKeys ()
544+ mockProvider := & mocks.NodeAuthProvider {}
545+ // Small 2 second leeway
546+ authenticator := NewNodeJWTAuthenticator (mockProvider , createTestLogger (), WithLeeway (2 * time .Second ))
547+
548+ testRequest := testRequest {Field : "test-request" }
549+ digest := utils .CalculateRequestDigest (testRequest )
550+
551+ // Create a token that expired 3 seconds ago (beyond 2s leeway)
552+ now := time .Now ()
553+ token := jwt .NewWithClaims (jwt .SigningMethodEdDSA , types.NodeJWTClaims {
554+ PublicKey : hex .EncodeToString (csaPubKey ),
555+ Digest : digest ,
556+ RegisteredClaims : jwt.RegisteredClaims {
557+ Issuer : hex .EncodeToString (csaPubKey ),
558+ Subject : hex .EncodeToString (csaPubKey ),
559+ ExpiresAt : jwt .NewNumericDate (now .Add (- 3 * time .Second )), // Expired 3s ago
560+ IssuedAt : jwt .NewNumericDate (now .Add (- 1 * time .Hour )),
561+ },
562+ })
563+
564+ jwtToken , err := token .SignedString (privateKey )
565+ require .NoError (t , err )
566+
567+ // When: Authenticate JWT
568+ valid , claims , err := authenticator .AuthenticateJWT (context .Background (), jwtToken , testRequest )
569+
570+ // Expect: Should fail as it's beyond 2s leeway
571+ require .Error (t , err )
572+ assert .False (t , valid )
573+ assert .NotNil (t , claims )
574+ assert .Contains (t , err .Error (), "token is expired" )
575+ })
576+
577+ t .Run ("zero leeway behaves like no leeway option" , func (t * testing.T ) {
578+ // Given
579+ privateKey , csaPubKey := createValidatorTestKeys ()
580+ mockProvider := & mocks.NodeAuthProvider {}
581+ // Explicitly set zero leeway
582+ authenticator := NewNodeJWTAuthenticator (mockProvider , createTestLogger (), WithLeeway (0 * time .Second ))
583+
584+ testRequest := testRequest {Field : "test-request" }
585+ digest := utils .CalculateRequestDigest (testRequest )
586+
587+ // Create a token that expired 1 second ago
588+ now := time .Now ()
589+ token := jwt .NewWithClaims (jwt .SigningMethodEdDSA , types.NodeJWTClaims {
590+ PublicKey : hex .EncodeToString (csaPubKey ),
591+ Digest : digest ,
592+ RegisteredClaims : jwt.RegisteredClaims {
593+ Issuer : hex .EncodeToString (csaPubKey ),
594+ Subject : hex .EncodeToString (csaPubKey ),
595+ ExpiresAt : jwt .NewNumericDate (now .Add (- 1 * time .Second )), // Expired 1s ago
596+ IssuedAt : jwt .NewNumericDate (now .Add (- 1 * time .Hour )),
597+ },
598+ })
599+
600+ jwtToken , err := token .SignedString (privateKey )
601+ require .NoError (t , err )
602+
603+ // When: Authenticate JWT
604+ valid , claims , err := authenticator .AuthenticateJWT (context .Background (), jwtToken , testRequest )
605+
606+ // Expect: Should fail with zero leeway
607+ require .Error (t , err )
608+ assert .False (t , valid )
609+ assert .NotNil (t , claims )
610+ assert .Contains (t , err .Error (), "token is expired" )
611+ })
612+ }
0 commit comments