some 2fa, split main into multiple files

This commit is contained in:
Your Name 2023-09-26 16:36:03 -04:00
parent 266e16a060
commit f3cc9cdeed
8 changed files with 382 additions and 225 deletions

44
2fa.go Normal file
View File

@ -0,0 +1,44 @@
package scsusers
import (
"crypto/rand"
"errors"
"fmt"
"math/big"
"time"
)
func generate2fa() string {
num, err := rand.Int(rand.Reader, big.NewInt(999999))
if err != nil {
return "918273"
}
return fmt.Sprintf("%06d", num)
}
func Validate2FA(u *UserData, challenge string) bool {
v, ok := u.Get("2fa")
return ok && v == challenge
}
func Send2FA(u *UserData) error {
code := generate2fa()
u.Set("2fa", code)
u.Set("2faexpires", fmt.Sprintf("%d", time.Now().Add(15*time.Minute).Unix()))
email,ok:=u.Get("email")
if !ok {
return errors.New("send2fa: no email")
}
firstname,ok:=u.Get("firstname")
if !ok {
return errors.New("send2fa: no firstname")
}
lastname,ok:=u.Get("lastname")
if !ok {
return errors.New("send2fa: no lastname")
}
fullname:=fmt.Sprintf("%s %s", firstname, lastname)
return Send2faEmail(email, fullname, code)
}

196
emails.go Normal file
View File

