Skip to content

Commit

Permalink
drop weird jwt stuff, send email for restricted groups, fix typo
Browse files Browse the repository at this point in the history
  • Loading branch information
lillian committed Jan 16, 2024
1 parent 8388763 commit 62f846f
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 79 deletions.
26 changes: 23 additions & 3 deletions cmd/web/ui/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -132,13 +132,33 @@ func registerPasswdRoutes(r chi.Router) {
return
}

token := auth.CreateResetToken(username)
token, err := auth.CreateResetToken(username)
if err != nil {
errReply.Error = err.Error()
MaybeHtmxComponent(rw, r, "passwd", errReply)
return
}

email, err := auth.GetEmailFromUsername(
"cn=password_self_service,ou=services,dc=hacklab,dc=to",
os.Getenv("LDAP_SELFSERVICE_PASSWORD"),
username,
)

if err == auth.ErrInvalidGroup {
err = auth.SendResetRestrictedEmail(email, username)
if err != nil {
errReply.Error = err.Error()
MaybeHtmxComponent(rw, r, "passwd", errReply)
return
}
MaybeHtmxComponent(rw, r, "confirmation", Confirmation{
Title: "Reset your password",
Message: "A confirmation email has been sent to the address associated with your account.",
})
return
}

if err != nil {
errReply.Error = err.Error()
MaybeHtmxComponent(rw, r, "passwd", errReply)
Expand All @@ -164,7 +184,7 @@ func registerPasswdRoutes(r chi.Router) {
return
}
newPassword := r.Form.Get("password")
if len(newPassword) > 12 { // arbitrary
if len(newPassword) < 12 { // arbitrary
errReply.Error = "password is too short (must be 12 characters)"
MaybeHtmxComponent(rw, r, "passwd", errReply)
return
Expand Down Expand Up @@ -201,7 +221,7 @@ func registerPasswdRoutes(r chi.Router) {
MaybeHtmxComponent(rw, r, "passwd", errReply)
return
}
if err := db.RedisDB.Set(context.Background(), "used-reset-token:"+token, 1, time.Hour*24).Err(); err != nil {
if err := db.RedisDB.Del(context.Background(), "reset-token:"+token).Err(); err != nil {
errReply.Error = err.Error()
MaybeHtmxComponent(rw, r, "passwd", errReply)
return
Expand Down
4 changes: 3 additions & 1 deletion internal/auth/ldap.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (

var invalidGroups = []string{"cn=operations", "cn=hacklab-sudoer", "cn=board", "ou=permissions"}

var ErrInvalidGroup = fmt.Errorf("user is in restricted group, unable to change password by email")

func GetEmailFromUsername(bindDN, bindPassword, targetUsername string) (string, error) {
ldapURL := os.Getenv("LDAP_URL")
if ldapURL == "" {
Expand Down Expand Up @@ -46,7 +48,7 @@ func GetEmailFromUsername(bindDN, bindPassword, targetUsername string) (string,
for _, val := range entry.GetAttributeValues("memberOf") {
for _, g := range invalidGroups {
if strings.Contains(val, g) {
return "", fmt.Errorf("user not allowed to reset password by email")
return entry.GetAttributeValue("mail"), ErrInvalidGroup
}
}
}
Expand Down
99 changes: 24 additions & 75 deletions internal/auth/resetpassword.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,33 @@ package auth

import (
"context"
"crypto/hmac"
"crypto/sha256"
"crypto/rand"
"encoding/base64"
"fmt"
"log"
"members-platform/internal/db"
"members-platform/internal/mailer"
"os"
"strconv"
"strings"
"time"

"github.com/redis/go-redis/v9"
)

// poor hacker's jwt
func CreateResetToken(username string) string {
signkey := os.Getenv("PASSWD_RESET_HASHER_SECRET")
if signkey == "" {
panic(fmt.Errorf("missing PASSWD_RESET_HASHER_SECRET in environment"))
func CreateResetToken(username string) (string, error) {
b := make([]byte, 48)
_, err := rand.Read(b)
if err != nil {
return "", fmt.Errorf("failed to read random bytes: %w", err)
}

hmacer := hmac.New(sha256.New, []byte(signkey))

var b strings.Builder

b.WriteString(base64.RawURLEncoding.EncodeToString([]byte(username)))
b.WriteString(".")
b.WriteString(base64.RawURLEncoding.EncodeToString([]byte(strconv.Itoa(int(time.Now().UTC().Unix())))))

hmacer.Write([]byte(b.String()))
b.WriteString(".")
b.WriteString(base64.RawURLEncoding.EncodeToString(hmacer.Sum(nil)))

return b.String()
token := base64.RawURLEncoding.EncodeToString(b)
return token, db.RedisDB.Set(context.Background(), "reset-token:"+token, username, time.Hour*24).Err()
}

func ValidateResetToken(token string) (string, bool) {
signkey := os.Getenv("PASSWD_RESET_HASHER_SECRET")
if signkey == "" {
panic(fmt.Errorf("missing PASSWD_RESET_HASHER_SECRET in environment"))
}

parts := strings.Split(token, ".")
if len(parts) != 3 {
return "", false
}

username, err := base64.RawURLEncoding.DecodeString(parts[0])
if err != nil {
return "", false
}

timestamp, err := base64.RawURLEncoding.DecodeString(parts[1])
if err != nil {
return "", false
}
if t, err := strconv.Atoi(string(timestamp)); err != nil {
return "", false
} else {
// if token created > 1 day ago
if (int(time.Now().UTC().Unix()) - t) > int((time.Hour * 24).Seconds()) {
return "", false
}
}

hmac_from_user, err := base64.RawURLEncoding.DecodeString(parts[2])
if err != nil {
return "", false
}

hmacer := hmac.New(sha256.New, []byte(signkey))

if _, err := hmacer.Write([]byte(parts[0] + "." + parts[1])); err != nil {
return "", false
}

if !hmac.Equal(hmac_from_user, hmacer.Sum(nil)) {
return "", false
}

_, err = db.RedisDB.Get(context.Background(), "used-reset-token:"+token).Result()
v, err := db.RedisDB.Get(context.Background(), "reset-token:"+token).Result()
switch {
// token not used
case err == redis.Nil:
return string(username), true
case err != nil:
log.Println(err)
return "", false
}
// if err == nil, token already used
return "", false
return v, true
}

func SendResetEmail(email, username, token string) error {
Expand All @@ -111,6 +46,20 @@ func SendResetEmail(email, username, token string) error {
return mailer.DoSendEmail(email, content)
}

func SendResetRestrictedEmail(email, username string) error {
d := mailer.ResetPasswordData{
ToAddress: email,
Username: username,
}

content, err := mailer.ExecuteTemplate("reset-password-restricted", d)
if err != nil {
return fmt.Errorf("build email content: %w", err)
}

return mailer.DoSendEmail(email, content)
}

func SendConfirmationEmail(email, username string) error {
d := mailer.ResetPasswordData{
ToAddress: email,
Expand Down
12 changes: 12 additions & 0 deletions internal/mailer/templates/reset-password-restricted.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
From: members.hacklab.to <operations+automated@hacklab.to>
Reply-To: operations@hacklab.to
To: {{ .ToAddress }}
Subject: Hacklab.to password reset

Hello {{ .Username }},

You (or someone else who knows your Hacklab username) has requested a password
reset for your account. Unfortunately, due to your membership in a protected
LDAP group (for instance "Operations" or "Board"), it is not possible to reset
your password via email.
Please contact operations@hacklab.to for assistance.

0 comments on commit 62f846f

Please sign in to comment.