oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package home
2
3import (
4 "net/http"
5 "net/http/httptest"
6 "net/url"
7 "os"
8 "strings"
9 "testing"
10)
11
12func TestConfiguredOriginsRequireAnIsolatedPair(t *testing.T) {
13 valid := Origins{"https://id.preview.example", "https://home.preview.example"}
14 for _, pair := range []Origins{{Origin, directoryOrigin}, valid, {"https://notid.oficina.works", "https://preview.oficina.works"}} {
15 if err := pair.validate(); err != nil {
16 t.Errorf("valid %+v: %v", pair, err)
17 }
18 }
19 for _, pair := range []Origins{
20 {}, {Origin, valid.Directory}, {valid.Credential, directoryOrigin},
21 {valid.Credential, valid.Credential}, {valid.Credential, "https://child.id.preview.example"},
22 {"https://preview.id.oficina.works", valid.Directory}, {valid.Credential, "https://child.id.oficina.works"},
23 } {
24 if err := pair.validate(); err == nil {
25 t.Errorf("accepted unsafe pair %+v", pair)
26 }
27 }
28 for _, bad := range []string{"", "http://id.preview.example", "https://ID.preview.example", "https://id.preview.example/", "https://id.preview.example:443", "https://user@id.preview.example", "https://id.preview.example?x", "https://id.preview.example#x", "https://127.0.0.1", "https://[::1]", "https://localhost", "https://id.preview.example.", "https://-id.example", "https://id_.example"} {
29 for _, pair := range []Origins{{bad, valid.Directory}, {valid.Credential, bad}} {
30 if err := pair.validate(); err == nil {
31 t.Errorf("accepted malformed pair %+v", pair)
32 }
33 }
34 }
35}
36
37func TestLoadOriginsRequiresBothVariablesOrNeither(t *testing.T) {
38 for _, key := range []string{"HOME_CREDENTIAL_ORIGIN", "HOME_DIRECTORY_ORIGIN"} {
39 t.Setenv(key, "")
40 if err := os.Unsetenv(key); err != nil {
41 t.Fatal(err)
42 }
43 }
44 if got, err := LoadOrigins(); err != nil || got != (Origins{Origin, directoryOrigin}) {
45 t.Fatalf("defaults %+v %v", got, err)
46 }
47 t.Setenv("HOME_CREDENTIAL_ORIGIN", "https://id.preview.example")
48 if _, err := LoadOrigins(); err == nil {
49 t.Fatal("accepted credential-only setting")
50 }
51 t.Setenv("HOME_DIRECTORY_ORIGIN", "https://home.preview.example")
52 if got, err := LoadOrigins(); err != nil || got.Credential != "https://id.preview.example" {
53 t.Fatalf("pair %+v %v", got, err)
54 }
55 t.Setenv("HOME_CREDENTIAL_ORIGIN", "")
56 if _, err := LoadOrigins(); err == nil {
57 t.Fatal("accepted explicit empty setting")
58 }
59}
60
61func TestCustomOriginsReachEveryCredentialSeam(t *testing.T) {
62 origins := Origins{"https://id.preview.example", "https://home.preview.example"}
63 app := newTestAppWithOrigins(t, origins)
64 if app.flow.Origin != origins.Credential || app.flow.Keymail("keymail.example").Origin != origins.Credential || app.passkeyCfg.Origin != origins.Credential || app.passkeyCfg.LegacyRPID != "" || app.passkeyPageData("subject", nil)["RPID"] != "id.preview.example" {
65 t.Fatal("credential origin did not reach signin/keymail/passkey configuration")
66 }
67 router := app.Router()
68 for _, c := range []struct {
69 origin, path string
70 status int
71 }{
72 {origins.Credential, "/signin", 200}, {origins.Directory, "/", 200}, {origins.Credential, "/", 303},
73 {origins.Directory, "/signin", 404}, {Origin, "/signin", 404}, {directoryOrigin, "/", 404},
74 {strings.ToUpper(origins.Credential), "/signin", 200},
75 } {
76 req := httptest.NewRequest(http.MethodGet, c.origin+c.path, nil)
77 rec := httptest.NewRecorder()
78 router.ServeHTTP(rec, req)
79 if rec.Code != c.status {
80 t.Errorf("%s%s=%d want%d", c.origin, c.path, rec.Code, c.status)
81 }
82 if c.status == 303 && (rec.Header().Get("Location") != origins.Directory+"/" || rec.Body.Len() != 0) {
83 t.Fatal("credential root escaped configured directory")
84 }
85 }
86 for _, origin := range []string{origins.Credential, origins.Directory} {
87 for _, path := range []string{"/healthz", "/static/tokens.css", "/static/theme.css"} {
88 rec := httptest.NewRecorder()
89 router.ServeHTTP(rec, httptest.NewRequest("GET", origin+path, nil))
90 if rec.Code != 200 {
91 t.Errorf("shared %s%s=%d", origin, path, rec.Code)
92 }
93 }
94 }
95 post := func(path, source string, values url.Values) *httptest.ResponseRecorder {
96 req := httptest.NewRequest("POST", origins.Credential+path, strings.NewReader(values.Encode()))
97 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
98 req.Header.Set("Origin", source)
99 rec := httptest.NewRecorder()
100 router.ServeHTTP(rec, req)
101 return rec
102 }
103 if rec := post("/signin", Origin, url.Values{"address": {"person@example.invalid"}}); rec.Code != 403 {
104 t.Fatalf("production CSRF origin accepted: %d", rec.Code)
105 }
106 if _, ok := app.mailer.last(); ok {
107 t.Fatal("wrong CSRF origin sent mail")
108 }
109 if rec := post("/signin", origins.Credential, url.Values{"address": {"person@example.invalid"}}); rec.Code != 303 {
110 t.Fatalf("configured signin=%d", rec.Code)
111 }
112 sent, ok := app.mailer.last()
113 if !ok {
114 t.Fatal("no captured mail")
115 }
116 var token string
117 for _, word := range strings.Fields(sent.Body) {
118 if strings.HasPrefix(word, origins.Credential+linkPath) {
119 u, err := url.Parse(word)
120 if err != nil {
121 t.Fatal(err)
122 }
123 token = u.Query().Get("token")
124 }
125 }
126 if token == "" {
127 t.Fatal("mail did not contain configured credential link")
128 }
129 rec := post(linkPath, origins.Credential, url.Values{"token": {token}})
130 cookie := sessionCookie(rec)
131 if rec.Code != 303 || cookie == nil {
132 t.Fatalf("redemption=%d missing session=%v", rec.Code, cookie == nil)
133 }
134 for _, c := range rec.Result().Cookies() {
135 if c.Domain != "" || !c.Secure || c.Path != "/" {
136 t.Errorf("unsafe configured cookie %s", c.Name)
137 }
138 }
139}
140