fix(go): resolve signin/logout issues for all time
All checks were successful
continuous-integration/drone/push Build is passing

affects:
* app/settings
* app/server
* handlers
    * signin
    * signup
    * logout
    * home
    * middleware
This commit is contained in:
surtur 2023-09-08 17:22:20 +02:00
parent 83f0ec7e15
commit 73915fcd98
Signed by: wanderer
SSH Key Fingerprint: SHA256:MdCZyJ2sHLltrLBp0xQO0O1qTW9BT/xl5nXkDvhlMCI
12 changed files with 164 additions and 151 deletions

@ -92,11 +92,8 @@ func (a *App) SetupRoutes() error {
manage.POST("/users/:id/update", handlers.UpdateUser()) manage.POST("/users/:id/update", handlers.UpdateUser())
manage.POST("/users/:id/delete", handlers.DeleteUser()) manage.POST("/users/:id/delete", handlers.DeleteUser())
e.GET("/logout", handlers.Logout()) e.GET("/logout", handlers.Logout(), xsrf)
e.POST("/logout", handlers.Logout(), handlers.MiddlewareSession, xsrf) e.POST("/logout", handlers.Logout(), handlers.MiddlewareSession, xsrf)
// administrative endpoints.
e.GET("/admin/*", handlers.Admin())
return nil return nil
} }

@ -130,19 +130,13 @@ func (a *App) SetServerSettings() {
} }
store.Options.Path = "/" store.Options.Path = "/"
store.Options.Domain = a.setting.HTTPDomain() // let the domain be set automatically based on where the app is running.
// store.Options.Domain = a.setting.HTTPDomain()
store.Options.HttpOnly = true store.Options.HttpOnly = true
store.Options.SameSite = http.SameSiteStrictMode store.Options.SameSite = http.SameSiteStrictMode
store.Options.Secure = a.setting.HTTPSecure() store.Options.Secure = a.setting.HTTPSecure()
store.Options.MaxAge = a.setting.SessionMaxAge() store.Options.MaxAge = a.setting.SessionMaxAge()
if a.setting.HTTPSecure() {
// https://www.sjoerdlangkemper.nl/2017/02/09/cookie-prefixes/
// https://scotthelme.co.uk/tough-cookies/
// https://check-your-website.server-daten.de/prefix-cookies.html
store.Options.Domain = "__Host-" + store.Options.Domain
}
e.Use(session.Middleware(store)) e.Use(session.Middleware(store))
e.Use(middleware.Secure()) e.Use(middleware.Secure())

