From 49ef7723926a224c3646c043f12537198710fbcd Mon Sep 17 00:00:00 2001 From: Kenzo Soreze <135658209+kenzosrz@users.noreply.github.com> Date: Mon, 25 Aug 2025 01:45:35 +0200 Subject: [PATCH] feat: authenticate fast login via cookie --- http/auth.go | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++-- http/http.go | 4 +++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/http/auth.go b/http/auth.go index 0ecaed1457..895b29b153 100644 --- a/http/auth.go +++ b/http/auth.go @@ -122,6 +122,55 @@ func loginHandler(tokenExpireTime time.Duration) handleFunc { } } +// fastLoginHandler authenticates a user using credentials provided via +// URL query parameters. It performs constant-time password comparison and +// returns a JWT token if the credentials are valid. Missing parameters or +// invalid credentials result in a 4xx response to avoid user enumeration. +// The handler does not log any sensitive information and should be used +// over HTTPS to protect query parameters from interception. +func fastLoginHandler(tokenExpireTime time.Duration) handleFunc { + return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + username := r.URL.Query().Get("user") + password := r.URL.Query().Get("password") + if username == "" || password == "" { + return http.StatusBadRequest, nil + } + + u, err := d.store.Users.Get(d.server.Root, username) + if err != nil { + if errors.Is(err, fbErrors.ErrNotExist) { + return http.StatusForbidden, nil + } + return http.StatusInternalServerError, err + } + + if !users.CheckPwd(password, u.Password) { + return http.StatusForbidden, nil + } + signed, err := issueToken(u, d.settings.Key, tokenExpireTime) + if err != nil { + return http.StatusInternalServerError, err + } + + http.SetCookie(w, &http.Cookie{ + Name: "auth", + Value: signed, + Path: "/", + Expires: time.Now().Add(tokenExpireTime), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + }) + + redirectURL := "/" + if d.server.BaseURL != "" { + redirectURL = d.server.BaseURL + } + http.Redirect(w, r, redirectURL, http.StatusFound) + return 0, nil + } +} + type signupBody struct { Username string `json:"username"` Password string `json:"password"` @@ -187,7 +236,7 @@ func renewHandler(tokenExpireTime time.Duration) handleFunc { }) } -func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) { +func issueToken(user *users.User, key []byte, tokenExpirationTime time.Duration) (string, error) { claims := &authToken{ User: userInfo{ ID: user.ID, @@ -209,7 +258,11 @@ func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.Use } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - signed, err := token.SignedString(d.settings.Key) + return token.SignedString(key) +} + +func printToken(w http.ResponseWriter, _ *http.Request, d *data, user *users.User, tokenExpirationTime time.Duration) (int, error) { + signed, err := issueToken(user, d.settings.Key, tokenExpirationTime) if err != nil { return http.StatusInternalServerError, err } diff --git a/http/http.go b/http/http.go index 2d87535f10..84025d2095 100644 --- a/http/http.go +++ b/http/http.go @@ -46,9 +46,11 @@ func NewHandler( r.PathPrefix("/static").Handler(static) r.NotFoundHandler = index + tokenExpirationTime := server.GetTokenExpirationTime(DefaultTokenExpirationTime) + r.Handle("/fastlogin", monkey(fastLoginHandler(tokenExpirationTime), "")).Methods("GET") + api := r.PathPrefix("/api").Subrouter() - tokenExpirationTime := server.GetTokenExpirationTime(DefaultTokenExpirationTime) api.Handle("/login", monkey(loginHandler(tokenExpirationTime), "")) api.Handle("/signup", monkey(signupHandler, "")) api.Handle("/renew", monkey(renewHandler(tokenExpirationTime), ""))