| @@ -68,6 +68,43 @@ func TestEmailLogin(t *testing.T) { |
| 68 | } |
68 | } |
| 69 | } |
69 | } |
| 70 | |
70 | |
| |
71 | // A verified secondary address stands in for an unverified primary: |
| |
72 | // resolution by username must not stop at the primary (#158). |
| |
73 | func TestEmailLoginResolvesVerifiedSecondary(t *testing.T) { |
| |
74 | smtp := startFakeSMTP(t) |
| |
75 | inst := startInstanceWith(t, fmt.Sprintf( |
| |
76 | "[web]\nmode = \"accounts\"\n[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n", |
| |
77 | smtp.addr)) |
| |
78 | |
| |
79 | key := inst.newKey(t, "gus") |
| |
80 | inst.admin(t, "admin", "user", "create", "gus", "--key", key+".pub", |
| |
81 | "--email", "gus@primary.test") // primary added, left unverified |
| |
82 | if _, errOut, code := inst.ssh(t, key, "", "email", "add", "gus@secondary.test"); code != 0 { |
| |
83 | t.Fatalf("email add: exit %d %s", code, errOut) |
| |
84 | } |
| |
85 | verifyCode := extractCode(t, smtp.waitMail(t, 0)) |
| |
86 | if _, errOut, code := inst.ssh(t, key, "", "email", "verify", verifyCode); code != 0 { |
| |
87 | t.Fatalf("email verify: exit %d %s", code, errOut) |
| |
88 | } |
| |
89 | |
| |
90 | browser := newBrowser(t) |
| |
91 | status, body := browserPost(t, browser, inst.base()+"/login", url.Values{"identifier": {"gus"}}) |
| |
92 | if status != 200 || !strings.Contains(body, "on its way") { |
| |
93 | t.Fatalf("POST /login by username with an unverified primary: %d %s", status, body) |
| |
94 | } |
| |
95 | |
| |
96 | link := loginLinkIn(smtp.waitFor(t, "gus@secondary.test", "/login?token=")) |
| |
97 | if status, _ := browserGet(t, browser, inst.base()+link); status != 200 { |
| |
98 | t.Fatalf("following the link: %d", status) |
| |
99 | } |
| |
100 | if _, body := browserGet(t, browser, inst.base()+"/settings"); !strings.Contains(body, "gus@secondary.test") { |
| |
101 | t.Fatal("not logged in via the verified secondary address") |
| |
102 | } |
| |
103 | if len(smtp.mailTo("gus@primary.test")) != 0 { |
| |
104 | t.Error("mailed the unverified primary") |
| |
105 | } |
| |
106 | } |
| |
107 | |
| 71 | // The response must not say whether an account exists. A different status, |
108 | // The response must not say whether an account exists. A different status, |
| 72 | // body, or destination answers "is this person here?" to anyone who asks. |
109 | // body, or destination answers "is this person here?" to anyone who asks. |
| 73 | func TestEmailLoginDoesNotEnumerate(t *testing.T) { |
110 | func TestEmailLoginDoesNotEnumerate(t *testing.T) { |
| @@ -80,6 +117,18 @@ func TestEmailLoginDoesNotEnumerate(t *testing.T) { |
| 80 | // An account whose address was never verified must look like an absent |
117 | // An account whose address was never verified must look like an absent |
| 81 | // one, or an unverified address becomes an oracle. |
118 | // one, or an unverified address becomes an oracle. |
| 82 | inst.admin(t, "admin", "user", "create", "eve", "--email", "eve@example.test") |
119 | inst.admin(t, "admin", "user", "create", "eve", "--email", "eve@example.test") |
| |
120 | // An unverified primary with a verified secondary resolves the same as |
| |
121 | // a normal hit (#158) — this must be indistinguishable too. |
| |
122 | frankKey := inst.newKey(t, "frank") |
| |
123 | inst.admin(t, "admin", "user", "create", "frank", "--key", frankKey+".pub", |
| |
124 | "--email", "frank@example.test") |
| |
125 | if _, errOut, code := inst.ssh(t, frankKey, "", "email", "add", "frank2@example.test"); code != 0 { |
| |
126 | t.Fatalf("email add: exit %d %s", code, errOut) |
| |
127 | } |
| |
128 | if _, errOut, code := inst.ssh(t, frankKey, "", "email", "verify", |
| |
129 | extractCode(t, smtp.waitMail(t, 0))); code != 0 { |
| |
130 | t.Fatalf("email verify: exit %d %s", code, errOut) |
| |
131 | } |
| 83 | |
132 | |
| 84 | browser := newBrowser(t) |
133 | browser := newBrowser(t) |
| 85 | real1, bodyReal := browserPost(t, browser, inst.base()+"/login", |
134 | real1, bodyReal := browserPost(t, browser, inst.base()+"/login", |
| @@ -97,6 +146,8 @@ func TestEmailLoginDoesNotEnumerate(t *testing.T) { |
| 97 | url.Values{"identifier": {""}}) |
146 | url.Values{"identifier": {""}}) |
| 98 | absentUser, bodyAbsentUser := browserPost(t, browser, inst.base()+"/login", |
147 | absentUser, bodyAbsentUser := browserPost(t, browser, inst.base()+"/login", |
| 99 | url.Values{"identifier": {"nosuchuser"}}) |
148 | url.Values{"identifier": {"nosuchuser"}}) |
| |
149 | unverPrimary, bodyUnverPrimary := browserPost(t, browser, inst.base()+"/login", |
| |
150 | url.Values{"identifier": {"frank"}}) |
| 100 | |
151 | |
| 101 | for _, c := range []struct { |
152 | for _, c := range []struct { |
| 102 | name string |
153 | name string |
| @@ -107,6 +158,7 @@ func TestEmailLoginDoesNotEnumerate(t *testing.T) { |
| 107 | {"unverified", unver, bodyUnver}, |
158 | {"unverified", unver, bodyUnver}, |
| 108 | {"empty", empty, bodyEmpty}, |
159 | {"empty", empty, bodyEmpty}, |
| 109 | {"absent-username", absentUser, bodyAbsentUser}, |
160 | {"absent-username", absentUser, bodyAbsentUser}, |
| |
161 | {"unverified-primary-verified-secondary", unverPrimary, bodyUnverPrimary}, |
| 110 | } { |
162 | } { |
| 111 | if c.status != real1 || c.body != bodyReal { |
163 | if c.status != real1 || c.body != bodyReal { |
| 112 | t.Errorf("%s differs from a real address: status %d vs %d", c.name, c.status, real1) |
164 | t.Errorf("%s differs from a real address: status %d vs %d", c.name, c.status, real1) |