1
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
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
71
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
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