/* Copyright 2015 The Kubernetes Authors. 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 oidc import ( "bytes" "context" "crypto" "crypto/x509" "encoding/hex" "encoding/json" "encoding/pem" "fmt" "io/ioutil" "net/http" "net/http/httptest" "os" "reflect" "strings" "testing" "text/template" "time" oidc "github.com/coreos/go-oidc" jose "gopkg.in/square/go-jose.v2" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/klog" ) // utilities for loading JOSE keys. func loadRSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { key, err := x509.ParsePKCS1PrivateKey(b) if err != nil { return nil, err } return key.Public(), nil }) } func loadRSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { return x509.ParsePKCS1PrivateKey(b) }) } func loadECDSAKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { key, err := x509.ParseECPrivateKey(b) if err != nil { return nil, err } return key.Public(), nil }) } func loadECDSAPrivKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm) *jose.JSONWebKey { return loadKey(t, filepath, alg, func(b []byte) (interface{}, error) { return x509.ParseECPrivateKey(b) }) } func loadKey(t *testing.T, filepath string, alg jose.SignatureAlgorithm, unmarshal func([]byte) (interface{}, error)) *jose.JSONWebKey { data, err := ioutil.ReadFile(filepath) if err != nil { t.Fatalf("load file: %v", err) } block, _ := pem.Decode(data) if block == nil { t.Fatalf("file contained no PEM encoded data: %s", filepath) } priv, err := unmarshal(block.Bytes) if err != nil { t.Fatalf("unmarshal key: %v", err) } key := &jose.JSONWebKey{Key: priv, Use: "sig", Algorithm: string(alg)} thumbprint, err := key.Thumbprint(crypto.SHA256) if err != nil { t.Fatalf("computing thumbprint: %v", err) } key.KeyID = hex.EncodeToString(thumbprint) return key } // staticKeySet implements oidc.KeySet. type staticKeySet struct { keys []*jose.JSONWebKey } func (s *staticKeySet) VerifySignature(ctx context.Context, jwt string) (payload []byte, err error) { jws, err := jose.ParseSigned(jwt) if err != nil { return nil, err } if len(jws.Signatures) == 0 { return nil, fmt.Errorf("jwt contained no signatures") } kid := jws.Signatures[0].Header.KeyID for _, key := range s.keys { if key.KeyID == kid { return jws.Verify(key) } } return nil, fmt.Errorf("no keys matches jwk keyid") } var ( expired, _ = time.Parse(time.RFC3339Nano, "2009-11-10T22:00:00Z") now, _ = time.Parse(time.RFC3339Nano, "2009-11-10T23:00:00Z") valid, _ = time.Parse(time.RFC3339Nano, "2009-11-11T00:00:00Z") ) type claimsTest struct { name string options Options now time.Time signingKey *jose.JSONWebKey pubKeys []*jose.JSONWebKey claims string want *user.DefaultInfo wantSkip bool wantErr bool wantInitErr bool claimToResponseMap map[string]string openIDConfig string reqAudiences authenticator.Audiences } // Replace formats the contents of v into the provided template. func replace(tmpl string, v interface{}) string { t := template.Must(template.New("test").Parse(tmpl)) buf := bytes.NewBuffer(nil) t.Execute(buf, &v) ret := buf.String() klog.V(4).Infof("Replaced: %v into: %v", tmpl, ret) return ret } // newClaimServer returns a new test HTTPS server, which is rigged to return // OIDC responses to requests that resolve distributed claims. signer is the // signer used for the served JWT tokens. claimToResponseMap is a map of // responses that the server will return for each claim it is given. func newClaimServer(t *testing.T, keys jose.JSONWebKeySet, signer jose.Signer, claimToResponseMap map[string]string, openIDConfig *string) *httptest.Server { ts := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { klog.V(5).Infof("request: %+v", *r) switch r.URL.Path { case "/.testing/keys": w.Header().Set("Content-Type", "application/json") keyBytes, err := json.Marshal(keys) if err != nil { t.Fatalf("unexpected error while marshaling keys: %v", err) } klog.V(5).Infof("%v: returning: %+v", r.URL, string(keyBytes)) w.Write(keyBytes) case "/.well-known/openid-configuration": w.Header().Set("Content-Type", "application/json") klog.V(5).Infof("%v: returning: %+v", r.URL, *openIDConfig) w.Write([]byte(*openIDConfig)) // These claims are tested in the unit tests. case "/groups": fallthrough case "/rabbits": if claimToResponseMap == nil { t.Errorf("no claims specified in response") } claim := r.URL.Path[1:] // "/groups" -> "groups" expectedAuth := fmt.Sprintf("Bearer %v_token", claim) auth := r.Header.Get("Authorization") if auth != expectedAuth { t.Errorf("bearer token expected: %q, was %q", expectedAuth, auth) } jws, err := signer.Sign([]byte(claimToResponseMap[claim])) if err != nil { t.Errorf("while signing response token: %v", err) } token, err := jws.CompactSerialize() if err != nil { t.Errorf("while serializing response token: %v", err) } w.Write([]byte(token)) default: w.WriteHeader(http.StatusNotFound) fmt.Fprintf(w, "unexpected URL: %v", r.URL) } })) klog.V(4).Infof("Serving OIDC at: %v", ts.URL) return ts } // writeTempCert writes out the supplied certificate into a temporary file in // PEM-encoded format. Returns the name of the temporary file used. The caller // is responsible for cleaning the file up. func writeTempCert(t *testing.T, cert []byte) string { tempFile, err := ioutil.TempFile("", "ca.crt") if err != nil { t.Fatalf("could not open temp file: %v", err) } block := &pem.Block{ Type: "CERTIFICATE", Bytes: cert, } if err := pem.Encode(tempFile, block); err != nil { t.Fatalf("could not write to temp file %v: %v", tempFile.Name(), err) } tempFile.Close() return tempFile.Name() } func toKeySet(keys []*jose.JSONWebKey) jose.JSONWebKeySet { ret := jose.JSONWebKeySet{} for _, k := range keys { ret.Keys = append(ret.Keys, *k) } return ret } func (c *claimsTest) run(t *testing.T) { var ( signer jose.Signer err error ) if c.signingKey != nil { // Initialize the signer only in the tests that make use of it. We can // not defer this initialization because the test server uses it too. signer, err = jose.NewSigner(jose.SigningKey{ Algorithm: jose.SignatureAlgorithm(c.signingKey.Algorithm), Key: c.signingKey, }, nil) if err != nil { t.Fatalf("initialize signer: %v", err) } } // The HTTPS server used for requesting distributed groups claims. ts := newClaimServer(t, toKeySet(c.pubKeys), signer, c.claimToResponseMap, &c.openIDConfig) defer ts.Close() // Make the certificate of the helper server available to the authenticator // by writing its root CA certificate into a temporary file. tempFileName := writeTempCert(t, ts.TLS.Certificates[0].Certificate[0]) defer os.Remove(tempFileName) c.options.CAFile = tempFileName // Allow claims to refer to the serving URL of the test server. For this, // substitute all references to {{.URL}} in appropriate places. v := struct{ URL string }{URL: ts.URL} c.claims = replace(c.claims, &v) c.openIDConfig = replace(c.openIDConfig, &v) c.options.IssuerURL = replace(c.options.IssuerURL, &v) for claim, response := range c.claimToResponseMap { c.claimToResponseMap[claim] = replace(response, &v) } // Initialize the authenticator. a, err := newAuthenticator(c.options, func(ctx context.Context, a *Authenticator, config *oidc.Config) { // Set the verifier to use the public key set instead of reading from a remote. a.setVerifier(oidc.NewVerifier( c.options.IssuerURL, &staticKeySet{keys: c.pubKeys}, config, )) }) if err != nil { if !c.wantInitErr { t.Fatalf("initialize authenticator: %v", err) } return } if c.wantInitErr { t.Fatalf("wanted initialization error") } // Sign and serialize the claims in a JWT. jws, err := signer.Sign([]byte(c.claims)) if err != nil { t.Fatalf("sign claims: %v", err) } token, err := jws.CompactSerialize() if err != nil { t.Fatalf("serialize token: %v", err) } ctx := context.Background() if c.reqAudiences != nil { ctx = authenticator.WithAudiences(ctx, c.reqAudiences) } got, ok, err := a.AuthenticateToken(ctx, token) if err != nil { if !c.wantErr { t.Fatalf("authenticate token: %v", err) } return } if c.wantErr { t.Fatalf("expected error authenticating token") } if !ok { if !c.wantSkip { // We don't have any cases where we return (nil, false, nil) t.Fatalf("no error but token not authenticated") } return } if c.wantSkip { t.Fatalf("expected authenticator to skip token") } gotUser := got.User.(*user.DefaultInfo) if !reflect.DeepEqual(gotUser, c.want) { t.Fatalf("wanted user=%#v, got=%#v", c.want, gotUser) } } func TestToken(t *testing.T) { synchronizeTokenIDVerifierForTest = true tests := []claimsTest{ { name: "token", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "no-username", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "email", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "email", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "email": "jane@example.com", "email_verified": true, "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane@example.com", }, }, { name: "email-not-verified", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "email", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "email": "jane@example.com", "email_verified": false, "exp": %d }`, valid.Unix()), wantErr: true, }, { // If "email_verified" isn't present, assume true name: "no-email-verified-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "email", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "email": "jane@example.com", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane@example.com", }, }, { name: "invalid-email-verified-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "email", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, // string value for "email_verified" claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "email": "jane@example.com", "email_verified": "false", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "groups", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", Groups: []string{"team1", "team2"}, }, }, { name: "groups-distributed", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, want: &user.DefaultInfo{ Name: "jane", Groups: []string{"team1", "team2"}, }, }, { name: "groups-distributed-malformed-claim-names", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "nonexistent-claim-source" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, wantErr: true, }, { name: "groups-distributed-malformed-names-and-sources", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, wantErr: true, }, { name: "groups-distributed-malformed-distributed-claim", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ // Doesn't contain the "groups" claim as it promises. "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, wantErr: true, }, { name: "groups-distributed-unusual-name", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "rabbits", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "rabbits": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/rabbits", "access_token": "rabbits_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "rabbits": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "rabbits": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, want: &user.DefaultInfo{ Name: "jane", Groups: []string{"team1", "team2"}, }, }, { name: "groups-distributed-wrong-audience", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ // Note mismatching "aud" "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "your-client", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, // "aud" was "your-client", not "my-client" wantErr: true, }, { name: "groups-distributed-wrong-audience", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ // Note expired timestamp. "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team1", "team2"], "exp": %d }`, expired.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, // The distributed token is expired. wantErr: true, }, { // Specs are unclear about this behavior. We adopt a behavior where // normal claim wins over a distributed claim by the same name. name: "groups-distributed-normal-claim-wins", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "groups": "team1", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, want: &user.DefaultInfo{ Name: "jane", // "team1" is from the normal "groups" claim. Groups: []string{"team1"}, }, }, { // Groups should be able to be a single string, not just a slice. name: "group-string-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "groups": "team1", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", Groups: []string{"team1"}, }, }, { // Groups should be able to be a single string, not just a slice. name: "group-string-claim-distributed", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": "team1", "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, want: &user.DefaultInfo{ Name: "jane", Groups: []string{"team1"}, }, }, { name: "group-string-claim-aggregated-not-supported", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "JWT": "some.jwt.token" } }, "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { // if the groups claim isn't provided, this shouldn't error out name: "no-groups-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "invalid-groups-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "groups": 42, "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "required-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", RequiredClaims: map[string]string{ "hd": "example.com", "sub": "test", }, now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "hd": "example.com", "sub": "test", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "no-required-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", RequiredClaims: map[string]string{ "hd": "example.com", }, now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "invalid-required-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", GroupsClaim: "groups", RequiredClaims: map[string]string{ "hd": "example.com", }, now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "hd": "example.org", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "invalid-signature", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_2.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "expired", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, expired.Unix()), wantErr: true, }, { name: "invalid-aud", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "not-my-client", "username": "jane", "exp": %d }`, valid.Unix()), wantErr: true, }, { // ID tokens may contain multiple audiences: // https://openid.net/specs/openid-connect-core-1_0.html#IDToken name: "multiple-audiences", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": ["not-my-client", "my-client"], "azp": "not-my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "invalid-issuer", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), wantSkip: true, }, { name: "username-prefix", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", UsernamePrefix: "oidc:", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "oidc:jane", }, }, { name: "groups-prefix", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", UsernamePrefix: "oidc:", GroupsClaim: "groups", GroupsPrefix: "groups:", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "oidc:jane", Groups: []string{"groups:team1", "groups:team2"}, }, }, { name: "groups-prefix-distributed", options: Options{ IssuerURL: "{{.URL}}", ClientID: "my-client", UsernameClaim: "username", UsernamePrefix: "oidc:", GroupsClaim: "groups", GroupsPrefix: "groups:", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "username": "jane", "_claim_names": { "groups": "src1" }, "_claim_sources": { "src1": { "endpoint": "{{.URL}}/groups", "access_token": "groups_token" } }, "exp": %d }`, valid.Unix()), claimToResponseMap: map[string]string{ "groups": fmt.Sprintf(`{ "iss": "{{.URL}}", "aud": "my-client", "groups": ["team1", "team2"], "exp": %d }`, valid.Unix()), }, openIDConfig: `{ "issuer": "{{.URL}}", "jwks_uri": "{{.URL}}/.testing/keys" }`, want: &user.DefaultInfo{ Name: "oidc:jane", Groups: []string{"groups:team1", "groups:team2"}, }, }, { name: "invalid-signing-alg", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, // Correct key but invalid signature algorithm "PS256" signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), wantErr: true, }, { name: "ps256", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", SupportedSigningAlgs: []string{"PS256"}, now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.PS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.PS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "es512", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", SupportedSigningAlgs: []string{"ES512"}, now: func() time.Time { return now }, }, signingKey: loadECDSAPrivKey(t, "testdata/ecdsa_2.pem", jose.ES512), pubKeys: []*jose.JSONWebKey{ loadECDSAKey(t, "testdata/ecdsa_1.pem", jose.ES512), loadECDSAKey(t, "testdata/ecdsa_2.pem", jose.ES512), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), want: &user.DefaultInfo{ Name: "jane", }, }, { name: "not-https", options: Options{ IssuerURL: "http://auth.example.com", ClientID: "my-client", UsernameClaim: "username", now: func() time.Time { return now }, }, pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, wantInitErr: true, }, { name: "no-username-claim", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", now: func() time.Time { return now }, }, pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, wantInitErr: true, }, { name: "invalid-sig-alg", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", UsernameClaim: "username", SupportedSigningAlgs: []string{"HS256"}, now: func() time.Time { return now }, }, pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, wantInitErr: true, }, { name: "accounts.google.com issuer", options: Options{ IssuerURL: "https://accounts.google.com", ClientID: "my-client", UsernameClaim: "email", now: func() time.Time { return now }, }, claims: fmt.Sprintf(`{ "iss": "accounts.google.com", "email": "thomas.jefferson@gmail.com", "aud": "my-client", "exp": %d }`, valid.Unix()), signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, want: &user.DefaultInfo{ Name: "thomas.jefferson@gmail.com", }, }, { name: "good token with api req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"api"}, want: &user.DefaultInfo{ Name: "jane", }, }, { name: "good token with multiple api req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api", "other"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"api"}, want: &user.DefaultInfo{ Name: "jane", }, }, { name: "good token with client_id req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"my-client"}, want: &user.DefaultInfo{ Name: "jane", }, }, { name: "good token with client_id and api req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"my-client", "api"}, want: &user.DefaultInfo{ Name: "jane", }, }, { name: "good token with client_id and api req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"my-client", "api"}, want: &user.DefaultInfo{ Name: "jane", }, }, { name: "good token with client_id and bad req audience", options: Options{ IssuerURL: "https://auth.example.com", ClientID: "my-client", APIAudiences: authenticator.Audiences{"api"}, UsernameClaim: "username", now: func() time.Time { return now }, }, signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256), pubKeys: []*jose.JSONWebKey{ loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256), }, claims: fmt.Sprintf(`{ "iss": "https://auth.example.com", "aud": "my-client", "username": "jane", "exp": %d }`, valid.Unix()), reqAudiences: authenticator.Audiences{"other"}, wantSkip: true, }, } for _, test := range tests { t.Run(test.name, test.run) } } func TestUnmarshalClaimError(t *testing.T) { // Ensure error strings returned by unmarshaling claims don't include the claim. const token = "96bb299a-02e9-11e8-8673-54ee7553240e" payload := fmt.Sprintf(`{ "token": "%s" }`, token) var c claims if err := json.Unmarshal([]byte(payload), &c); err != nil { t.Fatal(err) } var n int err := c.unmarshalClaim("token", &n) if err == nil { t.Fatal("expected error") } if strings.Contains(err.Error(), token) { t.Fatalf("unmarshal error included token") } } func TestUnmarshalClaim(t *testing.T) { tests := []struct { name string claims string do func(claims) (interface{}, error) want interface{} wantErr bool }{ { name: "string claim", claims: `{"aud":"foo"}`, do: func(c claims) (interface{}, error) { var s string err := c.unmarshalClaim("aud", &s) return s, err }, want: "foo", }, { name: "mismatched types", claims: `{"aud":"foo"}`, do: func(c claims) (interface{}, error) { var n int err := c.unmarshalClaim("aud", &n) return n, err }, wantErr: true, }, { name: "bool claim", claims: `{"email":"foo@coreos.com","email_verified":true}`, do: func(c claims) (interface{}, error) { var verified bool err := c.unmarshalClaim("email_verified", &verified) return verified, err }, want: true, }, { name: "strings claim", claims: `{"groups":["a","b","c"]}`, do: func(c claims) (interface{}, error) { var groups []string err := c.unmarshalClaim("groups", &groups) return groups, err }, want: []string{"a", "b", "c"}, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { var c claims if err := json.Unmarshal([]byte(test.claims), &c); err != nil { t.Fatal(err) } got, err := test.do(c) if err != nil { if test.wantErr { return } t.Fatalf("unexpected error: %v", err) } if test.wantErr { t.Fatalf("expected error") } if !reflect.DeepEqual(got, test.want) { t.Errorf("wanted=%#v, got=%#v", test.want, got) } }) } }