handler_test.go (22765B)
1 package login 2 3 import ( 4 "crypto/rand" 5 "crypto/rsa" 6 "crypto/x509" 7 "encoding/json" 8 "encoding/pem" 9 "errors" 10 "fmt" 11 "net/http" 12 "net/http/httptest" 13 "net/url" 14 "strconv" 15 "strings" 16 "testing" 17 "time" 18 19 "github.com/dgrijalva/jwt-go" 20 . "github.com/stretchr/testify/assert" 21 "github.com/tarent/loginsrv/model" 22 "github.com/tarent/loginsrv/oauth2" 23 ) 24 25 const TypeJSON = "Content-Type: application/json" 26 const TypeForm = "Content-Type: application/x-www-form-urlencoded" 27 const AcceptHTML = "Accept: text/html" 28 const AcceptJwt = "Accept: application/jwt" 29 30 func testConfig() *Config { 31 testConfig := DefaultConfig() 32 testConfig.LoginPath = "/context/login" 33 testConfig.CookieDomain = "example.com" 34 testConfig.CookieExpiry = 23 * time.Hour 35 testConfig.JwtRefreshes = 1 36 return testConfig 37 } 38 39 func TestHandler_NewFromConfig(t *testing.T) { 40 41 testCases := []struct { 42 config *Config 43 backendCount int 44 oauthCount int 45 expectError bool 46 }{ 47 { 48 &Config{ 49 Backends: Options{ 50 "simple": {"bob": "secret"}, 51 }, 52 Oauth: Options{ 53 "github": {"client_id": "xxx", "client_secret": "YYY"}, 54 }, 55 }, 56 1, 57 1, 58 false, 59 }, 60 { 61 &Config{Backends: Options{"simple": {"bob": "secret"}}}, 62 1, 63 0, 64 false, 65 }, 66 // error cases 67 { 68 // init error because no users are provided 69 &Config{Backends: Options{"simple": {}}}, 70 1, 71 0, 72 true, 73 }, 74 { 75 &Config{ 76 Oauth: Options{ 77 "FOOO": {"client_id": "xxx", "client_secret": "YYY"}, 78 }, 79 }, 80 0, 81 0, 82 true, 83 }, 84 { 85 &Config{}, 86 0, 87 0, 88 true, 89 }, 90 { 91 &Config{Backends: Options{"simpleFoo": {"bob": "secret"}}}, 92 1, 93 0, 94 true, 95 }, 96 } 97 for i, test := range testCases { 98 t.Run(fmt.Sprintf("test %v", i), func(t *testing.T) { 99 h, err := NewHandler(test.config) 100 if test.expectError { 101 Error(t, err) 102 } else { 103 NoError(t, err) 104 } 105 if err == nil { 106 Equal(t, test.backendCount, len(h.backends)) 107 Equal(t, test.oauthCount, len(h.oauth.(*oauth2.Manager).GetConfigs())) 108 } 109 }) 110 } 111 } 112 113 func TestHandler_LoginForm(t *testing.T) { 114 recorder := call(req("GET", "/context/login", "")) 115 Equal(t, 200, recorder.Code) 116 Contains(t, recorder.Body.String(), `class="container`) 117 Equal(t, "no-cache, no-store, must-revalidate", recorder.Header().Get("Cache-Control")) 118 } 119 120 func TestHandler_HEAD(t *testing.T) { 121 recorder := call(req("HEAD", "/context/login", "")) 122 Equal(t, 400, recorder.Code) 123 } 124 125 func TestHandler_404(t *testing.T) { 126 recorder := call(req("GET", "/context/", "")) 127 Equal(t, 404, recorder.Code) 128 129 recorder = call(req("GET", "/", "")) 130 Equal(t, 404, recorder.Code) 131 132 Equal(t, "Not Found: The requested page does not exist", recorder.Body.String()) 133 } 134 135 func TestHandler_LoginJson(t *testing.T) { 136 // success 137 recorder := call(req("POST", "/context/login", `{"username": "bob", "password": "secret"}`, TypeJSON, AcceptJwt)) 138 Equal(t, 200, recorder.Code) 139 Equal(t, recorder.Header().Get("Content-Type"), "application/jwt") 140 141 // verify the token 142 claims, err := tokenAsMap(recorder.Body.String()) 143 NoError(t, err) 144 Equal(t, "bob", claims["sub"]) 145 InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2) 146 147 // wrong credentials 148 recorder = call(req("POST", "/context/login", `{"username": "bob", "password": "FOOOBAR"}`, TypeJSON, AcceptJwt)) 149 Equal(t, 403, recorder.Code) 150 Equal(t, "Wrong credentials", recorder.Body.String()) 151 } 152 153 func TestHandler_HandleOauth(t *testing.T) { 154 managerMock := &oauth2ManagerMock{ 155 _GetConfigFromRequest: func(r *http.Request) (oauth2.Config, error) { 156 return oauth2.Config{}, nil 157 }, 158 } 159 handler := &Handler{ 160 oauth: managerMock, 161 config: DefaultConfig(), 162 } 163 164 // test start flow redirect 165 managerMock._Handle = func(w http.ResponseWriter, r *http.Request) ( 166 startedFlow bool, 167 authenticated bool, 168 userInfo model.UserInfo, 169 err error) { 170 w.Header().Set("Location", "http://example.com") 171 w.WriteHeader(303) 172 return true, false, model.UserInfo{}, nil 173 } 174 recorder := httptest.NewRecorder() 175 handler.ServeHTTP(recorder, req("GET", "/login/github", "")) 176 Equal(t, 303, recorder.Code) 177 Equal(t, "http://example.com", recorder.Header().Get("Location")) 178 179 // test authentication 180 managerMock._Handle = func(w http.ResponseWriter, r *http.Request) ( 181 startedFlow bool, 182 authenticated bool, 183 userInfo model.UserInfo, 184 err error) { 185 return false, true, model.UserInfo{Sub: "marvin"}, nil 186 } 187 recorder = httptest.NewRecorder() 188 handler.ServeHTTP(recorder, req("GET", "/login/github", "")) 189 Equal(t, 200, recorder.Code) 190 token, err := tokenAsMap(recorder.Body.String()) 191 NoError(t, err) 192 Equal(t, "marvin", token["sub"]) 193 194 // test error in oauth 195 managerMock._Handle = func(w http.ResponseWriter, r *http.Request) ( 196 startedFlow bool, 197 authenticated bool, 198 userInfo model.UserInfo, 199 err error) { 200 return false, false, model.UserInfo{}, errors.New("some error") 201 } 202 recorder = httptest.NewRecorder() 203 handler.ServeHTTP(recorder, req("GET", "/login/github", "")) 204 Equal(t, 500, recorder.Code) 205 206 // test failure if no oauth action would be taken, because the url parameters where 207 // missing an action parts 208 managerMock._Handle = func(w http.ResponseWriter, r *http.Request) ( 209 startedFlow bool, 210 authenticated bool, 211 userInfo model.UserInfo, 212 err error) { 213 return false, false, model.UserInfo{}, nil 214 } 215 recorder = httptest.NewRecorder() 216 handler.ServeHTTP(recorder, req("GET", "/login/github", "")) 217 Equal(t, 403, recorder.Code) 218 } 219 220 func TestHandler_LoginWeb(t *testing.T) { 221 // redirectSuccess 222 recorder := call(req("POST", "/context/login", "username=bob&password=secret", TypeForm, AcceptHTML)) 223 Equal(t, 303, recorder.Code) 224 Equal(t, "/", recorder.Header().Get("Location")) 225 226 // verify the token from the cookie 227 setCookieList := readSetCookies(recorder.Header()) 228 Equal(t, 1, len(setCookieList)) 229 230 cookie := setCookieList[0] 231 Equal(t, "jwt_token", cookie.Name) 232 Equal(t, "/", cookie.Path) 233 Equal(t, "example.com", cookie.Domain) 234 InDelta(t, time.Now().Add(testConfig().CookieExpiry).Unix(), cookie.Expires.Unix(), 2) 235 True(t, cookie.HttpOnly) 236 237 // check the token content 238 claims, err := tokenAsMap(cookie.Value) 239 NoError(t, err) 240 Equal(t, "bob", claims["sub"]) 241 InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2) 242 243 // show the login form again after authentication failed 244 recorder = call(req("POST", "/context/login", "username=bob&password=FOOBAR", TypeForm, AcceptHTML)) 245 Equal(t, 403, recorder.Code) 246 Contains(t, recorder.Body.String(), `class="container"`) 247 Equal(t, recorder.Header().Get("Set-Cookie"), "") 248 } 249 250 func TestHandler_SetSecureCookie(t *testing.T) { 251 tests := []struct { 252 name string 253 secure bool 254 }{ 255 {"secure", true}, 256 {"unsecure", false}, 257 } 258 for _, tt := range tests { 259 t.Run(tt.name, func(t *testing.T) { 260 r, err := http.NewRequest("OPTION", "/foobar", nil) 261 if err != nil { 262 t.Fatalf("Unable to create request: %v", err) 263 } 264 w := httptest.NewRecorder() 265 cfg := testConfig() 266 h := testHandler() 267 cfg.CookieSecure = tt.secure 268 h.config = cfg 269 270 h.respondAuthenticatedHTML(w, r, "RANDOM_TOKEN_VALUE") 271 272 cc := w.Result().Cookies() 273 foundCookie := false 274 for _, c := range cc { 275 if c.Name != cfg.CookieName { 276 continue 277 } 278 foundCookie = true 279 if c.Secure != tt.secure { 280 t.Errorf("Found token cookie %q with secure flag %t; expected %t", cfg.CookieName, c.Secure, tt.secure) 281 } 282 } 283 if !foundCookie { 284 t.Errorf("No token cookie with the name %q (Config.CookieName) found", cfg.CookieName) 285 } 286 }) 287 } 288 } 289 290 func TestHandler_Refresh(t *testing.T) { 291 h := testHandler() 292 input := model.UserInfo{Sub: "bob", Expiry: time.Now().Add(time.Second).Unix()} 293 token, err := h.createToken(input) 294 NoError(t, err) 295 296 cookieStr := "Cookie: " + h.config.CookieName + "=" + token + ";" 297 298 // refreshSuccess 299 recorder := call(req("POST", "/context/login", "", AcceptHTML, cookieStr)) 300 Equal(t, 303, recorder.Code) 301 302 // verify the token from the cookie 303 setCookieList := readSetCookies(recorder.Header()) 304 Equal(t, 1, len(setCookieList)) 305 306 cookie := setCookieList[0] 307 Equal(t, "jwt_token", cookie.Name) 308 Equal(t, "/", cookie.Path) 309 Equal(t, "example.com", cookie.Domain) 310 InDelta(t, time.Now().Add(testConfig().CookieExpiry).Unix(), cookie.Expires.Unix(), 2) 311 True(t, cookie.HttpOnly) 312 313 // check the token content 314 claims, err := tokenAsMap(cookie.Value) 315 NoError(t, err) 316 Equal(t, "bob", claims["sub"]) 317 InDelta(t, time.Now().Add(DefaultConfig().JwtExpiry).Unix(), claims["exp"], 2) 318 } 319 320 func TestHandler_Refresh_Expired(t *testing.T) { 321 h := testHandler() 322 input := model.UserInfo{Sub: "bob", Expiry: time.Now().Unix() - 1} 323 token, err := h.createToken(input) 324 NoError(t, err) 325 326 cookieStr := "Cookie: " + h.config.CookieName + "=" + token + ";" 327 328 // refreshSuccess 329 recorder := call(req("POST", "/context/login", "", AcceptHTML, cookieStr)) 330 Equal(t, 403, recorder.Code) 331 332 // verify the token from the cookie 333 setCookieList := readSetCookies(recorder.Header()) 334 Equal(t, 0, len(setCookieList)) 335 } 336 337 func TestHandler_Refresh_Invalid_Token(t *testing.T) { 338 h := testHandler() 339 340 cookieStr := "Cookie: " + h.config.CookieName + "=kjsbkabsdkjbasdbkasbdk.dfgdfg.fdgdfg;" 341 342 // refreshSuccess 343 recorder := call(req("POST", "/context/login", "", AcceptHTML, cookieStr)) 344 Equal(t, 403, recorder.Code) 345 346 // verify the token from the cookie 347 setCookieList := readSetCookies(recorder.Header()) 348 Equal(t, 0, len(setCookieList)) 349 } 350 351 func TestHandler_Refresh_Max_Refreshes_Reached(t *testing.T) { 352 h := testHandler() 353 input := model.UserInfo{Sub: "bob", Expiry: time.Now().Add(time.Second).Unix(), Refreshes: 1} 354 token, err := h.createToken(input) 355 NoError(t, err) 356 357 cookieStr := "Cookie: " + h.config.CookieName + "=" + token + ";" 358 359 // refreshSuccess 360 recorder := call(req("POST", "/context/login", "", AcceptJwt, cookieStr)) 361 Equal(t, 403, recorder.Code) 362 Contains(t, recorder.Body.String(), "reached") 363 364 // verify the token from the cookie 365 setCookieList := readSetCookies(recorder.Header()) 366 Equal(t, 0, len(setCookieList)) 367 } 368 369 func TestHandler_Logout(t *testing.T) { 370 // DELETE 371 recorder := call(req("DELETE", "/context/login", "")) 372 Equal(t, 200, recorder.Code) 373 checkDeleteCookei(t, recorder.Header()) 374 375 // GET + param 376 recorder = call(req("GET", "/context/login?logout=true", "")) 377 Equal(t, 200, recorder.Code) 378 checkDeleteCookei(t, recorder.Header()) 379 380 // POST + param 381 recorder = call(req("POST", "/context/login", "logout=true", TypeForm)) 382 Equal(t, 200, recorder.Code) 383 checkDeleteCookei(t, recorder.Header()) 384 385 Equal(t, "no-cache, no-store, must-revalidate", recorder.Header().Get("Cache-Control")) 386 } 387 388 func checkDeleteCookei(t *testing.T, h http.Header) { 389 setCookieList := readSetCookies(h) 390 Equal(t, 1, len(setCookieList)) 391 cookie := setCookieList[0] 392 393 Equal(t, "jwt_token", cookie.Name) 394 Equal(t, "/", cookie.Path) 395 Equal(t, "example.com", cookie.Domain) 396 Equal(t, int64(0), cookie.Expires.Unix()) 397 } 398 399 func TestHandler_CustomLogoutURL(t *testing.T) { 400 cfg := DefaultConfig() 401 cfg.LogoutURL = "http://example.com" 402 h := &Handler{ 403 oauth: oauth2.NewManager(), 404 config: cfg, 405 } 406 407 recorder := httptest.NewRecorder() 408 h.ServeHTTP(recorder, req("DELETE", "/login", "")) 409 Contains(t, recorder.Header().Get("Set-Cookie"), "jwt_token=delete; Path=/; Expires=Thu, 01 Jan 1970 00:00:00 GMT;") 410 Equal(t, 303, recorder.Code) 411 Equal(t, "http://example.com", recorder.Header().Get("Location")) 412 } 413 414 func TestHandler_LoginError(t *testing.T) { 415 h := testHandlerWithError() 416 417 // backend returning an error with result type == jwt 418 request := req("POST", "/context/login", `{"username": "bob", "password": "secret"}`, TypeJSON, AcceptJwt) 419 recorder := httptest.NewRecorder() 420 h.ServeHTTP(recorder, request) 421 422 Equal(t, 500, recorder.Code) 423 Equal(t, recorder.Header().Get("Content-Type"), "text/plain") 424 Equal(t, recorder.Body.String(), "Internal Server Error") 425 426 // backend returning an error with result type == html 427 request = req("POST", "/context/login", `{"username": "bob", "password": "secret"}`, TypeJSON, AcceptHTML) 428 recorder = httptest.NewRecorder() 429 h.ServeHTTP(recorder, request) 430 431 Equal(t, 500, recorder.Code) 432 Contains(t, recorder.Header().Get("Content-Type"), "text/html") 433 Contains(t, recorder.Body.String(), `class="container"`) 434 Contains(t, recorder.Body.String(), "Internal Error") 435 } 436 437 func TestHandler_LoginWithEmptyUsername(t *testing.T) { 438 h := testHandler() 439 440 // backend returning an error with result type == jwt 441 request := req("POST", "/context/login", `{"username": "", "password": ""}`, TypeJSON, AcceptJwt) 442 recorder := httptest.NewRecorder() 443 h.ServeHTTP(recorder, request) 444 445 Equal(t, 403, recorder.Code) 446 Equal(t, recorder.Header().Get("Content-Type"), "text/plain") 447 Equal(t, recorder.Body.String(), "Wrong credentials") 448 } 449 450 func TestHandler_getToken_Valid(t *testing.T) { 451 h := testHandler() 452 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 453 token, err := h.createToken(input) 454 NoError(t, err) 455 r := &http.Request{ 456 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 457 } 458 userInfo, valid := h.GetToken(r) 459 True(t, valid) 460 Equal(t, input, userInfo) 461 } 462 463 func TestHandler_ReturnUserInfoJSON(t *testing.T) { 464 h := testHandler() 465 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 466 token, err := h.createToken(input) 467 NoError(t, err) 468 url, _ := url.Parse("/context/login") 469 r := &http.Request{ 470 Method: "GET", 471 URL: url, 472 Header: http.Header{ 473 "Cookie": {h.config.CookieName + "=" + token + ";"}, 474 "Accept": {"application/json"}, 475 }, 476 } 477 478 recorder := call(r) 479 Equal(t, 200, recorder.Code) 480 Equal(t, "application/json", recorder.Header().Get("Content-Type")) 481 482 output := model.UserInfo{} 483 json.Unmarshal(recorder.Body.Bytes(), &output) 484 485 Equal(t, input, output) 486 } 487 488 func TestHandler_ReturnUserInfoJSON_InvalidToken(t *testing.T) { 489 h := testHandler() 490 url, _ := url.Parse("/context/login") 491 r := &http.Request{ 492 Method: "GET", 493 URL: url, 494 Header: http.Header{ 495 "Cookie": {h.config.CookieName + "= 123;"}, 496 "Accept": {"application/json"}, 497 }, 498 } 499 500 recorder := call(r) 501 Equal(t, 403, recorder.Code) 502 Equal(t, "application/json", recorder.Header().Get("Content-Type")) 503 output := map[string]interface{}{} 504 json.Unmarshal(recorder.Body.Bytes(), &output) 505 Equal(t, map[string]interface{}{"error": "Wrong credentials"}, output) 506 } 507 508 func TestHandler_signAndVerify_ES256(t *testing.T) { 509 h := testHandler() 510 h.config.JwtAlgo = "ES256" 511 h.config.JwtSecret = "MHcCAQEEIJKMecdA9ASkZArOu9b+cPmSiVfQaaeErHcvkqG2gVIOoAoGCCqGSM49AwEHoUQDQgAE1gae9/zJDLHeuFteUkKgVhLrwJPoA43goNacgwldOucBvVUzD0EFAcpCR+0UcOfQ99CxUyKxWtnvr9xpDIXU0w==" 512 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 513 token, err := h.createToken(input) 514 NoError(t, err) 515 r := &http.Request{ 516 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 517 } 518 userInfo, valid := h.GetToken(r) 519 True(t, valid) 520 Equal(t, input, userInfo) 521 } 522 523 func TestHandler_signAndVerify_RSA(t *testing.T) { 524 tt := []int{ 525 256, 526 384, 527 512, 528 } 529 for _, bits := range tt { 530 jwtAlgo := fmt.Sprintf("RS%d", bits) 531 t.Run(jwtAlgo, func(t *testing.T) { 532 t.Parallel() 533 534 key, err := rsa.GenerateKey(rand.Reader, bits*2) 535 NoError(t, err) 536 537 privateKey := &pem.Block{ 538 Type: "PRIVATE KEY", 539 Bytes: x509.MarshalPKCS1PrivateKey(key), 540 } 541 542 h := testHandler() 543 h.config.JwtAlgo = jwtAlgo 544 h.config.JwtSecret = string(pem.EncodeToMemory(privateKey)) 545 546 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 547 token, err := h.createToken(input) 548 NoError(t, err) 549 r := &http.Request{ 550 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 551 } 552 userInfo, valid := h.GetToken(r) 553 True(t, valid) 554 Equal(t, input, userInfo) 555 }) 556 } 557 558 t.Run("headerless", func(t *testing.T) { 559 h := testHandler() 560 h.config.JwtAlgo = "RS256" 561 h.config.JwtSecret = "" + 562 "MIICXAIBAAKBgQDSu7M1jiH06fGywhSw5jdjUdfX6b1yw8j2coVjAgT1oB44vU+S" + 563 "dgvak/tWoBkqG+Gdrn0m+3H/mRtGXWZDmh6VjQ5mnw91OGVJccL2UGdEbb4ub/9g" + 564 "4Bobn1ANUcbZvXWpmNP0kqyBwsXiaq6iL4TNW5iKdvnat7SwzLyIkGwPkQIDAQAB" + 565 "AoGAXpshs1Nh7z/v4F69R0WzbAVcL3SiNpmq6Ok09OP9MgB2UOa8iHYykCiLV7J8" + 566 "Wak2usGRMiUEYslrs0VPGd5hB9X94fDAh0SYC2wmBOJRBY2tU82pSkN5RjE8A3+f" + 567 "G6uwlZB2UtpYa/sihf7NkJCQh2ibT3YeelDUvEnfwALB6iECQQDck14kDckwi4mt" + 568 "LwWPqXTWAdKdTN1i6KGXDBt7Bi5lbVk3XFgQy/Z+GzRiBtjcWmcMw2VOUeFC9d/J" + 569 "WnRv8NklAkEA9JOsxEgzr7utqw3Zd1dDK5weDhAwXuaHiCIS+bDAsGor7pSgWOtU" + 570 "k+kpDdPe/TmtxJFhorJOsl+49VtYVoy+/QJAWJnlhcv31cUnL2ak8DkcUl53EHJw" + 571 "tytExVy6qScpedp7rM4uHckgITWiTAH+GD1ECY9vYQ9o0bHcC5CHFvQC9QJBAOwI" + 572 "2ONVCwy+A4zhgM472QdtU1QfK49qy8IFoGp4un2G+X720Qj/lFBq5MQDhWC9GYZr" + 573 "B98MVgavesDPtyFQE8ECQCRZaTDF4d5KBAvu5ogoqEATD5r21V4Zj5uZ/QSeI7+v" + 574 "UVncBYg6g4CIrczoqYpJ3aBF5MVJ0FEU9XCDO/iDvCU=" 575 576 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 577 token, err := h.createToken(input) 578 NoError(t, err) 579 r := &http.Request{ 580 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 581 } 582 userInfo, valid := h.GetToken(r) 583 True(t, valid) 584 Equal(t, input, userInfo) 585 }) 586 587 t.Run("garbage key", func(t *testing.T) { 588 h := testHandler() 589 h.config.JwtAlgo = "RS256" 590 h.config.JwtSecret = "-garbage-" 591 592 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 593 _, err := h.createToken(input) 594 Error(t, err) 595 }) 596 } 597 598 func TestHandler_getToken_InvalidSecret(t *testing.T) { 599 h := testHandler() 600 input := model.UserInfo{Sub: "marvin"} 601 token, err := h.createToken(input) 602 NoError(t, err) 603 r := &http.Request{ 604 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 605 } 606 // modify secret 607 h.config.JwtSecret = "foobar" 608 _, valid := h.GetToken(r) 609 False(t, valid) 610 } 611 612 func TestHandler_getToken_InvalidToken(t *testing.T) { 613 h := testHandler() 614 r := &http.Request{ 615 Header: http.Header{"Cookie": {h.config.CookieName + "=asdcsadcsadc"}}, 616 } 617 618 _, valid := h.GetToken(r) 619 False(t, valid) 620 } 621 622 func TestHandler_getToken_InvalidNoToken(t *testing.T) { 623 h := testHandler() 624 _, valid := h.GetToken(&http.Request{}) 625 False(t, valid) 626 } 627 628 func TestHandler_getToken_WithUserClaims(t *testing.T) { 629 h := testHandler() 630 input := model.UserInfo{Sub: "marvin", Expiry: time.Now().Add(time.Second).Unix()} 631 h.userClaims = func(userInfo model.UserInfo) (jwt.Claims, error) { 632 return customClaims{"sub": "Zappod", "origin": "fake", "exp": userInfo.Expiry}, nil 633 } 634 token, err := h.createToken(input) 635 636 NoError(t, err) 637 r := &http.Request{ 638 Header: http.Header{"Cookie": {h.config.CookieName + "=" + token + ";"}}, 639 } 640 userInfo, valid := h.GetToken(r) 641 True(t, valid) 642 Equal(t, "Zappod", userInfo.Sub) 643 Equal(t, "fake", userInfo.Origin) 644 } 645 646 func testHandler() *Handler { 647 return &Handler{ 648 backends: []Backend{ 649 NewSimpleBackend(map[string]string{"bob": "secret"}), 650 }, 651 oauth: oauth2.NewManager(), 652 config: testConfig(), 653 } 654 } 655 656 func testHandlerWithError() *Handler { 657 return &Handler{ 658 backends: []Backend{ 659 errorTestBackend("test error"), 660 }, 661 oauth: oauth2.NewManager(), 662 config: testConfig(), 663 } 664 } 665 666 func call(req *http.Request) *httptest.ResponseRecorder { 667 recorder := httptest.NewRecorder() 668 h := testHandler() 669 h.ServeHTTP(recorder, req) 670 return recorder 671 } 672 673 func req(method string, url string, body string, header ...string) *http.Request { 674 r, err := http.NewRequest(method, url, strings.NewReader(body)) 675 if err != nil { 676 panic(err) 677 } 678 for _, h := range header { 679 pair := strings.SplitN(h, ": ", 2) 680 r.Header.Add(pair[0], pair[1]) 681 } 682 return r 683 } 684 685 func tokenAsMap(tokenString string) (map[string]interface{}, error) { 686 token, err := jwt.Parse(tokenString, func(*jwt.Token) (interface{}, error) { 687 return []byte(DefaultConfig().JwtSecret), nil 688 }) 689 if err != nil { 690 return nil, err 691 } 692 693 if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { 694 return map[string]interface{}(claims), nil 695 } 696 697 return nil, errors.New("token not valid") 698 } 699 700 type errorTestBackend string 701 702 func (h errorTestBackend) Authenticate(username, password string) (bool, model.UserInfo, error) { 703 return false, model.UserInfo{}, errors.New(string(h)) 704 } 705 706 type oauth2ManagerMock struct { 707 _Handle func(w http.ResponseWriter, r *http.Request) ( 708 startedFlow bool, 709 authenticated bool, 710 userInfo model.UserInfo, 711 err error) 712 _AddConfig func(providerName string, opts map[string]string) error 713 _GetConfigFromRequest func(r *http.Request) (oauth2.Config, error) 714 } 715 716 func (m *oauth2ManagerMock) Handle(w http.ResponseWriter, r *http.Request) ( 717 startedFlow bool, 718 authenticated bool, 719 userInfo model.UserInfo, 720 err error) { 721 return m._Handle(w, r) 722 } 723 func (m *oauth2ManagerMock) AddConfig(providerName string, opts map[string]string) error { 724 return m._AddConfig(providerName, opts) 725 } 726 func (m *oauth2ManagerMock) GetConfigFromRequest(r *http.Request) (oauth2.Config, error) { 727 return m._GetConfigFromRequest(r) 728 } 729 730 // copied from golang: net/http/cookie.go 731 // with some simplifications for edge cases 732 // readSetCookies parses all "Set-Cookie" values from 733 // the header h and returns the successfully parsed Cookies. 734 func readSetCookies(h http.Header) []*http.Cookie { 735 cookieCount := len(h["Set-Cookie"]) 736 if cookieCount == 0 { 737 return []*http.Cookie{} 738 } 739 cookies := make([]*http.Cookie, 0, cookieCount) 740 for _, line := range h["Set-Cookie"] { 741 parts := strings.Split(strings.TrimSpace(line), ";") 742 if len(parts) == 1 && parts[0] == "" { 743 continue 744 } 745 parts[0] = strings.TrimSpace(parts[0]) 746 j := strings.Index(parts[0], "=") 747 if j < 0 { 748 continue 749 } 750 751 name, value := parts[0][:j], parts[0][j+1:] 752 753 c := &http.Cookie{ 754 Name: name, 755 Value: value, 756 Raw: line, 757 } 758 759 readCookiesParts(c, parts) 760 cookies = append(cookies, c) 761 } 762 return cookies 763 } 764 765 func readCookiesParts(c *http.Cookie, parts []string) { 766 for i := 1; i < len(parts); i++ { 767 parts[i] = strings.TrimSpace(parts[i]) 768 if len(parts[i]) == 0 { 769 continue 770 } 771 attr, val := parts[i], "" 772 if j := strings.Index(attr, "="); j >= 0 { 773 attr, val = attr[:j], attr[j+1:] 774 } 775 lowerAttr := strings.ToLower(attr) 776 switch lowerAttr { 777 case "secure": 778 c.Secure = true 779 continue 780 case "httponly": 781 c.HttpOnly = true 782 continue 783 case "domain": 784 c.Domain = val 785 continue 786 case "max-age": 787 secs, err := strconv.Atoi(val) 788 if err != nil { 789 break 790 } 791 c.MaxAge = secs 792 continue 793 case "expires": 794 c.RawExpires = val 795 exptime, err := time.Parse(time.RFC1123, val) 796 if err != nil { 797 exptime, err = time.Parse("Mon, 02-Jan-2006 15:04:05 MST", val) 798 if err != nil { 799 c.Expires = time.Time{} 800 break 801 } 802 } 803 c.Expires = exptime.UTC() 804 continue 805 case "path": 806 c.Path = val 807 continue 808 } 809 c.Unparsed = append(c.Unparsed, parts[i]) 810 } 811 }