@ -0,0 +1,196 @@
package scsusers
import (
"bytes"
"crypto/rand"
"database/sql"
"encoding/base32"
"fmt"
"log"
"net/smtp"
"strings"
"time"
)
func RecoverByUsername(username string) {
var email string
username = strings.ToLower(username)
q := fmt.Sprintf("select email from %s_auth where username = ?", c.TablePrefix)
err := c.db.Get(&email, q, username)
if err != sql.ErrNoRows {
recoverycode := randBytes(16)
qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix)
_, err := c.db.Exec(qq, recoverycode, username)
if err == nil {
SendRecoveryEmail(email, username, string(recoverycode))
}
}
}
func RecoverByEmail(e string) {
var username, email string
q := fmt.Sprintf("select username from %s_auth where email = ?", c.TablePrefix)
err := c.db.Get(&username, q, e)
if err != sql.ErrNoRows {
recoverycode := randBytes(16)
qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix)
_, err := c.db.Exec(qq, recoverycode, username)
if err == nil {
SendRecoveryEmail(email, username, string(recoverycode))
}
}
}
func randBytes(n int) []byte {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return []byte(base32.StdEncoding.EncodeToString(randomBytes)[:n])
}
func SendRegistrationEmail(recipient, username, password string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
Pass string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
Pass: password,
}
var body bytes.Buffer
err := c.Templates.Registration.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendRegistrationEmail: Registration template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("Welcome to %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.SendRegistrationEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func SendAlertEmail(username, recipient, message string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
Activity string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
Activity: message,
}
var body bytes.Buffer
err := c.Templates.Alert.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendAlertEmail: Alert template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("new Activity Notification on %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.sendAlertEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func SendRecoveryEmail(recipient, username, code string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
RecoveryCode string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
RecoveryCode: code,
}
var body bytes.Buffer
err := c.Templates.Registration.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendRecoveryEmail: Recovery template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("Account recovery at %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.sendRecoveryEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func Send2faEmail(recipient, fullname, code string) error {
data := struct {
SiteName string
FromEmail string
FullName string
Code string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
FullName: fullname,
Code: code,
}
var body bytes.Buffer
err := c.Templates.TwoFA.Execute(&body, data)
if err != nil {
return fmt.Errorf("scsusers.send2fayEmail: 2fa template failed to execute: %v returned %s", data, err.Error())
}
subject := fmt.Sprintf("Two Factor Authentication Code at %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
return err
}
func SendMail(addr, from, subject, body string, to string) error {
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
c, err := smtp.Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.Mail(r.Replace(from)); err != nil {
return err
}
to = r.Replace(to)
if err = c.Rcpt(to); err != nil {
return err
}
w, err := c.Data()
if err != nil {
return err
}
date := time.Now()
msg := "To: " + to + "\r\n" +
"From: " + from + "\r\n" +
"Subject: " + subject + "\r\n" +
"Content-Type: text/html; charset=\"UTF-8\"\r\n" +
"Date: " + date.Format(time.RFC1123Z) + "\r\n" +
"\r\n" + body
_, err = w.Write([]byte(msg))
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}

222
main.go
View File

@ -1,17 +1,12 @@
package scsusers package scsusers
import ( import (
"bytes"
"crypto/rand"
"database/sql" "database/sql"
"encoding/base32"
"encoding/json" "encoding/json"
"fmt" "fmt"
"html/template" "html/template"
"log" "log"
"net/smtp"
"strings" "strings"
"time"
"github.com/jmoiron/sqlx" "github.com/jmoiron/sqlx"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
@ -21,6 +16,7 @@ type templates struct {
Registration *template.Template Registration *template.Template
Alert *template.Template Alert *template.Template
Recovery *template.Template Recovery *template.Template
TwoFA *template.Template
} }
type config struct { type config struct {
@ -59,6 +55,7 @@ func Init(dbin *sqlx.DB, tp, sitename, fromaddr, smtpserver string) {
SetRegistrationTemplate("") SetRegistrationTemplate("")
SetAlertTemplate("") SetAlertTemplate("")
SetRecoveryTemplate("") SetRecoveryTemplate("")
Set2faTemplate("")
} }
func UsernameAvailable(username string) bool { func UsernameAvailable(username string) bool {
@ -307,218 +304,3 @@ func SetMeta(username string, metakey string, metavalue string) {
log.Printf("scsusers.SetMeta: %s %s %s %s\n", username, metakey, metavalue, err.Error()) log.Printf("scsusers.SetMeta: %s %s %s %s\n", username, metakey, metavalue, err.Error())
} }
} }
func RecoverByUsername(username string) {
var email string
username = strings.ToLower(username)
q := fmt.Sprintf("select email from %s_auth where username = ?", c.TablePrefix)
err := c.db.Get(&email, q, username)
if err != sql.ErrNoRows {
recoverycode := randBytes(16)
qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix)
_, err := c.db.Exec(qq, recoverycode, username)
if err == nil {
SendRecoveryEmail(email, username, string(recoverycode))
}
}
}
func RecoverByEmail(e string) {
var username, email string
q := fmt.Sprintf("select username from %s_auth where email ILIKE ?", c.TablePrefix)
err := c.db.Get(&username, q, e)
if err != sql.ErrNoRows {
recoverycode := randBytes(16)
qq := fmt.Sprintf("update %s_auth set recoverycode=?, recoverytime=NOW() where username = ?", c.TablePrefix)
_, err := c.db.Exec(qq, recoverycode, username)
if err == nil {
SendRecoveryEmail(email, username, string(recoverycode))
}
}
}
func randBytes(n int) []byte {
randomBytes := make([]byte, 32)
_, err := rand.Read(randomBytes)
if err != nil {
panic(err)
}
return []byte(base32.StdEncoding.EncodeToString(randomBytes)[:n])
}
func SendRegistrationEmail(recipient, username, password string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
Pass string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
Pass: password,
}
var body bytes.Buffer
err := c.Templates.Registration.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendRegistrationEmail: Registration template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("Welcome to %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.SendRegistrationEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func SendAlertEmail(username, recipient, message string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
Activity string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
Activity: message,
}
var body bytes.Buffer
err := c.Templates.Alert.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendAlertEmail: Alert template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("new Activity Notification on %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.sendAlertEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func SendRecoveryEmail(recipient, username, code string) bool {
data := struct {
SiteName string
FromEmail string
UserName string
RecoveryCode string
}{
SiteName: c.SiteName,
FromEmail: c.FromEmail,
UserName: username,
RecoveryCode: code,
}
var body bytes.Buffer
err := c.Templates.Registration.Execute(&body, data)
if err != nil {
log.Printf("scsusers.sendRecoveryEmail: Recovery template failed to execute: %v returned %s\n", data, err.Error())
return false
}
subject := fmt.Sprintf("Account recovery at %s", c.SiteName)
err = SendMail(c.SMTPServer, c.FromEmail, subject, body.String(), recipient)
if err != nil {
log.Printf("scsusers.sendRecoveryEmail: Error sending mail to %s: %s\n", recipient, err.Error())
return false
}
return true
}
func SetRegistrationTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("reg").Parse(t)
if err != nil {
c.Templates.Registration = r
return true
}
}
df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}<br>You can change your password to whatever you want once you log in.</p></body></html>`
r, err := template.New("reg").Parse(df)
if err != nil {
log.Fatal("scsusers.SetRegistrationTemplate: Default registration template MUST compile. Error: " + err.Error())
}
c.Templates.Registration = r
return false
}
func SetAlertTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("alert").Parse(t)
if err != nil {
c.Templates.Alert = r
return true
}
}
df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p> Hey {{.UserName}}! Just letting you know that {{.Activity}}.<br> You can disable future notifications in your user settings.</p></body></html>`
r, err := template.New("alert").Parse(df)
if err != nil {
log.Fatal("scsusers.SetAlertTemplate: Default alert template MUST compile. Error: " + err.Error())
}
c.Templates.Alert = r
return false
}
func SetRecoveryTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("recovery").Parse(t)
if err != nil {
c.Templates.Recovery = r
return true
}
}
df := `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html></head><body><p>Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery agt {{.SiteName}}. If this was yousername, enter the following code to regain access: {{.RecoveryCode}}<br> If this was not yousername, you can ignore this email.</p></body></html>`
r, err := template.New("recovery").Parse(df)
if err != nil {
log.Fatal("scsusers.SetRecoveryTemplate: Default recovery template MUST compile. Error: " + err.Error())
}
c.Templates.Recovery = r
return false
}
func SendMail(addr, from, subject, body string, to string) error {
r := strings.NewReplacer("\r\n", "", "\r", "", "\n", "", "%0a", "", "%0d", "")
c, err := smtp.Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.Mail(r.Replace(from)); err != nil {
return err
}
to = r.Replace(to)
if err = c.Rcpt(to); err != nil {
return err
}
w, err := c.Data()
if err != nil {
return err
}
date := time.Now()
msg := "To: " + to + "\r\n" +
"From: " + from + "\r\n" +
"Subject: " + subject + "\r\n" +
"Content-Type: text/html; charset=\"UTF-8\"\r\n" +
"Date: " + date.Format(time.RFC1123Z) + "\r\n" +
"\r\n" + body
_, err = w.Write([]byte(msg))
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}

