carlos / contra Public

Clone
git clone https://amadan.net/carlos/contra

Plain git — no account needed to clone.

Download

Download this file

1package contra
2
3import (
4 "log/slog"
5 "net/http"
6
7 "github.com/carlosframework/rastrillo"
8 "github.com/carlosframework/rastrillo/db"
9 "github.com/carlosframework/rastrillo/sessions"
10
11 "github.com/carlosframework/contra/internal/contra/auth"
12)
13
14// app holds what every handler needs.
15type app struct {
16 db *db.DB
17 logger *slog.Logger
18 sess *sessions.Sessions
19 auth *auth.Auth
20}
21
22// page is the data every template renders against.
23type page struct {
24 Version string
25 SignedIn bool
26 KDFParams string
27 LinkError string
28}
29
30func (a *app) pageData(r *http.Request) page {
31 _, signedIn := sessions.Current(r)
32 return page{
33 Version: rastrillo.BuildVersion,
34 SignedIn: signedIn,
35 KDFParams: auth.DefaultKDFParams,
36 }
37}
38
39// index names the running build, so looking at the canary tells you which
40// release you are looking at — the same fact GET /api/version reports to the
41// platform's deploy check, on the page a person actually opens.
42func (a *app) index(w http.ResponseWriter, r *http.Request) {
43 render(w, "index", a.pageData(r))
44}
45
46// The three signup screens. Each one knows only what it needs, so a failure
47// on any of them says which step it was.
48func (a *app) signupStart(w http.ResponseWriter, r *http.Request) {
49 d := a.pageData(r)
50 d.LinkError = linkErrors[r.URL.Query().Get("e")]
51 render(w, "signup", d)
52}
53
54func (a *app) signupSent(w http.ResponseWriter, r *http.Request) {
55 render(w, "signup_sent", a.pageData(r))
56}
57
58// signupPassphrase is only reachable with a verified signup session. Anyone
59// else goes back to the start rather than being shown a form that cannot
60// possibly work.
61func (a *app) signupPassphrase(w http.ResponseWriter, r *http.Request) {
62 if _, err := a.auth.SignupSessionOf(r); err != nil {
63 http.Redirect(w, r, "/signup?e=expired", http.StatusSeeOther)
64 return
65 }
66 render(w, "signup_passphrase", a.pageData(r))
67}
68
69// signupPasskey is the optional third step, and it is behind a real session:
70// by here the account exists.
71func (a *app) signupPasskey(w http.ResponseWriter, r *http.Request) {
72 if _, ok := sessions.Current(r); !ok {
73 http.Redirect(w, r, "/signin", http.StatusSeeOther)
74 return
75 }
76 render(w, "signup_passkey", a.pageData(r))
77}
78
79// linkErrors turns the query parameter a redirect carries into a sentence.
80// The redirect cannot carry the sentence itself: a message passed through a
81// URL is a message an attacker can choose.
82var linkErrors = map[string]string{
83 "badlink": "That link did not work. It may have been used already, or copied " +
84 "incompletely from the email.",
85 "expired": "That link has expired. Links last thirty minutes; ask for a new one.",
86 "failed": "Something went wrong following that link. Try asking for a new one.",
87}
88
89func (a *app) signinPage(w http.ResponseWriter, r *http.Request) {
90 render(w, "signin", a.pageData(r))
91}
92
93// vault is the signed-in surface, and at this slice it is deliberately almost
94// empty: the items themselves are web/6. What it proves today is that the
95// ceremony ends somewhere only a signed-in browser can reach.
96func (a *app) vault(w http.ResponseWriter, r *http.Request) {
97 render(w, "vault", a.pageData(r))
98}
99
100// device is the ladder: the screen a browser reaches when it is signed in but
101// has no Account Key, and the screen an enrolled browser reaches to approve
102// one that is waiting. Both roles live on one page because a person arriving
103// here has to be able to do either without being told which link to click.
104func (a *app) device(w http.ResponseWriter, r *http.Request) {
105 render(w, "device", a.pageData(r))
106}
107
108// kit is the reprint flow. PROTOCOL.md: if either document is lost while a
109// device is still enrolled, producing a new one must be obvious rather than
110// buried — it is the mitigation for the loss mode the Account Key introduced.
111func (a *app) kit(w http.ResponseWriter, r *http.Request) {
112 render(w, "kit", a.pageData(r))
113}
114