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 | t.Fatalf("no confirmation in body: %s", body) | 31 | t.Fatalf("no confirmation in body: %s", body) |
| 32 | } | 32 | } |
| 33 | 33 | ||
| 34 | msg := smtp.waitFor(t, "dana@example.test", "/login?token=") | 34 | link := loginLinkIn(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 | } | ||
| 40 | 35 | ||
| 41 | if status, _ := browserGet(t, browser, inst.base()+link); status != 200 { | 36 | if status, _ := browserGet(t, browser, inst.base()+link); status != 200 { |
| 42 | t.Fatalf("following the link: %d", status) | 37 | t.Fatalf("following the link: %d", status) |
| @@ -201,3 +196,48 @@ func TestEmailLoginRefusesDisabledAccount(t *testing.T) { | |||
| 201 | t.Error("a disabled account got a browser session") | 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 | } | ||