1// Copyright 2022 Google LLC 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14// 15//////////////////////////////////////////////////////////////////////////////// 16 17package jwt 18 19import ( 20 "strings" 21 "testing" 22 "time" 23 24 "github.com/google/go-cmp/cmp" 25 26 tpb "github.com/google/tink/go/proto/tink_go_proto" 27) 28 29func TestKIDForNonTinkKeysIsNil(t *testing.T) { 30 for _, op := range []tpb.OutputPrefixType{ 31 tpb.OutputPrefixType_LEGACY, 32 tpb.OutputPrefixType_RAW, 33 tpb.OutputPrefixType_CRUNCHY} { 34 if kid := keyID(1234, op); kid != nil { 35 t.Errorf("keyID(1234, %q) = %q, want nil", op, *kid) 36 } 37 } 38} 39 40func TestKeyIDForTinkKey(t *testing.T) { 41 want := "GsapRA" 42 kid := keyID(0x1ac6a944, tpb.OutputPrefixType_TINK) 43 if kid == nil { 44 t.Errorf("KeyID(0x1ac6a944, %q) = nil, want %q", tpb.OutputPrefixType_TINK, want) 45 } 46 if kid != nil && !cmp.Equal(*kid, want) { 47 t.Errorf("KeyID(0x1ac6a944, %q) = %q, want %q", tpb.OutputPrefixType_TINK, *kid, want) 48 } 49} 50 51type payloadTestCase struct { 52 tag string 53 rawJWT *RawJWT 54 opts *RawJWTOptions 55 tinkKID *string 56 customKID *string 57 algorithm string 58} 59 60func refString(a string) *string { 61 return &a 62} 63 64func refTime(ts int64) *time.Time { 65 t := time.Unix(ts, 0) 66 return &t 67} 68 69func TestBase64Encode(t *testing.T) { 70 // Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 71 want := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 72 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, 73 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} 74 got := base64Encode(payload) 75 if got != want { 76 t.Errorf("base64Encode() got %q want %q", got, want) 77 } 78} 79 80func TestBase64Decode(t *testing.T) { 81 // Examples from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 82 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, 83 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} 84 got, err := base64Decode("eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ") 85 if err != nil { 86 t.Errorf("base64Decode() err = %v, want nil", err) 87 } 88 if !cmp.Equal(got, want) { 89 t.Errorf("base64Decode() got %q, want %q", got, want) 90 } 91} 92 93func TestInvalidCharactersFailBase64Decode(t *testing.T) { 94 if _, err := base64Decode("iLA0KIC&hD"); err == nil { 95 t.Errorf("base64Decode() err = nil, want error") 96 } 97} 98 99func TestEncodeStaticHeaderWithPayloadIssuerTokenForSigning(t *testing.T) { 100 opts := &RawJWTOptions{ 101 WithoutExpiration: true, 102 Issuer: refString("tink-issuer"), 103 } 104 // Header 'RS256' alg from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1 105 // Payload: `{"iss":"tink-issuer"}` 106 wantUnsigned := "eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJ0aW5rLWlzc3VlciJ9" 107 rawJWT, err := NewRawJWT(opts) 108 if err != nil { 109 t.Fatalf("generating valid RawJWT: %v", err) 110 } 111 unsigned, err := createUnsigned(rawJWT, "RS256", nil, nil) 112 if err != nil { 113 t.Errorf("createUnsigned() err = %v, want nil", err) 114 } 115 116 if unsigned != wantUnsigned { 117 t.Errorf("got unsigned %q, want %q", unsigned, wantUnsigned) 118 } 119} 120 121func TestEncodeHeaderWithHeaderFieldsAndEmptyPayload(t *testing.T) { 122 type testCase struct { 123 tag string 124 opts *RawJWTOptions 125 wantHeaderSubstring string 126 customKID *string 127 tinkKID *string 128 } 129 for _, tc := range []testCase{ 130 { 131 tag: "type header", 132 opts: &RawJWTOptions{ 133 WithoutExpiration: true, 134 TypeHeader: refString("JWT"), 135 }, 136 wantHeaderSubstring: `"typ":"JWT"`, 137 }, 138 { 139 tag: "custom kid", 140 opts: &RawJWTOptions{ 141 WithoutExpiration: true, 142 }, 143 customKID: refString("custom"), 144 wantHeaderSubstring: `"kid":"custom"`, 145 }, 146 { 147 tag: "tink kid", 148 opts: &RawJWTOptions{ 149 WithoutExpiration: true, 150 }, 151 tinkKID: refString("tink"), 152 wantHeaderSubstring: `"kid":"tink"`, 153 }, 154 } { 155 rawJWT, err := NewRawJWT(tc.opts) 156 if err != nil { 157 t.Fatalf("generating valid RawJWT: %v", err) 158 } 159 unsigned, err := createUnsigned(rawJWT, "RS256", tc.tinkKID, tc.customKID) 160 if err != nil { 161 t.Errorf("createUnsigned() err = %v, want nil", err) 162 } 163 token := strings.Split(unsigned, ".") 164 if len(token) != 2 { 165 t.Errorf("token[0] not encoded in compact serialization format") 166 } 167 header, err := base64Decode(token[0]) 168 if err != nil { 169 t.Errorf("base64Decode(token[0] = %q)", token[0]) 170 } 171 if !strings.Contains(string(header), tc.wantHeaderSubstring) { 172 t.Errorf("header %q, doesn't contain: %q", string(header), tc.wantHeaderSubstring) 173 } 174 wantPayload := "e30" // `{}` 175 if string(token[1]) != wantPayload { 176 t.Errorf("token[1] = %q, want %q", token[1], wantPayload) 177 } 178 } 179} 180 181func TestCreateUnsignedWithNilRawJWTFails(t *testing.T) { 182 if _, err := createUnsigned(nil, "HS256", nil, nil); err == nil { 183 t.Errorf("createUnsigned(rawJWT = nil) err = nil, want error") 184 } 185} 186 187func TestCreateUnsignedCustomAndTinkKIDFail(t *testing.T) { 188 rawJWT, err := NewRawJWT(&RawJWTOptions{WithoutExpiration: true}) 189 if err != nil { 190 t.Fatalf("generating valid RawJWT: %v", err) 191 } 192 if _, err := createUnsigned(rawJWT, "HS256", refString("123"), refString("456")); err == nil { 193 t.Errorf("createUnsigned(tinkKID = 456, customKID = 123) err = nil, want error") 194 } 195} 196 197func TestCombineTokenAndSignature(t *testing.T) { 198 // https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.2.1 199 payload := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 200 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} 201 token := combineUnsignedAndSignature(payload, signature) 202 want := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 203 if !cmp.Equal(token, want) { 204 t.Errorf("combineUnsignedAndSignature(%q, %q) = %q, want %q", payload, signature, token, want) 205 } 206} 207 208func TestSplitSignedCompactInvalidInputs(t *testing.T) { 209 type testCases struct { 210 tag string 211 token string 212 } 213 for _, tc := range []testCases{ 214 { 215 tag: "empty payload", 216 token: "", 217 }, 218 { 219 tag: "not in compact serialization missing separators", 220 token: "Zm9vYmFyIVRpbms", 221 }, 222 { 223 tag: "not in compact serialization additional separators", 224 token: "Zm9vYmFyIVRpbms.Zm9vYmFyGVRpbms.Zm9vYmFyIVRpbms.Zm9vYmFyINRpbms", 225 }, 226 { 227 tag: "non web safe URL encoding character", 228 token: "Zm9vYmFyIVRpbms.m9vYmFy.Zm&mFyIVRpbms", 229 }, 230 { 231 tag: "no content", 232 token: ".Zm9vYmFyIVRpbms", 233 }, 234 { 235 tag: "no signature", 236 token: "Zm9vYmFyIVRpbms.Zm9vYmFyIVRpbms.", 237 }, 238 { 239 tag: "no signature and no content", 240 token: "..", 241 }, 242 } { 243 t.Run(tc.tag, func(t *testing.T) { 244 if _, _, err := splitSignedCompact(tc.token); err == nil { 245 t.Errorf("splitSignedCompact(%q) err = nil, want error", tc.token) 246 } 247 }) 248 } 249} 250 251func TestSplitSignedCompact(t *testing.T) { 252 // signed token from: https://datatracker.ietf.org/doc/html/rfc7515#appendix-A.1.1 253 signedToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk" 254 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} 255 wantToken := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" 256 sig, token, err := splitSignedCompact(signedToken) 257 if err != nil { 258 t.Errorf("splitSignedCompact(%q) err = %v, want nil", signedToken, err) 259 } 260 if !cmp.Equal(sig, wantSig) { 261 t.Errorf("splitSignedCompact() sig = %q, want %q", sig, wantSig) 262 } 263 if token != wantToken { 264 t.Errorf("splitSignedCompact() token = %q, want %q", token, wantToken) 265 } 266} 267 268func TestDecodeValidateInvalidHeaderFailures(t *testing.T) { 269 type testCases struct { 270 tag string 271 header string 272 alg string 273 tinkKID *string 274 customKID *string 275 } 276 for _, tc := range []testCases{ 277 { 278 tag: "invalid JSON header", 279 header: `JiVeQCo`, 280 }, 281 { 282 tag: "contains line feed", 283 header: "eyJ0eXAiOiJKV1Qi\nLA0KICJhbGciOiJIUzI1NiJ9", 284 alg: "HS256", 285 }, 286 { 287 tag: "header contains no fields", 288 header: base64Encode([]byte(`{}`)), 289 }, 290 { 291 tag: "type header not a string", 292 header: base64Encode([]byte(`{"alg":"HS256", "typ":5}`)), 293 alg: "HS256", 294 }, 295 { 296 tag: "wrong algorithm", 297 header: base64Encode([]byte(`{"alg":"HS256"}`)), 298 alg: "HS512", 299 }, 300 { 301 tag: "specyfing custom and tink kid", 302 header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), 303 alg: "HS256", 304 tinkKID: refString("tink"), 305 customKID: refString("custom"), 306 }, 307 { 308 tag: "invalid custom kid", 309 header: base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)), 310 customKID: refString("notCustom"), 311 alg: "HS256", 312 }, 313 { 314 tag: "invalid tink kid", 315 header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), 316 tinkKID: refString("notTink"), 317 alg: "HS256", 318 }, 319 { 320 tag: "specify tink kid and token without kig", 321 header: base64Encode([]byte(`{"alg":"HS256"}`)), 322 tinkKID: refString("notTink"), 323 alg: "HS256", 324 }, 325 { 326 tag: "crit header", 327 header: base64Encode([]byte(`{"alg":"HS256", "crit":"fooBar"}`)), 328 alg: "HS256", 329 }, 330 { 331 tag: "no compact serialization", 332 header: "asd.asd", 333 }, 334 { 335 tag: "invalid UTF16 encoding", 336 header: base64Encode([]byte(`{"alg":"HS256", "typ":"\uD834"}`)), 337 }, 338 } { 339 t.Run(tc.tag, func(t *testing.T) { 340 if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), tc.alg, tc.tinkKID, tc.customKID); err == nil { 341 t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error") 342 } 343 }) 344 } 345} 346 347func TestDecodeValidateKIDHeader(t *testing.T) { 348 type testCases struct { 349 tag string 350 header string 351 tinkKID *string 352 customKID *string 353 } 354 for _, tc := range []testCases{ 355 { 356 tag: "not kid header field", 357 header: base64Encode([]byte(`{"alg":"HS256"}`)), 358 }, 359 { 360 tag: "validates custom kid", 361 header: base64Encode([]byte(`{"alg":"HS256", "kid":"custom"}`)), 362 customKID: refString("custom"), 363 }, 364 { 365 tag: "validates tink kid", 366 header: base64Encode([]byte(`{"alg":"HS256", "kid":"tink"}`)), 367 tinkKID: refString("tink"), 368 }, 369 { 370 tag: "ignores kid if exists and tink kid isn't specified", 371 header: base64Encode([]byte(`{"alg":"HS256", "kid":"random"}`)), 372 }, 373 { 374 tag: "unkown headers are accepted", 375 header: base64Encode([]byte(`{"alg":"HS256","unknown":"header"}`)), 376 }, 377 } { 378 t.Run(tc.tag, func(t *testing.T) { 379 _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(tc.header, base64Encode([]byte("{}"))), "HS256", tc.tinkKID, tc.customKID) 380 if err != nil { 381 t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err) 382 } 383 }) 384 } 385} 386 387func TestDecodeVerifyTokenFixedValues(t *testing.T) { 388 header := "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9" // Header example from https://tools.ietf.org/html/rfc7519#section-3.1 389 payload := "eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ" // Payload example from https://tools.ietf.org/html/rfc7519#section-3.1 390 rawJWT, err := decodeUnsignedTokenAndValidateHeader(dotConcat(header, payload), "HS256", nil, nil) 391 if err != nil { 392 t.Errorf("decodeUnsignedTokenAndValidateHeader() err = %v, want nil", err) 393 } 394 iss, err := rawJWT.Issuer() 395 if err != nil { 396 t.Errorf("rawJWT.Issuer() err = %v, want nil", err) 397 } 398 if iss != "joe" { 399 t.Errorf("rawJWT.Issuer() = %q, want joe", iss) 400 } 401 exp, err := rawJWT.ExpiresAt() 402 if err != nil { 403 t.Errorf("rawJWT.ExpiresAt() err = %v, want nil", err) 404 } 405 wantExp := time.Unix(1300819380, 0) 406 if !exp.Equal(wantExp) { 407 t.Errorf("rawJWT.ExpiresAt() = %q, want %q", exp, wantExp) 408 } 409 cc, err := rawJWT.BooleanClaim("http://example.com/is_root") 410 if err != nil { 411 t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') err = %v want nil", err) 412 } 413 if cc != true { 414 t.Errorf("rawJWT.BooleanClaim('http://example.com/is_root') = %v, want true", cc) 415 } 416} 417 418func TestDecodeVerifyTokenPaylodWithInvalidEndcoding(t *testing.T) { 419 if _, err := decodeUnsignedTokenAndValidateHeader(dotConcat(base64Encode([]byte(`{"alg":"HS256"}`)), "_aSL&%"), "HS256", nil, nil); err == nil { 420 t.Errorf("decodeUnsignedTokenAndValidateHeader() err = nil, want error") 421 } 422} 423