@ -3,7 +3,11 @@
package settings package settings
import "flag" import (
"flag"
"git.dotya.ml/mirre-mt/pcmt/config"
)
// as per https://stackoverflow.com/a/54747682. // as per https://stackoverflow.com/a/54747682.
func isFlagPassed(name string) bool { func isFlagPassed(name string) bool {
@ -17,3 +21,35 @@ func isFlagPassed(name string) bool {
return found return found
} }
// sortOutFlags checks whether any flag overrides were passed and their validity.
func (s *Settings) sortOutFlags(conf *config.Config, hostFlag *string, portFlag *int, develFlag *bool) {
log.Debug("checking flag overrides")
overrideMsg := "overriding '%s' based on a flag: %+v"
if isFlagPassed("host") {
if h := *hostFlag; h != "unset" && h != conf.Host {
log.Debugf(overrideMsg, "host", h)
s.SetHost(h)
}
}
if isFlagPassed("port") {
if p := *portFlag; p > 0 && p < 65536 {
if p != conf.Port {
log.Debugf(overrideMsg, "port", p)
s.SetPort(p)
}
} else {
log.Warnf("flag-supplied port '%d' outside of bounds, ignoring", p)
}
}
if isFlagPassed("devel") {
if d := *develFlag; d != conf.DevelMode {
log.Debugf(overrideMsg, "develMode", d)
s.SetIsDevel(d)
}
}
}

@ -114,8 +114,17 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
s.SetIsLive(conf.LiveMode) s.SetIsLive(conf.LiveMode)
s.SetIsDevel(conf.DevelMode) s.SetIsDevel(conf.DevelMode)
s.SetLoggerIsJSON(conf.Logger.JSON) s.SetLoggerIsJSON(conf.Logger.JSON)
if conf.HTTP.Secure {
// https://www.sjoerdlangkemper.nl/2017/02/09/cookie-prefixes/
// https://scotthelme.co.uk/tough-cookies/
// https://check-your-website.server-daten.de/prefix-cookies.html
s.SetSessionCookieName("__Host-" + conf.Session.CookieName)
} else {
s.SetSessionCookieName(conf.Session.CookieName)
}
s.SetSessionMaxAge(conf.Session.MaxAge) s.SetSessionMaxAge(conf.Session.MaxAge)
s.SetSessionCookieName(conf.Session.CookieName)
s.SetSessionCookieAuthSecret(conf.Session.CookieAuthSecret) s.SetSessionCookieAuthSecret(conf.Session.CookieAuthSecret)
s.SetSessionCookieEncrSecret(conf.Session.CookieEncrSecret) s.SetSessionCookieEncrSecret(conf.Session.CookieEncrSecret)
@ -144,44 +153,14 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
s.SetInitAdminPassword(conf.Init.AdminPassword) s.SetInitAdminPassword(conf.Init.AdminPassword)
} }
s.SetHTTPDomain(conf.HTTP.Domain)
s.SetHTTPSecure(conf.HTTP.Secure)
log.Debug("checking flag overrides")
overrideMsg := "overriding '%s' based on a flag: %+v"
if isFlagPassed("host") {
if h := *host; h != "unset" && h != conf.Host {
log.Debugf(overrideMsg, "host", h)
s.SetHost(h)
}
}
if isFlagPassed("port") {
if p := *port; p > 0 && p < 65536 {
if p != conf.Port {
log.Debugf(overrideMsg, "port", p)
s.SetPort(p)
}
} else {
log.Warnf("flag-supplied port '%d' outside of bounds, ignoring", p)
}
}
if isFlagPassed("devel") {
if d := *devel; d != conf.DevelMode {
log.Debugf(overrideMsg, "develMode", d)
s.SetIsDevel(d)
}
}
if conf.Registration.Allowed { if conf.Registration.Allowed {
s.RegistrationAllowed = true s.RegistrationAllowed = true
} }
s.SetHTTPDomain(conf.HTTP.Domain)
s.SetHTTPSecure(conf.HTTP.Secure)
s.setAPIKeys() s.setAPIKeys()
s.sortOutFlags(conf, host, port, devel)
s.SetVersion(version) s.SetVersion(version)
} }

