package main import ( "net/http" "net/http/httptest" "strings" "testing" "time" "golang.org/x/crypto/bcrypt" ) const testPassword = "correct horse battery" func authServer(t *testing.T, totpSecret string) *Server { t.Helper() h, err := bcrypt.GenerateFromPassword([]byte(testPassword), bcrypt.MinCost) if err != nil { t.Fatal(err) } return &Server{ cfg: Config{PasswordHash: string(h), TOTPSecret: totpSecret}, sessions: newSessionStore("").set("ui"), fails: map[string][]time.Time{}, } } func doLogin(s *Server, body string) *httptest.ResponseRecorder { req := httptest.NewRequest(http.MethodPost, "/api/login", strings.NewReader(body)) req.RemoteAddr = "192.0.2.10:5000" w := httptest.NewRecorder() s.login(w, req) return w } func TestPasswordAloneLogsInWhenNoSecondFactorIsConfigured(t *testing.T) { s := authServer(t, "") w := doLogin(s, `{"password":"`+testPassword+`"}`) if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body.String()) } if len(w.Result().Cookies()) == 0 { t.Fatal("no session cookie") } } func TestWrongPasswordIsRefusedEvenWithoutSecondFactor(t *testing.T) { s := authServer(t, "") if w := doLogin(s, `{"password":"wrong password here"}`); w.Code != http.StatusUnauthorized { t.Fatalf("status %d", w.Code) } } func TestSecondFactorIsStillEnforcedWhenConfigured(t *testing.T) { secret := "JBSWY3DPEHPK3PXP" s := authServer(t, secret) if w := doLogin(s, `{"password":"`+testPassword+`"}`); w.Code != http.StatusUnauthorized { t.Fatalf("password without code must fail, got %d", w.Code) } code := totp(secret, time.Now()) if w := doLogin(s, `{"password":"`+testPassword+`","code":"`+code+`"}`); w.Code != http.StatusOK { t.Fatalf("password with a valid code must work, got %d", w.Code) } } func TestRepeatedFailuresLockTheAddress(t *testing.T) { s := authServer(t, "") for i := 0; i < 5; i++ { s.fail("192.0.2.10") } if w := doLogin(s, `{"password":"`+testPassword+`"}`); w.Code != http.StatusTooManyRequests { t.Fatalf("a locked address must get 429 even with the right password, got %d", w.Code) } } func TestAuthInfoTellsTheLoginPageWhetherToAskForACode(t *testing.T) { for secret, want := range map[string]string{"": `{"totp":false}`, "JBSWY3DPEHPK3PXP": `{"totp":true}`} { w := httptest.NewRecorder() authServer(t, secret).authInfo(w, httptest.NewRequest(http.MethodGet, "/api/auth-info", nil)) if got := strings.TrimSpace(w.Body.String()); got != want { t.Errorf("secret %q: got %s want %s", secret, got, want) } } } func TestPasswordRules(t *testing.T) { for p, want := range map[string]bool{ "": false, "11223344": false, // the old weak one "elevenchars": false, "twelve chars": true, "รหัสผ่านที่ยาวพอสมควรนะ": true, // 23 characters, 69 bytes: fits under the bcrypt limit strings.Repeat("ก", 25): false, // 25 characters but 75 bytes: bcrypt would silently cut it strings.Repeat("a", 72): true, strings.Repeat("a", 73): false, } { if got := passwordOK(p); got != want { t.Errorf("passwordOK(%q) = %v, want %v", p, got, want) } } }