// 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 import ( "strings" "testing" "time" "github.com/google/go-cmp/cmp" tpb "github.com/google/tink/go/proto/tink_go_proto" ) func TestKIDForNonTinkKeysIsNil(t *testing.T) { for _, op := range []tpb.OutputPrefixType{ tpb.OutputPrefixType_LEGACY, tpb.OutputPrefixType_RAW, tpb.OutputPrefixType_CRUNCHY} { if kid := keyID(1234, op); kid != nil { t.Errorf("keyID(1234, %q) = %q, want nil", op, *kid) } } } func TestKeyIDForTinkKey(t *testing.T) { want := "GsapRA" kid := keyID(0x1ac6a944, tpb.OutputPrefixType_TINK) if kid == nil { t.Errorf("KeyID(0x1ac6a944, %q) = nil, want %q", tpb.OutputPrefixType_TINK, want) } if kid != nil && !cmp.Equal(*kid, want) { t.Errorf("KeyID(0x1ac6a944, %q) = %q, want %q", tpb.OutputPrefixType_TINK, *kid, want) } } type payloadTestCase struct { tag string rawJWT *RawJWT opts *RawJWTOptions tinkKID *string customKID *string algorithm string } func refString(a string) *string { return &a } func refTime(ts int64) *time.Time { t := time.Unix(ts, 0) return &t } func TestBase64Encode(t *testing.T) { // Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 want := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" payload := []byte{123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125} got := base64Encode(payload) if got != want { t.Errorf("base64Encode() got %q want %q", got, want) } } func TestBase64Decode(t *testing.T) { // Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 want := []byte{123, 34, 105, 115, 115, 34, 58, 34, 106, 111, 101, 34, 44, 13, 10, 32, 34, 101, 120, 112, 34, 58, 49, 51, 48, 48, 56, 49, 57, 51, 56, 48, 44, 13, 10, 32, 34, 104, 116, 116, 112, 58, 47, 47, 101, 120, 97, 109, 112, 108, 101, 46, 99, 111, 109, 47, 105, 115, 95, 114, 111, 111, 116, 34, 58, 116, 114, 117, 101, 125} got, err := base64Decode("eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ") if err != nil { t.Errorf("base64Decode() err = %v, want nil", err) } if !cmp.Equal(got, want) { t.Errorf("base64Decode() got %q, want %q", got, want) } } func TestInvalidCharactersFailBase64Decode(t *testing.T) { if _, err := base64Decode("iLA0KIC&hD"); err == nil { t.Errorf("base64Decode() err = nil, want error") } } func TestEncodeStaticHeaderWithPayloadIssuerTokenForSigning(t *testing.T) { opts := &RawJWTOptions{ WithoutExpiration: true, Issuer: refString("tink-issuer"), } // Header 'RS256' alg from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1 // Payload: `{"iss":"tink-issuer"}` wantUnsigned := "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0aW5rLWlzc3VlciJ9" rawJWT, err := NewRawJWT(opts) if err != nil { t.Fatalf("generating valid RawJWT: %v", err) } unsigned, err := createUnsigned(rawJWT, "RS256", nil, nil) if err != nil { t.Errorf("createUnsigned() err = %v, want nil", err) } if unsigned != wantUnsigned { t.Errorf("got unsigned %q, want %q", unsigned, wantUnsigned) } } func TestEncodeHeaderWithHeaderFieldsAndEmptyPayload(t *testing.T) { type testCase struct { tag string opts *RawJWTOptions wantHeaderSubstring string customKID *string tinkKID *string } for _, tc := range []testCase{ { tag: "type header", opts: &RawJWTOptions{ WithoutExpiration: true, TypeHeader: refString("JWT"), }, wantHeaderSubstring: `"typ":"JWT"`, }, { tag: "custom kid", opts: &RawJWTOptions{ WithoutExpiration: true, }, customKID: refString("custom"), wantHeaderSubstring: `"kid":"custom"`, }, { tag: "tink kid", opts: &RawJWTOptions{ WithoutExpiration: true, }, tinkKID: refString("tink"), wantHeaderSubstring: `"kid":"tink"`, }, } { rawJWT, err := NewRawJWT(tc.opts) if err != nil { t.Fatalf("generating valid RawJWT: %v", err) } unsigned, err := createUnsigned(rawJWT, "RS256", tc.tinkKID, tc.customKID) if err != nil { t.Errorf("createUnsigned() err = %v, want nil", err) } token := strings.Split(unsigned, ".") if len(token) != 2 { t.Errorf("token[0] not encoded in compact serialization format") } header, err := base64Decode(token[0]) if err != nil { t.Errorf("base64Decode(token[0] = %q)", token[0]) } if !strings.Contains(string(header), tc.wantHeaderSubstring) { t.Errorf("header %q, doesn't contain: %q", string(header), tc.wantHeaderSubstring) } wantPayload := "e30" // `{}` if string(token[1]) != wantPayload { t.Errorf("token[1] = %q, want %q", token[1], wantPayload) } } } func TestCreateUnsignedWithNilRawJWTFails(t *testing.T) { if _, err := createUnsigned(nil, "HS256", nil, nil); err == nil { t.Errorf("createUnsigned(rawJWT = nil) err = nil, want error") } } func TestCreateUnsignedCustomAndTinkKIDFail(t *testing.T) { rawJWT, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true}) if err != nil { t.Fatalf("generating valid RawJWT: %v", err) } if _, err := createUnsigned(rawJWT, "HS256", refString("123"), refString("456")); err == nil { t.Errorf("createUnsigned(tinkKID = 456, customKID = 123) err = nil, want error") } } func TestCombineTokenAndSignature(t *testing.T) { // https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1 payload := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" signature := []byte{116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121} token := combineUnsignedAndSignature(payload, signature) want := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" if !cmp.Equal(token, want) { t.Errorf("combineUnsignedAndSignature(%q, %q) = %q, want %q", payload, signature, token, want) } } func TestSplitSignedCompactInvalidInputs(t *testing.T) { type testCases struct { tag string token string } for _, tc := range []testCases{ { tag: "empty payload", token: "", }, { tag: "not in compact serialization missing separators", token: "Zm9vYmFyIVRpbms", }, { tag: "not in compact serialization additional separators", token: "Zm9vYmFyIVRpbms.Zm9vYmFyGVRpbms.Zm9vYmFyIVRpbms.Zm9vYmFyINRpbms", }, { tag: "non web safe URL encoding character", token: "Zm9vYmFyIVRpbms.m9vYmFy.Zm&mFyIVRpbms", }, { tag: "no content", token: ".Zm9vYmFyIVRpbms", }, { tag: "no signature", token: "Zm9vYmFyIVRpbms.Zm9vYmFyIVRpbms.", }, { tag: "no signature and no content", token: "..", }, } { t.Run(tc.tag, func(t *testing.T) { if _, _, err := splitSignedCompact(tc.token); err == nil { t.Errorf("splitSignedCompact(%q) err = nil, want error", tc.token) } }) } } func TestSplitSignedCompact(t *testing.T) { // signed token from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 signedToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" wantSig := []byte{116, 24, 223, 180, 151, 153, 224, 37, 79, 250, 96, 125, 216, 173, 187, 186, 22, 212, 37, 77, 105, 214, 191, 240, 91, 88, 5, 88, 83, 132, 141, 121} wantToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" sig, token, err := splitSignedCompact(signedToken) if err != nil { t.Errorf("splitSignedCompact(%q) err = %v, want nil", signedToken, err) } if !cmp.Equal(sig, wantSig) { t.Errorf("splitSignedCompact() sig = %q, want %q", sig, wantSig) } if token != wantToken { t.Errorf("splitSignedCompact() token = %q, want %q", token, wantToken) } } func TestDecodeValidateInvalidHeaderFailures(t *testing.T) { type testCases struct { tag string header string alg string tinkKID *string customKID *string } for _, tc := range []testCases{ { tag: "invalid JSON header", header: `JiVeQCo`, }, { tag: "contains line feed", header: "eyJ0eXAiOiJKV1Qi\nLA0KICJhbGciOiJIUzI1NiJ9", alg: "HS256", }, { tag: "header contains no fields", header: base64Encode([]byte(`{}`)), }, { tag: "type header not a string", header: base64Encode([]byte(`{"alg":"HS256", "typ":5}`)), alg: "HS256", }, { tag: "wrong algorithm", header: base64Encode([]byte(`{"alg":"HS256"}`)), alg: "HS512", }, { tag: "specyfing custom and tink kid", header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), alg: "HS256", tinkKID: refString("tink"), customKID: refString("custom"), }, { tag: "invalid custom kid", header: base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)), customKID: refString("notCustom"), alg: "HS256", }, { tag: "invalid tink kid", header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), tinkKID: refString("notTink"), alg: "HS256", }, { tag: "specify tink kid and token without kig", header: base64Encode([]byte(`{"alg":"HS256"}`)), tinkKID: refString("notTink"), alg: "HS256", }, { tag: "crit header", header: base64Encode([]byte(`{"alg":"HS256", "crit":"fooBar"}`)), alg: "HS256", }, { tag: "no compact serialization", header: "asd.asd", }, { tag: "invalid UTF16 encoding", header: base64Encode([]byte(`{"alg":"HS256", "typ":"\uD834"}`)), }, } { t.Run(tc.tag, func(t *testing.T) { if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), tc.alg, tc.tinkKID, tc.customKID); err == nil { t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error") } }) } } func TestDecodeValidateKIDHeader(t *testing.T) { type testCases struct { tag string header string tinkKID *string customKID *string } for _, tc := range []testCases{ { tag: "not kid header field", header: base64Encode([]byte(`{"alg":"HS256"}`)), }, { tag: "validates custom kid", header: base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)), customKID: refString("custom"), }, { tag: "validates tink kid", header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), tinkKID: refString("tink"), }, { tag: "ignores kid if exists and tink kid isn't specified", header: base64Encode([]byte(`{"alg":"HS256", "kid":"random"}`)), }, { tag: "unkown headers are accepted", header: base64Encode([]byte(`{"alg":"HS256","unknown":"header"}`)), }, } { t.Run(tc.tag, func(t *testing.T) { _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), "HS256", tc.tinkKID, tc.customKID) if err != nil { t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err) } }) } } func TestDecodeVerifyTokenFixedValues(t *testing.T) { header := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" // Header example from https://tools.ietf.org/html/rfc7519#section-3.1 payload := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" // Payload example from https://tools.ietf.org/html/rfc7519#section-3.1 rawJWT, err := decodeUnsignedTokenAndValidateHeader(dotConcat(header, payload), "HS256", nil, nil) if err != nil { t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err) } iss, err := rawJWT.Issuer() if err != nil { t.Errorf("rawJWT.Issuer() err = %v, want nil", err) } if iss != "joe" { t.Errorf("rawJWT.Issuer() = %q, want joe", iss) } exp, err := rawJWT.ExpiresAt() if err != nil { t.Errorf("rawJWT.ExpiresAt() err = %v, want nil", err) } wantExp := time.Unix(1300819380, 0) if !exp.Equal(wantExp) { t.Errorf("rawJWT.ExpiresAt() = %q, want %q", exp, wantExp) } cc, err := rawJWT.BooleanClaim("http://example.com/is_root") if err != nil { t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') err = %v want nil", err) } if cc != true { t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') = %v, want true", cc) } } func TestDecodeVerifyTokenPaylodWithInvalidEndcoding(t *testing.T) { if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(base64Encode([]byte(`{"alg":"HS256"}`)), "_aSL&%"), "HS256", nil, nil); err == nil { t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error") } }