summaryrefslogtreecommitdiffstats
path: root/roseh.moe.go
diff options
context:
space:
mode:
authorRose Hogenson <rosehogenson@posteo.net>2026-10-20 15:24:13 -0700
committerRose Hogenson <rosehogenson@posteo.net>2026-10-20 16:05:36 -0700
commitda4fb79df70768d70e5d7e74ed7f3ceac21e77cc (patch)
treef16e82976cd23c772880e064d88bcc86efa19689 /roseh.moe.go
parentCompletely overhaul the auth again (diff)
downloadroseh.moe-da4fb79df70768d70e5d7e74ed7f3ceac21e77cc.tar.zst
Add a link to temporarily log in another device
Diffstat (limited to 'roseh.moe.go')
-rw-r--r--roseh.moe.go92
1 files changed, 88 insertions, 4 deletions
diff --git a/roseh.moe.go b/roseh.moe.go
index 8e2c999..f497c8e 100644
--- a/roseh.moe.go
+++ b/roseh.moe.go
@@ -777,9 +777,8 @@ const (
authCookieName = "a"
)
-func attachCookie(w http.ResponseWriter, expiry time.Time) {
- token := signMsg(base64.RawURLEncoding.EncodeToString(binary.AppendVarint(nil, expiry.Unix())), authVersion)
- http.SetCookie(w, &http.Cookie{
+func makeHTTPCookie(token string, expiry time.Time) *http.Cookie {
+ return &http.Cookie{
Name: authCookieName,
Value: token,
Path: "/",
@@ -789,7 +788,12 @@ func attachCookie(w http.ResponseWriter, expiry time.Time) {
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
Partitioned: true,
- })
+ }
+}
+
+func attachCookie(w http.ResponseWriter, expiry time.Time) {
+ token := signMsg(base64.RawURLEncoding.EncodeToString(binary.AppendVarint(nil, expiry.Unix())), authVersion)
+ http.SetCookie(w, makeHTTPCookie(token, expiry))
}
func cookieAuth(r *http.Request) bool {
@@ -833,6 +837,14 @@ type loginTemplateArgs struct {
}
func serveLogin(w http.ResponseWriter, r *http.Request) {
+ if cookieAuth(r) {
+ redirect, ok := verifyHMAC(r.FormValue("redirect"), "redirect")
+ if !ok {
+ redirect = "/"
+ }
+ http.Redirect(w, r, redirect, http.StatusSeeOther)
+ return
+ }
buf := make([]byte, 32)
rand.Read(buf)
challengeMu.Lock()
@@ -995,6 +1007,75 @@ func register(w http.ResponseWriter, r *http.Request) {
}
var (
+ otpMu sync.Mutex
+ otpLink string
+ otpGenerated time.Time
+)
+
+var (
+ //go:embed templates/otp.html.template
+ otpTemplateString string
+ otpTemplate = template.Must(outlinedTemplate(otpTemplateString))
+)
+
+type otpTemplateArgs struct {
+ URL string
+ QR string
+}
+
+func serveOTP(w http.ResponseWriter, r *http.Request) {
+ const nWords = 5
+ buf := make([]byte, 2*nWords)
+ rand.Read(buf)
+ words := make([]string, nWords)
+ for i := range len(buf) / 2 {
+ words[i] = wordlist.Words[binary.NativeEndian.Uint16(buf[2*i:])&0x1fff]
+ }
+ l := strings.Join(words, "-")
+ otpMu.Lock()
+ otpLink = l
+ otpGenerated = time.Now()
+ otpMu.Unlock()
+ otpURL := selfURL.String() + "/otp/" + l
+ if err := otpTemplate.Execute(w, otpTemplateArgs{
+ URL: otpURL,
+ QR: "/qr.png?t=" + url.QueryEscape(signMsg(otpURL, "qr")),
+ }); err != nil {
+ log.Printf("Warning: serveOTP: %s", err)
+ }
+}
+
+func otpLogin(w http.ResponseWriter, r *http.Request) {
+ otpMu.Lock()
+ myOTP := otpLink
+ otpLink = ""
+ generated := otpGenerated
+ otpMu.Unlock()
+ reqOTP := r.PathValue("otp")
+ if reqOTP == "" {
+ notFound(w, r)
+ return
+ }
+ myOTPHash := sha256.Sum256([]byte(myOTP))
+ reqOTPHash := sha256.Sum256([]byte(reqOTP))
+ if !hmac.Equal(myOTPHash[:], reqOTPHash[:]) {
+ notFound(w, r)
+ return
+ }
+ if time.Since(generated) > 10*time.Minute {
+ http.Error(w, "OTP is too old. Please generate a new link", http.StatusConflict)
+ return
+ }
+ attachCookie(w, time.Now().Add(24*time.Hour))
+ fmt.Fprintf(w, "Temporary 24 hour token issued. You are now logged in.\n")
+}
+
+func logout(w http.ResponseWriter, r *http.Request) {
+ http.SetCookie(w, makeHTTPCookie("!", time.Now()))
+ fmt.Fprintf(w, "You are now logged out.\n")
+}
+
+var (
//go:embed templates/note.html.template
notepadString string
notepadTemplate = template.Must(outlinedTemplate(notepadString))
@@ -1357,8 +1438,11 @@ func main() {
mux.HandleFunc("GET /wormhole/{hole}", wormholeDownload)
mux.Handle("GET /login", &timeoutHandler{h: http.HandlerFunc(serveLogin), timeout: 5 * time.Minute})
mux.Handle("POST /login", &timeoutHandler{h: http.HandlerFunc(login), timeout: 5 * time.Minute})
+ mux.Handle("GET /logout", &timeoutHandler{h: http.HandlerFunc(logout), timeout: 5 * time.Minute})
mux.Handle("GET /register", &timeoutHandler{h: http.HandlerFunc(serveRegister), timeout: 5 * time.Minute})
mux.Handle("POST /register", &timeoutHandler{h: http.HandlerFunc(register), timeout: 5 * time.Minute})
+ mux.Handle("GET /otp", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(serveOTP)}, timeout: 5 * time.Minute})
+ mux.Handle("GET /otp/{otp}", &timeoutHandler{h: http.HandlerFunc(otpLogin), timeout: 5 * time.Minute})
mux.Handle("GET /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(notepad)}, timeout: 5 * time.Minute})
mux.Handle("POST /notepad", &timeoutHandler{h: &authHandler{h: http.HandlerFunc(autosave), noRedirect: true}, timeout: 5 * time.Minute})
mux.Handle("GET /cmd/{$}", &timeoutHandler{h: http.HandlerFunc(cmdIndex), timeout: 5 * time.Minute})