loginsrv

Unnamed repository; edit this file 'description' to name the repository.
git clone git@jamesshield.xyz:repos/loginsrv.git
Log | Files | Refs | README | LICENSE

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 }