183 lines
5.2 KiB
Go
183 lines
5.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gorilla/mux"
|
|
"github.com/jinzhu/gorm"
|
|
"github.com/rs/zerolog/log"
|
|
"github.com/traggo/server/model"
|
|
)
|
|
|
|
type key string
|
|
|
|
var (
|
|
traggoSession = "traggo"
|
|
createSessionKey key = "createSession"
|
|
destroySessionKey key = "destroySession"
|
|
userKey key = "user"
|
|
deviceKey key = "device"
|
|
)
|
|
|
|
// Middleware is the auth middleware which sets user and device context parameters.
|
|
func Middleware(db *gorm.DB) mux.MiddlewareFunc {
|
|
return func(handler http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
handler.ServeHTTP(writer, sessionCallbacks(reqisterUser(request, writer, db), writer))
|
|
})
|
|
}
|
|
}
|
|
|
|
func sessionCallbacks(request *http.Request, writer http.ResponseWriter) *http.Request {
|
|
createSession := func(token string, maxAge int) {
|
|
http.SetCookie(writer, &http.Cookie{
|
|
Name: traggoSession,
|
|
Value: token,
|
|
MaxAge: maxAge,
|
|
})
|
|
}
|
|
destroySession := func() {
|
|
http.SetCookie(writer, &http.Cookie{
|
|
Name: traggoSession,
|
|
Value: "",
|
|
MaxAge: -1,
|
|
})
|
|
}
|
|
return request.WithContext(WithDestroySession(WithCreateSession(request.Context(), createSession), destroySession))
|
|
}
|
|
|
|
func reqisterUser(request *http.Request, writer http.ResponseWriter, db *gorm.DB) *http.Request {
|
|
token, err := getToken(request)
|
|
if err != nil {
|
|
return request
|
|
}
|
|
|
|
device := &model.Device{}
|
|
if db.Where("token = ?", token).Find(device).RecordNotFound() {
|
|
log.Debug().Str("token", token).Msg("No device with token found")
|
|
return request
|
|
}
|
|
|
|
user := &model.User{}
|
|
if db.Find(user, device.UserID).RecordNotFound() {
|
|
log.Panic().Int("userID", device.UserID).Int("deviceID", device.ID).Msg("User not found")
|
|
}
|
|
log.Debug().Int("userid", device.UserID).Str("username", user.Name).Msg("User found")
|
|
|
|
impersonate := request.Header.Get("X-Traggo-Impersonate")
|
|
if impersonate != "" {
|
|
if !user.Admin {
|
|
log.Info().Str("impersonate", impersonate).Msg("Trying to impersonate without being admin")
|
|
return request
|
|
}
|
|
userID, err := strconv.Atoi(impersonate)
|
|
if err != nil {
|
|
log.Info().Str("impersonate", impersonate).Msg("Unable to parse impersonation header")
|
|
return request
|
|
}
|
|
impersonateUser := &model.User{}
|
|
if db.Find(impersonateUser, userID).RecordNotFound() {
|
|
log.Info().Int("userid", userID).Msg("Impersonation user not found")
|
|
return request
|
|
}
|
|
user = impersonateUser
|
|
log.Info().Str("username", user.Name).Msg("Impersonation user")
|
|
}
|
|
|
|
if device.ActiveAt.Before(time.Now().Add(5 * -time.Minute)) {
|
|
log.Debug().Int("deviceId", device.ID).Str("deviceName", device.Name).Msg("update device activeAt")
|
|
device.ActiveAt = timeNow()
|
|
db.Save(device)
|
|
|
|
if cookie, err := request.Cookie(traggoSession); err == nil && cookie != nil {
|
|
cookie.MaxAge = device.Type.Seconds()
|
|
http.SetCookie(writer, cookie)
|
|
}
|
|
}
|
|
|
|
return request.WithContext(WithUser(WithDevice(request.Context(), device), user))
|
|
}
|
|
|
|
func getToken(request *http.Request) (string, error) {
|
|
if value := request.Header.Get("Authorization"); value != "" && strings.HasPrefix(value, "traggo ") {
|
|
log.Debug().Msg("Using authorization header for authentication")
|
|
key := strings.TrimPrefix(value, "traggo ")
|
|
return key, nil
|
|
}
|
|
|
|
if cookie, err := request.Cookie("traggo"); err == nil && cookie != nil && cookie.Value != "" {
|
|
log.Debug().Msg("Using cookie for authentication")
|
|
return cookie.Value, nil
|
|
}
|
|
|
|
if token := request.FormValue("token"); token != "" {
|
|
log.Debug().Msg("Using query parameter for authentication")
|
|
return token, nil
|
|
}
|
|
|
|
return "", errors.New("no token found")
|
|
}
|
|
|
|
// WithDevice adds the authenticated device to the context.
|
|
func WithDevice(ctx context.Context, device *model.Device) context.Context {
|
|
return context.WithValue(
|
|
ctx,
|
|
deviceKey,
|
|
device)
|
|
}
|
|
|
|
// WithUser adds the authenticated user to the context.
|
|
func WithUser(ctx context.Context, user *model.User) context.Context {
|
|
return context.WithValue(
|
|
ctx,
|
|
userKey,
|
|
user)
|
|
}
|
|
|
|
// WithCreateSession adds the create session method
|
|
func WithCreateSession(ctx context.Context, f func(token string, age int)) context.Context {
|
|
return context.WithValue(ctx, createSessionKey, f)
|
|
}
|
|
|
|
// WithDestroySession adds the destroy session method
|
|
func WithDestroySession(ctx context.Context, f func()) context.Context {
|
|
return context.WithValue(ctx, destroySessionKey, f)
|
|
}
|
|
|
|
// GetUser returns the authenticated user or panics.
|
|
func GetUser(ctx context.Context) *model.User {
|
|
if user, ok := ctx.Value(userKey).(*model.User); ok {
|
|
return user
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetDevice returns the authenticated device or nil.
|
|
func GetDevice(ctx context.Context) *model.Device {
|
|
if device, ok := ctx.Value(deviceKey).(*model.Device); ok {
|
|
return device
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetCreateSession gets the create session callback
|
|
func GetCreateSession(ctx context.Context) func(token string, age int) {
|
|
if f, ok := ctx.Value(createSessionKey).(func(string, int)); ok {
|
|
return f
|
|
}
|
|
panic("create session must exist")
|
|
}
|
|
|
|
// GetDestroySession gets the destroy session callback
|
|
func GetDestroySession(ctx context.Context) func() {
|
|
if f, ok := ctx.Value(destroySessionKey).(func()); ok {
|
|
return f
|
|
}
|
|
panic("create session must exist")
|
|
}
|