github.com/kiali/kiali@v1.84.0/business/authentication/openid_auth_controller_test.go (about) 1 package authentication 2 3 import ( 4 "crypto/sha256" 5 "crypto/x509" 6 "encoding/json" 7 "encoding/pem" 8 "fmt" 9 "net" 10 "net/http" 11 "net/http/httptest" 12 "net/url" 13 "strings" 14 "testing" 15 "time" 16 17 "github.com/go-jose/go-jose" 18 osproject_v1 "github.com/openshift/api/project/v1" 19 "github.com/stretchr/testify/assert" 20 core_v1 "k8s.io/api/core/v1" 21 meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 23 "github.com/kiali/kiali/config" 24 "github.com/kiali/kiali/kubernetes/cache" 25 "github.com/kiali/kiali/kubernetes/kubetest" 26 "github.com/kiali/kiali/util" 27 ) 28 29 // Token built with the debugger at jwt.io. Subject is system:serviceaccount:k8s_user 30 // { 31 // "sub": "jdoe@domain.com", 32 // "name": "John Doe", 33 // "iat": 1516239022, 34 // "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184", 35 // "exp": 1638316801 36 // } 37 38 const openIdTestToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDF9.agHBziXM7SDLBKCnA6BvjWenU1n6juL8Fz3go4MSzyw" 39 40 /*** Function tests ***/ 41 42 // see https://github.com/kiali/kiali/issues/6226 43 func TestVerifyAudienceClaim(t *testing.T) { 44 oidCfg := config.OpenIdConfig{ 45 ClientId: "kiali-client", 46 } 47 48 oip := openidFlowHelper{ 49 IdTokenPayload: map[string]interface{}{}, 50 } 51 52 oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId} 53 err := verifyAudienceClaim(&oip, oidCfg) 54 assert.Nil(t, err, "verifyAudienceClaim failed: %v", err) 55 56 oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId} 57 err = verifyAudienceClaim(&oip, oidCfg) 58 assert.Nil(t, err, "verifyAudienceClaim failed: %v", err) 59 60 oip.IdTokenPayload["aud"] = oidCfg.ClientId 61 err = verifyAudienceClaim(&oip, oidCfg) 62 assert.Nil(t, err, "verifyAudienceClaim failed: %v", err) 63 64 oip.IdTokenPayload["aud"] = []interface{}{oidCfg.ClientId + "DIFFERENT"} 65 err = verifyAudienceClaim(&oip, oidCfg) 66 assert.NotNil(t, err, "verifyAudienceClaim should have failed") 67 68 oip.IdTokenPayload["aud"] = []string{oidCfg.ClientId + "DIFFERENT"} 69 err = verifyAudienceClaim(&oip, oidCfg) 70 assert.NotNil(t, err, "verifyAudienceClaim should have failed") 71 72 oip.IdTokenPayload["aud"] = oidCfg.ClientId + "DIFFERENT" 73 err = verifyAudienceClaim(&oip, oidCfg) 74 assert.NotNil(t, err, "verifyAudienceClaim should have failed") 75 } 76 77 func TestValidateOpenIdTokenInHouse(t *testing.T) { 78 // These tokens were generated via https://dinochiesa.github.io/jwt/ using its own private/public keys generator. 79 // There is another public/private key generator website that could be used if needed in the future: https://mkjwk.org/ 80 // 81 // -----BEGIN PUBLIC KEY----- 82 // MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fEsycKFNuQn/IsW+t8+ 83 // RexDWxvJP37+5BuAviloJvmAYwQguGRN+qwi2Co/yImxZPZVuqY7/hf87bIKopZD 84 // CgZrUHtFERPL4qbR6gv+FbanR6ohCXa6f1S+AjXPiQcw0p49t4uT0WwjPD64mNdw 85 // IlzWLi+xO4rY1Mo98H2339WmvmgiEukLAa+f3Zz80jRDmWv23Td/ba5ASHVrvhRJ 86 // d2jLwOL/lxCHPtjDf6bBHKaPP/ezRjlPg9Qlse3au1KBsVrSThr0uz9G6cGeidEL 87 // fvdk9xmAoxhR2CbmGIQoRPiEEOVhKg/yHVqzz+2SvnIKnArx4ISv/yudbP8lU7vC 88 // QwIDAQAB 89 // -----END PUBLIC KEY----- 90 // 91 // -----BEGIN PRIVATE KEY----- 92 // MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDZ8SzJwoU25Cf8 93 // ixb63z5F7ENbG8k/fv7kG4C+KWgm+YBjBCC4ZE36rCLYKj/IibFk9lW6pjv+F/zt 94 // sgqilkMKBmtQe0URE8viptHqC/4VtqdHqiEJdrp/VL4CNc+JBzDSnj23i5PRbCM8 95 // PriY13AiXNYuL7E7itjUyj3wfbff1aa+aCIS6QsBr5/dnPzSNEOZa/bdN39trkBI 96 // dWu+FEl3aMvA4v+XEIc+2MN/psEcpo8/97NGOU+D1CWx7dq7UoGxWtJOGvS7P0bp 97 // wZ6J0Qt+92T3GYCjGFHYJuYYhChE+IQQ5WEqD/IdWrPP7ZK+cgqcCvHghK//K51s 98 // /yVTu8JDAgMBAAECggEAGGi2h3JN0TQEdnhtfnN6WgJ4GMAn7gCfM5UQ+jtQ+ux+ 99 // wJg5we0Z/rVAwc0Zj7A8Of6M43ayyWaOYWDLaCJEJ99ILZ9gwOTitOPSJtBpCK2I 100 // VrJrONAfWxt2nHDCaapwgWZPqzrqt03RNHIh4pxeZrrXEh0tUGnglxR/k2vBKERk 101 // dtP1xwydrMip7VUkUwGvLmYLClsbnkR7O65XzFIto7iG0/yuWA6yw9mNey5gTrr9 102 // 7iboE5v0vHOJW/1mVQP1xV/rI8VnLZlZz6XYPktjlzhR5yIzrZD2DZ20J8uo6Zrd 103 // iVEz1nE6FmuhfgbUFOt8SBrGUZhx0Zwo0GkLiFdDYQKBgQD+P49qNNw+Dwrjc8dK 104 // 85vr89RQ9MX3ioTIZnm+oXHjoLvVrPZmI3SjrZJrqMCZtxP6LpoDnlGjKeJJMA61 105 // wD1ceJS5YyDgLVv3jsi/zIpJDQTg2GXaDRY3c3PjHCmRocMvaPKiws8ZY7BoR8nF 106 // bpKxxa2C3LEqiyzil2H7NR16vQKBgQDbcZQO9LTiYitoW+qQPhbNHGlrlD6KfXTA 107 // SFQBAPFKtR4ncj/x3/i0L1P3+IabkdJQqxOHY68FNGg95sfD7/s9K72RvnRTyXCn 108 // Wd46QLULvHLTYWI39obKOZlrbRWBqr/kw1YIxjjgRWTShOuBkS2R6TtPDeoAtTds 109 // 995RRgCA/wKBgE4m2X2rC/wjgZRS9XKrmUUZKS1NYEDsGk7DeS7Iz4pJ0RMoXIEe 110 // 6u6ZHwXq1HErnn9rrbnpA20lJcKbfBoQIox3IDgwKV3fc4KQKFMUm3lDADnhKsWw 111 // +iBHY9ruwDRcxfOfzd2MBj7mrsYPMw12JK9ydRhhoC/UohJwuBSQyiP9AoGAeIg5 112 // F8HnPNVJHGgoPZQs9/pcGR/y/iSMpTTVFzwKTMuQxX/miZdIxsecKn7SiM6eo3pk 113 // HqBtOMGhZCbHoOLGr8G/vTbMNF1XyEP/YSW7i7e1pk8+IJkDTj42+5+OCYvdHO0B 114 // 643dHapgB5XEuYUhb5yY3AI7fqoKyIqZDTETA8cCgYEAt2AnN8HTLqIOxizkulNj 115 // adeC53dbv8MmPU0aA1EO5ipLVXcF4y/8zOzIKHijWAB8kEZXPzS/rUURwtyhRG3k 116 // f6lSPO2VFE1xNlGSlU3CvQz1VV2qgvbtogIOo7CoXYRdiJJ2j2M/n06MBK2bMoGb 117 // UGj7e/VHYwvMxZ6SoMYk3Hc= 118 // -----END PRIVATE KEY----- 119 // 120 // HEADER: 121 // { 122 // "alg": "RS256", 123 // "typ": "JWT", 124 // "kid": "kialikey" 125 // } 126 // 127 // PAYLOAD: 128 // { 129 // "sub": "jdoe@domain.com", 130 // "name": "John Doe", 131 // "iat": 1516239022, 132 // "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184", 133 // "exp": 1638316801, 134 // "iss": "http://127.0.0.1:33333", 135 // "aud": "kiali-client" 136 // } 137 openIdTestTokenWithGoodAud := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpYWxpa2V5In0.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzMzMiLCJhdWQiOiJraWFsaS1jbGllbnQifQ.bOu7M_a8DKctApb-RfbglUGyJslJrVjD6ZRU8XlRStQwp4QIQM-tCB5cqYv9OJeC9NSN46RO9jvJ5rw2WnVFNfujDwVWSvjUqQHmYiO3GSobmCfbAtG7ymRWnLLaQMheinpfPjXh5-ohVlSqB23wkk4viC9YCqqXaIKk4bLyZFf14F4u5Zqu2kfzwufjp-AgAt9W93loI6p6kHVyCnnDwPvfmfSmCUxaCPvrFGKnGe6hTCPCc2EBCRndW-si7hz9F693jAyD5OMvt1z_aX4tzPNsqZYuosXw6xwGGM-nepn6XtM6U_MS9-eRSoCyyMyZE6_xSZIO4ir1KeewbSvk1Q" 138 139 // { 140 // "sub": "jdoe@domain.com", 141 // "name": "John Doe", 142 // "iat": 1516239022, 143 // "nonce": "1ba9b834d08ac81feb34e208402eb18e909be084518c328510940184", 144 // "exp": 1638316801, 145 // "iss": "http://127.0.0.1:33333", 146 // "aud": "bad-aud-client" 147 // } 148 openIdTestTokenWithBadAud := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImtpYWxpa2V5In0.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOjE2MzgzMTY4MDEsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6MzMzMzMiLCJhdWQiOiJiYWQtYXVkLWNsaWVudCJ9.LicN-YgaHxfTTg_XtpyTlZBIQyj4BTvYHsXKtDRRskf2uuvKw36Y0WSJN570E0PSYYlAzyjrWp31S_PdZL75cwCIOYnhGbTat7pUVNoAn-aZc7FrMtzYNmQdChB2-ghE_RRQaXP1zNwgeNrQiEQ9jmD5ynd7Qm2esMYYbmCoj1ITM5Uospp5fbRg9eNdfrqXmwoGK3OITC5OVv8tbcb2HY_CUxJfSIC5pT5wBGxGRExjaeXNiIRS1600NmkfK6O-BPsmJhEYTxLIeWbtAn2pn7uZhWMiyJIIX9FHFLeTCIQh2xuwSuWLkyZoMsegr8A_rQqKg-iQkhfXxYxGtRY6mQ" 149 150 // we start with a good token - our second test will switch this to the token with the bad aud value 151 openIdTestTokenToUse := openIdTestTokenWithGoodAud 152 153 cachedOpenIdMetadata = nil 154 var oidcMetadata []byte 155 var jwksResponseBytes []byte 156 testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 157 if r.URL.Path == "/.well-known/openid-configuration" { 158 w.WriteHeader(200) 159 _, _ = w.Write(oidcMetadata) 160 } else if r.URL.Path == "/jwks" { 161 w.WriteHeader(200) 162 _, _ = w.Write(jwksResponseBytes) 163 } else if r.URL.Path == "/token" { 164 _ = r.ParseForm() 165 assert.Equal(t, "f0code", r.Form.Get("code")) 166 assert.Equal(t, "authorization_code", r.Form.Get("grant_type")) 167 assert.Equal(t, "kiali-client", r.Form.Get("client_id")) 168 assert.Equal(t, "https://kiali.io:44/kiali-test", r.Form.Get("redirect_uri")) 169 170 w.WriteHeader(200) 171 _, _ = w.Write([]byte("{ \"id_token\": \"" + openIdTestTokenToUse + "\" }")) 172 } 173 })) 174 defer testServer.Close() 175 176 // because we have a hardcoded token for this test that is pre-encrypted with the issuer URL, we need to start the server on that same URL 177 testServerListenerFixedPort, err := net.Listen("tcp", "127.0.0.1:33333") 178 assert.Nil(t, err, "Cannot start test server on fixed port") 179 testServer.Listener = testServerListenerFixedPort 180 testServer.Start() 181 182 oidcMeta := openIdMetadata{ 183 Issuer: testServer.URL, 184 AuthURL: testServer.URL + "/auth", 185 TokenURL: testServer.URL + "/token", 186 JWKSURL: testServer.URL + "/jwks", 187 UserInfoURL: "", 188 Algorithms: nil, 189 ScopesSupported: []string{"openid"}, 190 ResponseTypesSupported: []string{"code"}, 191 } 192 oidcMetadata, err = json.Marshal(oidcMeta) 193 assert.Nil(t, err) 194 195 // jwksResponseBytes is needed for the "/jwks" endpoint. It is used during 196 // the validation phase when Kiali wants to make sure the "kid" is valid. 197 publicKeyText := ` 198 -----BEGIN PUBLIC KEY----- 199 MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2fEsycKFNuQn/IsW+t8+ 200 RexDWxvJP37+5BuAviloJvmAYwQguGRN+qwi2Co/yImxZPZVuqY7/hf87bIKopZD 201 CgZrUHtFERPL4qbR6gv+FbanR6ohCXa6f1S+AjXPiQcw0p49t4uT0WwjPD64mNdw 202 IlzWLi+xO4rY1Mo98H2339WmvmgiEukLAa+f3Zz80jRDmWv23Td/ba5ASHVrvhRJ 203 d2jLwOL/lxCHPtjDf6bBHKaPP/ezRjlPg9Qlse3au1KBsVrSThr0uz9G6cGeidEL 204 fvdk9xmAoxhR2CbmGIQoRPiEEOVhKg/yHVqzz+2SvnIKnArx4ISv/yudbP8lU7vC 205 QwIDAQAB 206 -----END PUBLIC KEY-----` 207 block, _ := pem.Decode([]byte(publicKeyText)) 208 publicKeyRSA, _ := x509.ParsePKIXPublicKey(block.Bytes) 209 jwksResponseObject := jose.JSONWebKeySet{ 210 Keys: []jose.JSONWebKey{ 211 { 212 KeyID: "kialikey", 213 Algorithm: "RS256", 214 Key: publicKeyRSA, 215 }, 216 }, 217 } 218 jwksResponseBytes, err = json.Marshal(jwksResponseObject) 219 assert.Nil(t, err) 220 221 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 222 util.Clock = util.ClockMock{Time: clockTime} 223 224 conf := config.NewConfig() 225 conf.Server.WebRoot = "/kiali-test" 226 conf.LoginToken.SigningKey = "kiali67890123456" 227 conf.LoginToken.ExpirationSeconds = 1 228 conf.Auth.OpenId.IssuerUri = testServer.URL 229 conf.Auth.OpenId.ClientId = "kiali-client" 230 conf.Auth.OpenId.DisableRBAC = true // true is needed to trigger the call to validateOpenIdTokenInHouse 231 config.Set(conf) 232 233 // Returning some namespace when a cluster API call is made should have the result of 234 // a successful authentication. 235 k8s := kubetest.NewFakeK8sClient( 236 &osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}, 237 ) 238 k8s.OpenShift = true 239 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 240 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf) 241 242 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf)))) 243 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 244 request := httptest.NewRequest(http.MethodGet, uri, nil) 245 request.AddCookie(&http.Cookie{ 246 Name: OpenIdNonceCookieName, 247 Value: "nonceString", 248 }) 249 250 controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf) 251 252 expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC) 253 254 rr := httptest.NewRecorder() 255 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 256 assert.Failf(t, "Callback function shouldn't have been called.", "") 257 })).ServeHTTP(rr, request) 258 259 // Check that cookies are set and have the right expiration. 260 response := rr.Result() 261 assert.Len(t, response.Cookies(), 2) 262 263 // nonce cookie cleanup 264 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 265 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 266 assert.True(t, response.Cookies()[0].HttpOnly) 267 assert.True(t, response.Cookies()[0].Secure) // the test URL is https://kiali.io:44/kiali-test ; https: means it should be Secure 268 269 // Session cookie 270 assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name) 271 assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires) 272 assert.Equal(t, http.StatusFound, response.StatusCode) 273 assert.True(t, response.Cookies()[1].HttpOnly) 274 assert.True(t, response.Cookies()[1].Secure) // the test URL is https://kiali.io:44/kiali-test ; https: means it should be Secure 275 276 // Redirection to boot the UI 277 assert.Equal(t, "/kiali-test/", response.Header.Get("Location")) 278 279 /// 280 /// Now switch to using the token with the bad audience claim and make sure this fails 281 /// 282 283 openIdTestTokenToUse = openIdTestTokenWithBadAud 284 rr = httptest.NewRecorder() 285 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 286 assert.Failf(t, "Callback function shouldn't have been called.", "") 287 })).ServeHTTP(rr, request) 288 289 // Check that there is only one cookie (nonce) - the other AES cookie should be missing because the audience claim was bad 290 response = rr.Result() 291 assert.Len(t, response.Cookies(), 1) 292 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 293 assert.Equal(t, "/kiali-test/?openid_error=the+OpenID+token+was+rejected%3A+the+OpenId+token+is+not+targeted+for+Kiali%3B+got+aud+%3D+%27bad-aud-client%27", response.Header.Get("Location")) 294 } 295 296 /*** Implicit flow tests ***/ 297 298 func TestOpenIdAuthControllerRejectsImplicitFlow(t *testing.T) { 299 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 300 util.Clock = util.ClockMock{Time: clockTime} 301 302 conf := config.NewConfig() 303 conf.LoginToken.SigningKey = "kiali67890123456" 304 conf.LoginToken.ExpirationSeconds = 1 305 config.Set(conf) 306 307 // Returning some namespace when a cluster API call is made should have the result of 308 // a successful authentication. 309 k8s := kubetest.NewFakeK8sClient( 310 &osproject_v1.Project{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}, 311 ) 312 k8s.OpenShift = true 313 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 314 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf) 315 316 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf)))) 317 318 requestBody := strings.NewReader(fmt.Sprintf("id_token=%s&state=%x-%s", openIdTestToken, stateHash, clockTime.UTC().Format("060102150405"))) 319 request := httptest.NewRequest(http.MethodPost, "/api/authenticate", requestBody) 320 request.Header.Add("Content-Type", "application/x-www-form-urlencoded") 321 request.AddCookie(&http.Cookie{ 322 Name: OpenIdNonceCookieName, 323 Value: "nonceString", 324 }) 325 326 controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf) 327 rr := httptest.NewRecorder() 328 sData, err := controller.Authenticate(request, rr) 329 330 assert.Nil(t, sData) 331 assert.NotNil(t, err) 332 assert.Equal(t, err.Error(), "support for OpenID's implicit flow has been removed") 333 334 response := rr.Result() 335 assert.Len(t, response.Cookies(), 0) 336 } 337 338 /*** Authorization code flow tests ***/ 339 340 func TestOpenIdAuthControllerAuthenticatesCorrectlyWithAuthorizationCodeFlow(t *testing.T) { 341 cachedOpenIdMetadata = nil 342 var oidcMetadata []byte 343 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 344 if r.URL.Path == "/.well-known/openid-configuration" { 345 w.WriteHeader(200) 346 _, _ = w.Write(oidcMetadata) 347 } 348 if r.URL.Path == "/token" { 349 _ = r.ParseForm() 350 assert.Equal(t, "f0code", r.Form.Get("code")) 351 assert.Equal(t, "authorization_code", r.Form.Get("grant_type")) 352 assert.Equal(t, "kiali-client", r.Form.Get("client_id")) 353 assert.Equal(t, "https://kiali.io:44/kiali-test", r.Form.Get("redirect_uri")) 354 355 w.WriteHeader(200) 356 _, _ = w.Write([]byte("{ \"id_token\": \"" + openIdTestToken + "\" }")) 357 } 358 })) 359 defer testServer.Close() 360 361 oidcMeta := openIdMetadata{ 362 Issuer: testServer.URL, 363 AuthURL: testServer.URL + "/auth", 364 TokenURL: testServer.URL + "/token", 365 JWKSURL: testServer.URL + "/jwks", 366 UserInfoURL: "", 367 Algorithms: nil, 368 ScopesSupported: []string{"openid"}, 369 ResponseTypesSupported: []string{"code"}, 370 } 371 oidcMetadata, err := json.Marshal(oidcMeta) 372 assert.Nil(t, err) 373 374 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 375 util.Clock = util.ClockMock{Time: clockTime} 376 377 conf := config.NewConfig() 378 conf.Server.WebRoot = "/kiali-test" 379 conf.LoginToken.SigningKey = "kiali67890123456" 380 conf.LoginToken.ExpirationSeconds = 1 381 conf.Auth.OpenId.IssuerUri = testServer.URL 382 conf.Auth.OpenId.ClientId = "kiali-client" 383 conf.Identity.CertFile = "foo.cert" // setting conf.Identity will make it look as if the endpoint ... 384 conf.Identity.PrivateKeyFile = "foo.key" // ... is HTTPS - this causes the cookies' Secure flag to be true 385 config.Set(conf) 386 387 // Returning some namespace when a cluster API call is made should have the result of 388 // a successful authentication. 389 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 390 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 391 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *conf) 392 393 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(conf)))) 394 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 395 request := httptest.NewRequest(http.MethodGet, uri, nil) 396 request.AddCookie(&http.Cookie{ 397 Name: OpenIdNonceCookieName, 398 Value: "nonceString", 399 }) 400 401 controller := NewOpenIdAuthController(NewCookieSessionPersistor(conf), cache, mockClientFactory, conf) 402 403 rr := httptest.NewRecorder() 404 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 405 assert.Failf(t, "Callback function shouldn't have been called.", "") 406 })).ServeHTTP(rr, request) 407 408 expectedExpiration := time.Date(2021, 12, 1, 0, 0, 1, 0, time.UTC) 409 410 // Check that cookies are set and have the right expiration. 411 response := rr.Result() 412 assert.Len(t, response.Cookies(), 2) 413 414 // nonce cookie cleanup 415 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 416 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 417 assert.True(t, response.Cookies()[0].HttpOnly) 418 assert.True(t, response.Cookies()[0].Secure) 419 420 // Session cookie 421 assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name) 422 assert.Equal(t, expectedExpiration, response.Cookies()[1].Expires) 423 assert.Equal(t, http.StatusFound, response.StatusCode) 424 assert.True(t, response.Cookies()[1].HttpOnly) 425 assert.True(t, response.Cookies()[1].Secure) 426 427 // Redirection to boot the UI 428 assert.Equal(t, "/kiali-test/", response.Header.Get("Location")) 429 } 430 431 func TestOpenIdCodeFlowShouldFailWithMissingIdTokenFromOpenIdServer(t *testing.T) { 432 cachedOpenIdMetadata = nil 433 var oidcMetadata []byte 434 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 435 if r.URL.Path == "/.well-known/openid-configuration" { 436 w.WriteHeader(200) 437 _, _ = w.Write(oidcMetadata) 438 } 439 if r.URL.Path == "/token" { 440 w.WriteHeader(200) 441 _, _ = w.Write([]byte("{ \"access_token\": \"" + openIdTestToken + "\" }")) 442 } 443 })) 444 defer testServer.Close() 445 446 oidcMeta := openIdMetadata{ 447 Issuer: testServer.URL, 448 AuthURL: testServer.URL + "/auth", 449 TokenURL: testServer.URL + "/token", 450 JWKSURL: testServer.URL + "/jwks", 451 UserInfoURL: "", 452 Algorithms: nil, 453 ScopesSupported: []string{"openid"}, 454 ResponseTypesSupported: []string{"code"}, 455 } 456 oidcMetadata, err := json.Marshal(oidcMeta) 457 assert.Nil(t, err) 458 459 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 460 util.Clock = util.ClockMock{Time: clockTime} 461 462 cfg := config.NewConfig() 463 cfg.Server.WebRoot = "/kiali-test" 464 cfg.LoginToken.SigningKey = "kiali67890123456" 465 cfg.LoginToken.ExpirationSeconds = 1 466 cfg.Auth.OpenId.IssuerUri = testServer.URL 467 cfg.Auth.OpenId.ClientId = "kiali-client" 468 config.Set(cfg) 469 470 // Returning some namespace when a cluster API call is made should have the result of 471 // a successful authentication. 472 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 473 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 474 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 475 476 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 477 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 478 request := httptest.NewRequest(http.MethodGet, uri, nil) 479 request.AddCookie(&http.Cookie{ 480 Name: OpenIdNonceCookieName, 481 Value: "nonceString", 482 }) 483 484 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 485 486 rr := httptest.NewRecorder() 487 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 488 assert.Failf(t, "Callback function shouldn't have been called.", "") 489 })).ServeHTTP(rr, request) 490 491 // nonce cookie cleanup 492 response := rr.Result() 493 assert.Len(t, response.Cookies(), 1) 494 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 495 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 496 497 // Redirection to boot the UI 498 q := url.Values{} 499 q.Add("openid_error", "the IdP did not provide an id_token") 500 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 501 assert.Equal(t, http.StatusFound, response.StatusCode) 502 } 503 504 func TestOpenIdCodeFlowShouldFailWithBadResponseFromTokenEndpoint(t *testing.T) { 505 cachedOpenIdMetadata = nil 506 var oidcMetadata []byte 507 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 508 if r.URL.Path == "/.well-known/openid-configuration" { 509 w.WriteHeader(200) 510 _, _ = w.Write(oidcMetadata) 511 } 512 if r.URL.Path == "/token" { 513 w.WriteHeader(500) 514 _, _ = w.Write([]byte("{ }")) 515 } 516 })) 517 defer testServer.Close() 518 519 oidcMeta := openIdMetadata{ 520 Issuer: testServer.URL, 521 AuthURL: testServer.URL + "/auth", 522 TokenURL: testServer.URL + "/token", 523 JWKSURL: testServer.URL + "/jwks", 524 UserInfoURL: "", 525 Algorithms: nil, 526 ScopesSupported: []string{"openid"}, 527 ResponseTypesSupported: []string{"code"}, 528 } 529 oidcMetadata, err := json.Marshal(oidcMeta) 530 assert.Nil(t, err) 531 532 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 533 util.Clock = util.ClockMock{Time: clockTime} 534 535 cfg := config.NewConfig() 536 cfg.Server.WebRoot = "/kiali-test" 537 cfg.LoginToken.SigningKey = "kiali67890123456" 538 cfg.LoginToken.ExpirationSeconds = 1 539 cfg.Auth.OpenId.IssuerUri = testServer.URL 540 cfg.Auth.OpenId.ClientId = "kiali-client" 541 config.Set(cfg) 542 543 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 544 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 545 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 546 547 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 548 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 549 request := httptest.NewRequest(http.MethodGet, uri, nil) 550 request.AddCookie(&http.Cookie{ 551 Name: OpenIdNonceCookieName, 552 Value: "nonceString", 553 }) 554 555 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 556 557 rr := httptest.NewRecorder() 558 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 559 assert.Failf(t, "Callback function shouldn't have been called.", "") 560 })).ServeHTTP(rr, request) 561 562 // nonce cookie cleanup 563 response := rr.Result() 564 assert.Len(t, response.Cookies(), 1) 565 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 566 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 567 568 // Redirection to boot the UI 569 q := url.Values{} 570 q.Add("openid_error", "request failed (HTTP response status = 500 Internal Server Error)") 571 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 572 assert.Equal(t, http.StatusFound, response.StatusCode) 573 } 574 575 func TestOpenIdCodeFlowShouldFailWithNonJsonResponse(t *testing.T) { 576 cachedOpenIdMetadata = nil 577 var oidcMetadata []byte 578 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 579 if r.URL.Path == "/.well-known/openid-configuration" { 580 w.WriteHeader(200) 581 _, _ = w.Write(oidcMetadata) 582 } 583 if r.URL.Path == "/token" { 584 w.WriteHeader(200) 585 _, _ = w.Write([]byte("\"id_token\": \"foo\"")) 586 } 587 })) 588 defer testServer.Close() 589 590 oidcMeta := openIdMetadata{ 591 Issuer: testServer.URL, 592 AuthURL: testServer.URL + "/auth", 593 TokenURL: testServer.URL + "/token", 594 JWKSURL: testServer.URL + "/jwks", 595 UserInfoURL: "", 596 Algorithms: nil, 597 ScopesSupported: []string{"openid"}, 598 ResponseTypesSupported: []string{"code"}, 599 } 600 oidcMetadata, err := json.Marshal(oidcMeta) 601 assert.Nil(t, err) 602 603 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 604 util.Clock = util.ClockMock{Time: clockTime} 605 606 cfg := config.NewConfig() 607 cfg.Server.WebRoot = "/kiali-test" 608 cfg.LoginToken.SigningKey = "kiali67890123456" 609 cfg.LoginToken.ExpirationSeconds = 1 610 cfg.Auth.OpenId.IssuerUri = testServer.URL 611 cfg.Auth.OpenId.ClientId = "kiali-client" 612 config.Set(cfg) 613 614 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 615 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 616 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 617 618 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 619 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 620 request := httptest.NewRequest(http.MethodGet, uri, nil) 621 request.AddCookie(&http.Cookie{ 622 Name: OpenIdNonceCookieName, 623 Value: "nonceString", 624 }) 625 626 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 627 628 rr := httptest.NewRecorder() 629 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 630 assert.Failf(t, "Callback function shouldn't have been called.", "") 631 })).ServeHTTP(rr, request) 632 633 // nonce cookie cleanup 634 response := rr.Result() 635 assert.Len(t, response.Cookies(), 1) 636 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 637 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 638 639 // Redirection to boot the UI 640 u, _ := url.Parse(response.Header.Get("Location")) 641 q, _ := url.ParseQuery(u.RawQuery) 642 assert.Contains(t, q["openid_error"][0], "cannot parse OpenId token response:") 643 assert.Equal(t, http.StatusFound, response.StatusCode) 644 } 645 646 func TestOpenIdCodeFlowShouldFailWithNonJwtIdToken(t *testing.T) { 647 cachedOpenIdMetadata = nil 648 var oidcMetadata []byte 649 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 650 if r.URL.Path == "/.well-known/openid-configuration" { 651 w.WriteHeader(200) 652 _, _ = w.Write(oidcMetadata) 653 } 654 if r.URL.Path == "/token" { 655 w.WriteHeader(200) 656 _, _ = w.Write([]byte("{ \"id_token\": \"foo\" }")) 657 } 658 })) 659 defer testServer.Close() 660 661 oidcMeta := openIdMetadata{ 662 Issuer: testServer.URL, 663 AuthURL: testServer.URL + "/auth", 664 TokenURL: testServer.URL + "/token", 665 JWKSURL: testServer.URL + "/jwks", 666 UserInfoURL: "", 667 Algorithms: nil, 668 ScopesSupported: []string{"openid"}, 669 ResponseTypesSupported: []string{"code"}, 670 } 671 oidcMetadata, err := json.Marshal(oidcMeta) 672 assert.Nil(t, err) 673 674 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 675 util.Clock = util.ClockMock{Time: clockTime} 676 677 cfg := config.NewConfig() 678 cfg.Server.WebRoot = "/kiali-test" 679 cfg.LoginToken.SigningKey = "kiali67890123456" 680 cfg.LoginToken.ExpirationSeconds = 1 681 cfg.Auth.OpenId.IssuerUri = testServer.URL 682 cfg.Auth.OpenId.ClientId = "kiali-client" 683 config.Set(cfg) 684 685 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 686 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 687 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 688 689 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 690 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 691 request := httptest.NewRequest(http.MethodGet, uri, nil) 692 request.AddCookie(&http.Cookie{ 693 Name: OpenIdNonceCookieName, 694 Value: "nonceString", 695 }) 696 697 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 698 699 rr := httptest.NewRecorder() 700 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 701 assert.Failf(t, "Callback function shouldn't have been called.", "") 702 })).ServeHTTP(rr, request) 703 704 // nonce cookie cleanup 705 response := rr.Result() 706 assert.Len(t, response.Cookies(), 1) 707 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 708 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 709 710 // Redirection to boot the UI 711 u, _ := url.Parse(response.Header.Get("Location")) 712 q, _ := url.ParseQuery(u.RawQuery) 713 assert.Contains(t, q["openid_error"][0], "cannot parse received id_token from the OpenId provider") 714 assert.Equal(t, http.StatusFound, response.StatusCode) 715 } 716 717 func TestOpenIdCodeFlowShouldRejectMissingAuthorizationCode(t *testing.T) { 718 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 719 util.Clock = util.ClockMock{Time: clockTime} 720 721 cfg := config.NewConfig() 722 cfg.Server.WebRoot = "/kiali-test" 723 cfg.LoginToken.SigningKey = "kiali67890123456" 724 cfg.LoginToken.ExpirationSeconds = 1 725 config.Set(cfg) 726 727 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 728 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 729 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 730 731 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 732 uri := fmt.Sprintf("/api/authenticate?state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 733 request := httptest.NewRequest(http.MethodGet, uri, nil) 734 request.AddCookie(&http.Cookie{ 735 Name: OpenIdNonceCookieName, 736 Value: "nonceString", 737 }) 738 739 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 740 741 callbackCalled := false 742 rr := httptest.NewRecorder() 743 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 744 callbackCalled = true 745 })).ServeHTTP(rr, request) 746 747 response := rr.Result() 748 // No cleanup is done if there are not enough params so that the authorization code flow is triggered 749 assert.Len(t, response.Cookies(), 0) 750 751 // A missing State parameter has the effect that the auth controller ignores the request and 752 // passes it to the next handler. 753 assert.True(t, callbackCalled) 754 } 755 756 func TestOpenIdCodeFlowShouldFailWithIdTokenWithoutExpiration(t *testing.T) { 757 cachedOpenIdMetadata = nil 758 var oidcMetadata []byte 759 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 760 if r.URL.Path == "/.well-known/openid-configuration" { 761 w.WriteHeader(200) 762 _, _ = w.Write(oidcMetadata) 763 } 764 if r.URL.Path == "/token" { 765 w.WriteHeader(200) 766 _, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQifQ.ih34Mh3Sao9bnXCjaobfAEO1BnHnuuLBWxihAzwUqw8\" }")) 767 } 768 })) 769 defer testServer.Close() 770 771 oidcMeta := openIdMetadata{ 772 Issuer: testServer.URL, 773 AuthURL: testServer.URL + "/auth", 774 TokenURL: testServer.URL + "/token", 775 JWKSURL: testServer.URL + "/jwks", 776 UserInfoURL: "", 777 Algorithms: nil, 778 ScopesSupported: []string{"openid"}, 779 ResponseTypesSupported: []string{"code"}, 780 } 781 oidcMetadata, err := json.Marshal(oidcMeta) 782 assert.Nil(t, err) 783 784 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 785 util.Clock = util.ClockMock{Time: clockTime} 786 787 cfg := config.NewConfig() 788 cfg.Server.WebRoot = "/kiali-test" 789 cfg.LoginToken.SigningKey = "kiali67890123456" 790 cfg.LoginToken.ExpirationSeconds = 1 791 cfg.Auth.OpenId.IssuerUri = testServer.URL 792 cfg.Auth.OpenId.ClientId = "kiali-client" 793 config.Set(cfg) 794 795 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 796 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 797 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 798 799 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 800 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 801 request := httptest.NewRequest(http.MethodGet, uri, nil) 802 request.AddCookie(&http.Cookie{ 803 Name: OpenIdNonceCookieName, 804 Value: "nonceString", 805 }) 806 807 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 808 809 rr := httptest.NewRecorder() 810 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 811 assert.Failf(t, "Callback function shouldn't have been called.", "") 812 })).ServeHTTP(rr, request) 813 814 // nonce cookie cleanup 815 response := rr.Result() 816 assert.Len(t, response.Cookies(), 1) 817 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 818 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 819 820 // Redirection to boot the UI 821 q := url.Values{} 822 q.Add("openid_error", "the received id_token from the OpenId provider has missing the required 'exp' claim") 823 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 824 assert.Equal(t, http.StatusFound, response.StatusCode) 825 } 826 827 func TestOpenIdCodeFlowShouldFailWithIdTokenWithNonNumericExpClaim(t *testing.T) { 828 cachedOpenIdMetadata = nil 829 var oidcMetadata []byte 830 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 831 if r.URL.Path == "/.well-known/openid-configuration" { 832 w.WriteHeader(200) 833 _, _ = w.Write(oidcMetadata) 834 } 835 if r.URL.Path == "/token" { 836 w.WriteHeader(200) 837 _, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIm5vbmNlIjoiMWJhOWI4MzRkMDhhYzgxZmViMzRlMjA4NDAyZWIxOGU5MDliZTA4NDUxOGMzMjg1MTA5NDAxODQiLCJleHAiOiJmb28ifQ.wdM3yQPwAXLaqZbVku_fcXpisC3tzES8_UUwjbxSPrc\" }")) 838 } 839 })) 840 defer testServer.Close() 841 842 oidcMeta := openIdMetadata{ 843 Issuer: testServer.URL, 844 AuthURL: testServer.URL + "/auth", 845 TokenURL: testServer.URL + "/token", 846 JWKSURL: testServer.URL + "/jwks", 847 UserInfoURL: "", 848 Algorithms: nil, 849 ScopesSupported: []string{"openid"}, 850 ResponseTypesSupported: []string{"code"}, 851 } 852 oidcMetadata, err := json.Marshal(oidcMeta) 853 assert.Nil(t, err) 854 855 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 856 util.Clock = util.ClockMock{Time: clockTime} 857 858 cfg := config.NewConfig() 859 cfg.Server.WebRoot = "/kiali-test" 860 cfg.LoginToken.SigningKey = "kiali67890123456" 861 cfg.LoginToken.ExpirationSeconds = 1 862 cfg.Auth.OpenId.IssuerUri = testServer.URL 863 cfg.Auth.OpenId.ClientId = "kiali-client" 864 config.Set(cfg) 865 866 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 867 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 868 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 869 870 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 871 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 872 request := httptest.NewRequest(http.MethodGet, uri, nil) 873 request.AddCookie(&http.Cookie{ 874 Name: OpenIdNonceCookieName, 875 Value: "nonceString", 876 }) 877 878 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 879 880 rr := httptest.NewRecorder() 881 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 882 assert.Failf(t, "Callback function shouldn't have been called.", "") 883 })).ServeHTTP(rr, request) 884 885 // nonce cookie cleanup 886 response := rr.Result() 887 assert.Len(t, response.Cookies(), 1) 888 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 889 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 890 891 // Redirection to boot the UI 892 u, _ := url.Parse(response.Header.Get("Location")) 893 q, _ := url.ParseQuery(u.RawQuery) 894 assert.Contains(t, q["openid_error"][0], "token exp claim is present, but invalid") 895 assert.Equal(t, http.StatusFound, response.StatusCode) 896 } 897 898 func TestOpenIdCodeFlowShouldRejectInvalidState(t *testing.T) { 899 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 900 util.Clock = util.ClockMock{Time: clockTime} 901 902 cfg := config.NewConfig() 903 cfg.Server.WebRoot = "/kiali-test" 904 cfg.LoginToken.SigningKey = "kiali67890123456" 905 cfg.LoginToken.ExpirationSeconds = 1 906 config.Set(cfg) 907 908 // Calculate a hash of the wrong string. 909 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "badNonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 910 uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 911 request := httptest.NewRequest(http.MethodGet, uri, nil) 912 request.AddCookie(&http.Cookie{ 913 Name: OpenIdNonceCookieName, 914 Value: "nonceString", 915 }) 916 917 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 918 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 919 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 920 921 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 922 923 rr := httptest.NewRecorder() 924 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 925 assert.Failf(t, "Callback function shouldn't have been called.", "") 926 })).ServeHTTP(rr, request) 927 928 // nonce cookie cleanup 929 response := rr.Result() 930 assert.Len(t, response.Cookies(), 1) 931 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 932 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 933 934 // Redirection to boot the UI 935 q := url.Values{} 936 q.Add("openid_error", "Request rejected: CSRF mitigation") 937 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 938 assert.Equal(t, http.StatusFound, response.StatusCode) 939 } 940 941 func TestOpenIdCodeFlowShouldRejectBadStateFormat(t *testing.T) { 942 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 943 util.Clock = util.ClockMock{Time: clockTime} 944 945 cfg := config.NewConfig() 946 cfg.Server.WebRoot = "/kiali-test" 947 cfg.LoginToken.SigningKey = "kiali67890123456" 948 cfg.LoginToken.ExpirationSeconds = 1 949 config.Set(cfg) 950 951 // Calculate a hash of the wrong string. 952 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 953 uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%xp%s", stateHash, clockTime.UTC().Format("060102150405")) 954 request := httptest.NewRequest(http.MethodGet, uri, nil) 955 request.AddCookie(&http.Cookie{ 956 Name: OpenIdNonceCookieName, 957 Value: "nonceString", 958 }) 959 960 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 961 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 962 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 963 964 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 965 966 rr := httptest.NewRecorder() 967 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 968 assert.Failf(t, "Callback function shouldn't have been called.", "") 969 })).ServeHTTP(rr, request) 970 971 // nonce cookie cleanup 972 response := rr.Result() 973 assert.Len(t, response.Cookies(), 1) 974 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 975 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 976 977 // Redirection to boot the UI 978 q := url.Values{} 979 q.Add("openid_error", "Request rejected: State parameter is invalid") 980 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 981 assert.Equal(t, http.StatusFound, response.StatusCode) 982 } 983 984 func TestOpenIdCodeFlowShouldRejectMissingState(t *testing.T) { 985 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 986 util.Clock = util.ClockMock{Time: clockTime} 987 988 cfg := config.NewConfig() 989 cfg.Server.WebRoot = "/kiali-test" 990 cfg.LoginToken.SigningKey = "kiali67890123456" 991 cfg.LoginToken.ExpirationSeconds = 1 992 config.Set(cfg) 993 994 uri := "/api/authenticate?code=f0code" 995 request := httptest.NewRequest(http.MethodGet, uri, nil) 996 request.AddCookie(&http.Cookie{ 997 Name: OpenIdNonceCookieName, 998 Value: "nonceString", 999 }) 1000 1001 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 1002 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 1003 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 1004 1005 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 1006 1007 callbackCalled := false 1008 rr := httptest.NewRecorder() 1009 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1010 callbackCalled = true 1011 })).ServeHTTP(rr, request) 1012 1013 response := rr.Result() 1014 // No cleanup is done if there are not enough params so that the authorization code flow is triggered 1015 assert.Len(t, response.Cookies(), 0) 1016 1017 // A missing State parameter has the effect that the auth controller ignores the request and 1018 // passes it to the next handler. 1019 assert.True(t, callbackCalled) 1020 } 1021 1022 func TestOpenIdCodeFlowShouldRejectMissingNonceCookie(t *testing.T) { 1023 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 1024 util.Clock = util.ClockMock{Time: clockTime} 1025 1026 cfg := config.NewConfig() 1027 cfg.Server.WebRoot = "/kiali-test" 1028 cfg.LoginToken.SigningKey = "kiali67890123456" 1029 cfg.LoginToken.ExpirationSeconds = 1 1030 config.Set(cfg) 1031 1032 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 1033 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 1034 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 1035 1036 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 1037 uri := fmt.Sprintf("/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 1038 request := httptest.NewRequest(http.MethodGet, uri, nil) 1039 1040 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 1041 1042 callbackCalled := false 1043 rr := httptest.NewRecorder() 1044 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1045 callbackCalled = true 1046 })).ServeHTTP(rr, request) 1047 1048 // No cleanup is done if there are not enough params so that the authorization code flow is triggered 1049 response := rr.Result() 1050 assert.Len(t, response.Cookies(), 0) 1051 1052 // A missing nonce cookie has the effect that the auth controller ignores the request and 1053 // passes it to the next handler. 1054 assert.True(t, callbackCalled) 1055 } 1056 1057 func TestOpenIdCodeFlowShouldRejectMissingNonceInToken(t *testing.T) { 1058 cachedOpenIdMetadata = nil 1059 var oidcMetadata []byte 1060 testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1061 if r.URL.Path == "/.well-known/openid-configuration" { 1062 w.WriteHeader(200) 1063 _, _ = w.Write(oidcMetadata) 1064 } 1065 if r.URL.Path == "/token" { 1066 w.WriteHeader(200) 1067 _, _ = w.Write([]byte("{ \"id_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJqZG9lQGRvbWFpbi5jb20iLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsImV4cCI6MTMxMTI4MTk3MH0.xAoq7T-wti__Je1PDuTgNonoVSu059FzpOHsNm26YTg\" }")) 1068 } 1069 })) 1070 defer testServer.Close() 1071 1072 oidcMeta := openIdMetadata{ 1073 Issuer: testServer.URL, 1074 AuthURL: testServer.URL + "/auth", 1075 TokenURL: testServer.URL + "/token", 1076 JWKSURL: testServer.URL + "/jwks", 1077 UserInfoURL: "", 1078 Algorithms: nil, 1079 ScopesSupported: []string{"openid"}, 1080 ResponseTypesSupported: []string{"code"}, 1081 } 1082 oidcMetadata, err := json.Marshal(oidcMeta) 1083 assert.Nil(t, err) 1084 1085 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 1086 util.Clock = util.ClockMock{Time: clockTime} 1087 1088 cfg := config.NewConfig() 1089 cfg.Server.WebRoot = "/kiali-test" 1090 cfg.LoginToken.SigningKey = "kiali67890123456" 1091 cfg.LoginToken.ExpirationSeconds = 1 1092 cfg.Auth.OpenId.IssuerUri = testServer.URL 1093 cfg.Auth.OpenId.ClientId = "kiali-client" 1094 config.Set(cfg) 1095 1096 k8s := kubetest.NewFakeK8sClient(&core_v1.Namespace{ObjectMeta: meta_v1.ObjectMeta{Name: "Foo"}}) 1097 mockClientFactory := kubetest.NewK8SClientFactoryMock(k8s) 1098 cache := cache.NewTestingCacheWithFactory(t, mockClientFactory, *cfg) 1099 1100 stateHash := sha256.Sum224([]byte(fmt.Sprintf("%s+%s+%s", "nonceString", clockTime.UTC().Format("060102150405"), getSigningKey(cfg)))) 1101 uri := fmt.Sprintf("https://kiali.io:44/api/authenticate?code=f0code&state=%x-%s", stateHash, clockTime.UTC().Format("060102150405")) 1102 request := httptest.NewRequest(http.MethodGet, uri, nil) 1103 request.AddCookie(&http.Cookie{ 1104 Name: OpenIdNonceCookieName, 1105 Value: "nonceString", 1106 }) 1107 1108 controller := NewOpenIdAuthController(NewCookieSessionPersistor(cfg), cache, mockClientFactory, cfg) 1109 1110 rr := httptest.NewRecorder() 1111 controller.GetAuthCallbackHandler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 1112 assert.Failf(t, "Callback function shouldn't have been called.", "") 1113 })).ServeHTTP(rr, request) 1114 1115 // nonce cookie cleanup 1116 response := rr.Result() 1117 assert.Len(t, response.Cookies(), 1) 1118 assert.Equal(t, OpenIdNonceCookieName, response.Cookies()[0].Name) 1119 assert.True(t, clockTime.After(response.Cookies()[0].Expires)) 1120 1121 // Redirection to boot the UI 1122 q := url.Values{} 1123 q.Add("openid_error", "OpenId token rejected: nonce code mismatch") 1124 assert.Equal(t, "/kiali-test/?"+q.Encode(), response.Header.Get("Location")) 1125 assert.Equal(t, http.StatusFound, response.StatusCode) 1126 }