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

View File

@ -75,7 +75,7 @@ func (a *App) SetupRoutes() error {
user := e.Group("/user", handlers.MiddlewareSession, xsrf)
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.POST("/hibp-search", handlers.SearchHIBP())
user.GET("/hibp-breach-details/:name", handlers.ViewHIBP())

View File

@ -36,7 +36,7 @@ func Home(client *ent.Client) echo.HandlerFunc {
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)
// example denial.
@ -94,17 +94,22 @@ func Home(client *ent.Client) echo.HandlerFunc {
p.Name = username
p.User = u
data := make(map[string]any)
flash := sess.Values["flash"]
if flash != nil {
data["flash"] = flash.(string)
if flsh, ok := sess.Values["flash"].(string); ok {
p.Data["flash"] = flsh
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)
if err != nil {
c.Logger().Errorf("error: %q", err)

View File

@ -4,6 +4,7 @@
package handlers
import (
"context"
"errors"
"fmt"
"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)
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)
a := new(hibpSearch)

View File

@ -20,10 +20,27 @@ func Signin() echo.HandlerFunc {
return func(c echo.Context) error {
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"]
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.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(
http.StatusOK,
"signin.tmpl",

View File

@ -13,6 +13,107 @@ import (
"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 {
return func(c echo.Context) error {
addHeaders(c)
@ -42,105 +143,41 @@ func InitialPasswordChange() echo.HandlerFunc {
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
switch {
case c.Request().Method == "POST":
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(),
)
}
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,
case err != nil:
return renderErrorPage(
c,
http.StatusInternalServerError,
http.StatusText(http.StatusInternalServerError),
err.Error(),
)
if err != nil {
c.Logger().Errorf("error: %q", err)
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
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

View File

@ -4,13 +4,22 @@
<main class="grow">
<div class="px-2 md:px-0 place-items-center text-center">
{{ 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 }}
</h1>
{{- 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 -}}
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
Welcome, <code>{{.Name}}</code>!<br>
<h1 class="mt-14 text-2xl text-pink-400 font-bold">
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>
{{if .User}}
{{if .User.IsAdmin}}

View File

@ -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>
</div>
{{- 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 -->
<div class="relative flex items-center">
<span class="absolute" role="img" aria-label="person outline icon for username">

View File

@ -10,16 +10,30 @@
{{- end }}
{{ if .User -}}
<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>
<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">
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>
<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">
<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">
{{ template "svg-password.tmpl" }}
</span>