From 88b3a014ba018eb701142c990d5ca4f45e575667 Mon Sep 17 00:00:00 2001 From: Kenzo Soreze <135658209+kenzosrz@users.noreply.github.com> Date: Mon, 25 Aug 2025 02:02:05 +0200 Subject: [PATCH] feat: set auth cookie on login --- http/auth.go | 75 +++++++++++++++++++++++++++++++++++++++++++--------- http/http.go | 4 ++- 2 files changed, 65 insertions(+), 14 deletions(-) diff --git a/http/auth.go b/http/auth.go index 0ecaed14577..cb68f2f9b28 100644 --- a/http/auth.go +++ b/http/auth.go @@ -117,11 +117,63 @@ func loginHandler(tokenExpireTime time.Duration) handleFunc { case err != nil: return http.StatusInternalServerError, err } + signed, err := issueToken(user, d.settings.Key, tokenExpireTime) + if err != nil { + return http.StatusInternalServerError, err + } + setAuthCookie(w, r, signed, tokenExpireTime) + w.WriteHeader(http.StatusNoContent) + return 0, nil + } +} + +// 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 + } - return printToken(w, r, d, user, tokenExpireTime) + 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 + } + setAuthCookie(w, r, signed, tokenExpireTime) + w.WriteHeader(http.StatusNoContent) + return 0, nil } } +func setAuthCookie(w http.ResponseWriter, r *http.Request, token string, exp time.Duration) { + http.SetCookie(w, &http.Cookie{ + Name: "auth", + Value: token, + Path: "/", + Expires: time.Now().Add(exp), + HttpOnly: true, + Secure: r.TLS != nil, + SameSite: http.SameSiteLaxMode, + }) +} + type signupBody struct { Username string `json:"username"` Password string `json:"password"` @@ -183,11 +235,17 @@ var signupHandler = func(_ http.ResponseWriter, r *http.Request, d *data) (int, func renewHandler(tokenExpireTime time.Duration) handleFunc { return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { w.Header().Set("X-Renew-Token", "false") - return printToken(w, r, d, d.user, tokenExpireTime) + signed, err := issueToken(d.user, d.settings.Key, tokenExpireTime) + if err != nil { + return http.StatusInternalServerError, err + } + setAuthCookie(w, r, signed, tokenExpireTime) + w.WriteHeader(http.StatusNoContent) + return 0, nil }) } -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,14 +267,5 @@ 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) - if err != nil { - return http.StatusInternalServerError, err - } - - w.Header().Set("Content-Type", "text/plain") - if _, err := w.Write([]byte(signed)); err != nil { - return http.StatusInternalServerError, err - } - return 0, nil + return token.SignedString(key) } diff --git a/http/http.go b/http/http.go index 2d87535f10a..84025d20957 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), ""))