The address that a program prints carries a launch token. The first navigation exchanges it for a session cookie. Each session has a CSRF token, which each request that can change something sends back.
- The program makes a token with NewToken, and prints the address with it.
- The browser opens the address. The Manager sets the session cookie and redirects to the address without the token.
- A form or a script sends the CSRF token back with each request that can change something.
A form or a script sends the CSRF token back with each request that can change something.
token := "LAUNCHTOKEN" // session.NewToken() in a program
sessions, err := session.New(session.Options{
Token: token, CookiePrefix: "prog_ui_", CSRFHeader: "X-Prog-CSRF",
Refuse: func(w http.ResponseWriter, r *http.Request, f localweb.Refusal) {
f.Message += "\nOpen the address that prog ui printed in your terminal, or run prog ui again."
localweb.Refuse(w, r, f)
},
})
if err != nil {
fmt.Println(err)
return
}
page := template.Must(template.New("page").Parse(
`<body hx-headers='{"X-Prog-CSRF": "{{.}}"}'><form method="post"><input type="hidden" name="csrf" value="{{.}}">`))
mux := http.NewServeMux()
mux.HandleFunc("GET /{$}", func(w http.ResponseWriter, r *http.Request) {
_ = page.Execute(w, session.CSRF(r.Context()))
})
h := sessions.Middleware(mux)
for _, target := range []string{"/?token=" + token, "/?token=OLD", "/"} {
w := httptest.NewRecorder()
r := httptest.NewRequest(http.MethodGet, target, nil)
r.Host = "127.0.0.1:7374"
h.ServeHTTP(w, r)
if w.Code == http.StatusSeeOther {
fmt.Println(w.Code, "to", w.Header().Get("Location"), "with the cookie", w.Result().Cookies()[0].Name)
} else {
fmt.Print(w.Code, " ", w.Body)
}
}303 to / with the cookie prog_ui_7374
403 This link holds an old or wrong token.
Open the address that prog ui printed in your terminal, or run prog ui again.
403 This browser is not signed in.
Open the address that prog ui printed in your terminal, or run prog ui again.localweb/session/example_test.go. CI runs it and checks its output.The cookie#
HttpOnly, so no script reads it.SameSite=Strict, so a browser leaves it out of a request that another site starts.- Its name ends in the port of the Host, because a browser keeps cookies by host name and not by port. Two servers on one machine each keep their own session.
- No
Secureattribute, which a browser refuses over the plain HTTP of a loopback listener, and no lifetime.
The names and texts that this package sends are an interface (decision 34).