...

Source file src/k8s.io/kubernetes/pkg/serviceaccount/openidmetadata_test.go

Documentation: k8s.io/kubernetes/pkg/serviceaccount

     1  /*
     2  Copyright 2019 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package serviceaccount_test
    18  
    19  import (
    20  	"crypto/ecdsa"
    21  	"crypto/rsa"
    22  	"crypto/x509"
    23  	"encoding/json"
    24  	"math/big"
    25  	"net/http"
    26  	"net/http/httptest"
    27  	"net/url"
    28  	"testing"
    29  
    30  	restful "github.com/emicklei/go-restful/v3"
    31  	"github.com/google/go-cmp/cmp"
    32  	jose "gopkg.in/square/go-jose.v2"
    33  
    34  	"k8s.io/kubernetes/pkg/routes"
    35  	"k8s.io/kubernetes/pkg/serviceaccount"
    36  )
    37  
    38  const (
    39  	exampleIssuer = "https://issuer.example.com"
    40  )
    41  
    42  func setupServer(t *testing.T, iss string, keys []interface{}) (*httptest.Server, string) {
    43  	t.Helper()
    44  
    45  	c := restful.NewContainer()
    46  	s := httptest.NewServer(c)
    47  
    48  	// JWKS needs to be https, so swap that for the test
    49  	jwksURI, err := url.Parse(s.URL)
    50  	if err != nil {
    51  		t.Fatal(err)
    52  	}
    53  	jwksURI.Scheme = "https"
    54  	jwksURI.Path = serviceaccount.JWKSPath
    55  
    56  	md, err := serviceaccount.NewOpenIDMetadata(
    57  		iss, jwksURI.String(), "", keys)
    58  	if err != nil {
    59  		t.Fatal(err)
    60  	}
    61  
    62  	srv := routes.NewOpenIDMetadataServer(md.ConfigJSON, md.PublicKeysetJSON)
    63  	srv.Install(c)
    64  
    65  	return s, jwksURI.String()
    66  }
    67  
    68  var defaultKeys = []interface{}{getPublicKey(rsaPublicKey), getPublicKey(ecdsaPublicKey)}
    69  
    70  // Configuration is an OIDC configuration, including most but not all required fields.
    71  // https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata
    72  type Configuration struct {
    73  	Issuer        string   `json:"issuer"`
    74  	JWKSURI       string   `json:"jwks_uri"`
    75  	ResponseTypes []string `json:"response_types_supported"`
    76  	SigningAlgs   []string `json:"id_token_signing_alg_values_supported"`
    77  	SubjectTypes  []string `json:"subject_types_supported"`
    78  }
    79  
    80  func TestServeConfiguration(t *testing.T) {
    81  	s, jwksURI := setupServer(t, exampleIssuer, defaultKeys)
    82  	defer s.Close()
    83  
    84  	want := Configuration{
    85  		Issuer:        exampleIssuer,
    86  		JWKSURI:       jwksURI,
    87  		ResponseTypes: []string{"id_token"},
    88  		SubjectTypes:  []string{"public"},
    89  		SigningAlgs:   []string{"ES256", "RS256"},
    90  	}
    91  
    92  	reqURL := s.URL + "/.well-known/openid-configuration"
    93  
    94  	resp, err := http.Get(reqURL)
    95  	if err != nil {
    96  		t.Fatalf("Get(%s) = %v, %v want: <response>, <nil>", reqURL, resp, err)
    97  	}
    98  	defer resp.Body.Close()
    99  
   100  	if resp.StatusCode != http.StatusOK {
   101  		t.Errorf("Get(%s) = %v, _ want: %v, _", reqURL, resp.StatusCode, http.StatusOK)
   102  	}
   103  
   104  	if got, want := resp.Header.Get("Content-Type"), "application/json"; got != want {
   105  		t.Errorf("Get(%s) Content-Type = %q, _ want: %q, _", reqURL, got, want)
   106  	}
   107  	if got, want := resp.Header.Get("Cache-Control"), "public, max-age=3600"; got != want {
   108  		t.Errorf("Get(%s) Cache-Control = %q, _ want: %q, _", reqURL, got, want)
   109  	}
   110  
   111  	var got Configuration
   112  	if err := json.NewDecoder(resp.Body).Decode(&got); err != nil {
   113  		t.Fatalf("Decode(_) = %v, want: <nil>", err)
   114  	}
   115  
   116  	if !cmp.Equal(want, got) {
   117  		t.Errorf("unexpected diff in received configuration (-want, +got):%s",
   118  			cmp.Diff(want, got))
   119  	}
   120  }
   121  
   122  func TestServeKeys(t *testing.T) {
   123  	wantPubRSA := getPublicKey(rsaPublicKey).(*rsa.PublicKey)
   124  	wantPubECDSA := getPublicKey(ecdsaPublicKey).(*ecdsa.PublicKey)
   125  	var serveKeysTests = []struct {
   126  		Name     string
   127  		Keys     []interface{}
   128  		WantKeys []jose.JSONWebKey
   129  	}{
   130  		{
   131  			Name: "configured public keys",
   132  			Keys: []interface{}{
   133  				getPublicKey(rsaPublicKey),
   134  				getPublicKey(ecdsaPublicKey),
   135  			},
   136  			WantKeys: []jose.JSONWebKey{
   137  				{
   138  					Algorithm:                   "RS256",
   139  					Key:                         wantPubRSA,
   140  					KeyID:                       rsaKeyID,
   141  					Use:                         "sig",
   142  					Certificates:                []*x509.Certificate{},
   143  					CertificateThumbprintSHA1:   []uint8{},
   144  					CertificateThumbprintSHA256: []uint8{},
   145  				},
   146  				{
   147  					Algorithm:                   "ES256",
   148  					Key:                         wantPubECDSA,
   149  					KeyID:                       ecdsaKeyID,
   150  					Use:                         "sig",
   151  					Certificates:                []*x509.Certificate{},
   152  					CertificateThumbprintSHA1:   []uint8{},
   153  					CertificateThumbprintSHA256: []uint8{},
   154  				},
   155  			},
   156  		},
   157  		{
   158  			Name: "only publishes public keys",
   159  			Keys: []interface{}{
   160  				getPrivateKey(rsaPrivateKey),
   161  				getPrivateKey(ecdsaPrivateKey),
   162  			},
   163  			WantKeys: []jose.JSONWebKey{
   164  				{
   165  					Algorithm:                   "RS256",
   166  					Key:                         wantPubRSA,
   167  					KeyID:                       rsaKeyID,
   168  					Use:                         "sig",
   169  					Certificates:                []*x509.Certificate{},
   170  					CertificateThumbprintSHA1:   []uint8{},
   171  					CertificateThumbprintSHA256: []uint8{},
   172  				},
   173  				{
   174  					Algorithm:                   "ES256",
   175  					Key:                         wantPubECDSA,
   176  					KeyID:                       ecdsaKeyID,
   177  					Use:                         "sig",
   178  					Certificates:                []*x509.Certificate{},
   179  					CertificateThumbprintSHA1:   []uint8{},
   180  					CertificateThumbprintSHA256: []uint8{},
   181  				},
   182  			},
   183  		},
   184  	}
   185  
   186  	for _, tt := range serveKeysTests {
   187  		t.Run(tt.Name, func(t *testing.T) {
   188  			s, _ := setupServer(t, exampleIssuer, tt.Keys)
   189  			defer s.Close()
   190  
   191  			reqURL := s.URL + "/openid/v1/jwks"
   192  
   193  			resp, err := http.Get(reqURL)
   194  			if err != nil {
   195  				t.Fatalf("Get(%s) = %v, %v want: <response>, <nil>", reqURL, resp, err)
   196  			}
   197  			defer resp.Body.Close()
   198  
   199  			if resp.StatusCode != http.StatusOK {
   200  				t.Errorf("Get(%s) = %v, _ want: %v, _", reqURL, resp.StatusCode, http.StatusOK)
   201  			}
   202  
   203  			if got, want := resp.Header.Get("Content-Type"), "application/jwk-set+json"; got != want {
   204  				t.Errorf("Get(%s) Content-Type = %q, _ want: %q, _", reqURL, got, want)
   205  			}
   206  			if got, want := resp.Header.Get("Cache-Control"), "public, max-age=3600"; got != want {
   207  				t.Errorf("Get(%s) Cache-Control = %q, _ want: %q, _", reqURL, got, want)
   208  			}
   209  
   210  			ks := &jose.JSONWebKeySet{}
   211  			if err := json.NewDecoder(resp.Body).Decode(ks); err != nil {
   212  				t.Fatalf("Decode(_) = %v, want: <nil>", err)
   213  			}
   214  
   215  			bigIntComparer := cmp.Comparer(
   216  				func(x, y *big.Int) bool {
   217  					return x.Cmp(y) == 0
   218  				})
   219  			if !cmp.Equal(tt.WantKeys, ks.Keys, bigIntComparer) {
   220  				t.Errorf("unexpected diff in JWKS keys (-want, +got): %v",
   221  					cmp.Diff(tt.WantKeys, ks.Keys, bigIntComparer))
   222  			}
   223  		})
   224  	}
   225  }
   226  
   227  func TestURLBoundaries(t *testing.T) {
   228  	s, _ := setupServer(t, exampleIssuer, defaultKeys)
   229  	defer s.Close()
   230  
   231  	for _, tt := range []struct {
   232  		Name   string
   233  		Path   string
   234  		WantOK bool
   235  	}{
   236  		{"OIDC config path", "/.well-known/openid-configuration", true},
   237  		{"JWKS path", "/openid/v1/jwks", true},
   238  		{"well-known", "/.well-known", false},
   239  		{"subpath", "/openid/v1/jwks/foo", false},
   240  		{"query", "/openid/v1/jwks?format=yaml", true},
   241  		{"fragment", "/openid/v1/jwks#issuer", true},
   242  	} {
   243  		t.Run(tt.Name, func(t *testing.T) {
   244  			resp, err := http.Get(s.URL + tt.Path)
   245  			if err != nil {
   246  				t.Fatal(err)
   247  			}
   248  
   249  			if tt.WantOK && (resp.StatusCode != http.StatusOK) {
   250  				t.Errorf("Get(%v)= %v, want %v", tt.Path, resp.StatusCode, http.StatusOK)
   251  			}
   252  			if !tt.WantOK && (resp.StatusCode != http.StatusNotFound) {
   253  				t.Errorf("Get(%v)= %v, want %v", tt.Path, resp.StatusCode, http.StatusNotFound)
   254  			}
   255  		})
   256  	}
   257  }
   258  
   259  func TestNewOpenIDMetadata(t *testing.T) {
   260  	cases := []struct {
   261  		name            string
   262  		issuerURL       string
   263  		jwksURI         string
   264  		externalAddress string
   265  		keys            []interface{}
   266  		wantConfig      string
   267  		wantKeyset      string
   268  		err             bool
   269  	}{
   270  		{
   271  			name:       "valid inputs",
   272  			issuerURL:  exampleIssuer,
   273  			jwksURI:    exampleIssuer + serviceaccount.JWKSPath,
   274  			keys:       defaultKeys,
   275  			wantConfig: `{"issuer":"https://issuer.example.com","jwks_uri":"https://issuer.example.com/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES256","RS256"]}`,
   276  			wantKeyset: `{"keys":[{"use":"sig","kty":"RSA","kid":"JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU","alg":"RS256","n":"249XwEo9k4tM8fMxV7zxOhcrP-WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt-ecIzshKuv1gKIxbbLQMOuK1eA_4HALyEkFgmS_tleLJrhc65tKPMGD-pKQ_xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh_AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv-OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ_Jnc4Bp1zUtFXt7-QeevePtQ2EcyELXE0i63T1CujRMWw","e":"AQAB"},{"use":"sig","kty":"EC","kid":"SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc","crv":"P-256","alg":"ES256","x":"H6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp_C_ASqiI","y":"BlHnikLV9PyEd6gl8k4T_3Wwoh6xd79XLoQTh2PAi1Y"}]}`,
   277  		},
   278  		{
   279  			name:      "valid inputs, default JWKSURI to external address",
   280  			issuerURL: exampleIssuer,
   281  			jwksURI:   "",
   282  			// We expect host + port, no scheme, when API server calculates ExternalAddress.
   283  			externalAddress: "192.0.2.1:80",
   284  			keys:            defaultKeys,
   285  			wantConfig:      `{"issuer":"https://issuer.example.com","jwks_uri":"https://192.0.2.1:80/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES256","RS256"]}`,
   286  			wantKeyset:      `{"keys":[{"use":"sig","kty":"RSA","kid":"JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU","alg":"RS256","n":"249XwEo9k4tM8fMxV7zxOhcrP-WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt-ecIzshKuv1gKIxbbLQMOuK1eA_4HALyEkFgmS_tleLJrhc65tKPMGD-pKQ_xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh_AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv-OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ_Jnc4Bp1zUtFXt7-QeevePtQ2EcyELXE0i63T1CujRMWw","e":"AQAB"},{"use":"sig","kty":"EC","kid":"SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc","crv":"P-256","alg":"ES256","x":"H6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp_C_ASqiI","y":"BlHnikLV9PyEd6gl8k4T_3Wwoh6xd79XLoQTh2PAi1Y"}]}`,
   287  		},
   288  		{
   289  			name:       "valid inputs, IP addresses instead of domains",
   290  			issuerURL:  "https://192.0.2.1:80",
   291  			jwksURI:    "https://192.0.2.1:80" + serviceaccount.JWKSPath,
   292  			keys:       defaultKeys,
   293  			wantConfig: `{"issuer":"https://192.0.2.1:80","jwks_uri":"https://192.0.2.1:80/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES256","RS256"]}`,
   294  			wantKeyset: `{"keys":[{"use":"sig","kty":"RSA","kid":"JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU","alg":"RS256","n":"249XwEo9k4tM8fMxV7zxOhcrP-WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt-ecIzshKuv1gKIxbbLQMOuK1eA_4HALyEkFgmS_tleLJrhc65tKPMGD-pKQ_xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh_AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv-OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ_Jnc4Bp1zUtFXt7-QeevePtQ2EcyELXE0i63T1CujRMWw","e":"AQAB"},{"use":"sig","kty":"EC","kid":"SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc","crv":"P-256","alg":"ES256","x":"H6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp_C_ASqiI","y":"BlHnikLV9PyEd6gl8k4T_3Wwoh6xd79XLoQTh2PAi1Y"}]}`,
   295  		},
   296  		{
   297  			name:       "response only contains public keys, even when private keys are provided",
   298  			issuerURL:  exampleIssuer,
   299  			jwksURI:    exampleIssuer + serviceaccount.JWKSPath,
   300  			keys:       []interface{}{getPrivateKey(rsaPrivateKey), getPrivateKey(ecdsaPrivateKey)},
   301  			wantConfig: `{"issuer":"https://issuer.example.com","jwks_uri":"https://issuer.example.com/openid/v1/jwks","response_types_supported":["id_token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["ES256","RS256"]}`,
   302  			wantKeyset: `{"keys":[{"use":"sig","kty":"RSA","kid":"JHJehTTTZlsspKHT-GaJxK7Kd1NQgZJu3fyK6K_QDYU","alg":"RS256","n":"249XwEo9k4tM8fMxV7zxOhcrP-WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt-ecIzshKuv1gKIxbbLQMOuK1eA_4HALyEkFgmS_tleLJrhc65tKPMGD-pKQ_xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh_AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv-OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ_Jnc4Bp1zUtFXt7-QeevePtQ2EcyELXE0i63T1CujRMWw","e":"AQAB"},{"use":"sig","kty":"EC","kid":"SoABiieYuNx4UdqYvZRVeuC6SihxgLrhLy9peHMHpTc","crv":"P-256","alg":"ES256","x":"H6cuzP8XuD5wal6wf9M6xDljTOPLX2i8uIp_C_ASqiI","y":"BlHnikLV9PyEd6gl8k4T_3Wwoh6xd79XLoQTh2PAi1Y"}]}`,
   303  		},
   304  		{
   305  			name:      "issuer missing https",
   306  			issuerURL: "http://issuer.example.com",
   307  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   308  			keys:      defaultKeys,
   309  			err:       true,
   310  		},
   311  		{
   312  			name:      "issuer missing scheme",
   313  			issuerURL: "issuer.example.com",
   314  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   315  			keys:      defaultKeys,
   316  			err:       true,
   317  		},
   318  		{
   319  			name:      "issuer includes query",
   320  			issuerURL: "https://issuer.example.com?foo=bar",
   321  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   322  			keys:      defaultKeys,
   323  			err:       true,
   324  		},
   325  		{
   326  			name:      "issuer includes fragment",
   327  			issuerURL: "https://issuer.example.com#baz",
   328  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   329  			keys:      defaultKeys,
   330  			err:       true,
   331  		},
   332  		{
   333  			name:      "issuer includes query and fragment",
   334  			issuerURL: "https://issuer.example.com?foo=bar#baz",
   335  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   336  			keys:      defaultKeys,
   337  			err:       true,
   338  		},
   339  		{
   340  			name:      "issuer is not a valid URL",
   341  			issuerURL: "issuer",
   342  			jwksURI:   exampleIssuer + serviceaccount.JWKSPath,
   343  			keys:      defaultKeys,
   344  			err:       true,
   345  		},
   346  		{
   347  			name:      "jwks missing https",
   348  			issuerURL: exampleIssuer,
   349  			jwksURI:   "http://issuer.example.com" + serviceaccount.JWKSPath,
   350  			keys:      defaultKeys,
   351  			err:       true,
   352  		},
   353  		{
   354  			name:      "jwks missing scheme",
   355  			issuerURL: exampleIssuer,
   356  			jwksURI:   "issuer.example.com" + serviceaccount.JWKSPath,
   357  			keys:      defaultKeys,
   358  			err:       true,
   359  		},
   360  		{
   361  			name:      "jwks is not a valid URL",
   362  			issuerURL: exampleIssuer,
   363  			jwksURI:   "issuer" + serviceaccount.JWKSPath,
   364  			keys:      defaultKeys,
   365  			err:       true,
   366  		},
   367  		{
   368  			name:            "external address also has a scheme",
   369  			issuerURL:       exampleIssuer,
   370  			externalAddress: "https://192.0.2.1:80",
   371  			keys:            defaultKeys,
   372  			err:             true,
   373  		},
   374  		{
   375  			name:      "missing external address and jwks",
   376  			issuerURL: exampleIssuer,
   377  			keys:      defaultKeys,
   378  			err:       true,
   379  		},
   380  	}
   381  	for _, tc := range cases {
   382  		t.Run(tc.name, func(t *testing.T) {
   383  			md, err := serviceaccount.NewOpenIDMetadata(tc.issuerURL, tc.jwksURI, tc.externalAddress, tc.keys)
   384  			if tc.err {
   385  				if err == nil {
   386  					t.Fatalf("got <nil>, want error")
   387  				}
   388  				return
   389  			} else if !tc.err && err != nil {
   390  				t.Fatalf("got error %v, want <nil>", err)
   391  			}
   392  
   393  			config := string(md.ConfigJSON)
   394  			keyset := string(md.PublicKeysetJSON)
   395  			if config != tc.wantConfig {
   396  				t.Errorf("got metadata %s, want %s", config, tc.wantConfig)
   397  			}
   398  			if keyset != tc.wantKeyset {
   399  				t.Errorf("got keyset %s, want %s", keyset, tc.wantKeyset)
   400  			}
   401  		})
   402  	}
   403  }
   404  

View as plain text