diff options
| -rw-r--r-- | roseh.moe.go | 92 | ||||
| -rw-r--r-- | templates/otp.html.template | 25 |
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> |
