xref: /aosp_15_r20/external/tink/go/jwt/raw_jwt_test.go (revision e7b1675dde1b92d52ec075b0a92829627f2c52a5)
1// Copyright 2021 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_test
18
19import (
20	"testing"
21	"time"
22
23	"github.com/google/go-cmp/cmp"
24	"github.com/google/go-cmp/cmp/cmpopts"
25	"github.com/google/tink/go/jwt"
26)
27
28const (
29	invalidUTF8     = "\xF4\x7F\xBF\xBF"
30	validExpiration = 1640043004
31)
32
33type testCase struct {
34	tag   string
35	opts  *jwt.RawJWTOptions
36	json  string
37	token *jwt.RawJWT
38}
39
40func refString(a string) *string {
41	return &a
42}
43
44func refTime(ts int64) *time.Time {
45	t := time.Unix(ts, 0)
46	return &t
47}
48
49func TestCreatingRawJWTWithAllClaims(t *testing.T) {
50	json := `{
51				"sub": "tink-test-subject",
52				"iss": "tink-test-issuer",
53				"jti": "tink-jwt-id",
54				"aud": ["aud-1", "aud-2"],
55				"exp": 457888,
56				"nbf": 450888,
57				"iat": 400888,
58				"cc-num": 1.67,
59				"cc-bool": true,
60				"cc-null": null,
61				"cc-array": [1,2,3],
62				"cc-string": "cc-val",
63				"cc-object": {"nested-cc-num": 5.5}
64			}`
65
66	opts := &jwt.RawJWTOptions{
67		TypeHeader: refString("typeHeader"),
68		Subject:    refString("tink-test-subject"),
69		Issuer:     refString("tink-test-issuer"),
70		JWTID:      refString("tink-jwt-id"),
71		Audiences:  []string{"aud-1", "aud-2"},
72		ExpiresAt:  refTime(457888),
73		NotBefore:  refTime(450888),
74		IssuedAt:   refTime(400888),
75		CustomClaims: map[string]interface{}{
76			"cc-num":    1.67,
77			"cc-bool":   true,
78			"cc-null":   nil,
79			"cc-string": "cc-val",
80			"cc-array":  []interface{}{1.0, 2.0, 3.0},
81			"cc-object": map[string]interface{}{"nested-cc-num": 5.5},
82		},
83	}
84	fromJSON, err := jwt.NewRawJWTFromJSON(refString("typeHeader"), []byte(json))
85	if err != nil {
86		t.Fatalf("jwt.NewRawJWTFromJSON(%q): %v", json, err)
87	}
88	fromOpts, err := jwt.NewRawJWT(opts)
89	if err != nil {
90		t.Fatalf("jwt.NewRawJWT(%v): %v", opts, err)
91	}
92	for _, tc := range []testCase{
93		{
94			tag:   "jwt.NewRawJWTFromJSON",
95			token: fromJSON,
96		},
97		{
98			tag:   "NewRawJWT",
99			token: fromOpts,
100		},
101	} {
102		if !tc.token.HasTypeHeader() {
103			t.Errorf("tc.token.HasTypeHeader() = false, want true")
104		}
105		if !tc.token.HasAudiences() {
106			t.Errorf("tc.token.HasAudiences() = false, want true")
107		}
108		if !tc.token.HasSubject() {
109			t.Errorf("tc.token.HasSubject() = false, want true")
110		}
111		if !tc.token.HasIssuer() {
112			t.Errorf("tc.token.HasIssuer() = false, want true")
113		}
114		if !tc.token.HasJWTID() {
115			t.Errorf("tc.token.HasJWTID() = false, want true")
116		}
117		if !tc.token.HasExpiration() {
118			t.Errorf("tc.token.HasExpiration() = false, want true")
119		}
120		if !tc.token.HasNotBefore() {
121			t.Errorf("tc.token.HasNotBefore() = false, want true")
122		}
123		if !tc.token.HasIssuedAt() {
124			t.Errorf("tc.token.HasIssuedAt() = false, want true")
125		}
126
127		typeHeader, err := tc.token.TypeHeader()
128		if err != nil {
129			t.Errorf("tc.token.TypeHeader() err = %v, want nil", err)
130		}
131		if !cmp.Equal(typeHeader, *opts.TypeHeader) {
132			t.Errorf("tc.token.TypeHeader() = %q, want %q", typeHeader, *opts.TypeHeader)
133		}
134		audiences, err := tc.token.Audiences()
135		if err != nil {
136			t.Errorf("tc.token.Audiences() err = %v, want nil", err)
137		}
138		if !cmp.Equal(audiences, opts.Audiences) {
139			t.Errorf("tc.token.Audiences() = %q, want %q", audiences, opts.Audiences)
140		}
141		subject, err := tc.token.Subject()
142		if err != nil {
143			t.Errorf("tc.token.Subject() err = %v, want nil", err)
144		}
145		if !cmp.Equal(subject, *opts.Subject) {
146			t.Errorf("tc.token.Subject() = %q, want %q", subject, *opts.Subject)
147		}
148		issuer, err := tc.token.Issuer()
149		if err != nil {
150			t.Errorf("tc.token.Issuer() err = %v, want nil", err)
151		}
152		if !cmp.Equal(issuer, *opts.Issuer) {
153			t.Errorf("tc.token.Issuer() = %q, want %q", issuer, *opts.Issuer)
154		}
155		jwtID, err := tc.token.JWTID()
156		if err != nil {
157			t.Errorf("tc.token.JWTID() err = %v, want nil", err)
158		}
159		if !cmp.Equal(jwtID, *opts.JWTID) {
160			t.Errorf("tc.token.JWTID() = %q, want %q", jwtID, *opts.JWTID)
161		}
162		expiresAt, err := tc.token.ExpiresAt()
163		if err != nil {
164			t.Errorf("tc.token.ExpiresAt() err = %v, want nil", err)
165		}
166		if !cmp.Equal(expiresAt, *opts.ExpiresAt) {
167			t.Errorf("tc.token.ExpiresAt() = %q, want %q", expiresAt, *opts.ExpiresAt)
168		}
169		issuedAt, err := tc.token.IssuedAt()
170		if err != nil {
171			t.Errorf("tc.token.IssuedAt() err = %v, want nil", err)
172		}
173		if !cmp.Equal(issuedAt, *opts.IssuedAt) {
174			t.Errorf("tc.token.IssuedAt() = %q, want %q", issuedAt, *opts.IssuedAt)
175		}
176		notBefore, err := tc.token.NotBefore()
177		if err != nil {
178			t.Errorf("tc.token.NotBefore() err = %v, want nil", err)
179		}
180		if !cmp.Equal(notBefore, *opts.NotBefore) {
181			t.Errorf("tc.token.NotBefore() = %q, want %q", notBefore, *opts.NotBefore)
182		}
183		wantCustomClaims := []string{"cc-num", "cc-bool", "cc-null", "cc-string", "cc-array", "cc-object"}
184		if !cmp.Equal(tc.token.CustomClaimNames(), wantCustomClaims, cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
185			t.Errorf("tc.token.CustomClaimNames() = %q, want %q", tc.token.CustomClaimNames(), wantCustomClaims)
186		}
187		if !tc.token.HasNumberClaim("cc-num") {
188			t.Errorf("tc.token.HasNumberClaim('cc-num') = false, want true")
189		}
190		if !tc.token.HasBooleanClaim("cc-bool") {
191			t.Errorf("tc.token.HasBooleanClaim('cc-bool') = false, want true")
192		}
193		if !tc.token.HasNullClaim("cc-null") {
194			t.Errorf("tc.token.HasNullClaim('cc-null') = false, want true")
195		}
196		if !tc.token.HasStringClaim("cc-string") {
197			t.Errorf("tc.token.HasStringClaim('cc-string') = false, want true")
198		}
199		if !tc.token.HasArrayClaim("cc-array") {
200			t.Errorf("tc.token.HasArrayClaim('cc-array') = false, want true")
201		}
202		if !tc.token.HasObjectClaim("cc-object") {
203			t.Errorf("tc.token.HasObjectClaim('cc-object') = false, want true")
204		}
205
206		number, err := tc.token.NumberClaim("cc-num")
207		if err != nil {
208			t.Errorf("tc.token.NumberClaim('cc-num') err = %v, want nil", err)
209		}
210		if !cmp.Equal(number, opts.CustomClaims["cc-num"]) {
211			t.Errorf("tc.token.NumberClaim('cc-num') = %f, want %f", number, opts.CustomClaims["cc-num"])
212		}
213		boolean, err := tc.token.BooleanClaim("cc-bool")
214		if err != nil {
215			t.Errorf("tc.token.BooleanClaim('cc-bool') err = %v, want nil", err)
216		}
217		if !cmp.Equal(boolean, opts.CustomClaims["cc-bool"]) {
218			t.Errorf("tc.token.BooleanClaim('cc-bool') = %v, want %v", boolean, opts.CustomClaims["cc-bool"])
219		}
220		str, err := tc.token.StringClaim("cc-string")
221		if err != nil {
222			t.Errorf("tc.token.StringClaim('cc-string') err = %v, want nil", err)
223		}
224		if !cmp.Equal(str, opts.CustomClaims["cc-string"]) {
225			t.Errorf("tc.token.StringClaim('cc-string') = %q, want %q", str, opts.CustomClaims["cc-string"])
226		}
227		array, err := tc.token.ArrayClaim("cc-array")
228		if err != nil {
229			t.Errorf("tc.token.ArrayClaim('cc-array') err = %v, want nil", err)
230		}
231		if !cmp.Equal(array, opts.CustomClaims["cc-array"]) {
232			t.Errorf("tc.token.ArrayClaim('cc-array') = %q, want %q", array, opts.CustomClaims["cc-array"])
233		}
234		object, err := tc.token.ObjectClaim("cc-object")
235		if err != nil {
236			t.Errorf("tc.token.ObjectClaim('cc-object') err = %v, want nil", err)
237		}
238		if !cmp.Equal(object, opts.CustomClaims["cc-object"]) {
239			t.Errorf("tc.token.ObjectClaim('cc-object') = %q, want %q", object, opts.CustomClaims["cc-object"])
240		}
241	}
242}
243
244func TestGeneratingRawJWTWithoutClaims(t *testing.T) {
245	jsonToken, err := jwt.NewRawJWTFromJSON(nil, []byte("{}"))
246	if err != nil {
247		t.Fatalf("jwt.NewRawJWTFromJSON({}): %v", err)
248	}
249	optsToken, err := jwt.NewRawJWT(&jwt.RawJWTOptions{WithoutExpiration: true})
250	if err != nil {
251		t.Fatalf("NewRawJWT with no claims: %v", err)
252	}
253	for _, tc := range []testCase{
254		{
255			tag:   "jwt.NewRawJWTFromJSON",
256			token: jsonToken,
257		},
258		{
259			tag:   "NewRawJWT",
260			token: optsToken,
261		},
262	} {
263		if tc.token.HasTypeHeader() {
264			t.Errorf("tc.token.HasTypeHeader() = true, want false")
265		}
266		if tc.token.HasAudiences() {
267			t.Errorf("tc.token.HasAudiences() = true, want false")
268		}
269		if tc.token.HasSubject() {
270			t.Errorf("tc.token.HasSubject() = true, want false")
271		}
272		if tc.token.HasIssuer() {
273			t.Errorf("tc.token.HasIssuer() = true, want false")
274		}
275		if tc.token.HasJWTID() {
276			t.Errorf("tc.token.HasJWTID() = true, want false")
277		}
278		if tc.token.HasExpiration() {
279			t.Errorf("tc.token.HasExpiration() = true, want false")
280		}
281		if tc.token.HasNotBefore() {
282			t.Errorf("tc.token.HasNotBefore() = true, want false")
283		}
284		if tc.token.HasIssuedAt() {
285			t.Errorf("tc.token.HasIssuedAt() = true, want false")
286		}
287		if _, err := tc.token.Audiences(); err == nil {
288			t.Errorf("tc.token.Audiences() err = nil, want error")
289		}
290		if _, err := tc.token.Subject(); err == nil {
291			t.Errorf("tc.token.Subject() err = nil, want error")
292		}
293		if _, err := tc.token.Issuer(); err == nil {
294			t.Errorf("tc.token.Issuer() err = nil, want error")
295		}
296		if _, err := tc.token.JWTID(); err == nil {
297			t.Errorf("tc.token.JWTID() err = nil, want error")
298		}
299		if _, err := tc.token.ExpiresAt(); err == nil {
300			t.Errorf("tc.token.ExpiresAt() err = nil, want error")
301		}
302		if _, err := tc.token.IssuedAt(); err == nil {
303			t.Errorf("tc.token.IssuedAt() err = nil, want error")
304		}
305		if _, err := tc.token.NotBefore(); err == nil {
306			t.Errorf("tc.token.NotBefore() err = nil, want error")
307		}
308		if !cmp.Equal(tc.token.CustomClaimNames(), []string{}) {
309			t.Errorf("tc.token.CustomClaimNames() = %q want %q", tc.token.CustomClaimNames(), []string{})
310		}
311	}
312}
313
314func TestNewRawJWTLargeValidTimestamps(t *testing.T) {
315	opts := &jwt.RawJWTOptions{
316		TypeHeader: refString("typeHeader"),
317		ExpiresAt:  refTime(253402300799),
318		NotBefore:  refTime(253402300700),
319		IssuedAt:   refTime(253402300000),
320	}
321	token, err := jwt.NewRawJWT(opts)
322	if err != nil {
323		t.Fatalf("generating RawJWT with valid timestamps (%q, %q, %q): %v", opts.ExpiresAt, opts.NotBefore, opts.IssuedAt, err)
324	}
325	expiresAt, err := token.ExpiresAt()
326	if err != nil {
327		t.Errorf("tc.token.ExpiresAt() err = %v, want nil", err)
328	}
329	if !cmp.Equal(expiresAt, *opts.ExpiresAt) {
330		t.Errorf("tc.token.ExpiresAt() = %q want %q", expiresAt, *opts.ExpiresAt)
331	}
332
333	notBefore, err := token.NotBefore()
334	if err != nil {
335		t.Errorf("tc.token.NotBefore() err = %v, want nil", err)
336	}
337	if !cmp.Equal(notBefore, *opts.NotBefore) {
338		t.Errorf("tc.token.NotBefore() = %q want %q", notBefore, *opts.NotBefore)
339	}
340
341	issuedAt, err := token.IssuedAt()
342	if err != nil {
343		t.Errorf("tc.token.IssuedAt() err = %v, want nil", err)
344	}
345	if !cmp.Equal(issuedAt, *opts.IssuedAt) {
346		t.Errorf("tc.token.IssuedAt() = %q want %q", issuedAt, *opts.IssuedAt)
347	}
348}
349
350func TestNewRawJWTSingleStringAudience(t *testing.T) {
351	opts := &jwt.RawJWTOptions{
352		WithoutExpiration: true,
353		Audience:          refString("tink-aud"),
354	}
355	rawJWT, err := jwt.NewRawJWT(opts)
356	if err != nil {
357		t.Fatalf("generating RawJWT with a single audience: %v", err)
358	}
359	aud, err := rawJWT.Audiences()
360	if err != nil {
361		t.Errorf("getting audience from token: %v", err)
362	}
363	want := []string{*opts.Audience}
364	if !cmp.Equal(aud, want) {
365		t.Errorf("rawJWT.Audiences() = %q, want %q", aud, want)
366	}
367}
368
369func TestSingleStringAudienceFromJSON(t *testing.T) {
370	rawJWT, err := jwt.NewRawJWTFromJSON(nil, []byte(`{"aud": "tink-aud"}`))
371	if err != nil {
372		t.Fatalf("parsing valid RawJWT: %v", err)
373	}
374	aud, err := rawJWT.Audiences()
375	if err != nil {
376		t.Errorf("getting audience from token: %v", err)
377	}
378	want := []string{"tink-aud"}
379	if !cmp.Equal(aud, want) {
380		t.Errorf("rawJWT.Audiences() = %q, want %q", aud, want)
381	}
382}
383
384func TestNewRawJWTValidationFailures(t *testing.T) {
385	testCases := []testCase{
386		{
387			tag: "empty jwt.RawJWTOptions options fails",
388		},
389		{
390			tag: "no ExpiresAt specified and WithoutExpiration = false fails",
391			opts: &jwt.RawJWTOptions{
392				Audiences: []string{"tink-foo"},
393			},
394		},
395		{
396			tag: "ExpiresAt and WithoutExpiration = true fails",
397			opts: &jwt.RawJWTOptions{
398				Audiences:         []string{"tink-foo"},
399				ExpiresAt:         refTime(validExpiration),
400				WithoutExpiration: true,
401			},
402		},
403		{
404			tag: "specifying Audenience and Audiences fails",
405			opts: &jwt.RawJWTOptions{
406				Audiences:         []string{"tink-foo"},
407				Audience:          refString("tink-bar"),
408				WithoutExpiration: true,
409			},
410		},
411		{
412			tag: "empty audiences array fails",
413			opts: &jwt.RawJWTOptions{
414				ExpiresAt: refTime(validExpiration),
415				Audiences: []string{},
416			},
417		},
418		{
419			tag: "audiences with invalid UTF-8 string fails",
420			opts: &jwt.RawJWTOptions{
421				WithoutExpiration: true,
422				Audiences:         []string{"valid", invalidUTF8},
423			},
424		},
425		{
426			tag: "custom claims containing registered subject claims fails",
427			opts: &jwt.RawJWTOptions{
428				Audiences: []string{"tink-foo"},
429				ExpiresAt: refTime(validExpiration),
430				CustomClaims: map[string]interface{}{
431					"sub": "overwrite",
432				},
433			},
434		},
435		{
436			tag: "custom claims containing registered issuer claims fails",
437			opts: &jwt.RawJWTOptions{
438				Audiences: []string{"tink-foo"},
439				ExpiresAt: refTime(validExpiration),
440				CustomClaims: map[string]interface{}{
441					"iss": "overwrite",
442				},
443			},
444		},
445		{
446			tag: "custom claims containing registered jwt id claims fails",
447			opts: &jwt.RawJWTOptions{
448				Audiences: []string{"tink-foo"},
449				ExpiresAt: refTime(validExpiration),
450				CustomClaims: map[string]interface{}{
451					"jti": "overwrite",
452				},
453			},
454		},
455		{
456			tag: "custom claims containing registered expiration claims fails",
457			opts: &jwt.RawJWTOptions{
458				Audiences: []string{"tink-foo"},
459				ExpiresAt: refTime(validExpiration),
460				CustomClaims: map[string]interface{}{
461					"exp": "overwrite",
462				},
463			},
464		},
465		{
466			tag: "custom claims containing registered audience claims fails",
467			opts: &jwt.RawJWTOptions{
468				Audiences:         []string{"tink-foo"},
469				WithoutExpiration: true,
470				CustomClaims: map[string]interface{}{
471					"aud": []interface{}{"overwrite"},
472				},
473			},
474		},
475		{
476			tag: "custom claims with non standard JSON types fails",
477			opts: &jwt.RawJWTOptions{
478				Audiences: []string{"tink-foo"},
479				ExpiresAt: refTime(validExpiration),
480				CustomClaims: map[string]interface{}{
481					"complex": time.Time{},
482				},
483			},
484		},
485		{
486			tag: "non UTF-8 string on isser claim fails",
487			opts: &jwt.RawJWTOptions{
488				Audiences: []string{"tink-foo"},
489				ExpiresAt: refTime(validExpiration),
490				Issuer:    refString(invalidUTF8),
491			},
492		},
493		{
494			tag: "non UTF-8 string on subject claim fails",
495			opts: &jwt.RawJWTOptions{
496				Audiences:         []string{"tink-foo"},
497				WithoutExpiration: true,
498				Subject:           refString(invalidUTF8),
499			},
500		},
501		{
502			tag: "non UTF-8 string on JWT ID claim fails",
503			opts: &jwt.RawJWTOptions{
504				Audiences:         []string{"tink-foo"},
505				WithoutExpiration: true,
506				JWTID:             refString(invalidUTF8),
507			},
508		},
509		{
510			tag: "non UTF-8 string on custom claim fails",
511			opts: &jwt.RawJWTOptions{
512				Audiences: []string{"tink-foo"},
513				Issuer:    refString("ise-testing"),
514				ExpiresAt: refTime(validExpiration),
515				CustomClaims: map[string]interface{}{
516					"esoteric": invalidUTF8,
517				},
518			},
519		},
520		{
521			tag: "issued at timestamp greater than valid JWT max time fails",
522			opts: &jwt.RawJWTOptions{
523				Audiences: []string{"tink-foo"},
524				ExpiresAt: refTime(validExpiration),
525				IssuedAt:  refTime(253402300800),
526			},
527		},
528		{
529			tag: "expires at timestamp greater than valid JWT max time fails",
530			opts: &jwt.RawJWTOptions{
531				Audiences: []string{"tink-foo"},
532				ExpiresAt: refTime(253402300800),
533			},
534		},
535		{
536			tag: "not before timestamp smaller than valid JWT min time fails",
537			opts: &jwt.RawJWTOptions{
538				Audiences: []string{"tink-foo"},
539				ExpiresAt: refTime(validExpiration),
540				NotBefore: refTime(-5),
541			},
542		},
543	}
544	for _, tc := range testCases {
545		t.Run(tc.tag, func(t *testing.T) {
546			_, err := jwt.NewRawJWT(tc.opts)
547			if err == nil {
548				t.Errorf("expected error instead got nil")
549			}
550		})
551	}
552}
553
554func TestJSONPayload(t *testing.T) {
555	for _, tc := range []testCase{
556		{
557			tag: "subject",
558			opts: &jwt.RawJWTOptions{
559				WithoutExpiration: true,
560				Subject:           refString("tink-subject"),
561			},
562			json: `{"sub":"tink-subject"}`,
563		},
564		{
565			tag: "audience list",
566			opts: &jwt.RawJWTOptions{
567				WithoutExpiration: true,
568				Audiences:         []string{"one"},
569			},
570			json: `{"aud":["one"]}`,
571		},
572		{
573			tag: "audience string",
574			opts: &jwt.RawJWTOptions{
575				WithoutExpiration: true,
576				Audience:          refString("one"),
577			},
578			json: `{"aud":"one"}`,
579		},
580		{
581			tag: "issuer",
582			opts: &jwt.RawJWTOptions{
583				WithoutExpiration: true,
584				Issuer:            refString("tink-test"),
585			},
586			json: `{"iss":"tink-test"}`,
587		},
588		{
589			tag: "jwt id",
590			opts: &jwt.RawJWTOptions{
591				WithoutExpiration: true,
592				JWTID:             refString("tink-id"),
593			},
594			json: `{"jti":"tink-id"}`,
595		},
596		{
597			tag: "issued at",
598			opts: &jwt.RawJWTOptions{
599				WithoutExpiration: true,
600				IssuedAt:          refTime(78324),
601			},
602			json: `{"iat":78324}`,
603		},
604		{
605			tag: "not before",
606			opts: &jwt.RawJWTOptions{
607				WithoutExpiration: true,
608				NotBefore:         refTime(78324),
609			},
610			json: `{"nbf":78324}`,
611		},
612		{
613			tag: "expiration",
614			opts: &jwt.RawJWTOptions{
615				ExpiresAt: refTime(78324),
616			},
617			json: `{"exp":78324}`,
618		},
619		{
620			tag: "integer",
621			opts: &jwt.RawJWTOptions{
622				WithoutExpiration: true,
623				CustomClaims: map[string]interface{}{
624					"num": 1,
625				},
626			},
627			json: `{"num":1}`,
628		},
629		{
630			tag: "custom-claim",
631			opts: &jwt.RawJWTOptions{
632				WithoutExpiration: true,
633				CustomClaims: map[string]interface{}{
634					"cust": []interface{}{map[string]interface{}{"key": "val"}},
635				},
636			},
637			json: `{"cust":[{"key":"val"}]}`,
638		},
639		{
640			tag: "no claims",
641			opts: &jwt.RawJWTOptions{
642				WithoutExpiration: true,
643			},
644			json: `{}`,
645		},
646	} {
647		t.Run(tc.tag, func(t *testing.T) {
648			token, err := jwt.NewRawJWT(tc.opts)
649			if err != nil {
650				t.Errorf("generating valid RawJWT: %v", err)
651			}
652			j, err := token.JSONPayload()
653			if err != nil {
654				t.Errorf("calling JSONPayload() on rawJWT: %v", err)
655			}
656			if !cmp.Equal(string(j), tc.json) {
657				t.Fatalf("JSONPayload output got %v, expected %v", string(j), tc.json)
658			}
659		})
660	}
661}
662
663func TestFromJSONValidationFailures(t *testing.T) {
664	testCases := []testCase{
665		{
666			tag:  "json with empty audience",
667			json: `{"sub": "tink", "aud": []}`,
668		},
669		{
670			tag:  "json with audience of wrong type",
671			json: `{"aud": 5}`,
672		},
673		{
674			tag:  "json with audiences of wrong type",
675			json: `{"aud": ["one", null]}`,
676		},
677		{
678			tag:  "json with registered claim with wrong type",
679			json: `{"sub": 1}`,
680		},
681		{
682			tag:  "json with non UTF-8 string on subject claim fails",
683			json: `{"sub": "\xF4\x7F\xBF\xBF"}`,
684		},
685		{
686			tag:  "json with non UTF-8 string on issuer claim fails",
687			json: `{"iss": "\xF4\x7F\xBF\xBF"}`,
688		},
689		{
690			tag:  "json with non UTF-8 string on jwt id claim fails",
691			json: `{"jti": "\xF4\x7F\xBF\xBF"}`,
692		},
693		{
694			tag:  "json with `not before` timestamp claim greater than valid JWT max time fails",
695			json: `{"nbf": 253402301799}`,
696		},
697		{
698			tag:  "json with `issued at` timestamp claim greater than valid JWT max time fails",
699			json: `{"iat": 253402301799}`,
700		},
701		{
702			tag:  "json with `expiration` timestamp claim greater than valid JWT max time fails",
703			json: `{"exp": 253402301799}`,
704		},
705		{
706			tag:  "json with `not before` timestamp claim smaller than valid JWT min time fails",
707			json: `{"nbf": -4}`,
708		},
709		{
710			tag:  "json with `issued at` timestamp claim smaller than valid JWT min time fails",
711			json: `{"iat": -4}`,
712		},
713		{
714			tag:  "json with `expiration` timestamp claim smaller than valid JWT min time fails",
715			json: `{"exp": -4}`,
716		},
717		{
718			tag:  "json with `not before` claim of non numeric type fails",
719			json: `{"nbf": "invalid"}`,
720		},
721		{
722			tag:  "json with `issued at` claim of non numeric type fails",
723			json: `{"iat": "invalid"}`,
724		},
725		{
726			tag:  "json with `expiration` claim of non numeric type fails",
727			json: `{"exp": "invalid"}`,
728		},
729	}
730
731	for _, tc := range testCases {
732		t.Run(tc.tag, func(t *testing.T) {
733			if _, err := jwt.NewRawJWTFromJSON(nil, []byte(tc.json)); err == nil {
734				t.Errorf("expected error instead got nil")
735			}
736		})
737	}
738}
739
740func TestHasCustomClaimsOfKind(t *testing.T) {
741	opts := &jwt.RawJWTOptions{
742		TypeHeader:        refString("typeHeader"),
743		WithoutExpiration: true,
744		CustomClaims: map[string]interface{}{
745			"cc-num":    1.67,
746			"cc-bool":   false,
747			"cc-nil":    nil,
748			"cc-list":   []interface{}{1.0, 2.0, 3.0},
749			"cc-string": "cc-val",
750			"cc-object": map[string]interface{}{
751				"nested-cc-num": 5.5,
752			},
753		},
754	}
755	token, err := jwt.NewRawJWT(opts)
756	if err != nil {
757		t.Fatalf("generating valid RawJWT: %v", err)
758	}
759	if token.HasBooleanClaim("cc-num") {
760		t.Errorf("custom number claim 'cc-num' should return false when queried for another type")
761	}
762	if token.HasNullClaim("cc-bool") {
763		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
764	}
765	if token.HasNumberClaim("cc-bool") {
766		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
767	}
768	if token.HasStringClaim("cc-bool") {
769		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
770	}
771	if token.HasArrayClaim("cc-bool") {
772		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
773	}
774	if token.HasObjectClaim("cc-bool") {
775		t.Errorf("custom boolean claim 'cc-bool' should return false when queried for another type")
776	}
777}
778
779func TestGettingRegisteredClaimsThroughCustomFails(t *testing.T) {
780	opts := &jwt.RawJWTOptions{
781		TypeHeader: refString("typeHeader"),
782		Subject:    refString("tink-test-subject"),
783		Issuer:     refString("tink-test-issuer"),
784		JWTID:      refString("tink-jwt-id-1"),
785		Audiences:  []string{"aud-1", "aud-2"},
786		ExpiresAt:  refTime(validExpiration),
787		IssuedAt:   refTime(validExpiration - 100),
788		NotBefore:  refTime(validExpiration - 50),
789	}
790	token, err := jwt.NewRawJWT(opts)
791	if err != nil {
792		t.Fatalf("generating valid RawJWT: %v", err)
793	}
794	if !cmp.Equal(token.CustomClaimNames(), []string{}) {
795		t.Errorf("tc.token.CustomClaimNames() = %q want %q", token.CustomClaimNames(), []string{})
796	}
797	for _, c := range []string{"sub", "iss", "aud", "nbf", "exp", "iat", "jti"} {
798		if token.HasNullClaim(c) {
799			t.Errorf("registered '%q' claim should return false when calling HasNullClaim", c)
800		}
801		if token.HasBooleanClaim(c) {
802			t.Errorf("registered '%q' claim should return false when calling HasBooleanClaim", c)
803		}
804		if _, err := token.BooleanClaim(c); err == nil {
805			t.Errorf("expected error when calling token.BoolClaim(%q) instead got nil", c)
806		}
807		if token.HasNumberClaim(c) {
808			t.Errorf("registered '%q' claim should return false when calling HasNumberClaim", c)
809		}
810		if _, err := token.NumberClaim(c); err == nil {
811			t.Errorf("expected error when calling token.NumberClaim(%q) instead got nil", c)
812		}
813		if token.HasStringClaim(c) {
814			t.Errorf("registered '%q' claim should return false when calling HasStringClaim", c)
815		}
816		if _, err := token.StringClaim(c); err == nil {
817			t.Errorf("expected error when calling token.StringClaim(%q) instead got nil", c)
818		}
819		if token.HasArrayClaim(c) {
820			t.Errorf("registered '%q' claim should return false when calling HasArrayClaim", c)
821		}
822		if _, err := token.ArrayClaim(c); err == nil {
823			t.Errorf("expected error when calling token.ListClaim(%q) instead got nil", c)
824		}
825		if token.HasObjectClaim(c) {
826			t.Errorf("registered '%q' claim should return false when calling HasObjectClaim", c)
827		}
828		if _, err := token.ObjectClaim(c); err == nil {
829			t.Errorf("expected error when calling token.JSONClaim(%q) instead got nil", c)
830		}
831	}
832}
833