1/*
2Copyright 2015 The Kubernetes Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16
17package oidc
18
19import (
20	"bytes"
21	"context"
22	"crypto"
23	"crypto/x509"
24	"encoding/hex"
25	"encoding/json"
26	"encoding/pem"
27	"fmt"
28	"io/ioutil"
29	"net/http"
30	"net/http/httptest"
31	"os"
32	"reflect"
33	"strings"
34	"testing"
35	"text/template"
36	"time"
37
38	oidc "github.com/coreos/go-oidc"
39	jose "gopkg.in/square/go-jose.v2"
40	"k8s.io/apiserver/pkg/authentication/user"
41	"k8s.io/klog/v2"
42)
43
44// utilities for loading JOSE keys.
45
46func loadRSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
47	return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
48		key, err := x509.ParsePKCS1PrivateKey(b)
49		if err != nil {
50			return nil, err
51		}
52		return key.Public(), nil
53	})
54}
55
56func loadRSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
57	return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
58		return x509.ParsePKCS1PrivateKey(b)
59	})
60}
61
62func loadECDSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
63	return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
64		key, err := x509.ParseECPrivateKey(b)
65		if err != nil {
66			return nil, err
67		}
68		return key.Public(), nil
69	})
70}
71
72func loadECDSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey {
73	return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) {
74		return x509.ParseECPrivateKey(b)
75	})
76}
77
78func loadKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm, unmarshal func([]byte) (interface{}, error)) *jose.JSONWebKey {
79	data, err := ioutil.ReadFile(filepath)
80	if err != nil {
81		t.Fatalf("load file: %v", err)
82	}
83	block, _ := pem.Decode(data)
84	if block == nil {
85		t.Fatalf("file contained no PEM encoded data: %s", filepath)
86	}
87	priv, err := unmarshal(block.Bytes)
88	if err != nil {
89		t.Fatalf("unmarshal key: %v", err)
90	}
91	key := &jose.JSONWebKey{Key: priv, Use: "sig", Algorithm: string(alg)}
92	thumbprint, err := key.Thumbprint(crypto.SHA256)
93	if err != nil {
94		t.Fatalf("computing thumbprint: %v", err)
95	}
96	key.KeyID = hex.EncodeToString(thumbprint)
97	return key
98}
99
100// staticKeySet implements oidc.KeySet.
101type staticKeySet struct {
102	keys []*jose.JSONWebKey
103}
104
105func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) {
106	jws, err := jose.ParseSigned(jwt)
107	if err != nil {
108		return nil, err
109	}
110	if len(jws.Signatures) == 0 {
111		return nil, fmt.Errorf("jwt contained no signatures")
112	}
113	kid := jws.Signatures[0].Header.KeyID
114
115	for _, key := range s.keys {
116		if key.KeyID == kid {
117			return jws.Verify(key)
118		}
119	}
120
121	return nil, fmt.Errorf("no keys matches jwk keyid")
122}
123
124var (
125	expired, _ = time.Parse(time.RFC3339Nano, "2009-11-10T22:00:00Z")
126	now, _     = time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z")
127	valid, _   = time.Parse(time.RFC3339Nano, "2009-11-11T00:00:00Z")
128)
129
130type claimsTest struct {
131	name               string
132	options            Options
133	signingKey         *jose.JSONWebKey
134	pubKeys            []*jose.JSONWebKey
135	claims             string
136	want               *user.DefaultInfo
137	wantSkip           bool
138	wantErr            bool
139	wantInitErr        bool
140	claimToResponseMap map[string]string
141	openIDConfig       string
142}
143
144// Replace formats the contents of v into the provided template.
145func replace(tmpl string, v interface{}) string {
146	t := template.Must(template.New("test").Parse(tmpl))
147	buf := bytes.NewBuffer(nil)
148	t.Execute(buf, &v)
149	ret := buf.String()
150	klog.V(4).Infof("Replaced: %v into: %v", tmpl, ret)
151	return ret
152}
153
154// newClaimServer returns a new test HTTPS server, which is rigged to return
155// OIDC responses to requests that resolve distributed claims. signer is the
156// signer used for the served JWT tokens.  claimToResponseMap is a map of
157// responses that the server will return for each claim it is given.
158func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server {
159	ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
160		klog.V(5).Infof("request: %+v", *r)
161		switch r.URL.Path {
162		case "/.testing/keys":
163			w.Header().Set("Content-Type", "application/json")
164			keyBytes, err := json.Marshal(keys)
165			if err != nil {
166				t.Fatalf("unexpected error while marshaling keys: %v", err)
167			}
168			klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes))
169			w.Write(keyBytes)
170
171		case "/.well-known/openid-configuration":
172			w.Header().Set("Content-Type", "application/json")
173			klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig)
174			w.Write([]byte(*openIDConfig))
175		// These claims are tested in the unit tests.
176		case "/groups":
177			fallthrough
178		case "/rabbits":
179			if claimToResponseMap == nil {
180				t.Errorf("no claims specified in response")
181			}
182			claim := r.URL.Path[1:] // "/groups" -> "groups"
183			expectedAuth := fmt.Sprintf("Bearer %v_token", claim)
184			auth := r.Header.Get("Authorization")
185			if auth != expectedAuth {
186				t.Errorf("bearer token expected: %q, was %q", expectedAuth, auth)
187			}
188			jws, err := signer.Sign([]byte(claimToResponseMap[claim]))
189			if err != nil {
190				t.Errorf("while signing response token: %v", err)
191			}
192			token, err := jws.CompactSerialize()
193			if err != nil {
194				t.Errorf("while serializing response token: %v", err)
195			}
196			w.Write([]byte(token))
197		default:
198			w.WriteHeader(http.StatusNotFound)
199			fmt.Fprintf(w, "unexpected URL: %v", r.URL)
200		}
201	}))
202	klog.V(4).Infof("Serving OIDC at: %v", ts.URL)
203	return ts
204}
205
206// writeTempCert writes out the supplied certificate into a temporary file in
207// PEM-encoded format.  Returns the name of the temporary file used.  The caller
208// is responsible for cleaning the file up.
209func writeTempCert(t *testing.T, cert []byte) string {
210	tempFile, err := ioutil.TempFile("", "ca.crt")
211	if err != nil {
212		t.Fatalf("could not open temp file: %v", err)
213	}
214	block := &pem.Block{
215		Type:  "CERTIFICATE",
216		Bytes: cert,
217	}
218	if err := pem.Encode(tempFile, block); err != nil {
219		t.Fatalf("could not write to temp file %v: %v", tempFile.Name(), err)
220	}
221	tempFile.Close()
222	return tempFile.Name()
223}
224
225func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet {
226	ret := jose.JSONWebKeySet{}
227	for _, k := range keys {
228		ret.Keys = append(ret.Keys, *k)
229	}
230	return ret
231}
232
233func (c *claimsTest) run(t *testing.T) {
234	var (
235		signer jose.Signer
236		err    error
237	)
238	if c.signingKey != nil {
239		// Initialize the signer only in the tests that make use of it.  We can
240		// not defer this initialization because the test server uses it too.
241		signer, err = jose.NewSigner(jose.SigningKey{
242			Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm),
243			Key:       c.signingKey,
244		}, nil)
245		if err != nil {
246			t.Fatalf("initialize signer: %v", err)
247		}
248	}
249	// The HTTPS server used for requesting distributed groups claims.
250	ts := newClaimServer(t, toKeySet(c.pubKeys), signer, c.claimToResponseMap, &c.openIDConfig)
251	defer ts.Close()
252
253	// Make the certificate of the helper server available to the authenticator
254	// by writing its root CA certificate into a temporary file.
255	tempFileName := writeTempCert(t, ts.TLS.Certificates[0].Certificate[0])
256	defer os.Remove(tempFileName)
257	c.options.CAFile = tempFileName
258
259	// Allow claims to refer to the serving URL of the test server.  For this,
260	// substitute all references to {{.URL}} in appropriate places.
261	v := struct{ URL string }{URL: ts.URL}
262	c.claims = replace(c.claims, &v)
263	c.openIDConfig = replace(c.openIDConfig, &v)
264	c.options.IssuerURL = replace(c.options.IssuerURL, &v)
265	for claim, response := range c.claimToResponseMap {
266		c.claimToResponseMap[claim] = replace(response, &v)
267	}
268
269	// Initialize the authenticator.
270	a, err := newAuthenticator(c.options, func(ctx context.Context, a *Authenticator, config *oidc.Config) {
271		// Set the verifier to use the public key set instead of reading from a remote.
272		a.setVerifier(oidc.NewVerifier(
273			c.options.IssuerURL,
274			&staticKeySet{keys: c.pubKeys},
275			config,
276		))
277	})
278	if err != nil {
279		if !c.wantInitErr {
280			t.Fatalf("initialize authenticator: %v", err)
281		}
282		return
283	}
284	if c.wantInitErr {
285		t.Fatalf("wanted initialization error")
286	}
287
288	// Sign and serialize the claims in a JWT.
289	jws, err := signer.Sign([]byte(c.claims))
290	if err != nil {
291		t.Fatalf("sign claims: %v", err)
292	}
293	token, err := jws.CompactSerialize()
294	if err != nil {
295		t.Fatalf("serialize token: %v", err)
296	}
297
298	got, ok, err := a.AuthenticateToken(context.Background(), token)
299
300	if err != nil {
301		if !c.wantErr {
302			t.Fatalf("authenticate token: %v", err)
303		}
304		return
305	}
306
307	if c.wantErr {
308		t.Fatalf("expected error authenticating token")
309	}
310	if !ok {
311		if !c.wantSkip {
312			// We don't have any cases where we return (nil, false, nil)
313			t.Fatalf("no error but token not authenticated")
314		}
315		return
316	}
317	if c.wantSkip {
318		t.Fatalf("expected authenticator to skip token")
319	}
320
321	gotUser := got.User.(*user.DefaultInfo)
322	if !reflect.DeepEqual(gotUser, c.want) {
323		t.Fatalf("wanted user=%#v, got=%#v", c.want, gotUser)
324	}
325}
326
327func TestToken(t *testing.T) {
328	synchronizeTokenIDVerifierForTest = true
329	tests := []claimsTest{
330		{
331			name: "token",
332			options: Options{
333				IssuerURL:     "https://auth.example.com",
334				ClientID:      "my-client",
335				UsernameClaim: "username",
336				now:           func() time.Time { return now },
337			},
338			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
339			pubKeys: []*jose.JSONWebKey{
340				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
341			},
342			claims: fmt.Sprintf(`{
343				"iss": "https://auth.example.com",
344				"aud": "my-client",
345				"username": "jane",
346				"exp": %d
347			}`, valid.Unix()),
348			want: &user.DefaultInfo{
349				Name: "jane",
350			},
351		},
352		{
353			name: "no-username",
354			options: Options{
355				IssuerURL:     "https://auth.example.com",
356				ClientID:      "my-client",
357				UsernameClaim: "username",
358				now:           func() time.Time { return now },
359			},
360			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
361			pubKeys: []*jose.JSONWebKey{
362				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
363			},
364			claims: fmt.Sprintf(`{
365				"iss": "https://auth.example.com",
366				"aud": "my-client",
367				"exp": %d
368			}`, valid.Unix()),
369			wantErr: true,
370		},
371		{
372			name: "email",
373			options: Options{
374				IssuerURL:     "https://auth.example.com",
375				ClientID:      "my-client",
376				UsernameClaim: "email",
377				now:           func() time.Time { return now },
378			},
379			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
380			pubKeys: []*jose.JSONWebKey{
381				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
382			},
383			claims: fmt.Sprintf(`{
384				"iss": "https://auth.example.com",
385				"aud": "my-client",
386				"email": "jane@example.com",
387				"email_verified": true,
388				"exp": %d
389			}`, valid.Unix()),
390			want: &user.DefaultInfo{
391				Name: "jane@example.com",
392			},
393		},
394		{
395			name: "email-not-verified",
396			options: Options{
397				IssuerURL:     "https://auth.example.com",
398				ClientID:      "my-client",
399				UsernameClaim: "email",
400				now:           func() time.Time { return now },
401			},
402			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
403			pubKeys: []*jose.JSONWebKey{
404				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
405			},
406			claims: fmt.Sprintf(`{
407				"iss": "https://auth.example.com",
408				"aud": "my-client",
409				"email": "jane@example.com",
410				"email_verified": false,
411				"exp": %d
412			}`, valid.Unix()),
413			wantErr: true,
414		},
415		{
416			// If "email_verified" isn't present, assume true
417			name: "no-email-verified-claim",
418			options: Options{
419				IssuerURL:     "https://auth.example.com",
420				ClientID:      "my-client",
421				UsernameClaim: "email",
422				now:           func() time.Time { return now },
423			},
424			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
425			pubKeys: []*jose.JSONWebKey{
426				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
427			},
428			claims: fmt.Sprintf(`{
429				"iss": "https://auth.example.com",
430				"aud": "my-client",
431				"email": "jane@example.com",
432				"exp": %d
433			}`, valid.Unix()),
434			want: &user.DefaultInfo{
435				Name: "jane@example.com",
436			},
437		},
438		{
439			name: "invalid-email-verified-claim",
440			options: Options{
441				IssuerURL:     "https://auth.example.com",
442				ClientID:      "my-client",
443				UsernameClaim: "email",
444				now:           func() time.Time { return now },
445			},
446			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
447			pubKeys: []*jose.JSONWebKey{
448				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
449			},
450			// string value for "email_verified"
451			claims: fmt.Sprintf(`{
452				"iss": "https://auth.example.com",
453				"aud": "my-client",
454				"email": "jane@example.com",
455				"email_verified": "false",
456				"exp": %d
457			}`, valid.Unix()),
458			wantErr: true,
459		},
460		{
461			name: "groups",
462			options: Options{
463				IssuerURL:     "https://auth.example.com",
464				ClientID:      "my-client",
465				UsernameClaim: "username",
466				GroupsClaim:   "groups",
467				now:           func() time.Time { return now },
468			},
469			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
470			pubKeys: []*jose.JSONWebKey{
471				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
472			},
473			claims: fmt.Sprintf(`{
474				"iss": "https://auth.example.com",
475				"aud": "my-client",
476				"username": "jane",
477				"groups": ["team1", "team2"],
478				"exp": %d
479			}`, valid.Unix()),
480			want: &user.DefaultInfo{
481				Name:   "jane",
482				Groups: []string{"team1", "team2"},
483			},
484		},
485		{
486			name: "groups-distributed",
487			options: Options{
488				IssuerURL:     "{{.URL}}",
489				ClientID:      "my-client",
490				UsernameClaim: "username",
491				GroupsClaim:   "groups",
492				now:           func() time.Time { return now },
493			},
494			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
495			pubKeys: []*jose.JSONWebKey{
496				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
497			},
498			claims: fmt.Sprintf(`{
499				"iss": "{{.URL}}",
500				"aud": "my-client",
501				"username": "jane",
502				"_claim_names": {
503						"groups": "src1"
504				},
505				"_claim_sources": {
506						"src1": {
507								"endpoint": "{{.URL}}/groups",
508								"access_token": "groups_token"
509						}
510				},
511				"exp": %d
512			}`, valid.Unix()),
513			claimToResponseMap: map[string]string{
514				"groups": fmt.Sprintf(`{
515					"iss": "{{.URL}}",
516				    "aud": "my-client",
517					"groups": ["team1", "team2"],
518					"exp": %d
519			     }`, valid.Unix()),
520			},
521			openIDConfig: `{
522					"issuer": "{{.URL}}",
523					"jwks_uri": "{{.URL}}/.testing/keys"
524			}`,
525			want: &user.DefaultInfo{
526				Name:   "jane",
527				Groups: []string{"team1", "team2"},
528			},
529		},
530		{
531			name: "groups-distributed-malformed-claim-names",
532			options: Options{
533				IssuerURL:     "{{.URL}}",
534				ClientID:      "my-client",
535				UsernameClaim: "username",
536				GroupsClaim:   "groups",
537				now:           func() time.Time { return now },
538			},
539			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
540			pubKeys: []*jose.JSONWebKey{
541				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
542			},
543			claims: fmt.Sprintf(`{
544				"iss": "{{.URL}}",
545				"aud": "my-client",
546				"username": "jane",
547				"_claim_names": {
548						"groups": "nonexistent-claim-source"
549				},
550				"_claim_sources": {
551						"src1": {
552								"endpoint": "{{.URL}}/groups",
553								"access_token": "groups_token"
554						}
555				},
556				"exp": %d
557			}`, valid.Unix()),
558			claimToResponseMap: map[string]string{
559				"groups": fmt.Sprintf(`{
560					"iss": "{{.URL}}",
561				    "aud": "my-client",
562					"groups": ["team1", "team2"],
563					"exp": %d
564			     }`, valid.Unix()),
565			},
566			openIDConfig: `{
567					"issuer": "{{.URL}}",
568					"jwks_uri": "{{.URL}}/.testing/keys"
569			}`,
570			wantErr: true,
571		},
572		{
573			name: "groups-distributed-malformed-names-and-sources",
574			options: Options{
575				IssuerURL:     "{{.URL}}",
576				ClientID:      "my-client",
577				UsernameClaim: "username",
578				GroupsClaim:   "groups",
579				now:           func() time.Time { return now },
580			},
581			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
582			pubKeys: []*jose.JSONWebKey{
583				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
584			},
585			claims: fmt.Sprintf(`{
586				"iss": "{{.URL}}",
587				"aud": "my-client",
588				"username": "jane",
589				"_claim_names": {
590						"groups": "src1"
591				},
592				"exp": %d
593			}`, valid.Unix()),
594			claimToResponseMap: map[string]string{
595				"groups": fmt.Sprintf(`{
596					"iss": "{{.URL}}",
597				    "aud": "my-client",
598					"groups": ["team1", "team2"],
599					"exp": %d
600			     }`, valid.Unix()),
601			},
602			openIDConfig: `{
603					"issuer": "{{.URL}}",
604					"jwks_uri": "{{.URL}}/.testing/keys"
605			}`,
606			wantErr: true,
607		},
608		{
609			name: "groups-distributed-malformed-distributed-claim",
610			options: Options{
611				IssuerURL:     "{{.URL}}",
612				ClientID:      "my-client",
613				UsernameClaim: "username",
614				GroupsClaim:   "groups",
615				now:           func() time.Time { return now },
616			},
617			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
618			pubKeys: []*jose.JSONWebKey{
619				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
620			},
621			claims: fmt.Sprintf(`{
622				"iss": "{{.URL}}",
623				"aud": "my-client",
624				"username": "jane",
625				"_claim_names": {
626						"groups": "src1"
627				},
628				"_claim_sources": {
629						"src1": {
630								"endpoint": "{{.URL}}/groups",
631								"access_token": "groups_token"
632						}
633				},
634				"exp": %d
635			}`, valid.Unix()),
636			claimToResponseMap: map[string]string{
637				// Doesn't contain the "groups" claim as it promises.
638				"groups": fmt.Sprintf(`{
639					"iss": "{{.URL}}",
640				    "aud": "my-client",
641					"exp": %d
642			     }`, valid.Unix()),
643			},
644			openIDConfig: `{
645					"issuer": "{{.URL}}",
646					"jwks_uri": "{{.URL}}/.testing/keys"
647			}`,
648			wantErr: true,
649		},
650		{
651			name: "groups-distributed-unusual-name",
652			options: Options{
653				IssuerURL:     "{{.URL}}",
654				ClientID:      "my-client",
655				UsernameClaim: "username",
656				GroupsClaim:   "rabbits",
657				now:           func() time.Time { return now },
658			},
659			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
660			pubKeys: []*jose.JSONWebKey{
661				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
662			},
663			claims: fmt.Sprintf(`{
664				"iss": "{{.URL}}",
665				"aud": "my-client",
666				"username": "jane",
667				"_claim_names": {
668						"rabbits": "src1"
669				},
670				"_claim_sources": {
671						"src1": {
672								"endpoint": "{{.URL}}/rabbits",
673								"access_token": "rabbits_token"
674						}
675				},
676				"exp": %d
677			}`, valid.Unix()),
678			claimToResponseMap: map[string]string{
679				"rabbits": fmt.Sprintf(`{
680					"iss": "{{.URL}}",
681				    "aud": "my-client",
682					"rabbits": ["team1", "team2"],
683					"exp": %d
684			     }`, valid.Unix()),
685			},
686			openIDConfig: `{
687					"issuer": "{{.URL}}",
688					"jwks_uri": "{{.URL}}/.testing/keys"
689			}`,
690			want: &user.DefaultInfo{
691				Name:   "jane",
692				Groups: []string{"team1", "team2"},
693			},
694		},
695		{
696			name: "groups-distributed-wrong-audience",
697			options: Options{
698				IssuerURL:     "{{.URL}}",
699				ClientID:      "my-client",
700				UsernameClaim: "username",
701				GroupsClaim:   "groups",
702				now:           func() time.Time { return now },
703			},
704			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
705			pubKeys: []*jose.JSONWebKey{
706				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
707			},
708			claims: fmt.Sprintf(`{
709				"iss": "{{.URL}}",
710				"aud": "my-client",
711				"username": "jane",
712				"_claim_names": {
713						"groups": "src1"
714				},
715				"_claim_sources": {
716						"src1": {
717								"endpoint": "{{.URL}}/groups",
718								"access_token": "groups_token"
719						}
720				},
721				"exp": %d
722			}`, valid.Unix()),
723			claimToResponseMap: map[string]string{
724				// Note mismatching "aud"
725				"groups": fmt.Sprintf(`{
726					"iss": "{{.URL}}",
727				    "aud": "your-client",
728					"groups": ["team1", "team2"],
729					"exp": %d
730			     }`, valid.Unix()),
731			},
732			openIDConfig: `{
733					"issuer": "{{.URL}}",
734					"jwks_uri": "{{.URL}}/.testing/keys"
735			}`,
736			// "aud" was "your-client", not "my-client"
737			wantErr: true,
738		},
739		{
740			name: "groups-distributed-wrong-audience",
741			options: Options{
742				IssuerURL:     "{{.URL}}",
743				ClientID:      "my-client",
744				UsernameClaim: "username",
745				GroupsClaim:   "groups",
746				now:           func() time.Time { return now },
747			},
748			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
749			pubKeys: []*jose.JSONWebKey{
750				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
751			},
752			claims: fmt.Sprintf(`{
753				"iss": "{{.URL}}",
754				"aud": "my-client",
755				"username": "jane",
756				"_claim_names": {
757						"groups": "src1"
758				},
759				"_claim_sources": {
760						"src1": {
761								"endpoint": "{{.URL}}/groups",
762								"access_token": "groups_token"
763						}
764				},
765				"exp": %d
766			}`, valid.Unix()),
767			claimToResponseMap: map[string]string{
768				// Note expired timestamp.
769				"groups": fmt.Sprintf(`{
770					"iss": "{{.URL}}",
771				    "aud": "my-client",
772					"groups": ["team1", "team2"],
773					"exp": %d
774			     }`, expired.Unix()),
775			},
776			openIDConfig: `{
777					"issuer": "{{.URL}}",
778					"jwks_uri": "{{.URL}}/.testing/keys"
779			}`,
780			// The distributed token is expired.
781			wantErr: true,
782		},
783		{
784			// Specs are unclear about this behavior.  We adopt a behavior where
785			// normal claim wins over a distributed claim by the same name.
786			name: "groups-distributed-normal-claim-wins",
787			options: Options{
788				IssuerURL:     "{{.URL}}",
789				ClientID:      "my-client",
790				UsernameClaim: "username",
791				GroupsClaim:   "groups",
792				now:           func() time.Time { return now },
793			},
794			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
795			pubKeys: []*jose.JSONWebKey{
796				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
797			},
798			claims: fmt.Sprintf(`{
799				"iss": "{{.URL}}",
800				"aud": "my-client",
801				"username": "jane",
802				"groups": "team1",
803				"_claim_names": {
804						"groups": "src1"
805				},
806				"_claim_sources": {
807						"src1": {
808								"endpoint": "{{.URL}}/groups",
809								"access_token": "groups_token"
810						}
811				},
812				"exp": %d
813			}`, valid.Unix()),
814			claimToResponseMap: map[string]string{
815				"groups": fmt.Sprintf(`{
816					"iss": "{{.URL}}",
817				    "aud": "my-client",
818					"groups": ["team2"],
819					"exp": %d
820			     }`, valid.Unix()),
821			},
822			openIDConfig: `{
823					"issuer": "{{.URL}}",
824					"jwks_uri": "{{.URL}}/.testing/keys"
825			}`,
826			want: &user.DefaultInfo{
827				Name: "jane",
828				// "team1" is from the normal "groups" claim.
829				Groups: []string{"team1"},
830			},
831		},
832		{
833			// Groups should be able to be a single string, not just a slice.
834			name: "group-string-claim",
835			options: Options{
836				IssuerURL:     "https://auth.example.com",
837				ClientID:      "my-client",
838				UsernameClaim: "username",
839				GroupsClaim:   "groups",
840				now:           func() time.Time { return now },
841			},
842			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
843			pubKeys: []*jose.JSONWebKey{
844				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
845			},
846			claims: fmt.Sprintf(`{
847				"iss": "https://auth.example.com",
848				"aud": "my-client",
849				"username": "jane",
850				"groups": "team1",
851				"exp": %d
852			}`, valid.Unix()),
853			want: &user.DefaultInfo{
854				Name:   "jane",
855				Groups: []string{"team1"},
856			},
857		},
858		{
859			// Groups should be able to be a single string, not just a slice.
860			name: "group-string-claim-distributed",
861			options: Options{
862				IssuerURL:     "{{.URL}}",
863				ClientID:      "my-client",
864				UsernameClaim: "username",
865				GroupsClaim:   "groups",
866				now:           func() time.Time { return now },
867			},
868			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
869			pubKeys: []*jose.JSONWebKey{
870				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
871			},
872			claims: fmt.Sprintf(`{
873				"iss": "{{.URL}}",
874				"aud": "my-client",
875				"username": "jane",
876				"_claim_names": {
877						"groups": "src1"
878				},
879				"_claim_sources": {
880						"src1": {
881								"endpoint": "{{.URL}}/groups",
882								"access_token": "groups_token"
883						}
884				},
885				"exp": %d
886			}`, valid.Unix()),
887			claimToResponseMap: map[string]string{
888				"groups": fmt.Sprintf(`{
889					"iss": "{{.URL}}",
890				    "aud": "my-client",
891					"groups": "team1",
892					"exp": %d
893			     }`, valid.Unix()),
894			},
895			openIDConfig: `{
896					"issuer": "{{.URL}}",
897					"jwks_uri": "{{.URL}}/.testing/keys"
898			}`,
899			want: &user.DefaultInfo{
900				Name:   "jane",
901				Groups: []string{"team1"},
902			},
903		},
904		{
905			name: "group-string-claim-aggregated-not-supported",
906			options: Options{
907				IssuerURL:     "https://auth.example.com",
908				ClientID:      "my-client",
909				UsernameClaim: "username",
910				GroupsClaim:   "groups",
911				now:           func() time.Time { return now },
912			},
913			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
914			pubKeys: []*jose.JSONWebKey{
915				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
916			},
917			claims: fmt.Sprintf(`{
918				"iss": "https://auth.example.com",
919				"aud": "my-client",
920				"username": "jane",
921				"_claim_names": {
922						"groups": "src1"
923				},
924				"_claim_sources": {
925						"src1": {
926								"JWT": "some.jwt.token"
927						}
928				},
929				"exp": %d
930			}`, valid.Unix()),
931			want: &user.DefaultInfo{
932				Name: "jane",
933			},
934		},
935		{
936			// if the groups claim isn't provided, this shouldn't error out
937			name: "no-groups-claim",
938			options: Options{
939				IssuerURL:     "https://auth.example.com",
940				ClientID:      "my-client",
941				UsernameClaim: "username",
942				GroupsClaim:   "groups",
943				now:           func() time.Time { return now },
944			},
945			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
946			pubKeys: []*jose.JSONWebKey{
947				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
948			},
949			claims: fmt.Sprintf(`{
950				"iss": "https://auth.example.com",
951				"aud": "my-client",
952				"username": "jane",
953				"exp": %d
954			}`, valid.Unix()),
955			want: &user.DefaultInfo{
956				Name: "jane",
957			},
958		},
959		{
960			name: "invalid-groups-claim",
961			options: Options{
962				IssuerURL:     "https://auth.example.com",
963				ClientID:      "my-client",
964				UsernameClaim: "username",
965				GroupsClaim:   "groups",
966				now:           func() time.Time { return now },
967			},
968			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
969			pubKeys: []*jose.JSONWebKey{
970				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
971			},
972			claims: fmt.Sprintf(`{
973				"iss": "https://auth.example.com",
974				"aud": "my-client",
975				"username": "jane",
976				"groups": 42,
977				"exp": %d
978			}`, valid.Unix()),
979			wantErr: true,
980		},
981		{
982			name: "required-claim",
983			options: Options{
984				IssuerURL:     "https://auth.example.com",
985				ClientID:      "my-client",
986				UsernameClaim: "username",
987				GroupsClaim:   "groups",
988				RequiredClaims: map[string]string{
989					"hd":  "example.com",
990					"sub": "test",
991				},
992				now: func() time.Time { return now },
993			},
994			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
995			pubKeys: []*jose.JSONWebKey{
996				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
997			},
998			claims: fmt.Sprintf(`{
999				"iss": "https://auth.example.com",
1000				"aud": "my-client",
1001				"username": "jane",
1002				"hd": "example.com",
1003				"sub": "test",
1004				"exp": %d
1005			}`, valid.Unix()),
1006			want: &user.DefaultInfo{
1007				Name: "jane",
1008			},
1009		},
1010		{
1011			name: "no-required-claim",
1012			options: Options{
1013				IssuerURL:     "https://auth.example.com",
1014				ClientID:      "my-client",
1015				UsernameClaim: "username",
1016				GroupsClaim:   "groups",
1017				RequiredClaims: map[string]string{
1018					"hd": "example.com",
1019				},
1020				now: func() time.Time { return now },
1021			},
1022			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1023			pubKeys: []*jose.JSONWebKey{
1024				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1025			},
1026			claims: fmt.Sprintf(`{
1027				"iss": "https://auth.example.com",
1028				"aud": "my-client",
1029				"username": "jane",
1030				"exp": %d
1031			}`, valid.Unix()),
1032			wantErr: true,
1033		},
1034		{
1035			name: "invalid-required-claim",
1036			options: Options{
1037				IssuerURL:     "https://auth.example.com",
1038				ClientID:      "my-client",
1039				UsernameClaim: "username",
1040				GroupsClaim:   "groups",
1041				RequiredClaims: map[string]string{
1042					"hd": "example.com",
1043				},
1044				now: func() time.Time { return now },
1045			},
1046			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1047			pubKeys: []*jose.JSONWebKey{
1048				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1049			},
1050			claims: fmt.Sprintf(`{
1051				"iss": "https://auth.example.com",
1052				"aud": "my-client",
1053				"username": "jane",
1054				"hd": "example.org",
1055				"exp": %d
1056			}`, valid.Unix()),
1057			wantErr: true,
1058		},
1059		{
1060			name: "invalid-signature",
1061			options: Options{
1062				IssuerURL:     "https://auth.example.com",
1063				ClientID:      "my-client",
1064				UsernameClaim: "username",
1065				now:           func() time.Time { return now },
1066			},
1067			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1068			pubKeys: []*jose.JSONWebKey{
1069				loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256),
1070			},
1071			claims: fmt.Sprintf(`{
1072				"iss": "https://auth.example.com",
1073				"aud": "my-client",
1074				"username": "jane",
1075				"exp": %d
1076			}`, valid.Unix()),
1077			wantErr: true,
1078		},
1079		{
1080			name: "expired",
1081			options: Options{
1082				IssuerURL:     "https://auth.example.com",
1083				ClientID:      "my-client",
1084				UsernameClaim: "username",
1085				now:           func() time.Time { return now },
1086			},
1087			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1088			pubKeys: []*jose.JSONWebKey{
1089				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1090			},
1091			claims: fmt.Sprintf(`{
1092				"iss": "https://auth.example.com",
1093				"aud": "my-client",
1094				"username": "jane",
1095				"exp": %d
1096			}`, expired.Unix()),
1097			wantErr: true,
1098		},
1099		{
1100			name: "invalid-aud",
1101			options: Options{
1102				IssuerURL:     "https://auth.example.com",
1103				ClientID:      "my-client",
1104				UsernameClaim: "username",
1105				now:           func() time.Time { return now },
1106			},
1107			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1108			pubKeys: []*jose.JSONWebKey{
1109				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1110			},
1111			claims: fmt.Sprintf(`{
1112				"iss": "https://auth.example.com",
1113				"aud": "not-my-client",
1114				"username": "jane",
1115				"exp": %d
1116			}`, valid.Unix()),
1117			wantErr: true,
1118		},
1119		{
1120			// ID tokens may contain multiple audiences:
1121			// https://openid.net/specs/openid-connect-core-1_0.html#IDToken
1122			name: "multiple-audiences",
1123			options: Options{
1124				IssuerURL:     "https://auth.example.com",
1125				ClientID:      "my-client",
1126				UsernameClaim: "username",
1127				now:           func() time.Time { return now },
1128			},
1129			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1130			pubKeys: []*jose.JSONWebKey{
1131				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1132			},
1133			claims: fmt.Sprintf(`{
1134				"iss": "https://auth.example.com",
1135				"aud": ["not-my-client", "my-client"],
1136				"azp": "not-my-client",
1137				"username": "jane",
1138				"exp": %d
1139			}`, valid.Unix()),
1140			want: &user.DefaultInfo{
1141				Name: "jane",
1142			},
1143		},
1144		{
1145			name: "invalid-issuer",
1146			options: Options{
1147				IssuerURL:     "https://auth.example.com",
1148				ClientID:      "my-client",
1149				UsernameClaim: "username",
1150				now:           func() time.Time { return now },
1151			},
1152			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1153			pubKeys: []*jose.JSONWebKey{
1154				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1155			},
1156			claims: fmt.Sprintf(`{
1157				"iss": "https://example.com",
1158				"aud": "my-client",
1159				"username": "jane",
1160				"exp": %d
1161			}`, valid.Unix()),
1162			wantSkip: true,
1163		},
1164		{
1165			name: "username-prefix",
1166			options: Options{
1167				IssuerURL:      "https://auth.example.com",
1168				ClientID:       "my-client",
1169				UsernameClaim:  "username",
1170				UsernamePrefix: "oidc:",
1171				now:            func() time.Time { return now },
1172			},
1173			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1174			pubKeys: []*jose.JSONWebKey{
1175				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1176			},
1177			claims: fmt.Sprintf(`{
1178				"iss": "https://auth.example.com",
1179				"aud": "my-client",
1180				"username": "jane",
1181				"exp": %d
1182			}`, valid.Unix()),
1183			want: &user.DefaultInfo{
1184				Name: "oidc:jane",
1185			},
1186		},
1187		{
1188			name: "groups-prefix",
1189			options: Options{
1190				IssuerURL:      "https://auth.example.com",
1191				ClientID:       "my-client",
1192				UsernameClaim:  "username",
1193				UsernamePrefix: "oidc:",
1194				GroupsClaim:    "groups",
1195				GroupsPrefix:   "groups:",
1196				now:            func() time.Time { return now },
1197			},
1198			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1199			pubKeys: []*jose.JSONWebKey{
1200				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1201			},
1202			claims: fmt.Sprintf(`{
1203				"iss": "https://auth.example.com",
1204				"aud": "my-client",
1205				"username": "jane",
1206				"groups": ["team1", "team2"],
1207				"exp": %d
1208			}`, valid.Unix()),
1209			want: &user.DefaultInfo{
1210				Name:   "oidc:jane",
1211				Groups: []string{"groups:team1", "groups:team2"},
1212			},
1213		},
1214		{
1215			name: "groups-prefix-distributed",
1216			options: Options{
1217				IssuerURL:      "{{.URL}}",
1218				ClientID:       "my-client",
1219				UsernameClaim:  "username",
1220				UsernamePrefix: "oidc:",
1221				GroupsClaim:    "groups",
1222				GroupsPrefix:   "groups:",
1223				now:            func() time.Time { return now },
1224			},
1225			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1226			pubKeys: []*jose.JSONWebKey{
1227				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1228			},
1229			claims: fmt.Sprintf(`{
1230				"iss": "{{.URL}}",
1231				"aud": "my-client",
1232				"username": "jane",
1233				"_claim_names": {
1234						"groups": "src1"
1235				},
1236				"_claim_sources": {
1237						"src1": {
1238								"endpoint": "{{.URL}}/groups",
1239								"access_token": "groups_token"
1240						}
1241				},
1242				"exp": %d
1243			}`, valid.Unix()),
1244			claimToResponseMap: map[string]string{
1245				"groups": fmt.Sprintf(`{
1246					"iss": "{{.URL}}",
1247				    "aud": "my-client",
1248					"groups": ["team1", "team2"],
1249					"exp": %d
1250			     }`, valid.Unix()),
1251			},
1252			openIDConfig: `{
1253					"issuer": "{{.URL}}",
1254					"jwks_uri": "{{.URL}}/.testing/keys"
1255			}`,
1256			want: &user.DefaultInfo{
1257				Name:   "oidc:jane",
1258				Groups: []string{"groups:team1", "groups:team2"},
1259			},
1260		},
1261		{
1262			name: "invalid-signing-alg",
1263			options: Options{
1264				IssuerURL:     "https://auth.example.com",
1265				ClientID:      "my-client",
1266				UsernameClaim: "username",
1267				now:           func() time.Time { return now },
1268			},
1269			// Correct key but invalid signature algorithm "PS256"
1270			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256),
1271			pubKeys: []*jose.JSONWebKey{
1272				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1273			},
1274			claims: fmt.Sprintf(`{
1275				"iss": "https://auth.example.com",
1276				"aud": "my-client",
1277				"username": "jane",
1278				"exp": %d
1279			}`, valid.Unix()),
1280			wantErr: true,
1281		},
1282		{
1283			name: "ps256",
1284			options: Options{
1285				IssuerURL:            "https://auth.example.com",
1286				ClientID:             "my-client",
1287				UsernameClaim:        "username",
1288				SupportedSigningAlgs: []string{"PS256"},
1289				now:                  func() time.Time { return now },
1290			},
1291			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256),
1292			pubKeys: []*jose.JSONWebKey{
1293				loadRSAKey(t, "testdata/rsa_1.pem", jose.PS256),
1294			},
1295			claims: fmt.Sprintf(`{
1296				"iss": "https://auth.example.com",
1297				"aud": "my-client",
1298				"username": "jane",
1299				"exp": %d
1300			}`, valid.Unix()),
1301			want: &user.DefaultInfo{
1302				Name: "jane",
1303			},
1304		},
1305		{
1306			name: "es512",
1307			options: Options{
1308				IssuerURL:            "https://auth.example.com",
1309				ClientID:             "my-client",
1310				UsernameClaim:        "username",
1311				SupportedSigningAlgs: []string{"ES512"},
1312				now:                  func() time.Time { return now },
1313			},
1314			signingKey: loadECDSAPrivKey(t, "testdata/ecdsa_2.pem", jose.ES512),
1315			pubKeys: []*jose.JSONWebKey{
1316				loadECDSAKey(t, "testdata/ecdsa_1.pem", jose.ES512),
1317				loadECDSAKey(t, "testdata/ecdsa_2.pem", jose.ES512),
1318			},
1319			claims: fmt.Sprintf(`{
1320				"iss": "https://auth.example.com",
1321				"aud": "my-client",
1322				"username": "jane",
1323				"exp": %d
1324			}`, valid.Unix()),
1325			want: &user.DefaultInfo{
1326				Name: "jane",
1327			},
1328		},
1329		{
1330			name: "not-https",
1331			options: Options{
1332				IssuerURL:     "http://auth.example.com",
1333				ClientID:      "my-client",
1334				UsernameClaim: "username",
1335				now:           func() time.Time { return now },
1336			},
1337			pubKeys: []*jose.JSONWebKey{
1338				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1339			},
1340			wantInitErr: true,
1341		},
1342		{
1343			name: "no-username-claim",
1344			options: Options{
1345				IssuerURL: "https://auth.example.com",
1346				ClientID:  "my-client",
1347				now:       func() time.Time { return now },
1348			},
1349			pubKeys: []*jose.JSONWebKey{
1350				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1351			},
1352			wantInitErr: true,
1353		},
1354		{
1355			name: "invalid-sig-alg",
1356			options: Options{
1357				IssuerURL:            "https://auth.example.com",
1358				ClientID:             "my-client",
1359				UsernameClaim:        "username",
1360				SupportedSigningAlgs: []string{"HS256"},
1361				now:                  func() time.Time { return now },
1362			},
1363			pubKeys: []*jose.JSONWebKey{
1364				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1365			},
1366			wantInitErr: true,
1367		},
1368		{
1369			name: "accounts.google.com issuer",
1370			options: Options{
1371				IssuerURL:     "https://accounts.google.com",
1372				ClientID:      "my-client",
1373				UsernameClaim: "email",
1374				now:           func() time.Time { return now },
1375			},
1376			claims: fmt.Sprintf(`{
1377				"iss": "accounts.google.com",
1378				"email": "thomas.jefferson@gmail.com",
1379				"aud": "my-client",
1380				"exp": %d
1381			}`, valid.Unix()),
1382			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1383			pubKeys: []*jose.JSONWebKey{
1384				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1385			},
1386			want: &user.DefaultInfo{
1387				Name: "thomas.jefferson@gmail.com",
1388			},
1389		},
1390		{
1391			name: "good token with bad client id",
1392			options: Options{
1393				IssuerURL:     "https://auth.example.com",
1394				ClientID:      "my-client",
1395				UsernameClaim: "username",
1396				now:           func() time.Time { return now },
1397			},
1398			signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
1399			pubKeys: []*jose.JSONWebKey{
1400				loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
1401			},
1402			claims: fmt.Sprintf(`{
1403				"iss": "https://auth.example.com",
1404				"aud": "my-wrong-client",
1405				"username": "jane",
1406				"exp": %d
1407			}`, valid.Unix()),
1408			wantErr: true,
1409		},
1410	}
1411	for _, test := range tests {
1412		t.Run(test.name, test.run)
1413	}
1414}
1415
1416func TestUnmarshalClaimError(t *testing.T) {
1417	// Ensure error strings returned by unmarshaling claims don't include the claim.
1418	const token = "96bb299a-02e9-11e8-8673-54ee7553240e" // Fake token for testing.
1419	payload := fmt.Sprintf(`{
1420		"token": "%s"
1421	}`, token)
1422
1423	var c claims
1424	if err := json.Unmarshal([]byte(payload), &c); err != nil {
1425		t.Fatal(err)
1426	}
1427	var n int
1428	err := c.unmarshalClaim("token", &n)
1429	if err == nil {
1430		t.Fatal("expected error")
1431	}
1432
1433	if strings.Contains(err.Error(), token) {
1434		t.Fatalf("unmarshal error included token")
1435	}
1436}
1437
1438func TestUnmarshalClaim(t *testing.T) {
1439	tests := []struct {
1440		name    string
1441		claims  string
1442		do      func(claims) (interface{}, error)
1443		want    interface{}
1444		wantErr bool
1445	}{
1446		{
1447			name:   "string claim",
1448			claims: `{"aud":"foo"}`,
1449			do: func(c claims) (interface{}, error) {
1450				var s string
1451				err := c.unmarshalClaim("aud", &s)
1452				return s, err
1453			},
1454			want: "foo",
1455		},
1456		{
1457			name:   "mismatched types",
1458			claims: `{"aud":"foo"}`,
1459			do: func(c claims) (interface{}, error) {
1460				var n int
1461				err := c.unmarshalClaim("aud", &n)
1462				return n, err
1463
1464			},
1465			wantErr: true,
1466		},
1467		{
1468			name:   "bool claim",
1469			claims: `{"email":"foo@coreos.com","email_verified":true}`,
1470			do: func(c claims) (interface{}, error) {
1471				var verified bool
1472				err := c.unmarshalClaim("email_verified", &verified)
1473				return verified, err
1474			},
1475			want: true,
1476		},
1477		{
1478			name:   "strings claim",
1479			claims: `{"groups":["a","b","c"]}`,
1480			do: func(c claims) (interface{}, error) {
1481				var groups []string
1482				err := c.unmarshalClaim("groups", &groups)
1483				return groups, err
1484			},
1485			want: []string{"a", "b", "c"},
1486		},
1487	}
1488
1489	for _, test := range tests {
1490		t.Run(test.name, func(t *testing.T) {
1491			var c claims
1492			if err := json.Unmarshal([]byte(test.claims), &c); err != nil {
1493				t.Fatal(err)
1494			}
1495
1496			got, err := test.do(c)
1497			if err != nil {
1498				if test.wantErr {
1499					return
1500				}
1501				t.Fatalf("unexpected error: %v", err)
1502			}
1503			if test.wantErr {
1504				t.Fatalf("expected error")
1505			}
1506
1507			if !reflect.DeepEqual(got, test.want) {
1508				t.Errorf("wanted=%#v, got=%#v", test.want, got)
1509			}
1510		})
1511	}
1512}
1513