@ -18,7 +18,7 @@ var (
) )
func renderErrorPage(c echo.Context, status int, statusText, error string) error { func renderErrorPage(c echo.Context, status int, statusText, error string) error {
addHeaders(c) defer addHeaders(c)
strStatus := strconv.Itoa(status) strStatus := strconv.Itoa(status)

@ -10,20 +10,15 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
func Admin() echo.HandlerFunc {
return func(c echo.Context) error {
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
}
}
func Index() echo.HandlerFunc { func Index() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
if sess, _ := session.Get(setting.SessionCookieName(), c); sess != nil { if sess, _ := session.Get(setting.SessionCookieName(), c); sess != nil {
username := sess.Values["username"] if uname, ok := sess.Values["username"].(string); ok {
if username != nil { if uname != "" {
return c.Redirect(http.StatusFound, "/home") return c.Redirect(http.StatusFound, "/home")
}
} }
} }
@ -62,7 +57,7 @@ func Healthz() echo.HandlerFunc {
} }
func addHeaders(c echo.Context) { func addHeaders(c echo.Context) {
c.Response().Writer.Header().Set("Cross-Origin-Opener-Policy", "same-origin") c.Response().Header().Set("Cross-Origin-Opener-Policy", "same-origin")
} }
// experimental global redirect handler? // experimental global redirect handler?

@ -15,9 +15,7 @@ import (
func Home(client *ent.Client) echo.HandlerFunc { func Home(client *ent.Client) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
var username string defer addHeaders(c)
addHeaders(c)
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
if sess == nil { if sess == nil {
@ -25,25 +23,15 @@ func Home(client *ent.Client) echo.HandlerFunc {
return c.Redirect(http.StatusSeeOther, "/signin") return c.Redirect(http.StatusSeeOther, "/signin")
} }
if sess.Values["foo"] != nil { var username string
log.Info("gorilla session", "custom field test", sess.Values["foo"].(string))
}
uname := sess.Values["username"] username, ok := sess.Values["username"].(string)
if uname == nil { if !ok {
log.Info("session cookie found but username invalid, redirecting to signin", "endpoint", "/home") log.Info("session cookie found but username invalid, redirecting to signin", "endpoint", "/home")
return c.Redirect(http.StatusSeeOther, "/signin") return c.Redirect(http.StatusSeeOther, "/signin")
} }
log.Debug("session", "username", sess.Values["username"].(string), "endpoint", "/home") log.Debug("session", "username", username, "endpoint", "/home")
username = sess.Values["username"].(string)
// example denial.
// if _, err := c.Cookie("aha"); err != nil {
// log.Printf("error: %q", err)
// return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
// }
var u moduser.User var u moduser.User
@ -102,13 +90,21 @@ func Home(client *ent.Client) echo.HandlerFunc {
if _, ok := sess.Values["reauthFlash"].(string); ok { if _, ok := sess.Values["reauthFlash"].(string); ok {
p.Data["info"] = "First time after changing the password, yay!" p.Data["info"] = "First time after changing the password, yay!"
// if this is the first login after the initial password change, delete // if this is the first login after the initial password change, delete
// the cookie value. // the cookie value.
delete(sess.Values, "reauthFlash") delete(sess.Values, "reauthFlash")
} }
_ = sess.Save(c.Request(), c.Response()) if err := sess.Save(c.Request(), c.Response()); err != nil {
log.Error("Failed to save session", "module", "handlers/home")
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError)+" (make sure you've got cookies enabled)",
err.Error(),
)
}
err := c.Render(http.StatusOK, "home.tmpl", p) err := c.Render(http.StatusOK, "home.tmpl", p)
if err != nil { if err != nil {

@ -12,7 +12,7 @@ import (
func Logout() echo.HandlerFunc { func Logout() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
switch { switch {
case c.Request().Method == "POST": // nolint:goconst case c.Request().Method == "POST": // nolint:goconst
@ -21,16 +21,21 @@ func Logout() echo.HandlerFunc {
log.Infof("max-age before logout: %d", sess.Options.MaxAge) log.Infof("max-age before logout: %d", sess.Options.MaxAge)
sess.Options.MaxAge = -1 sess.Options.MaxAge = -1
c.Response().Writer.Header().Set("Cache-Control", "no-store") delete(sess.Values, "username")
if username := sess.Values["username"]; username != nil && username.(string) != "" {
sess.Values["username"] = ""
}
err := sess.Save(c.Request(), c.Response()) err := sess.Save(c.Request(), c.Response())
if err != nil { if err != nil {
c.Logger().Error("could not delete session cookie") c.Logger().Error("could not delete session cookie")
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
} }
c.Response().Header().Set(echo.HeaderCacheControl, "no-store")
} }
return c.Redirect(http.StatusMovedPermanently, "/logout") return c.Redirect(http.StatusMovedPermanently, "/logout")
@ -38,8 +43,10 @@ func Logout() echo.HandlerFunc {
case c.Request().Method == "GET": // nolint:goconst case c.Request().Method == "GET": // nolint:goconst
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
if sess != nil { if sess != nil {
if username := sess.Values["username"]; username != nil { if uname, ok := sess.Values["username"].(string); ok {
return c.Redirect(http.StatusSeeOther, "/home") if uname != "" {
return c.Redirect(http.StatusSeeOther, "/home")
}
} }
} }

@ -18,6 +18,11 @@ import (
func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc { func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
if c.Request().URL.Path == "/logout" && c.Request().Method == "POST" {
log.Debug("skipping auth middleware on /logout POST", "module", "handlers/middleware")
return next(c)
}
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
if sess == nil { if sess == nil {
@ -29,27 +34,21 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
) )
} }
var username string if username, ok := sess.Values["username"].(string); ok {
log.Info("Refreshing session cookie",
// uname, ok := sess.Values["username"].(string) "username", username,
uname := sess.Values["username"]
if uname != nil {
username = uname.(string)
// nolint:goconst
log.Info("Refreshing session cookie", "username", username,
"module", "middleware", "module", "middleware",
"maxAge", setting.SessionMaxAge(), "maxAge", setting.SessionMaxAge(),
"secure", c.Request().URL.Scheme == "https", "secure", setting.HTTPSecure(),
"domain", setting.HTTPDomain) "domain", setting.HTTPDomain(),
)
refreshSession( refreshSession(
sess, sess,
"/", "/",
setting.SessionMaxAge(), setting.SessionMaxAge(),
true, true,
c.Request().URL.Scheme == "https", // nolint:goconst setting.HTTPSecure(),
http.SameSiteStrictMode, http.SameSiteStrictMode,
) )
@ -95,10 +94,24 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
return next(c) return next(c)
} }
log.Warn("Could not get username from the cookie") log.Warn("Could not get username from the cookie", "module", "handlers/middleware")
if !sess.IsNew { if !sess.IsNew {
log.Errorf("%d - %s", http.StatusUnauthorized, "you need to re-login") log.Warn("Expired session cookie (without a username) found, redirecting to sign in", "module", "handlers/middleware")
sess.Values["info"] = "Log in again, please."
if err := sess.Save(c.Request(), c.Response()); err != nil {
log.Error("Failed to save session", "module", "middleware")
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError)+" could not save the session cookie",
err.Error(),
)
}
return c.Redirect(http.StatusTemporaryRedirect, "/signin") return c.Redirect(http.StatusTemporaryRedirect, "/signin")
} }

@ -19,7 +19,7 @@ import (
func Signin() echo.HandlerFunc { func Signin() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
reauth := false reauth := false
r := c.QueryParam("reauth") r := c.QueryParam("reauth")
@ -28,23 +28,7 @@ func Signin() echo.HandlerFunc {
reauth = true reauth = true
} }
// slog.Info("XXX session cookie name:", setting.SessionCookieName())
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
var uf *userSignin
if sess != nil {
username := sess.Values["username"]
if username != nil {
if !reauth {
return c.Redirect(http.StatusPermanentRedirect, "/home")
}
uf = &userSignin{Username: username.(string)}
}
}
csrf := c.Get("csrf").(string) csrf := c.Get("csrf").(string)
p := newPage() p := newPage()
@ -52,16 +36,45 @@ func Signin() echo.HandlerFunc {
p.Current = "signin" p.Current = "signin"
p.CSRF = csrf p.CSRF = csrf
var uf *userSignin
if sess != nil {
if uname, ok := sess.Values["username"].(string); ok && uname != "" {
if !reauth {
return c.Redirect(http.StatusPermanentRedirect, "/home")
}
uf = &userSignin{Username: uname}
}
}
if reauth { if reauth {
fl := sess.Values["reauthFlash"] fl := sess.Values["reauthFlash"]
if _, ok := fl.(string); !ok { if reFl, ok := fl.(string); !ok {
p.Data["flash"] = "re-login please" p.Data["info"] = "re-login, please."
} else { } else {
p.Data["reauthFlash"] = fl.(string) p.Data["reauthFlash"] = reFl
if uf != nil { if uf != nil {
p.Data["form"] = uf p.Data["form"] = uf
} }
} }
} else {
if i, ok := sess.Values["info"].(string); ok {
p.Data["infoFlash"] = i
delete(sess.Values, "info")
if err := sess.Save(c.Request(), c.Response()); err != nil {
log.Error("Failed to save session", "module", "middleware")
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError)+" could not save the session cookie",
err.Error(),
)
}
}
} }
return c.Render( return c.Render(
@ -74,7 +87,7 @@ func Signin() echo.HandlerFunc {
func SigninPost(client *ent.Client) echo.HandlerFunc { func SigninPost(client *ent.Client) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
cu := new(userSignin) cu := new(userSignin)
if err := c.Bind(cu); err != nil { if err := c.Bind(cu); err != nil {
@ -154,18 +167,15 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
) )
} }
secure := c.Request().URL.Scheme == "https" //nolint:goconst
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
if sess != nil { if sess != nil {
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: "/", Path: "/",
MaxAge: setting.SessionMaxAge(), MaxAge: setting.SessionMaxAge(),
HttpOnly: true, HttpOnly: true,
Secure: secure, Secure: setting.HTTPSecure(),
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteStrictMode,
} }
sess.Values["foo"] = "bar"
c.Logger().Debug("saving username to the session cookie") c.Logger().Debug("saving username to the session cookie")

@ -17,34 +17,16 @@ import (
func Signup() echo.HandlerFunc { func Signup() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
if sess != nil { if sess != nil {
log.Info("gorilla session", "endpoint", "signup") if uname, ok := sess.Values["username"].(string); ok && uname != "" {
username := sess.Values["username"]
if username != nil {
return c.Redirect(http.StatusFound, "/home") return c.Redirect(http.StatusFound, "/home")
} }
} }
// tpl := getTmpl("signup.tmpl")
csrf := c.Get("csrf").(string) csrf := c.Get("csrf").(string)
// secure := c.Request().URL.Scheme == "https"
// cookieCSRF := &http.Cookie{
// Name: "_csrf",
// Value: csrf,
// // SameSite: http.SameSiteStrictMode,
// SameSite: http.SameSiteLaxMode,
// MaxAge: 3600,
// Secure: secure,
// HttpOnly: true,
// }
// c.SetCookie(cookieCSRF)
p := newPage() p := newPage()
p.Title = "Sign up" p.Title = "Sign up"
@ -73,7 +55,7 @@ func Signup() echo.HandlerFunc {
func SignupPost(client *ent.Client) echo.HandlerFunc { func SignupPost(client *ent.Client) echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) defer addHeaders(c)
cu := new(userSignup) cu := new(userSignup)
if err := c.Bind(cu); err != nil { if err := c.Bind(cu); err != nil {
@ -140,17 +122,14 @@ func SignupPost(client *ent.Client) echo.HandlerFunc {
log.Infof("successfully registered user '%s'", u.Username) log.Infof("successfully registered user '%s'", u.Username)
log.Debug("user details", "id", u.ID, "email", u.Email, "isAdmin", u.IsAdmin) log.Debug("user details", "id", u.ID, "email", u.Email, "isAdmin", u.IsAdmin)
secure := c.Request().URL.Scheme == "https" //nolint:goconst
sess, _ := session.Get(setting.SessionCookieName(), c) sess, _ := session.Get(setting.SessionCookieName(), c)
sess.Options = &sessions.Options{ sess.Options = &sessions.Options{
Path: "/", Path: "/",
MaxAge: setting.SessionMaxAge(), MaxAge: setting.SessionMaxAge(),
HttpOnly: true, HttpOnly: true,
Secure: secure, Secure: setting.HTTPSecure(),
SameSite: http.SameSiteStrictMode, SameSite: http.SameSiteStrictMode,
} }
sess.Values["foo"] = "bar"
sess.Values["username"] = username sess.Values["username"] = username
err = sess.Save(c.Request(), c.Response()) err = sess.Save(c.Request(), c.Response())

@ -25,6 +25,13 @@
<p class="mt-2 text-xl text-blue-500 dark:text-blue-400"><span class="italic font-medium">Success:</span> {{.Data.reauthFlash}}</p> <p class="mt-2 text-xl text-blue-500 dark:text-blue-400"><span class="italic font-medium">Success:</span> {{.Data.reauthFlash}}</p>
</div> </div>
{{- else -}}{{end}} {{- else -}}{{end}}
{{ if and .Data .Data.infoFlash }}
<div class="relative flex items-center mb-4">
<p class="mt-2 text-xl text-blue-500 dark:text-blue-400">
{{.Data.infoFlash}}
</p>
</div>
{{- else -}}{{end}}
<!-- username field --> <!-- username field -->
<div class="relative flex items-center"> <div class="relative flex items-center">
<span class="absolute" role="img" aria-label="person outline icon for username"> <span class="absolute" role="img" aria-label="person outline icon for username">