Commit c7a2a1a6ab
Verified · cmc ci/build: success ci/sonar: success ci/test: success ci/vuln: success
Layout: unified · split
e2e/emaillogin_test.go +46 −6
| @@ -31,12 +31,7 @@ func TestEmailLogin(t *testing.T) { | ||
| 31 | 31 | t.Fatalf("no confirmation in body: %s", body) |
| 32 | 32 | } |
| 33 | 33 | |
| 34 | msg := smtp.waitFor(t, "dana@example.test", "/login?token=") | |
| 35 | i := strings.Index(msg, "/login?token=") | |
| 36 | link := msg[i:] | |
| 37 | if j := strings.IndexAny(link, " \r\n"); j >= 0 { | |
| 38 | link = link[:j] | |
| 39 | } | |
| 34 | link := loginLinkIn(smtp.waitFor(t, "dana@example.test", "/login?token=")) | |
| 40 | 35 | |
| 41 | 36 | if status, _ := browserGet(t, browser, inst.base()+link); status != 200 { |
| 42 | 37 | t.Fatalf("following the link: %d", status) |
| @@ -201,3 +196,48 @@ func TestEmailLoginRefusesDisabledAccount(t *testing.T) { | ||
| 201 | 196 | t.Error("a disabled account got a browser session") |
| 202 | 197 | } |
| 203 | 198 | } |
| 199 | ||
| 200 | // The other ordering: the link is minted while the account is in good | |
| 201 | // standing and followed after it is suspended. Suspension drops the pending | |
| 202 | // login tokens, and login() refuses a disabled account after consuming one, | |
| 203 | // so neither the window nor a token that somehow survives it opens a session. | |
| 204 | func TestEmailLoginRefusesLinkMintedBeforeSuspension(t *testing.T) { | |
| 205 | smtp := startFakeSMTP(t) | |
| 206 | inst := startInstanceWith(t, fmt.Sprintf( | |
| 207 | "[web]\nmode = \"accounts\"\n[mail]\nsmtp_host = %q\nfrom = \"noreply@gitbay.test\"\n", | |
| 208 | smtp.addr)) | |
| 209 | inst.admin(t, "admin", "user", "create", "dana", | |
| 210 | "--email", "dana@example.test", "--verified") | |
| 211 | ||
| 212 | browser := newBrowser(t) | |
| 213 | if status, _ := browserPost(t, browser, inst.base()+"/login", | |
| 214 | url.Values{"identifier": {"dana@example.test"}}); status != 200 { | |
| 215 | t.Fatalf("POST /login: %d", status) | |
| 216 | } | |
| 217 | link := loginLinkIn(smtp.waitFor(t, "dana@example.test", "/login?token=")) | |
| 218 | ||
| 219 | inst.admin(t, "admin", "user", "disable", "dana") | |
| 220 | ||
| 221 | _, body := browserGet(t, browser, inst.base()+link) | |
| 222 | if !strings.Contains(body, "invalid, expired, or already used") { | |
| 223 | t.Errorf("no refusal on the login page: %s", body) | |
| 224 | } | |
| 225 | // A refusal that reads differently from an ordinary bad token says the | |
| 226 | // account exists and is suspended, which is the leak the endpoint is | |
| 227 | // built to avoid. | |
| 228 | if _, bogus := browserGet(t, browser, inst.base()+"/login?token=notatoken"); body != bogus { | |
| 229 | t.Error("a suspended account's refusal differs from a bad token's") | |
| 230 | } | |
| 231 | if _, body := browserGet(t, browser, inst.base()+"/settings"); strings.Contains(body, "dana@example.test") { | |
| 232 | t.Error("a link minted before suspension still opened a session") | |
| 233 | } | |
| 234 | } | |
| 235 | ||
| 236 | // loginLinkIn pulls the /login?token=... path out of a login mail. | |
| 237 | func loginLinkIn(msg string) string { | |
| 238 | link := msg[strings.Index(msg, "/login?token="):] | |
| 239 | if j := strings.IndexAny(link, " \r\n"); j >= 0 { | |
| 240 | link = link[:j] | |
| 241 | } | |
| 242 | return link | |
| 243 | } | |