blob: 2bb4cb455f980b2df587e78c87c7b51ee9cb36ac [file] [log] [blame]
// 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")
}
}