From 14c18bd66812fc7009ee633e70bd60acbd15ab67 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Wed, 21 Jun 2023 21:14:41 +0100 Subject: [PATCH] form: rework email confirmation realized half the info from the signup form wasnt being stored in the JWT used to create the account after email confirmation, and instead of adding them, the -whole request- from the browser is stored temporarily by the server, indexed by a smaller JWT that only includes the invite code. Someone complained on reddit about me storing the password in the JWT a while back, and although security-wise that isn't an issue (only the server can decrypt the token), it doesn't happen anymore. Happy? --- api-users.go | 15 ++++++++++----- main.go | 45 ++++++++++++++++++++++++--------------------- storage.go | 11 +++++------ views.go | 27 ++++++++++++++++++++------- 4 files changed, 59 insertions(+), 39 deletions(-) diff --git a/api-users.go b/api-users.go index 3b2e934..b73f591 100644 --- a/api-users.go +++ b/api-users.go @@ -242,12 +242,17 @@ func (app *appContext) newUser(req newUserDTO, confirmed bool) (f errorFunc, suc success = false return } - inv, _ := app.storage.GetInvitesKey(req.Code) - if inv.ConfirmationKeys == nil { - inv.ConfirmationKeys = map[string]newUserDTO{} + if app.ConfirmationKeys == nil { + app.ConfirmationKeys = map[string]map[string]newUserDTO{} } - inv.ConfirmationKeys[key] = req - app.storage.SetInvitesKey(req.Code, inv) + cKeys, ok := app.ConfirmationKeys[req.Code] + if !ok { + cKeys = map[string]newUserDTO{} + } + cKeys[key] = req + app.confirmationKeysLock.Lock() + app.ConfirmationKeys[req.Code] = cKeys + app.confirmationKeysLock.Unlock() f = func(gc *gin.Context) { app.debug.Printf("%s: Email confirmation required", req.Code) respond(401, "confirmEmail", gc) diff --git a/main.go b/main.go index 40ce405..79643cd 100644 --- a/main.go +++ b/main.go @@ -17,6 +17,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "syscall" "time" @@ -90,27 +91,29 @@ type appContext struct { adminUsers []User invalidTokens []string // Keeping jf name because I can't think of a better one - jf *mediabrowser.MediaBrowser - authJf *mediabrowser.MediaBrowser - ombi *ombi.Ombi - datePattern string - timePattern string - storage Storage - validator Validator - email *Emailer - telegram *TelegramDaemon - discord *DiscordDaemon - matrix *MatrixDaemon - info, debug, err *logger.Logger - host string - port int - version string - URLBase string - updater *Updater - newUpdate bool // Whether whatever's in update is new. - tag Tag - update Update - internalPWRs map[string]InternalPWR + jf *mediabrowser.MediaBrowser + authJf *mediabrowser.MediaBrowser + ombi *ombi.Ombi + datePattern string + timePattern string + storage Storage + validator Validator + email *Emailer + telegram *TelegramDaemon + discord *DiscordDaemon + matrix *MatrixDaemon + info, debug, err *logger.Logger + host string + port int + version string + URLBase string + updater *Updater + newUpdate bool // Whether whatever's in update is new. + tag Tag + update Update + internalPWRs map[string]InternalPWR + ConfirmationKeys map[string]map[string]newUserDTO // Map of invite code to jwt to request + confirmationKeysLock sync.Mutex } func generateSecret(length int) (string, error) { diff --git a/storage.go b/storage.go index 415c798..873d6c4 100644 --- a/storage.go +++ b/storage.go @@ -274,12 +274,11 @@ type Invite struct { UserMinutes int `json:"user-minutes,omitempty"` SendTo string `json:"email"` // Used to be stored as formatted time, now as Unix. - UsedBy [][]string `json:"used-by"` - Notify map[string]map[string]bool `json:"notify"` - Profile string `json:"profile"` - Label string `json:"label,omitempty"` - ConfirmationKeys map[string]newUserDTO `json:"-"` // map of JWT confirmation keys to their original requests - Captchas map[string]*captcha.Data // Map of Captcha IDs to answers + UsedBy [][]string `json:"used-by"` + Notify map[string]map[string]bool `json:"notify"` + Profile string `json:"profile"` + Label string `json:"label,omitempty"` + Captchas map[string]*captcha.Data // Map of Captcha IDs to answers } type Lang struct { diff --git a/views.go b/views.go index 8ce557b..5a20ae2 100644 --- a/views.go +++ b/views.go @@ -512,7 +512,6 @@ func (app *appContext) InviteProxy(gc *gin.Context) { return } if key := gc.Query("key"); key != "" && app.config.Section("email_confirmation").Key("enabled").MustBool(false) { - req, ok := inv.ConfirmationKeys[key] fail := func() { gcHTML(gc, 404, "404.html", gin.H{ "cssClass": app.cssClass, @@ -520,6 +519,18 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "contactMessage": app.config.Section("ui").Key("contact_message").String(), }) } + var req newUserDTO + if app.ConfirmationKeys == nil { + fail() + return + } + + invKeys, ok := app.ConfirmationKeys[code] + if !ok { + fail() + return + } + req, ok = invKeys[key] if !ok { fail() return @@ -537,8 +548,11 @@ func (app *appContext) InviteProxy(gc *gin.Context) { app.debug.Printf("Invalid key") return } - _, success := app.newUser(req, true) + f, success := app.newUser(req, true) if !success { + app.err.Printf("Failed to create new user") + // Not meant for us. Calling this will be a mess, but at least it might give us some information. + f(gc) fail() return } @@ -554,11 +568,10 @@ func (app *appContext) InviteProxy(gc *gin.Context) { "jfLink": jfLink, }) } - inv, ok := app.storage.GetInvitesKey(code) - if ok { - delete(inv.ConfirmationKeys, key) - app.storage.SetInvitesKey(code, inv) - } + delete(invKeys, key) + app.confirmationKeysLock.Lock() + app.ConfirmationKeys[code] = invKeys + app.confirmationKeysLock.Unlock() return } email := ""