k8s.io/kubernetes@v1.29.3/pkg/serviceaccount/openidmetadata_test.go (about) 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 }