🌱🏠 instant least-authority port-forwarding (with automatic HTTPS) for anyone, anywhere! We **really** don't want your TLS private keys, you can keep them 😃 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.

349 lines
13 KiB

package main
import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"fmt"
"net/http"
"regexp"
"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
email := request.PostFormValue("email")
tenantId, databasesHashedPassword, emailVerified := app.Model.GetLoginInfo(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, request, err)
return
}
hmacFromDB := hmac.New(sha256.New, databasesHashedPasswordBytes)
hmacFromDB.Write([]byte(timestampString))
hmacFromDBResultBytes := hmacFromDB.Sum(nil)
loginSuccess = (remoteUsersHMAC == base64.StdEncoding.EncodeToString(hmacFromDBResultBytes))
}
}
returnTo := "/profile"
if request.PostFormValue("returnTo") != "" && app.basicURLPathRegex.MatchString(request.PostFormValue("returnTo")) {
returnTo = request.PostFormValue("returnTo")
}
loginTelemetry := fmt.Sprintf(
"JS HMAC Login: %t, loginSuccess: %t, emailVerified: %t, returnTo: %s",
remoteUsersHMAC != "", loginSuccess, emailVerified, returnTo,
)
go postTelemetry("login", getRemoteIpFromRequest(request), email, strconv.Itoa(tenantId), loginTelemetry)
if loginSuccess {
err := app.setSession(
responseWriter,
&Session{
TenantId: tenantId,
Email: strings.ToLower(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, request, err)
} else {
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, request, session, "login.html", data)
})
app.handleWithSessionNotRequired("/register", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
if !app.EnableRegistration {
app.setFlash(responseWriter, session, "error", "registration is currently disabled 😥\n")
http.Redirect(responseWriter, request, "/", http.StatusFound)
return
}
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, request, 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, request, err)
return
}
protocol := "https"
specialPort := ""
if app.Domain == "localhost" {
protocol = "http"
specialPort = fmt.Sprintf(":%d", app.Port)
}
err = emailService.SendEmail(
fmt.Sprintf("Please verify your email on %s", app.Domain),
data.Email,
fmt.Sprintf(
"Please click the following link to verify your email: %s://%s%s/verify-email/%s",
protocol, app.Domain, specialPort, emailVerificationToken,
),
)
if err != nil {
(*session.Flash)["error"] += fmt.Sprintln(err)
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t, tokenHash=%s) send email failed: %s",
hashedPassword != "", getHashForTelemetry(app, emailVerificationToken), (*session.Flash)["error"],
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, strconv.Itoa(tenantId), registerTelemetry)
} else {
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t, tokenHash=%s) success!!",
hashedPassword != "", getHashForTelemetry(app, emailVerificationToken),
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, strconv.Itoa(tenantId), registerTelemetry)
http.Redirect(responseWriter, request, "/verify-email", http.StatusFound)
return
}
}
} else {
registerTelemetry := fmt.Sprintf(
"registration (JS HMAC: %t) not attempted because: %s",
hashedPassword != "", (*session.Flash)["error"],
)
go postTelemetry("register", getRemoteIpFromRequest(request), data.Email, "", registerTelemetry)
}
}
app.buildPageFromTemplate(responseWriter, request, 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 {
go postTelemetryFromRequest("verify-email", request, app, fmt.Sprintf(
"tokenHash=%s InitializeTenant failed: %s",
getHashForTelemetry(app, mux.Vars(request)["token"]), err,
))
app.unhandledError(responseWriter, request, 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, request, err)
return
}
go postTelemetryFromRequest("verify-email", request, app, fmt.Sprintf(
"tokenHash=%s success!!",
getHashForTelemetry(app, mux.Vars(request)["token"]),
))
app.setFlash(responseWriter, session, "info", "Your email address has been verified!")
http.Redirect(responseWriter, request, "/profile", http.StatusFound)
return
}
}
app.buildPageFromTemplate(responseWriter, request, 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) {
if session.TenantId != 0 {
err := app.Model.LogoutTenant(session.TenantId)
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
}
app.deleteCookie(responseWriter, "sessionId")
app.deleteCookie(responseWriter, "sessionIdLax")
http.Redirect(responseWriter, request, "/", http.StatusFound)
})
app.handleWithSessionNotRequired("/app-auth-session", func(responseWriter http.ResponseWriter, request *http.Request, _ Session) {
appAuthSessionId := request.URL.Query().Get("session")
if request.Method == "POST" {
name := request.URL.Query().Get("name")
if !regexp.MustCompile("[A-Za-z0-9_-]+").MatchString(name) {
http.Error(responseWriter, "400 Bad Request: app-auth-session name may only contain letters, numbers, dashes, and underscores", http.StatusBadRequest)
return
}
telemetryValue := fmt.Sprintf("POST id=%s name=%s", getHashForTelemetry(app, appAuthSessionId), name)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", "", telemetryValue)
app.applicationAuthSessions[appAuthSessionId] = &ApplicationAuthSession{
Name: name,
}
responseWriter.Write([]byte("OK"))
} else {
telemetryValue := fmt.Sprintf("GET id=%s", getHashForTelemetry(app, appAuthSessionId))
appAuthSession, hasAppAuthSession := app.applicationAuthSessions[appAuthSessionId]
if hasAppAuthSession {
if appAuthSession.TenantId != 0 {
telemetryValue = fmt.Sprintf("%s name=%s", telemetryValue, appAuthSession.Name)
apiToken, _, err := app.Backend.CreateAPIToken(appAuthSession.TenantId, appAuthSession.Name)
if err != nil {
app.unhandledError(responseWriter, request, err)
return
}
telemetryValue = fmt.Sprintf("%s success!!", telemetryValue)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", strconv.Itoa(appAuthSession.TenantId), telemetryValue)
responseWriter.Header().Set("Content-Type", "text/plain")
responseWriter.Write([]byte(apiToken))
} else {
telemetryValue = fmt.Sprintf("%s 401 unauthorized", telemetryValue)
// the client polls this so we dont wanna report this particular error, its normal and happens all the time.
// go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", strconv.Itoa(appAuthSession.TenantId), telemetryValue)
http.Error(responseWriter, "401 unauthorized", http.StatusUnauthorized)
}
} else {
telemetryValue = fmt.Sprintf("%s 404 not found", telemetryValue)
go postTelemetry("app-auth-session", getRemoteIpFromRequest(request), "", "", telemetryValue)
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
}
})
app.handleWithSession("/app-connect", func(responseWriter http.ResponseWriter, request *http.Request, session Session) {
appAuthSessionId := request.URL.Query().Get("session")
telemetryValue := fmt.Sprintf("GET id=%s", getHashForTelemetry(app, appAuthSessionId))
appAuthSession, hasAppAuthSession := app.applicationAuthSessions[appAuthSessionId]
if hasAppAuthSession && appAuthSession.TenantId == 0 {
app.applicationAuthSessions[appAuthSessionId].TenantId = session.TenantId
app.buildPageFromTemplate(responseWriter, request, session, "app-connect.html", nil)
telemetryValue = fmt.Sprintf("%s success!!", telemetryValue)
go postTelemetryFromRequest("app-connect", request, app, telemetryValue)
} else {
telemetryValue = fmt.Sprintf("%s 404 not found", telemetryValue)
go postTelemetryFromRequest("app-connect", request, app, telemetryValue)
http.Error(responseWriter, "404 not found", http.StatusNotFound)
}
})
}