package auth import ( "net/http/httptest" "strings" "testing" ) func TestPasswordRoundTrip(t *testing.T) { hash, err := HashPassword("correct horse battery") if err != nil { t.Fatal(err) } if strings.Contains(hash, "correct") { t.Fatal("the hash contains the password") } if !VerifyPassword(hash, "correct horse battery") { t.Fatal("the right password was rejected") } if VerifyPassword(hash, "correct horse batteru") { t.Fatal("a wrong password was accepted") } } func TestPasswordPolicyIsLengthOnly(t *testing.T) { if _, err := HashPassword("short"); err == nil { t.Fatal("a five-character password was accepted") } // Composition rules push people towards Passw0rd! for the same annoyance, // so a long all-lower-case passphrase must be fine. if _, err := HashPassword("all lower case and long enough"); err != nil { t.Fatalf("a good passphrase was refused: %v", err) } if _, err := HashPassword(strings.Repeat("x", 300)); err == nil { t.Fatal("a 300-character password was accepted") } } // DummyHash exists so an unknown address costs the same time as a wrong // password. A hash that does not parse returns instantly and puts the timing // leak straight back, which is why this asserts it actually verifies. func TestDummyHashIsARealBcryptHashThatNothingMatches(t *testing.T) { // Two separate properties, because the suite runs at a lowered cost and // only one of them is about the cost. // // 1. Production hashes at 12. Asserted against the constant rather than // against a hash, so lowering the cost for the tests cannot silently // lower it for real users too. if ProductionBcryptCost != 12 { t.Errorf("production bcrypt cost is %d - login should stay expensive", ProductionBcryptCost) } // 2. DummyHash is a real, parseable bcrypt hash. That is what makes an // unknown address cost the same time as a wrong password; a hash that // fails to parse returns instantly and puts the timing leak straight // back. if !strings.HasPrefix(DummyHash, "$2a$") { t.Fatalf("dummy hash is not a bcrypt hash at all: %q", DummyHash) } if VerifyPassword(DummyHash, "") || VerifyPassword(DummyHash, "password") { t.Fatal("something matched the dummy hash") } } func TestTokensAreDistinctAndOnlyTheHashIsStorable(t *testing.T) { a, err := NewToken() if err != nil { t.Fatal(err) } b, err := NewToken() if err != nil { t.Fatal(err) } if a.Plain == b.Plain { t.Fatal("two tokens came out the same") } if len(a.Plain) < 40 { t.Fatalf("token is only %d characters", len(a.Plain)) } if strings.Contains(string(a.Hash), a.Plain) { t.Fatal("the stored hash contains the token") } if string(HashToken(a.Plain)) != string(a.Hash) { t.Fatal("hashing the token again gave a different answer") } // URL-safe alphabet: this ends up in config files and gets copied by hand. if strings.ContainsAny(a.Plain, "+/=") { t.Fatalf("token needs escaping: %q", a.Plain) } } func TestBearerTokenIsReadFromTheHeaderOnly(t *testing.T) { r := httptest.NewRequest("GET", "/api/auth/me?access_token=leaked", nil) if got := BearerToken(r); got != "" { t.Fatalf("a query parameter was accepted as a credential: %q", got) } r.Header.Set("Authorization", "bearer abc123") if got := BearerToken(r); got != "abc123" { t.Fatalf("got %q", got) } r.Header.Set("Authorization", "Basic abc123") if got := BearerToken(r); got != "" { t.Fatalf("Basic was read as a bearer token: %q", got) } } func TestNormalizeCodeAcceptsHowPeopleActuallyType(t *testing.T) { want := "ABCDEF123456" for _, in := range []string{ "ABCDEF-123456", "abcdef-123456", "abcdef 123456", "ABC DEF-123 456", "ABCDEF123456", } { if got := NormalizeCode(in); got != want { t.Errorf("NormalizeCode(%q) = %q, want %q", in, got, want) } } } func TestRolesDefaultToDenying(t *testing.T) { if (Principal{Role: "auditor"}).CanWriteProfiles() { t.Fatal("an unrecognised role could write customer details") } if (Principal{}).CanManageSites() { t.Fatal("the zero-value principal could manage sites") } if !(Principal{Role: "staff"}).CanWriteProfiles() { t.Fatal("staff must be able to fill in the in-store form") } // Staff fill the form in; they do not hand out broker credentials. if (Principal{Role: "staff"}).CanManageSites() { t.Fatal("staff could manage sites") } } func TestNormalizeEmailMatchesTheUniqueIndex(t *testing.T) { // The index is on lower(email); anything reaching the database must already // agree with it or the constraint stops meaning what it says. if NormalizeEmail(" Asha@Acme.COM ") != "asha@acme.com" { t.Fatal("email normalisation disagrees with lower(email)") } }