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 )
}
} )
}