github.com/kiali/kiali@v1.84.0/business/authentication/session_persistor_test.go (about) 1 package authentication 2 3 import ( 4 "encoding/base64" 5 "encoding/json" 6 "net/http" 7 "net/http/httptest" 8 "strings" 9 "testing" 10 "time" 11 12 "github.com/stretchr/testify/assert" 13 14 "github.com/kiali/kiali/config" 15 "github.com/kiali/kiali/util" 16 ) 17 18 type testSessionPayload struct { 19 FirstField string `json:"firstField,omitempty"` 20 } 21 22 // TestSecureFlag tests that the cookie Secure flag is set to true when using HTTPS. 23 func TestSecureFlag(t *testing.T) { 24 cfg := config.NewConfig() 25 cfg.Server.WebRoot = "/kiali-app" 26 cfg.LoginToken.SigningKey = "kiali67890123456" 27 cfg.Identity.CertFile = "foo.cert" // setting conf.Identity will make it look as if the endpoint ... 28 cfg.Identity.PrivateKeyFile = "foo.key" // ... is HTTPS - this causes the cookies' Secure flag to be true 29 config.Set(cfg) 30 31 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 32 util.Clock = util.ClockMock{Time: clockTime} 33 34 payload := testSessionPayload{ 35 FirstField: "Foo", 36 } 37 38 rr := httptest.NewRecorder() 39 persistor := NewCookieSessionPersistor(cfg) 40 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 41 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 42 43 response := rr.Result() 44 45 assert.Nil(t, err) 46 assert.Len(t, response.Cookies(), 1) 47 48 cookie := response.Cookies()[0] 49 assert.True(t, cookie.HttpOnly) 50 assert.True(t, cfg.IsServerHTTPS()) 51 assert.True(t, cookie.Secure) 52 } 53 54 // TestCreateSessionNoChunks tests that the CookieSessionPersistor correctly 55 // sets one cookie if the payload of a session fits in one browser cookie 56 func TestCreateSessionNoChunks(t *testing.T) { 57 cfg := config.NewConfig() 58 cfg.Server.WebRoot = "/kiali-app" 59 cfg.LoginToken.SigningKey = "kiali67890123456" 60 config.Set(cfg) 61 62 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 63 util.Clock = util.ClockMock{Time: clockTime} 64 65 payload := testSessionPayload{ 66 FirstField: "Foo", 67 } 68 69 rr := httptest.NewRecorder() 70 persistor := NewCookieSessionPersistor(cfg) 71 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 72 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 73 74 response := rr.Result() 75 76 assert.Nil(t, err) 77 assert.Len(t, response.Cookies(), 1) 78 79 cookie := response.Cookies()[0] 80 assert.True(t, cookie.HttpOnly) 81 assert.False(t, cfg.IsServerHTTPS()) 82 assert.False(t, cookie.Secure) 83 assert.Equal(t, AESSessionCookieName, cookie.Name) 84 assert.Equal(t, "/kiali-app", cookie.Path) 85 assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) 86 assert.Equal(t, expiresTime, cookie.Expires) 87 88 // Unfortunately, the internals of the CreateSession is using a "nonce" to encrypt data. 89 // This means that the output is not predictable. The only thing is possible to test here 90 // is to check that the returned cookie won't have a plain text payload on it (which is the "Foo" text). 91 decodedB64Cookie, err := base64.StdEncoding.DecodeString(response.Cookies()[0].Value) 92 assert.Nil(t, err) 93 payloadJson, err1 := json.Marshal(payload) 94 assert.Nil(t, err1) 95 // sometimes (randomly) the result of "nonce" encrypted data can contain the plain text, so avoiding "NotContains" direct assertion here and checking the JSON 96 assert.NotEqual(t, cookie.Value, "Foo") 97 assert.NotEqual(t, string(decodedB64Cookie), "Foo") 98 assert.NotContains(t, string(decodedB64Cookie), string(payloadJson)) 99 assert.NotContains(t, cookie.Value, string(payloadJson)) 100 } 101 102 // TestCreateSessionWithChunks tests that the CookieSessionPersistor correctly 103 // sets the needed browser cookies if the payload does not fit in one browser cookie. 104 func TestCreateSessionWithChunks(t *testing.T) { 105 cfg := config.NewConfig() 106 cfg.Server.WebRoot = "/kiali-app" 107 cfg.LoginToken.SigningKey = "kiali67890123456" 108 config.Set(cfg) 109 110 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 111 util.Clock = util.ClockMock{Time: clockTime} 112 113 // Create a long enough payload to overflow our maximum size of a cookie. 114 payload := testSessionPayload{ 115 FirstField: strings.Repeat("1234567890", SessionCookieMaxSize/len("1234567890")), 116 } 117 118 rr := httptest.NewRecorder() 119 persistor := NewCookieSessionPersistor(cfg) 120 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 121 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 122 123 response := rr.Result() 124 125 assert.Nil(t, err) 126 assert.Len(t, response.Cookies(), 3) 127 assert.Equal(t, AESSessionCookieName, response.Cookies()[0].Name) 128 assert.Equal(t, config.TokenCookieName+"-aes-1", response.Cookies()[1].Name) 129 assert.Equal(t, config.TokenCookieName+"-chunks", response.Cookies()[2].Name) 130 assert.Equal(t, "2", response.Cookies()[2].Value) 131 132 for _, cookie := range response.Cookies() { 133 assert.True(t, cookie.HttpOnly) 134 assert.False(t, cookie.Secure) 135 assert.Equal(t, "/kiali-app", cookie.Path) 136 assert.Equal(t, http.SameSiteStrictMode, cookie.SameSite) 137 assert.Equal(t, expiresTime, cookie.Expires) 138 } 139 } 140 141 // TestCreateSessionRejectsNilPayload tests that the CookieSessionPersistor rejects 142 // creating a session if a nil payload is passed. 143 func TestCreateSessionRejectsNilPayload(t *testing.T) { 144 rr := httptest.NewRecorder() 145 persistor := NewCookieSessionPersistor(config.NewConfig()) 146 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 147 err := persistor.CreateSession(nil, rr, "test", expiresTime, nil) 148 149 response := rr.Result() 150 151 assert.NotNil(t, err) 152 assert.Len(t, response.Cookies(), 0) 153 } 154 155 // TestCreateSessionRejectsEmptyStrategy tests that the CookieSessionPersistor rejects 156 // creating a session if an empty strategy is passed. 157 func TestCreateSessionRejectsEmptyStrategy(t *testing.T) { 158 payload := testSessionPayload{ 159 FirstField: "1234567890", 160 } 161 162 rr := httptest.NewRecorder() 163 persistor := NewCookieSessionPersistor(config.NewConfig()) 164 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 165 err := persistor.CreateSession(nil, rr, "", expiresTime, payload) 166 167 response := rr.Result() 168 169 assert.NotNil(t, err) 170 assert.Len(t, response.Cookies(), 0) 171 } 172 173 // TestCreateSessionRejectsExpireTimeInThePast tests that the CookieSessionPersistor rejects 174 // creating a session if the indicated expiration time is already in the past. 175 func TestCreateSessionRejectsExpireTimeInThePast(t *testing.T) { 176 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 1, time.UTC) 177 util.Clock = util.ClockMock{Time: clockTime} 178 179 payload := testSessionPayload{ 180 FirstField: "1234567890", 181 } 182 183 rr := httptest.NewRecorder() 184 persistor := NewCookieSessionPersistor(config.NewConfig()) 185 expiresTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 186 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 187 188 response := rr.Result() 189 190 assert.NotNil(t, err) 191 assert.Len(t, response.Cookies(), 0) 192 } 193 194 // TestReadSessionWithNoActiveSession tests that the CookieSessionPersistor does not emit 195 // an error when restoring a session if the HTTP request contains no active session. 196 func TestReadSessionWithNoActiveSession(t *testing.T) { 197 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 198 199 var payload testSessionPayload 200 rr := httptest.NewRecorder() 201 persistor := NewCookieSessionPersistor(config.NewConfig()) 202 sData, err := persistor.ReadSession(request, rr, payload) 203 204 assert.Nil(t, sData) 205 assert.Nil(t, err) 206 } 207 208 // TestReadSessionWithSingleCookie tests that the CookieSessionPersistor correctly 209 // restores a session that fit in a single browser cookie. 210 func TestReadSessionWithSingleCookie(t *testing.T) { 211 cfg := config.NewConfig() 212 cfg.Auth.Strategy = "test" 213 cfg.LoginToken.SigningKey = "kiali67890123456" 214 config.Set(cfg) 215 216 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 217 util.Clock = util.ClockMock{Time: clockTime} 218 219 payload := testSessionPayload{ 220 FirstField: "FooBar", 221 } 222 223 rr := httptest.NewRecorder() 224 persistor := NewCookieSessionPersistor(cfg) 225 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 226 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 227 assert.Nil(t, err) 228 229 response := rr.Result() 230 231 // Create a request containing the cookies of the response 232 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 233 assert.Len(t, response.Cookies(), 1) 234 for _, c := range response.Cookies() { 235 request.AddCookie(c) 236 } 237 238 // Read/restore the session. 239 rr = httptest.NewRecorder() 240 restoredPayload := testSessionPayload{} 241 sData, err := persistor.ReadSession(request, rr, &restoredPayload) 242 243 assert.Nil(t, err) 244 assert.NotNil(t, sData) 245 assert.Equal(t, expiresTime, sData.ExpiresOn) 246 assert.Equal(t, "test", sData.Strategy) 247 assert.Equal(t, "FooBar", restoredPayload.FirstField) 248 } 249 250 // TestReadSessionWithTwoCookies tests that the CookieSessionPersistor correctly 251 // restores a session that didn't fit in a single browser cookie. 252 func TestReadSessionWithTwoCookies(t *testing.T) { 253 cfg := config.NewConfig() 254 cfg.Auth.Strategy = "test" 255 cfg.LoginToken.SigningKey = "kiali67890123456" 256 config.Set(cfg) 257 258 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 259 util.Clock = util.ClockMock{Time: clockTime} 260 261 payloadStr := strings.Repeat("FooBar", SessionCookieMaxSize/len("FooBar")) 262 payload := testSessionPayload{ 263 FirstField: payloadStr, 264 } 265 266 rr := httptest.NewRecorder() 267 persistor := NewCookieSessionPersistor(cfg) 268 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 269 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 270 assert.Nil(t, err) 271 272 response := rr.Result() 273 274 // Create a request containing the cookies of the response 275 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 276 assert.Len(t, response.Cookies(), 3) 277 for _, c := range response.Cookies() { 278 request.AddCookie(c) 279 } 280 281 // Read/restore the session. 282 rr = httptest.NewRecorder() 283 restoredPayload := testSessionPayload{} 284 sData, err := persistor.ReadSession(request, rr, &restoredPayload) 285 286 assert.Nil(t, err) 287 assert.NotNil(t, sData) 288 assert.Equal(t, expiresTime, sData.ExpiresOn) 289 assert.Equal(t, "test", sData.Strategy) 290 assert.Equal(t, payloadStr, restoredPayload.FirstField) 291 } 292 293 // TestReadSessionRejectsExpired tests that the CookieSessionPersistor does 294 // not restore a session that is already expired. 295 func TestReadSessionRejectsExpired(t *testing.T) { 296 cfg := config.NewConfig() 297 cfg.Auth.Strategy = "test" 298 cfg.LoginToken.SigningKey = "kiali67890123456" 299 config.Set(cfg) 300 301 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 302 util.Clock = util.ClockMock{Time: clockTime} 303 304 payload := testSessionPayload{ 305 FirstField: "FooBar", 306 } 307 308 rr := httptest.NewRecorder() 309 persistor := NewCookieSessionPersistor(cfg) 310 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 311 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 312 assert.Nil(t, err) 313 314 response := rr.Result() 315 316 // Create a request containing the cookies of the response 317 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 318 for _, c := range response.Cookies() { 319 request.AddCookie(c) 320 } 321 322 // Go to the future 323 util.Clock = util.ClockMock{Time: expiresTime} 324 325 // Read/restore the session. 326 rr = httptest.NewRecorder() 327 restoredPayload := testSessionPayload{} 328 sData, err := persistor.ReadSession(request, rr, &restoredPayload) 329 330 assert.Nil(t, err) 331 assert.Nil(t, sData) 332 assert.Empty(t, restoredPayload.FirstField) 333 } 334 335 // TestReadSessionRejectsDifferentStrategy tests that the CookieSessionPersistor does 336 // not restore a session that was created with a strategy different from the currently configured one. 337 func TestReadSessionRejectsDifferentStrategy(t *testing.T) { 338 cfg := config.NewConfig() 339 cfg.LoginToken.SigningKey = "kiali67890123456" 340 config.Set(cfg) 341 342 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 343 util.Clock = util.ClockMock{Time: clockTime} 344 345 payload := testSessionPayload{ 346 FirstField: "FooBar", 347 } 348 349 rr := httptest.NewRecorder() 350 persistor := NewCookieSessionPersistor(cfg) 351 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 352 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 353 assert.Nil(t, err) 354 355 response := rr.Result() 356 357 // Create a request containing the cookies of the response 358 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 359 for _, c := range response.Cookies() { 360 request.AddCookie(c) 361 } 362 363 // Read/restore the session. 364 rr = httptest.NewRecorder() 365 restoredPayload := testSessionPayload{} 366 sData, err := persistor.ReadSession(request, rr, &restoredPayload) 367 368 assert.Nil(t, err) 369 assert.Nil(t, sData) 370 assert.Empty(t, restoredPayload.FirstField) 371 } 372 373 // TestReadSessionRejectsDifferentSigningKey tests that the CookieSessionPersistor does 374 // not restore a session that was created with an old Kiali signing key 375 func TestReadSessionRejectsDifferentSigningKey(t *testing.T) { 376 cfg := config.NewConfig() 377 cfg.Auth.Strategy = "test" 378 cfg.LoginToken.SigningKey = "kiali67890123456" 379 config.Set(cfg) 380 381 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 382 util.Clock = util.ClockMock{Time: clockTime} 383 384 payload := testSessionPayload{ 385 FirstField: "FooBar", 386 } 387 388 rr := httptest.NewRecorder() 389 persistor := NewCookieSessionPersistor(cfg) 390 expiresTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 391 err := persistor.CreateSession(nil, rr, "test", expiresTime, payload) 392 assert.Nil(t, err) 393 394 response := rr.Result() 395 396 // Create a request containing the cookies of the response 397 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 398 for _, c := range response.Cookies() { 399 request.AddCookie(c) 400 } 401 402 // Set a new signing key 403 cfg.LoginToken.SigningKey = "kiali-----------" 404 config.Set(cfg) 405 406 // Read/restore the session. 407 rr = httptest.NewRecorder() 408 restoredPayload := testSessionPayload{} 409 sData, err := persistor.ReadSession(request, rr, &restoredPayload) 410 411 assert.NotNil(t, err) // When the signing key does not match, an error is generated. 412 assert.Nil(t, sData) 413 assert.Empty(t, restoredPayload.FirstField) 414 } 415 416 // TestTerminateSessionClearsNonAesSession tests that the CookieSessionPersistor correctly clears 417 // a session that was created with the old JWT method. 418 func TestTerminateSessionClearsNonAesSession(t *testing.T) { 419 c := config.NewConfig() 420 c.Server.WebRoot = "/kiali-app" 421 config.Set(c) 422 423 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 424 util.Clock = util.ClockMock{Time: clockTime} 425 426 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 427 cookie := http.Cookie{ 428 Name: config.TokenCookieName, 429 Value: "", 430 Expires: time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC), 431 } 432 request.AddCookie(&cookie) 433 434 persistor := NewCookieSessionPersistor(c) 435 436 rr := httptest.NewRecorder() 437 persistor.TerminateSession(request, rr) 438 439 response := rr.Result() 440 assert.Len(t, response.Cookies(), 1) 441 assert.Equal(t, config.TokenCookieName, response.Cookies()[0].Name) 442 assert.Empty(t, response.Cookies()[0].Value) 443 assert.True(t, response.Cookies()[0].Expires.Before(util.Clock.Now())) 444 assert.Equal(t, "/kiali-app", response.Cookies()[0].Path) 445 } 446 447 // TestTerminateSessionClearsAesSession tests that the CookieSessionPersistor correctly clears 448 // a session that is using encrypted cookies with the AES-GCM algorithm where no "-chunks" cookie is set. 449 func TestTerminateSessionClearsAesSession(t *testing.T) { 450 c := config.NewConfig() 451 c.Server.WebRoot = "/kiali-app" 452 config.Set(c) 453 454 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 455 util.Clock = util.ClockMock{Time: clockTime} 456 457 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 458 cookie := http.Cookie{ 459 Name: AESSessionCookieName, 460 Value: "", 461 Expires: time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC), 462 } 463 request.AddCookie(&cookie) 464 465 persistor := NewCookieSessionPersistor(c) 466 467 rr := httptest.NewRecorder() 468 persistor.TerminateSession(request, rr) 469 470 response := rr.Result() 471 assert.Len(t, response.Cookies(), 1) 472 assert.Equal(t, AESSessionCookieName, response.Cookies()[0].Name) 473 assert.Empty(t, response.Cookies()[0].Value) 474 assert.True(t, response.Cookies()[0].Expires.Before(util.Clock.Now())) 475 assert.Equal(t, "/kiali-app", response.Cookies()[0].Path) 476 } 477 478 // TestTerminateSessionClearsAesSessionWithOneChunk tests that the CookieSessionPersistor correctly 479 // clears a session where the payload fit in a single browser cookie, yet the "-chunks" cookie is set. 480 func TestTerminateSessionClearsAesSessionWithOneChunk(t *testing.T) { 481 c := config.NewConfig() 482 c.Server.WebRoot = "/kiali-app" 483 config.Set(c) 484 485 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 486 util.Clock = util.ClockMock{Time: clockTime} 487 488 expireTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 489 490 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 491 cookie := http.Cookie{ 492 Name: AESSessionCookieName, 493 Value: "", 494 Expires: expireTime, 495 } 496 request.AddCookie(&cookie) 497 cookie = http.Cookie{ 498 Name: config.TokenCookieName + "-chunks", 499 Value: "1", 500 Expires: expireTime, 501 } 502 request.AddCookie(&cookie) 503 504 persistor := NewCookieSessionPersistor(c) 505 506 rr := httptest.NewRecorder() 507 persistor.TerminateSession(request, rr) 508 509 response := rr.Result() 510 assert.Len(t, response.Cookies(), 2) 511 assert.Equal(t, AESSessionCookieName, response.Cookies()[0].Name) 512 assert.Equal(t, config.TokenCookieName+"-chunks", response.Cookies()[1].Name) 513 514 for i := 0; i < 2; i++ { 515 assert.True(t, response.Cookies()[i].Expires.Before(util.Clock.Now())) 516 assert.Equal(t, "/kiali-app", response.Cookies()[i].Path) 517 assert.Empty(t, response.Cookies()[i].Value) 518 } 519 } 520 521 // TestTerminateSessionClearsAesSessionWithTwoChunks tests that the CookieSessionPersistor correctly 522 // clears a session where the payload didn't fit in a single browser cookie. 523 func TestTerminateSessionClearsAesSessionWithTwoChunks(t *testing.T) { 524 c := config.NewConfig() 525 c.Server.WebRoot = "/kiali-app" 526 config.Set(c) 527 528 clockTime := time.Date(2021, 12, 1, 0, 0, 0, 0, time.UTC) 529 util.Clock = util.ClockMock{Time: clockTime} 530 531 expireTime := time.Date(2021, 12, 1, 1, 0, 0, 0, time.UTC) 532 533 request := httptest.NewRequest(http.MethodGet, "/api/logout", nil) 534 cookie := http.Cookie{ 535 Name: AESSessionCookieName, 536 Value: "", 537 Expires: expireTime, 538 } 539 request.AddCookie(&cookie) 540 cookie = http.Cookie{ 541 Name: config.TokenCookieName + "-chunks", 542 Value: "2", 543 Expires: expireTime, 544 } 545 request.AddCookie(&cookie) 546 cookie = http.Cookie{ 547 Name: config.TokenCookieName + "-aes-1", 548 Value: "x", 549 Expires: expireTime, 550 } 551 request.AddCookie(&cookie) 552 553 persistor := NewCookieSessionPersistor(c) 554 555 rr := httptest.NewRecorder() 556 persistor.TerminateSession(request, rr) 557 558 response := rr.Result() 559 assert.Len(t, response.Cookies(), 3) 560 assert.Equal(t, config.TokenCookieName+"-aes-1", response.Cookies()[0].Name) 561 assert.Equal(t, AESSessionCookieName, response.Cookies()[1].Name) 562 assert.Equal(t, config.TokenCookieName+"-chunks", response.Cookies()[2].Name) 563 564 for i := 0; i < 3; i++ { 565 assert.True(t, response.Cookies()[i].Expires.Before(util.Clock.Now())) 566 assert.Equal(t, "/kiali-app", response.Cookies()[i].Path) 567 assert.Empty(t, response.Cookies()[i].Value) 568 } 569 }