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 "strings"
8 "testing"
9 "time"
10)
11
12func returnRequest(app *testApp, method, path string, form url.Values, cookies ...*http.Cookie) *httptest.ResponseRecorder {
13 req := httptest.NewRequest(method, path, strings.NewReader(form.Encode()))
14 req.Host = credentialHost
15 req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
16 req.Header.Set("Origin", Origin)
17 for _, c := range cookies {
18 if c != nil {
19 req.AddCookie(c)
20 }
21 }
22 w := httptest.NewRecorder()
23 app.Router().ServeHTTP(w, req)
24 return w
25}
26
27func intentFrom(w *httptest.ResponseRecorder) *http.Cookie {
28 for _, c := range w.Result().Cookies() {
29 if c.Name == "__Host-oficina-return" {
30 return c
31 }
32 }
33 return nil
34}
35
36// Mutation-checked 2026-09-07: discarding validated intent restores the
37// original wrong destination; trusting cookie values fails hostile cases.
38func TestCredentialReturnMagicLink(t *testing.T) {
39 for _, target := range []string{"/passkey/enrol", "/passkey/codes"} {
40 t.Run(target, func(t *testing.T) {
41 app := newTestApp(t)
42 session := signInFully(t, app, "person@example.invalid")
43 old := time.Now().Add(-24 * time.Hour).UTC().Format(time.RFC3339)
44 if _, err := app.db.Writer().Exec(`UPDATE sessions SET auth_time=?,created_at=?`, old, old); err != nil {
45 t.Fatal(err)
46 }
47 stale := returnRequest(app, "GET", target, nil, session)
48 form := returnRequest(app, "GET", stale.Header().Get("Location"), nil, session)
49 intent := intentFrom(form)
50 returnRequest(app, "POST", "/signin", url.Values{"address": {"person@example.invalid"}}, intent)
51 got := returnRequest(app, "POST", linkPath, url.Values{"token": {capturedLink(t, app)}}, intent)
52 if got.Header().Get("Location") != target {
53 t.Fatalf("location=%q, want%q", got.Header().Get("Location"), target)
54 }
55 if sessionCookie(got) == nil {
56 t.Fatal("missing verified session")
57 }
58 if r := returnRequest(app, "GET", target, nil, sessionCookie(got)); r.Code != 200 {
59 t.Fatalf("fresh destination=%d", r.Code)
60 }
61 })
62 }
63}
64
65func TestCredentialReturnKeymailAndFallback(t *testing.T) {
66 for _, fallback := range []bool{false, true} {
67 t.Run(string(rune('0'+boolInt(fallback))), func(t *testing.T) {
68 app := newTestApp(t)
69 newKeymailStub(t, app, "person@keymail.invalid")
70 intent := intentFrom(returnRequest(app, "GET", "/signin?return_to=%2Fpasskey%2Fcodes", nil))
71 pending, state := beginKeymail(t, app, "person@keymail.invalid")
72 code := "code-1"
73 if fallback {
74 state = "invalid-state"
75 }
76 got := callback(t, app, code, state, pending, intent)
77 if fallback {
78 if got.Header().Get("Location") != "/signin?force=1" || sessionCookie(got) != nil {
79 t.Fatal("invalid Keymail callback did not fall back")
80 }
81 form := returnRequest(app, "GET", got.Header().Get("Location"), nil, intent)
82 if changed := intentFrom(form); changed != nil {
83 intent = changed
84 }
85 returnRequest(app, "POST", "/signin?force=1", url.Values{"address": {"person@keymail.invalid"}}, intent)
86 got = returnRequest(app, "POST", linkPath, url.Values{"token": {capturedLink(t, app)}}, intent)
87 }
88 if got.Header().Get("Location") != "/passkey/codes" || sessionCookie(got) == nil {
89 t.Fatalf("continuation=%q status=%d", got.Header().Get("Location"), got.Code)
90 }
91 })
92 }
93}
94
95func boolInt(b bool) int {
96 if b {
97 return 1
98 }
99 return 0
100}
101
102func TestCredentialReturnHostileAndMissingIntent(t *testing.T) {
103 for _, value := range []string{"https://evil.invalid/", "//evil.invalid/", "/passkey/codes?x=1", "/passkey/enrol/", "%2f%2fevil.invalid", "/passkey/codes\r\nLocation: x", "/"} {
104 t.Run(value, func(t *testing.T) {
105 app := newTestApp(t)
106 c := intentFrom(returnRequest(app, "GET", "/signin?return_to="+url.QueryEscape(value), nil))
107 if c == nil || c.MaxAge >= 0 {
108 t.Fatal("unsafe target retained")
109 }
110 })
111 }
112 for _, query := range []string{"", "?return_to=/passkey/enrol&return_to=/passkey/codes", "?return_to=%zz"} {
113 app := newTestApp(t)
114 old := &http.Cookie{Name: returnCookie, Value: "/passkey/codes"}
115 c := intentFrom(returnRequest(app, "GET", "/signin"+query, nil, old))
116 if c == nil || c.MaxAge >= 0 {
117 t.Fatal("ambiguous/plain sign-in retained old intent")
118 }
119 }
120 for _, cookies := range [][]*http.Cookie{nil, {{Name: returnCookie, Value: "https://evil.invalid"}}, {{Name: returnCookie, Value: "/passkey/codes"}, {Name: returnCookie, Value: "/passkey/enrol"}}} {
121 app := newTestApp(t)
122 signIn(t, app, "person@example.invalid")
123 got := returnRequest(app, "POST", linkPath+"?return_to=/passkey/codes", url.Values{"token": {capturedLink(t, app)}, "return_to": {"/passkey/enrol"}}, cookies...)
124 if got.Header().Get("Location") != "/" || sessionCookie(got) == nil {
125 t.Fatalf("missing/hostile cookie continuation=%q", got.Header().Get("Location"))
126 }
127 }
128}
129
130func TestCredentialReturnCookieGrantsNothing(t *testing.T) {
131 app := newTestApp(t)
132 c := intentFrom(returnRequest(app, "GET", "/signin?return_to=/passkey/codes", nil))
133 if c == nil || !c.Secure || !c.HttpOnly || c.Domain != "" || c.Path != "/" || c.SameSite != http.SameSiteLaxMode || c.MaxAge != 600 {
134 t.Fatalf("invalid intent cookie attributes: %+v", c)
135 }
136 if got := returnRequest(app, "GET", "/passkey/codes", nil, c); got.Code == 200 {
137 t.Fatal("intent granted access without session")
138 }
139 signIn(t, app, "person@example.invalid")
140 got := returnRequest(app, "POST", linkPath, url.Values{"token": {capturedLink(t, app)}}, c)
141 if gone := intentFrom(got); gone == nil || gone.MaxAge >= 0 {
142 t.Fatal("successful completion did not clear intent")
143 }
144}
145
146// Mutation-checked 2026-09-07: replacing AuthTime with time.Now grants
147// access after an old gesture and fails this freshness check.
148func TestCredentialReturnDoesNotRefreshOldKeymailGesture(t *testing.T) {
149 app := newTestApp(t)
150 stub := newKeymailStub(t, app, "person@keymail.invalid")
151 stub.authTime = time.Now().Add(-24 * time.Hour)
152 intent := intentFrom(returnRequest(app, "GET", "/signin?return_to=/passkey/codes", nil))
153 pending, state := beginKeymail(t, app, "person@keymail.invalid")
154 got := callback(t, app, "code-1", state, pending, intent)
155 if sessionCookie(got) == nil || got.Header().Get("Location") != "/passkey/codes" {
156 t.Fatal("verified sign-in lost navigation")
157 }
158 if dest := returnRequest(app, "GET", "/passkey/codes", nil, sessionCookie(got)); dest.Code == 200 {
159 t.Fatal("navigation intent refreshed stale credential authentication")
160 }
161}
162