fix(go,tmpl): solve the Chromium/Safari logout...
All checks were successful
continuous-integration/drone/push Build is passing

...issue by deleting the session cookie after successful password change
and forcing the user to re-authenticate.

additionally, split the InitialPasswordChange func into separate "GET"
and "POST" variants.
This commit is contained in:
surtur 2023-09-04 19:21:01 +02:00
parent e8515d9a89
commit 1b2d860beb
Signed by: wanderer
SSH Key Fingerprint: SHA256:MdCZyJ2sHLltrLBp0xQO0O1qTW9BT/xl5nXkDvhlMCI
8 changed files with 257 additions and 115 deletions

@ -75,7 +75,7 @@ func (a *App) SetupRoutes() error {
user := e.Group("/user", handlers.MiddlewareSession, xsrf) user := e.Group("/user", handlers.MiddlewareSession, xsrf)
user.GET("/initial-password-change", handlers.InitialPasswordChange()) user.GET("/initial-password-change", handlers.InitialPasswordChange())
user.POST("/initial-password-change", handlers.InitialPasswordChange()) user.POST("/initial-password-change", handlers.InitialPasswordChangePost())
user.GET("/hibp-search", handlers.GetSearchHIBP()) user.GET("/hibp-search", handlers.GetSearchHIBP())
user.POST("/hibp-search", handlers.SearchHIBP()) user.POST("/hibp-search", handlers.SearchHIBP())
user.GET("/hibp-breach-details/:name", handlers.ViewHIBP()) user.GET("/hibp-breach-details/:name", handlers.ViewHIBP())

@ -36,7 +36,7 @@ func Home(client *ent.Client) echo.HandlerFunc {
return c.Redirect(http.StatusSeeOther, "/signin") return c.Redirect(http.StatusSeeOther, "/signin")
} }
log.Info("gorilla session", "username", sess.Values["username"].(string)) log.Debug("session", "username", sess.Values["username"].(string), "endpoint", "/home")
username = sess.Values["username"].(string) username = sess.Values["username"].(string)
// example denial. // example denial.
@ -94,17 +94,22 @@ func Home(client *ent.Client) echo.HandlerFunc {
p.Name = username p.Name = username
p.User = u p.User = u
data := make(map[string]any) if flsh, ok := sess.Values["flash"].(string); ok {
flash := sess.Values["flash"] p.Data["flash"] = flsh
if flash != nil {
data["flash"] = flash.(string)
delete(sess.Values, "flash") delete(sess.Values, "flash")
_ = sess.Save(c.Request(), c.Response())
} }
if _, ok := sess.Values["reauthFlash"].(string); ok {
p.Data["info"] = "First time after changing the password, yay!"
// if this is the first login after the initial password change, delete
// the cookie value.
delete(sess.Values, "reauthFlash")
}
_ = sess.Save(c.Request(), c.Response())
err := c.Render(http.StatusOK, "home.tmpl", p) err := c.Render(http.StatusOK, "home.tmpl", p)
if err != nil { if err != nil {
c.Logger().Errorf("error: %q", err) c.Logger().Errorf("error: %q", err)

@ -4,6 +4,7 @@
package handlers package handlers
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -30,6 +31,25 @@ func GetSearchHIBP() echo.HandlerFunc {
) )
} }
if !u.IsAdmin {
ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
if err != nil {
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
if !f {
log.Warn("resource access attempt without performing the initial password change", "user", u.Username, "endpoint", "/user/hibp-search")
return c.Redirect(http.StatusSeeOther, "/user/initial-password-change")
}
}
csrf := c.Get("csrf").(string) csrf := c.Get("csrf").(string)
p := newPage() p := newPage()
@ -90,6 +110,29 @@ func SearchHIBP() echo.HandlerFunc {
) )
} }
if !u.IsAdmin {
ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
if err != nil {
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
if !f {
return renderErrorPage(
c,
http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized)+" - you need to perform your initial password change before accessing this resource",
"user never changed password",
)
}
}
csrf := c.Get("csrf").(string) csrf := c.Get("csrf").(string)
a := new(hibpSearch) a := new(hibpSearch)

