// Copyright 2022 Google LLC // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////////// package jwt_test import ( "testing" "time" "github.com/google/tink/go/jwt" ) type validationTestCase struct { tag string tokenOpts *jwt.RawJWTOptions validatorOpts *jwt.ValidatorOpts } func TestNewValidatorFailure(t *testing.T) { for _, tc := range []validationTestCase{ { tag: "combining ExpectedTypeHeader and IgnoreTypeHeader", validatorOpts: &jwt.ValidatorOpts{ ExpectedTypeHeader: refString("should fail"), IgnoreTypeHeader: true, }, }, { tag: "combining ExpectedIssuer and IgnoreIssuer", validatorOpts: &jwt.ValidatorOpts{ ExpectedIssuer: refString("should fail"), IgnoreIssuer: true, }, }, { tag: "combining ExpectedAudience and IgnoreAudiences", validatorOpts: &jwt.ValidatorOpts{ ExpectedAudience: refString("should fail"), IgnoreAudiences: true, }, }, { tag: "combining ExpectedAudiences and IgnoreAudiences", validatorOpts: &jwt.ValidatorOpts{ ExpectedAudiences: refString("should fail"), IgnoreAudiences: true, }, }, { tag: "both ExpectedAudience and ExpectedAudiences are set", validatorOpts: &jwt.ValidatorOpts{ ExpectedAudience: refString("aud"), ExpectedAudiences: refString("aud"), }, }, { tag: "invalid clock skew", validatorOpts: &jwt.ValidatorOpts{ ClockSkew: time.Minute * 11, }, }, { tag: "validator opts can't be nil", }, } { t.Run(tc.tag, func(t *testing.T) { if _, err := jwt.NewValidator(tc.validatorOpts); err == nil { t.Errorf("NewValidator(%v) err = nil, want error", tc.validatorOpts) } }) } } func TestValidationFailures(t *testing.T) { for _, tc := range []validationTestCase{ { tag: "expired token", tokenOpts: &jwt.RawJWTOptions{ ExpiresAt: refTime(100), }, validatorOpts: &jwt.ValidatorOpts{ FixedNow: time.Unix(500, 0), }, }, { tag: "no expiration and AllowMissingExpiration = false", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{}, }, { tag: "token expiry equals current time", tokenOpts: &jwt.RawJWTOptions{ ExpiresAt: refTime(123), }, validatorOpts: &jwt.ValidatorOpts{ FixedNow: time.Unix(123, 0), }, }, { tag: "not before in the future", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, NotBefore: refTime(1500), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(1000, 0), }, }, { tag: "issued in the future with ExpectIssuedInThePast = true", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, IssuedAt: refTime(5000), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(1000, 0), ExpectIssuedInThePast: true, }, }, { tag: "without issued at with ExpectIssuedInThePast = true", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectIssuedInThePast: true, }, }, { tag: "no type header and RequiresTypeHeader = true", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedTypeHeader: refString("typeHeader"), }, }, { tag: "invalid type header", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, TypeHeader: refString("typeHeader"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedTypeHeader: refString("different"), }, }, { tag: "type header in token but no type header in validator", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, TypeHeader: refString("typeHeader"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, }, }, { tag: "issuer required but not specified", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedIssuer: refString("tink-issuer"), }, }, { tag: "invalid issuer", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Issuer: refString("tink-issuer"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedIssuer: refString("different"), }, }, { tag: "issuer in token but not in validator", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Issuer: refString("issuer"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, }, }, { tag: "audience required but no specified", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudience: refString("tink-audience"), }, }, { tag: "audience required but no specified, deprecated", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudiences: refString("tink-audience"), }, }, { tag: "invalid audience", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("tink-audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudience: refString("audience"), }, }, { tag: "invalid audience, deprecated", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("tink-audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudiences: refString("audience"), }, }, { tag: "audience in token but not in validator", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, }, }, } { t.Run(tc.tag, func(t *testing.T) { token, err := jwt.NewRawJWT(tc.tokenOpts) if err != nil { t.Fatalf("jwt.NewRawJWT(%v) err = %v, want nil", tc.tokenOpts, err) } validator, err := jwt.NewValidator(tc.validatorOpts) if err != nil { t.Fatalf("jwt.NewValidator(%v) err = %v, want nil", tc.validatorOpts, err) } if err := validator.Validate(token); err == nil { t.Errorf("validator.Validate(%v) err = nil, want error", token) } }) } } func TestExpiredTokenValidationReturnsExpiredErr(t *testing.T) { tokenOpts := &jwt.RawJWTOptions{ ExpiresAt: refTime(100), } expiredToken, err := jwt.NewRawJWT(tokenOpts) if err != nil { t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err) } validatorOpts := &jwt.ValidatorOpts{ FixedNow: time.Unix(500, 0), } validator, err := jwt.NewValidator(validatorOpts) if err != nil { t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err) } validationErr := validator.Validate(expiredToken) if validationErr == nil { t.Errorf("validator.Validate(expiredToken) err = nil, want error") } if !jwt.IsExpirationErr(validationErr) { t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true") } } func TestExpirationGetsValidatedFirst(t *testing.T) { tokenOpts := &jwt.RawJWTOptions{ ExpiresAt: refTime(100), Audience: refString("invalidAudience"), } expiredTokenWithInvalidAudience, err := jwt.NewRawJWT(tokenOpts) if err != nil { t.Fatalf("jwt.NewRawJWT(tokenOpts) err = %v, want nil", err) } validatorOpts := &jwt.ValidatorOpts{ ExpectedAudiences: refString("audience"), FixedNow: time.Unix(500, 0), } validator, err := jwt.NewValidator(validatorOpts) if err != nil { t.Fatalf("jwt.NewValidator(validatorOpts) err = %v, want nil", err) } validationErr := validator.Validate(expiredTokenWithInvalidAudience) if validationErr == nil { t.Errorf("validator.Validate(expiredTokenWithInvalidAudience) err = nil, want error") } if !jwt.IsExpirationErr(validationErr) { t.Errorf("jwt.IsExpirationErr(validationErr) = false, want true") } } func TestValidationSuccess(t *testing.T) { for _, tc := range []validationTestCase{ { tag: "unexpired token", tokenOpts: &jwt.RawJWTOptions{ ExpiresAt: refTime(1000), }, validatorOpts: &jwt.ValidatorOpts{ FixedNow: time.Unix(100, 0), }, }, { tag: "expired with clock slew", tokenOpts: &jwt.RawJWTOptions{ ExpiresAt: refTime(400), }, validatorOpts: &jwt.ValidatorOpts{ FixedNow: time.Unix(500, 0), ClockSkew: time.Second * 200, }, }, { tag: "not before in the past", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, NotBefore: refTime(500), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(1000, 0), }, }, { tag: "not before equals now", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, NotBefore: refTime(500), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(500, 0), }, }, { tag: "not before in near future with clock skew", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, NotBefore: refTime(600), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(500, 0), ClockSkew: time.Second * 200, }, }, { tag: "issued in the past", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, IssuedAt: refTime(500), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(1000, 0), }, }, { tag: "issued in the future", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, IssuedAt: refTime(5000), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, FixedNow: time.Unix(1000, 0), }, }, { tag: "without issued at", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, }, }, { tag: "issued in the past with ExpectIssuedInThePast", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, IssuedAt: refTime(500), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectIssuedInThePast: true, FixedNow: time.Unix(1000, 0), }, }, { tag: "issued in the past with ExpectIssuedInThePast and clock skew", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, IssuedAt: refTime(1100), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectIssuedInThePast: true, FixedNow: time.Unix(1000, 0), ClockSkew: time.Second * 200, }, }, { tag: "expected type header", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, TypeHeader: refString("typeHeader"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedTypeHeader: refString("typeHeader"), }, }, { tag: "ignore type header", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, TypeHeader: refString("typeHeader"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, IgnoreTypeHeader: true, }, }, { tag: "expected issuer", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Issuer: refString("issuer"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedIssuer: refString("issuer"), }, }, { tag: "ignore issuer", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Issuer: refString("issuer"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, IgnoreIssuer: true, }, }, { tag: "expected audience", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudience: refString("audience"), }, }, { tag: "deprecated expected audiences", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, ExpectedAudiences: refString("audience"), }, }, { tag: "ignore audience", tokenOpts: &jwt.RawJWTOptions{ WithoutExpiration: true, Audience: refString("audience"), }, validatorOpts: &jwt.ValidatorOpts{ AllowMissingExpiration: true, IgnoreAudiences: true, }, }, } { t.Run(tc.tag, func(t *testing.T) { token, err := jwt.NewRawJWT(tc.tokenOpts) if err != nil { t.Fatalf("NewRawJWT(%v) err = %v, want nil", tc.tokenOpts, err) } validator, err := jwt.NewValidator(tc.validatorOpts) if err != nil { t.Fatalf("NewValidator(%v) err = %v, want nil", tc.validatorOpts, err) } if err := validator.Validate(token); err != nil { t.Errorf("validator.Validate(%v) err = %v, want nil", token, err) } }) } }