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.
200 lines
4.8 KiB
200 lines
4.8 KiB
package main
|
|
|
|
import (
|
|
"context"
|
|
cryptorand "crypto/rand"
|
|
"encoding/json"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"time"
|
|
|
|
"go.mongodb.org/mongo-driver/bson"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
type Session struct {
|
|
ID [32]byte `json:"id" bson:"id"`
|
|
AuthToken [32]byte `json:"auth_token" bson:"auth_token"`
|
|
Timestamp int64 `json:"timestamp" bson:"timestamp"`
|
|
}
|
|
|
|
// ------------------------------- MIDDLEWARE ----------------------------------
|
|
type ctxKey int
|
|
|
|
const currUserKey ctxKey = 0
|
|
|
|
// validateSession verifies that the client has a session that is still valid
|
|
func validateSession(h http.HandlerFunc, role string) http.HandlerFunc {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
// Allow to access required files pre login
|
|
if r.URL.Path == "/static/css/bs1in.css" ||
|
|
r.URL.Path == "/static/js/helper.js" ||
|
|
r.URL.Path == "/static/fonts/OpenSans-Regular.ttf" {
|
|
h(w, r)
|
|
return
|
|
}
|
|
|
|
cookie, err := r.Cookie("SESSION")
|
|
|
|
if err == http.ErrNoCookie {
|
|
// Redirect to /login
|
|
w.Header().Set("Location", "/login?r="+r.URL.Path)
|
|
w.WriteHeader(http.StatusFound)
|
|
log.Println(err)
|
|
return
|
|
} else if err != nil {
|
|
log.Fatal(err)
|
|
return
|
|
}
|
|
|
|
// TESTING: Show cookie content
|
|
// log.Printf("%+v", *cookie)
|
|
requestedUser := checkSession([]byte(cookie.Value))
|
|
|
|
if requestedUser == nil {
|
|
unsetCookie(w)
|
|
w.Header().Set("Location", "/login")
|
|
w.WriteHeader(http.StatusFound)
|
|
return
|
|
}
|
|
// Set new timestamp
|
|
requestedUser.CurrSession.Timestamp = time.Now().UTC().UnixNano()
|
|
// Save to database
|
|
updateSession(requestedUser)
|
|
|
|
setEncryptedCookie(w, requestedUser)
|
|
ctx := context.WithValue(r.Context(), currUserKey, *requestedUser)
|
|
requestWithUser := r.WithContext(ctx)
|
|
|
|
// ORIGINAL HANDLER
|
|
switch role {
|
|
case "":
|
|
h(w, requestWithUser)
|
|
case "admin":
|
|
if isAdmin(requestedUser) {
|
|
h(w, requestWithUser)
|
|
} else {
|
|
w.Header().Set("Location", "/")
|
|
w.WriteHeader(http.StatusFound)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func isAdmin(u *User) bool {
|
|
if u.Role == "admin" {
|
|
log.Println("IS ADMIN")
|
|
return true
|
|
} else {
|
|
log.Println("IS NOT ADMIN")
|
|
return false
|
|
}
|
|
}
|
|
|
|
func encryptSession(s Session) (es []byte) {
|
|
js, err := json.Marshal(s)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
es, err = encrypt(js, encKey)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
return
|
|
}
|
|
|
|
func setEncryptedCookie(w http.ResponseWriter, u *User) {
|
|
cValue := encryptSession(u.CurrSession)
|
|
cookie := &http.Cookie{
|
|
Name: "SESSION",
|
|
Value: string(cValue),
|
|
MaxAge: int(6 * time.Hour / time.Second),
|
|
Path: "/",
|
|
HttpOnly: false,
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
}
|
|
|
|
func unsetCookie(w http.ResponseWriter) {
|
|
cookie := &http.Cookie{
|
|
Name: "SESSION",
|
|
MaxAge: -69,
|
|
Path: "/",
|
|
}
|
|
http.SetCookie(w, cookie)
|
|
}
|
|
|
|
// checkCredentials compares the pw provided by the user to the hashed version
|
|
// in the database and returns true for success and false otherwise
|
|
func checkCredentials(u *User, pw string) bool {
|
|
hashedPw := u.Pass
|
|
err := bcrypt.CompareHashAndPassword([]byte(hashedPw), []byte(pw))
|
|
if err != nil {
|
|
log.Print(err)
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// checkSession compares the encrypted session string with the current Session
|
|
// set for the user and returns true if they match
|
|
func checkSession(receivedSession []byte) *User {
|
|
// Test
|
|
dec, err := decrypt(receivedSession, encKey)
|
|
if err != nil {
|
|
log.Println("Encryption key changed, deleting remaining cookies!")
|
|
return nil
|
|
}
|
|
// log.Println(string(dec))
|
|
decryptedSession := Session{}
|
|
err = json.Unmarshal(dec, &decryptedSession)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// There is no current user but a (mayb) valid session -> get user with session from db
|
|
requestedUser, err := getUserBySession(&decryptedSession)
|
|
if err != nil {
|
|
log.Println("No user with that session found in db:", err)
|
|
return nil
|
|
}
|
|
|
|
// 6 hours ago
|
|
maxSessionAge := time.Now().UTC().UnixNano() - int64(6*time.Hour)
|
|
|
|
// Check if session is still valid (Other values must match anyways)
|
|
if requestedUser.CurrSession.Timestamp >= maxSessionAge {
|
|
return requestedUser
|
|
} else {
|
|
log.Println("User logged out because Session is no longer valid")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func updateSession(u *User) {
|
|
filter := bson.D{{"uid", u.UID}}
|
|
update := bson.D{{"$set", bson.D{{"curr_session", u.CurrSession}}}}
|
|
res, err := colUsers.UpdateOne(mongoCtx, filter, update)
|
|
if err != nil {
|
|
log.Fatal(err, res)
|
|
}
|
|
}
|
|
|
|
func createSession() (s Session) {
|
|
var sID, aToken [32]byte
|
|
_, err := io.ReadFull(cryptorand.Reader, sID[:])
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
_, err = io.ReadFull(cryptorand.Reader, aToken[:])
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
// MAYB: Also hash the random number? Any advantages?
|
|
// sID := sha256.Sum256(nonce)
|
|
// aToken := sha256.Sum256(nonce)
|
|
return Session{ID: sID, AuthToken: aToken, Timestamp: time.Now().UTC().UnixNano()}
|
|
}
|