@ -20,10 +20,27 @@ func Signin() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) addHeaders(c)
if sess, _ := session.Get(setting.SessionCookieName(), c); sess != nil { reauth := false
r := c.QueryParam("reauth")
if r == "true" {
reauth = true
}
// slog.Info("XXX session cookie name:", setting.SessionCookieName())
sess, _ := session.Get(setting.SessionCookieName(), c)
var uf *userSignin
if sess != nil {
username := sess.Values["username"] username := sess.Values["username"]
if username != nil { if username != nil {
return c.Redirect(http.StatusFound, "/home") if !reauth {
return c.Redirect(http.StatusPermanentRedirect, "/home")
}
uf = &userSignin{Username: username.(string)}
} }
} }
@ -34,6 +51,18 @@ func Signin() echo.HandlerFunc {
p.Current = "signin" p.Current = "signin"
p.CSRF = csrf p.CSRF = csrf
if reauth {
fl := sess.Values["reauthFlash"]
if _, ok := fl.(string); !ok {
p.Data["flash"] = "re-login please"
} else {
p.Data["reauthFlash"] = fl.(string)
if uf != nil {
p.Data["form"] = uf
}
}
}
return c.Render( return c.Render(
http.StatusOK, http.StatusOK,
"signin.tmpl", "signin.tmpl",

@ -13,6 +13,107 @@ import (
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
) )
func InitialPasswordChangePost() echo.HandlerFunc {
return func(c echo.Context) error {
addHeaders(c)
u, ok := c.Get("sessUsr").(moduser.User)
if !ok {
return renderErrorPage(
c,
http.StatusUnauthorized,
http.StatusText(http.StatusUnauthorized)+", perhaps you need to log in first?",
"Username was nil",
)
} else if u.IsAdmin {
status := http.StatusUnauthorized
msg := http.StatusText(status)
return renderErrorPage(
c, status, msg+": You should not be here", "This endpoint is for users only",
)
}
ctx, ok := c.Get("sloggerCtx").(context.Context)
if !ok {
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
}
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
switch {
case err != nil:
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
case f:
return c.Redirect(http.StatusSeeOther, "/user/init-password-change")
}
pw := new(initPasswordChange)
if err := c.Bind(pw); err != nil {
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest),
err.Error(),
)
}
err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword)
if err != nil {
c.Logger().Errorf("error changing initial user password: %q", err)
switch {
case errors.Is(err, moduser.ErrPasswordEmpty):
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest),
err.Error(),
)
case errors.Is(err, moduser.ErrNewPasswordCannotEqual):
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest)+" - the new password needs to be different from the original",
err.Error(),
)
}
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
log.Info("successfully performed initial password change", "user", u.Username)
if sess, ok := c.Get("sess").(*sessions.Session); ok {
sess.Values["reauthFlash"] = "Successfully updated your password, log in again, please"
if err = sess.Save(c.Request(), c.Response()); err != nil {
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError)+" - could not change the session cookie",
err.Error(),
)
}
}
return c.Redirect(http.StatusSeeOther, "/signin?reauth=true")
}
}
func InitialPasswordChange() echo.HandlerFunc { func InitialPasswordChange() echo.HandlerFunc {
return func(c echo.Context) error { return func(c echo.Context) error {
addHeaders(c) addHeaders(c)
@ -42,105 +143,41 @@ func InitialPasswordChange() echo.HandlerFunc {
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID) f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
switch { switch {
case c.Request().Method == "POST": case err != nil:
switch { return renderErrorPage(
case err != nil: c,
return renderErrorPage( http.StatusInternalServerError,
c, http.StatusText(http.StatusInternalServerError),
http.StatusInternalServerError, err.Error(),
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
case f:
return c.Redirect(http.StatusSeeOther, "/user/init-password-change")
}
pw := new(initPasswordChange)
if err := c.Bind(pw); err != nil {
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest),
err.Error(),
)
}
err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword)
if err != nil {
c.Logger().Errorf("error changing initial user password: %q", err)
switch {
case errors.Is(err, moduser.ErrPasswordEmpty):
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest),
err.Error(),
)
case errors.Is(err, moduser.ErrNewPasswordCannotEqual):
return renderErrorPage(
c,
http.StatusBadRequest,
http.StatusText(http.StatusBadRequest)+" - the new password needs to be different from the original",
err.Error(),
)
}
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
}
if sess, ok := c.Get("sess").(*sessions.Session); ok {
sess.Values["flash"] = "Successfully updated your password"
_ = sess.Save(c.Request(), c.Response())
}
return c.Redirect(http.StatusSeeOther, "/home")
case c.Request().Method == "GET":
switch {
case err != nil:
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
case f:
return c.Redirect(http.StatusSeeOther, "/home")
}
csrf := c.Get("csrf").(string)
p := newPage()
p.Title = "Initial password change"
p.Current = "init-password-change"
p.CSRF = csrf
p.User = u
err := c.Render(
http.StatusOK,
"user/init-password-change.tmpl",
p,
) )
if err != nil {
c.Logger().Errorf("error: %q", err)
return renderErrorPage( case f:
c, return c.Redirect(http.StatusSeeOther, "/home")
http.StatusInternalServerError, }
http.StatusText(http.StatusInternalServerError),
err.Error(), csrf := c.Get("csrf").(string)
) p := newPage()
}
p.Title = "Initial password change"
p.Current = "init-password-change"
p.CSRF = csrf
p.User = u
p.Name = u.Username
err = c.Render(
http.StatusOK,
"user/init-password-change.tmpl",
p,
)
if err != nil {
c.Logger().Errorf("error: %q", err)
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
} }
return nil return nil