96
templates.go Normal file
View File

@ -0,0 +1,96 @@
package scsusers
import (
"embed"
"html/template"
"log"
)
//go:embed templates/*.html
var FS embed.FS
func SetRegistrationTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("reg").Parse(t)
if err != nil {
c.Templates.Registration = r
return true
}
}
df, err := FS.ReadFile("templates/registration.html")
if err != nil {
log.Fatal("Missing recovery template")
}
r, err := template.New("reg").Parse(string(df))
if err != nil {
log.Fatal("scsusers.SetRegistrationTemplate: Default registration template MUST compile. Error: " + err.Error())
}
c.Templates.Registration = r
return false
}
func Set2faTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("2fa").Parse(t)
if err != nil {
c.Templates.Registration = r
return true
}
}
df, err := FS.ReadFile("templates/2fa.html")
if err != nil {
log.Fatal("Missing 2fa template")
}
r, err := template.New("2fa").Parse(string(df))
if err != nil {
log.Fatal("scsusers.Set2faTemplate: Default 2fa template MUST compile. Error: " + err.Error())
}
c.Templates.Registration = r
return false
}
func SetAlertTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("alert").Parse(t)
if err != nil {
c.Templates.Alert = r
return true
}
}
df, err := FS.ReadFile("templates/alert.html")
if err != nil {
log.Fatal("Missing recovery template")
}
r, err := template.New("alert").Parse(string(df))
if err != nil {
log.Fatal("scsusers.SetAlertTemplate: Default alert template MUST compile. Error: " + err.Error())
}
c.Templates.Alert = r
return false
}
func SetRecoveryTemplate(t string) bool {
if len(t) != 0 {
r, err := template.New("recovery").Parse(t)
if err != nil {
c.Templates.Recovery = r
return true
}
}
df, err := FS.ReadFile("templates/recovery.html")
if err != nil {
log.Fatal("Missing recovery template")
}
r, err := template.New("recovery").Parse(string(df))
if err != nil {
log.Fatal("scsusers.SetRecoveryTemplate: Default recovery template MUST compile. Error: " + err.Error())
}
c.Templates.Recovery = r
return false
}

16
templates/2fa.html Normal file
View File

@ -0,0 +1,16 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
</head>
<body>
<p>
Hello {{.UserName}}! This is your Two Factor Authentication code to finish logging in to {{.SiteName}}. If you did not request this code,
please contact us immediately. This code will expire in 30 minutes.<br><br>
<h1>Your 2FA code is: {{.Code}}</h1>
</p>
</body>
</html>

11
templates/alert.html Normal file
View File

@ -0,0 +1,11 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
</head>
<body>
<p> Hey {{.UserName}}! Just letting you know that {{.Activity}}.<br> You can disable future notifications in your
user settings.</p>
</body>
</html>

12
templates/recovery.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html
PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
</head>
<body>
<p>Hello {{.UserName}}! Someone (hopefully you) has attempted an account recovery at {{.SiteName}}. If this was
you, enter the following code to regain access: {{.RecoveryCode}}<br> If this was not you, you can safely
ignore this email.</p>
</body>
</html>

View File

@ -7,7 +7,7 @@
<body> <body>
<p> <p>
Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}<br> Hello {{.UserName}}! Welcome to {{.SiteName}}! We've created your account with the username you selected and the following password: {{.Pass}}<br>
You can change your password to whatever you want once you log in. You can change your password once you log in.
</p> </p>
</body> </body>