hmn/src/website/login.go

104 lines
3.1 KiB
Go

package website
import (
"errors"
"net/http"
"git.handmade.network/hmn/hmn/src/auth"
"git.handmade.network/hmn/hmn/src/db"
"git.handmade.network/hmn/hmn/src/logging"
"git.handmade.network/hmn/hmn/src/models"
"git.handmade.network/hmn/hmn/src/oops"
)
func Login(c *RequestContext) ResponseData {
// TODO: Update this endpoint to give uniform responses on errors and to be resilient to timing attacks.
form, err := c.GetFormValues()
if err != nil {
return ErrorResponse(http.StatusBadRequest, NewSafeError(err, "request must contain form data"))
}
username := form.Get("username")
password := form.Get("password")
if username == "" || password == "" {
return ErrorResponse(http.StatusBadRequest, NewSafeError(err, "you must provide both a username and password"))
}
redirect := form.Get("redirect")
if redirect == "" {
redirect = "/"
}
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE LOWER(username) = LOWER($1)", username)
if err != nil {
if errors.Is(err, db.ErrNoMatchingRows) {
return ResponseData{
StatusCode: http.StatusUnauthorized,
}
} else {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username"))
}
}
user := userRow.(*models.User)
hashed, err := auth.ParsePasswordString(user.Password)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse password string"))
}
passwordsMatch, err := auth.CheckPassword(password, hashed)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to check password against hash"))
}
if passwordsMatch {
// re-hash and save the user's password if necessary
if hashed.IsOutdated() {
newHashed, err := auth.HashPassword(password)
if err == nil {
err := auth.UpdatePassword(c.Context(), c.Conn, user.Username, newHashed)
if err != nil {
c.Logger.Error().Err(err).Msg("failed to update user's password")
}
} else {
c.Logger.Error().Err(err).Msg("failed to re-hash password")
}
// If errors happen here, we can still continue with logging them in
}
session, err := auth.CreateSession(c.Context(), c.Conn, user.Username)
if err != nil {
return ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to create session"))
}
res := c.Redirect(redirect, http.StatusSeeOther)
res.SetCookie(auth.NewSessionCookie(session))
return res
} else {
return c.Redirect("/", http.StatusSeeOther) // TODO: Redirect to standalone login page with error
}
}
func Logout(c *RequestContext) ResponseData {
sessionCookie, err := c.Req.Cookie(auth.SessionCookieName)
if err == nil {
// clear the session from the db immediately, no expiration
err := auth.DeleteSession(c.Context(), c.Conn, sessionCookie.Value)
if err != nil {
logging.Error().Err(err).Msg("failed to delete session on logout")
}
}
redir := c.Req.URL.Query().Get("redirect")
if redir == "" {
redir = "/"
}
res := c.Redirect(redir, http.StatusSeeOther)
res.SetCookie(auth.DeleteSessionCookie)
return res
}