summaryrefslogtreecommitdiffstats
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
parentCompletely overhaul the auth again (diff)
downloadroseh.moe-da4fb79df70768d70e5d7e74ed7f3ceac21e77cc.tar.zst
Add a link to temporarily log in another device
-rw-r--r--roseh.moe.go92
-rw-r--r--templates/otp.html.template25
2 files changed, 113 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})
diff --git a/templates/otp.html.template b/templates/otp.html.template
new file mode 100644
index 0000000..b20d71c
--- /dev/null
+++ b/templates/otp.html.template
@@ -0,0 +1,25 @@
+<!-- Copyright 2026 Rose Hogenson <r@roseh.moe>
+ --
+ -- This file is part of roseh.moe.
+ --
+ -- roseh.moe is free software: you can redistribute it and/or modify
+ -- it under the terms of the GNU Affero General Public License
+ -- as published by the Free Software Foundation, either version 3 of
+ -- the License, or (at your option) any later version.
+ --
+ -- roseh.moe is distributed in the hope that it will be useful,
+ -- but WITHOUT ANY WARRANTY; without even the implied warranty
+ -- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ -- GNU Affero General Public License for more details.
+ --
+ -- You should have received a copy of the GNU Affero General Public
+ -- License along with roseh.moe. If not, see
+ -- <https://www.gnu.org/licenses/>.
+ -->
+
+<title>OTP</title>
+
+<div style="display:flex;flex-direction:column;align-items:center;font-size:50px">
+ <p>Your OTP link: <a href="{{.URL}}">{{.URL}}</a></p>
+ <img src="{{.QR}}" alt="QR code for {{.URL}}" width="500px">
+</div>