openidmetadata_test.go 14 KB

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