From fe360e23d335c58ef7a29c4449cb14f457023526 Mon Sep 17 00:00:00 2001 From: Amy Gale Ruth Bowersox Date: Mon, 6 Oct 2025 16:42:23 -0600 Subject: [PATCH] introduce the password reminder infrastructure --- database/user.go | 38 ++++++++++++++++++++++++++------- email/templates/pass_remind.jet | 2 +- login.go | 3 ++- util/random.go | 10 +++++++-- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/database/user.go b/database/user.go index 5b2f4fb..5a1ddfd 100644 --- a/database/user.go +++ b/database/user.go @@ -15,7 +15,6 @@ import ( "errors" "fmt" "hash/crc32" - "math/rand" "strconv" "strings" "sync" @@ -26,6 +25,33 @@ import ( log "github.com/sirupsen/logrus" ) +// PasswordChangeRequest represents a temporary password change request. +type PasswordChangeRequest struct { + Uid int32 + Username string + Email string + Authentication int32 + Expires time.Time +} + +// passwordRequests contains a map of password change requests currently managed. +var passwordRequests map[int32]*PasswordChangeRequest = make(map[int32]*PasswordChangeRequest) + +/* AmNewPasswordChangeRequest creates a new password change request and enrolls it. + * Parameters: + * uid - The UID of the user. + * username - The user name of the user. + * email - The E-mail address of the user. + * Returns: + * Pointer to the new PasswordChangeRequest. + */ +func AmNewPasswordChangeRequest(uid int32, username string, email string) *PasswordChangeRequest { + rc := PasswordChangeRequest{Uid: uid, Username: username, Email: email, + Authentication: util.GenerateRandomConfirmationNumber(), Expires: time.Now().Add(time.Hour)} + passwordRequests[uid] = &rc + return &rc +} + // User represents a user in the Amsterdam database. type User struct { Mutex sync.RWMutex @@ -140,7 +166,7 @@ func (u *User) ConfirmEMailAddress(confnum int32, remoteIP string) error { func (u *User) NewEmailConfirmationNumber() error { u.Mutex.Lock() defer u.Mutex.Unlock() - newnum := newEmailConfirmationNumber() + newnum := util.GenerateRandomConfirmationNumber() _, err := amdb.Exec("UPDATE user SET email_confnum = ? WHERE uid = ?", newnum, u.Uid) if err != nil { u.EmailConfNum = newnum @@ -384,11 +410,6 @@ func AmAuthenticateUserByToken(authString string, remoteIP string) (*User, error return user, nil } -// newEmailConfirmationNumber returns a new E-mail confirmation number. -func newEmailConfirmationNumber() int32 { - return rand.Int31n(9000000) + 1000000 -} - /* AmCreateNewUser creates a new user record in the database. * Parameters: * username - New user name. @@ -421,7 +442,8 @@ func AmCreateNewUser(username string, password string, reminder string, dob *tim // Insert the user record. _, err2 := amdb.Exec(`INSERT INTO users (username, passhash, verify_email, lockout, email_confnum, base_lvl, created, lastaccess, passreminder, description, dob) VALUES (?, ?, 0, 0, ?, ?, NOW(), NOW(), ?, '', ?)`, - username, hashPassword(password), newEmailConfirmationNumber(), AmDefaultRole("Global.NewUser").Level(), reminder, *dob) + username, hashPassword(password), util.GenerateRandomConfirmationNumber(), AmDefaultRole("Global.NewUser").Level(), + reminder, *dob) if err2 != nil { return nil, err2 } diff --git a/email/templates/pass_remind.jet b/email/templates/pass_remind.jet index bc28588..d83bfa4 100644 --- a/email/templates/pass_remind.jet +++ b/email/templates/pass_remind.jet @@ -7,7 +7,7 @@ If this reminder is not sufficient for you to remember what your password is, then the system can change your password for you. To do so, please visit the following URL: -http://example.com/passrecovery/{{ change_uid }}.{{ change_auth }} +http://example.com/passrecovery/{{ change_uid }}/{{ change_auth }} Your password will be changed and a new password will be E-mailed to you at this address. diff --git a/login.go b/login.go index 072db1e..ea9df52 100644 --- a/login.go +++ b/login.go @@ -80,13 +80,14 @@ func Login(ctxt ui.AmContext) (string, any, error) { ci, uerr = user.ContactInfo() if uerr == nil { if ci != nil && ci.Email != nil && *ci.Email != "" { + pchange := database.AmNewPasswordChangeRequest(user.Uid, user.Username, *ci.Email) msg := email.AmNewEmailMessage(ctxt.CurrentUserId(), ctxt.RemoteIP()) msg.AddTo(*ci.Email, "") msg.SetTemplate("pass_remind.jet") msg.AddVariable("username", user.Username) msg.AddVariable("reminder", user.PassReminder) msg.AddVariable("change_uid", user.Uid) - msg.AddVariable("change_auth", "TODO") // TODO: add change auth link + msg.AddVariable("change_auth", pchange.Authentication) msg.Send() } else { uerr = errors.New("cannot find email address") diff --git a/util/random.go b/util/random.go index ea8300d..9842abb 100644 --- a/util/random.go +++ b/util/random.go @@ -11,8 +11,9 @@ package util import ( - "crypto/rand" + crand "crypto/rand" "io" + mrand "math/rand" ) // authAlphabet is the set of characters from which we generate auth strings. @@ -24,7 +25,7 @@ const authStringLen = 32 // GenerateRandomAuthString generates a random authentication string. func GenerateRandomAuthString() string { b := make([]byte, authStringLen) - if _, err := io.ReadFull(rand.Reader, b); err != nil { + if _, err := io.ReadFull(crand.Reader, b); err != nil { // can't happen (at least on a modern OS) panic("failed to read random: " + err.Error()) } @@ -33,3 +34,8 @@ func GenerateRandomAuthString() string { } return string(b) } + +// GenerateRandomConfirmationNumber generates a random 7-digit confirmation number. +func GenerateRandomConfirmationNumber() int32 { + return mrand.Int31n(9000000) + 1000000 +}