@ -4,13 +4,22 @@
<main class="grow"> <main class="grow">
<div class="px-2 md:px-0 place-items-center text-center"> <div class="px-2 md:px-0 place-items-center text-center">
{{ if and .Data .Data.flash }} {{ if and .Data .Data.flash }}
<h1 class="text-xl text-pink-600 dark:text-pink-500 py-2"> <h1 class="mt-8 text-xl text-pink-600 dark:text-pink-500 py-2">
{{ .Data.flash }} {{ .Data.flash }}
</h1> </h1>
{{- end }} {{- end }}
{{ if and .Data .Data.info }}
<h2 class="mt-4 text-xl font-bold text-green-600 dark:text-green-500 py-2">
{{ .Data.info }}
</h2>
{{- end }}
{{ if .Name -}} {{ if .Name -}}
<h1 class="mt-20 text-2xl text-pink-400 font-bold"> <h1 class="mt-14 text-2xl text-pink-400 font-bold">
Welcome, <code>{{.Name}}</code>!<br> Welcome to
<code class="p-1 rounded-sm border-l-2 border-fuchsia-200">
{{ .AppName }}</code>,
<code class="p-1 rounded-sm bg-fuchsia-200">
{{.Name}}</code>!
</h1> </h1>
{{if .User}} {{if .User}}
{{if .User.IsAdmin}} {{if .User.IsAdmin}}

@ -20,6 +20,11 @@
<p class="mt-2 text-md text-rose-800 dark:text-rose-500"><span class="font-medium">Error:</span> {{.Data.flash}}</p> <p class="mt-2 text-md text-rose-800 dark:text-rose-500"><span class="font-medium">Error:</span> {{.Data.flash}}</p>
</div> </div>
{{- else -}}{{end}} {{- else -}}{{end}}
{{ if and .Data .Data.reauthFlash }}
<div class="relative flex items-center mb-4">
<p class="mt-2 text-xl text-blue-500 dark:text-blue-400"><span class="italic font-medium">Success:</span> {{.Data.reauthFlash}}</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">

@ -10,16 +10,30 @@
{{- end }} {{- end }}
{{ if .User -}} {{ if .User -}}
<h1 class="mt-20 text-2xl text-pink-400 font-bold"> <h1 class="mt-20 text-2xl text-pink-400 font-bold">
Welcome, <code>{{.Name}}</code>, since you have never changed your password here before, now is the time to do that!<br> Welcome,
<code class="p-1 rounded-sm bg-fuchsia-200">
{{.Name}}</code>!
</h1> </h1>
<h3 class="mt-2 mb-4 text-2xl text-purple-500 dark:text-purple-300 font-bold">
Since you have never changed your password here before, now is the time to do that!
</h3>
<span class="text-lg text-gray-500 dark:text-gray-300 font-italic"> <span class="text-lg text-gray-500 dark:text-gray-300 font-italic">
This is necessary in order to make sure that only you know the password, since the account was pre-created <em>for</em> you. This is necessary in order to make sure that <b>only you</b> know the
password, as the account was pre-created <em>for</em> you.
</span> </span>
<div class="block"> <div class="block">
<div class="mt-8 md:flex md:items-center md:place-items-center md:justify-between"> <div class="mt-6 md:flex md:items-center md:place-items-center md:justify-between">
<form method="POST" class="w-full md:w-5xl" action="/user/initial-password-change"> <form method="POST" class="w-full md:w-5xl" action="/user/initial-password-change">
<input type="hidden" name="csrf" value="{{- .CSRF -}}"> <input type="hidden" name="csrf" value="{{- .CSRF -}}">
<div class="relative flex items-center mt-4"> <label class="group relative mt-2 font-bold text-fuchsia-500 dark:text-fuchsia-300 text-sm">
Your new password &#128712;
<span
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
Please, save this password into a password manager because after clicking
submit you will be logged out and will need to re-login again.
</span>
</label>
<div class="relative flex items-center">
<span class="absolute" role="img" aria-label="password lock icon"> <span class="absolute" role="img" aria-label="password lock icon">
{{ template "svg-password.tmpl" }} {{ template "svg-password.tmpl" }}
</span> </span>