e2e/websessions_test.go
111 lines · 4119 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "net/http"
6 "strings"
7 "testing"
8)
9
10// A browser session can be listed and ended from SSH, one at a time or
11// all at once, and only its owner sees it.
12func TestWebSessionsListRevoke(t *testing.T) {
13 t.Parallel()
14 inst := startInstanceWith(t, "[web]\nmode = \"accounts\"\n")
15 aliceKey := inst.newKey(t, "alice")
16 bobKey := inst.newKey(t, "bob")
17 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
18 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
19
20 if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "list", "--json"); code != 0 || !strings.Contains(out, `"data":[]`) {
21 t.Fatalf("no sessions yet: exit %d %s", code, out)
22 }
23 first := inst.login(t, aliceKey)
24 second := inst.login(t, aliceKey)
25 list := func() []struct {
26 ID string `json:"id"`
27 } {
28 t.Helper()
29 out, errOut, code := inst.ssh(t, aliceKey, "", "web", "sessions", "list", "--json")
30 if code != 0 {
31 t.Fatalf("list: %s", errOut)
32 }
33 var env struct {
34 Data []struct {
35 ID string `json:"id"`
36 } `json:"data"`
37 }
38 if err := json.Unmarshal([]byte(out), &env); err != nil {
39 t.Fatalf("list json: %v\n%s", err, out)
40 }
41 return env.Data
42 }
43 sessions := list()
44 if len(sessions) != 2 || len(sessions[0].ID) != 12 {
45 t.Fatalf("two sessions expected: %+v", sessions)
46 }
47 // Bob sees none of them, and cannot revoke one by id.
48 if out, _, _ := inst.ssh(t, bobKey, "", "web", "sessions", "list", "--json"); !strings.Contains(out, `"data":[]`) {
49 t.Fatalf("bob sees alice's sessions:\n%s", out)
50 }
51 if _, _, code := inst.ssh(t, bobKey, "", "web", "sessions", "revoke", sessions[0].ID); code != 3 {
52 t.Fatalf("bob revoked alice's session: exit %d", code)
53 }
54 // Both browsers work; revoking the newest logs that one out.
55 // The client follows the logged-out redirect to /login, so the page
56 // body tells the two apart, not the status.
57 loggedIn := func(c *http.Client) bool {
58 _, body := browserGet(t, c, inst.base()+"/settings")
59 return strings.Contains(body, "SSH keys")
60 }
61 if !loggedIn(first) || !loggedIn(second) {
62 t.Fatal("both browsers should be logged in")
63 }
64 if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", sessions[0].ID); code != 0 || !strings.Contains(out, "revoked browser session") {
65 t.Fatalf("revoke: exit %d %s", code, out)
66 }
67 if got := list(); len(got) != 1 {
68 t.Fatalf("one session left expected: %+v", got)
69 }
70 okCount := 0
71 for _, c := range []*http.Client{first, second} {
72 if loggedIn(c) {
73 okCount++
74 }
75 }
76 if okCount != 1 {
77 t.Fatalf("exactly one browser should still be logged in, got %d", okCount)
78 }
79 if out, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", "--all"); code != 0 || !strings.Contains(out, "revoked 1 browser sessions") {
80 t.Fatalf("revoke --all: exit %d %s", code, out)
81 }
82 if loggedIn(first) || loggedIn(second) {
83 t.Fatal("a browser is still logged in after revoke --all")
84 }
85 if _, _, code := inst.ssh(t, aliceKey, "", "web", "sessions", "revoke", "abcdefabcdef"); code != 3 {
86 t.Fatal("unknown id accepted")
87 }
88 // An anonymous visit to a page that needs a session lands on the
89 // login page, which says where the visitor was going; the login
90 // link then returns them there.
91 anon := newBrowser(t)
92 status, body := browserGet(t, anon, inst.base()+"/settings")
93 if status != 200 || !strings.Contains(body, "continue to <code>/settings</code>") {
94 t.Fatalf("login page without the destination: %d\n%s", status, body)
95 }
96 out, _, _ := inst.ssh(t, aliceKey, "", "web", "login", "--json")
97 var env struct {
98 Data struct {
99 URL string `json:"url"`
100 } `json:"data"`
101 }
102 json.Unmarshal([]byte(out), &env)
103 link := inst.base() + env.Data.URL[strings.Index(env.Data.URL, "/login"):]
104 if status, body := browserGet(t, anon, link); status != 200 || !strings.Contains(body, "Account settings") {
105 t.Fatalf("login did not return to /settings: %d\n%s", status, body)
106 }
107 // The destination is used once.
108 if _, body := browserGet(t, anon, inst.base()+"/login"); strings.Contains(body, "continue to") {
109 t.Fatal("next survived its use")
110 }
111}