🌱🏠 a cloud service to enable your own server (owned by you and running on your computer) to be accessible on the internet in seconds, no credit card required https://greenhouse.server.garden/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

233 lines
8.1 KiB

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"log"
"net/http"
"strconv"
"strings"
"time"
"github.com/gorilla/mux"
)
func registerLoginRoutes(app *FrontendApp, emailService *EmailService) {
app.handleWithSessionNotRequired("/login", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if request.Method == "POST" {
remoteUsersHMAC := request.PostFormValue("hmac")
loginSuccess := false
tenantId, databasesHashedPassword, emailVerified := app.Model.GetLoginInfo(request.PostFormValue("email"))
if remoteUsersHMAC == "" {
saltedPassword := fmt.Sprintf("%s%s", app.PasswordHashSalt, request.PostFormValue("password"))
hashedPasswordByteArray := sha256.Sum256([]byte(saltedPassword))
remoteUsersHashedPassword := base64.StdEncoding.EncodeToString(hashedPasswordByteArray[:])
loginSuccess = (remoteUsersHashedPassword == databasesHashedPassword)
} else {
timestampString := request.PostFormValue("timestamp")
timestamp, err := strconv.ParseInt(timestampString, 10, 64)
elapsedSeconds := time.Now().Unix() - timestamp
if err != nil || elapsedSeconds < 0 || elapsedSeconds > 5*60 {
(*session.Flash)["error"] += "Invalid timestamp\n"
} else {
databasesHashedPasswordBytes, err := base64.StdEncoding.DecodeString(databasesHashedPassword)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
hmacFromDB := hmac.New(sha256.New, databasesHashedPasswordBytes)
hmacFromDB.Write([]byte(timestampString))
hmacFromDBResultBytes := hmacFromDB.Sum(nil)
loginSuccess = (remoteUsersHMAC == base64.StdEncoding.EncodeToString(hmacFromDBResultBytes))
}
}
if loginSuccess {
err := app.setSession(
responseWriter,
&Session{
TenantId: tenantId,
Email: strings.ToLower(request.PostFormValue("email")),
EmailVerified: emailVerified,
// we will use the SameSite Lax cookie policy until the first time that the user re-logs-in AFTER confirming thier
// email address. After that we will use the SameSite Strict cookie policy
// this is a "have your cake and eat it too" heuristic solution to
// https://security.stackexchange.com/questions/220292/preventing-csrf-with-samesite-strict-without-degrading-user-experience
// See also: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#SameSite_attribute
LaxCookie: !emailVerified,
Expires: time.Now().Add(time.Hour),
Flash: &(map[string]string{}),
},
)
if err != nil {
app.unhandledError(responseWriter, err)
} else {
returnTo := "/profile"
if request.PostFormValue("returnTo") != "" && app.basicURLPathRegex.MatchString(request.PostFormValue("returnTo")) {
returnTo = request.PostFormValue("returnTo")
}
http.Redirect(responseWriter, request, returnTo, http.StatusFound)
}
return
} // else
(*session.Flash)["error"] += "Invalid login credentials. Passwords are case-sensitive\n"
}
returnTo := ""
if app.basicURLPathRegex.MatchString((*session.Flash)["returnTo"]) {
returnTo = (*session.Flash)["returnTo"]
}
data := struct {
PasswordHashSalt string
Timestamp string
ReturnTo string
}{
PasswordHashSalt: app.PasswordHashSalt,
Timestamp: fmt.Sprintf("%d", time.Now().Unix()),
ReturnTo: returnTo,
}
app.buildPageFromTemplate(responseWriter, session, "login.html", data)
})
app.handleWithSessionNotRequired("/register", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
data := struct {
Email string
PasswordHashSalt string
}{
PasswordHashSalt: app.PasswordHashSalt,
}
if request.Method == "POST" {
data.Email = request.PostFormValue("email")
emailError := emailService.ValidateEmailAddress(data.Email)
if emailError != "" {
(*session.Flash)["error"] += fmt.Sprintln(emailError)
}
hashedPassword := request.PostFormValue("hashedPassword")
if hashedPassword == "" {
password := request.PostFormValue("password")
if len(password) < 6 {
(*session.Flash)["error"] += "password must be at least 6 characters\n"
}
saltedPassword := fmt.Sprintf("%s%s", app.PasswordHashSalt, password)
hashedPasswordByteArray := sha256.Sum256([]byte(saltedPassword))
hashedPassword = base64.StdEncoding.EncodeToString(hashedPasswordByteArray[:])
}
if (*session.Flash)["error"] == "" {
tenantId, err := app.Model.Register(data.Email, hashedPassword)
if err != nil {
(*session.Flash)["error"] += fmt.Sprintln(err)
} else {
emailVerificationTokenBuffer := make([]byte, 4)
rand.Read(emailVerificationTokenBuffer)
emailVerificationToken := hex.EncodeToString(emailVerificationTokenBuffer)
err := app.Model.CreateEmailVerificationToken(emailVerificationToken, tenantId, time.Now().Add(time.Hour))
if err != nil {
app.unhandledError(responseWriter, err)
return
}
err = app.setSession(
responseWriter,
&Session{
TenantId: tenantId,
Email: strings.ToLower(data.Email),
LaxCookie: true,
Expires: time.Now().Add(time.Hour),
Flash: &(map[string]string{}),
},
)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
protocol := "https"
if app.Domain == "localhost" {
protocol = "http"
}
specialPort := ""
if app.Port != 80 && app.Port != 443 {
specialPort = fmt.Sprintf(":%d", app.Port)
}
err = emailService.SendEmail(
fmt.Sprintf("Please verify your account on %s", app.Domain),
data.Email,
fmt.Sprintf(
"Please click the following link to verify your account: %s://%s%s/verify-email/%s",
protocol, app.Domain, specialPort, emailVerificationToken,
),
)
if err != nil {
(*session.Flash)["error"] += fmt.Sprintln(err)
} else {
http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
return
}
}
}
}
app.buildPageFromTemplate(responseWriter, session, "register.html", data)
})
verifyEmailHandler := func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if session.EmailVerified {
app.setFlash(responseWriter, session, "info", "Your email address has already been verified")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
log.Printf("verify email handler: [%s] [%d]\n", mux.Vars(request)["token"], session.TenantId)
if mux.Vars(request)["token"] != "" && session.TenantId != 0 {
err := app.Model.VerifyEmail(mux.Vars(request)["token"], session.TenantId)
if err != nil {
(*session.Flash)["error"] += fmt.Sprintln(err)
} else {
err = app.Backend.InitializeTenant(session.TenantId, session.Email)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
session.EmailVerified = true
newSession := &Session{
TenantId: session.TenantId,
Email: session.Email,
EmailVerified: true,
LaxCookie: true,
Expires: time.Now().Add(time.Hour),
Flash: &(map[string]string{}),
}
err = app.setSession(responseWriter, newSession)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
app.setFlash(responseWriter, session, "info", "Your email address has been verified!")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
}
app.buildPageFromTemplate(responseWriter, session, "verify-email.html", nil)
}
app.handleWithSession("/verify-email", verifyEmailHandler)
app.handleWithSession("/verify-email/{token}", verifyEmailHandler)
app.handleWithSessionNotRequired("/logout", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
err := app.Model.LogoutTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, err)
return
}
app.deleteCookie(responseWriter, "sessionId")
app.deleteCookie(responseWriter, "sessionIdLax")
http.Redirect(responseWriter, request, "/", http.StatusFound)
})
}