148 lines
3.6 KiB
Go
148 lines
3.6 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
const (
|
|
adminRole = "admin"
|
|
adminSessionCookie = "mesh_admin_session"
|
|
)
|
|
|
|
type adminUserDTO struct {
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
}
|
|
|
|
type sessionClaims struct {
|
|
UserID uint64 `json:"user_id"`
|
|
Username string `json:"username"`
|
|
Role string `json:"role"`
|
|
Expires int64 `json:"expires"`
|
|
}
|
|
|
|
type sessionManager struct {
|
|
secret []byte
|
|
secure bool
|
|
ttl time.Duration
|
|
}
|
|
|
|
func newSessionManager(cfg webAdminConfig) (*sessionManager, error) {
|
|
secret := strings.TrimSpace(cfg.SessionSecret)
|
|
if secret == "" {
|
|
generated := make([]byte, 32)
|
|
if _, err := rand.Read(generated); err != nil {
|
|
return nil, fmt.Errorf("generate admin session secret: %w", err)
|
|
}
|
|
return &sessionManager{secret: generated, secure: cfg.SessionSecure, ttl: 24 * time.Hour}, nil
|
|
}
|
|
return &sessionManager{secret: []byte(secret), secure: cfg.SessionSecure, ttl: 24 * time.Hour}, nil
|
|
}
|
|
|
|
func hashPassword(password string) (string, error) {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return string(hash), nil
|
|
}
|
|
|
|
func verifyPassword(hash, password string) bool {
|
|
return bcrypt.CompareHashAndPassword([]byte(hash), []byte(password)) == nil
|
|
}
|
|
|
|
func adminUserResponse(user userRecord) adminUserDTO {
|
|
return adminUserDTO{Username: user.Username, Role: user.Role}
|
|
}
|
|
|
|
func (sm *sessionManager) newCookie(user userRecord) (*http.Cookie, error) {
|
|
claims := sessionClaims{UserID: user.ID, Username: user.Username, Role: user.Role, Expires: time.Now().Add(sm.ttl).Unix()}
|
|
data, err := json.Marshal(claims)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload := base64.RawURLEncoding.EncodeToString(data)
|
|
signature := sm.sign(payload)
|
|
return &http.Cookie{
|
|
Name: adminSessionCookie,
|
|
Value: payload + "." + signature,
|
|
Path: "/",
|
|
MaxAge: int(sm.ttl.Seconds()),
|
|
HttpOnly: true,
|
|
Secure: sm.secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
}, nil
|
|
}
|
|
|
|
func (sm *sessionManager) clearCookie() *http.Cookie {
|
|
return &http.Cookie{
|
|
Name: adminSessionCookie,
|
|
Value: "",
|
|
Path: "/",
|
|
MaxAge: -1,
|
|
HttpOnly: true,
|
|
Secure: sm.secure,
|
|
SameSite: http.SameSiteLaxMode,
|
|
}
|
|
}
|
|
|
|
func (sm *sessionManager) claimsFromRequest(c *gin.Context) (*sessionClaims, error) {
|
|
cookie, err := c.Cookie(adminSessionCookie)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
parts := strings.Split(cookie, ".")
|
|
if len(parts) != 2 {
|
|
return nil, errors.New("invalid session")
|
|
}
|
|
if !hmac.Equal([]byte(parts[1]), []byte(sm.sign(parts[0]))) {
|
|
return nil, errors.New("invalid session signature")
|
|
}
|
|
data, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var claims sessionClaims
|
|
if err := json.Unmarshal(data, &claims); err != nil {
|
|
return nil, err
|
|
}
|
|
if claims.Expires <= time.Now().Unix() {
|
|
return nil, errors.New("session expired")
|
|
}
|
|
if claims.Role != adminRole {
|
|
return nil, errors.New("admin required")
|
|
}
|
|
return &claims, nil
|
|
}
|
|
|
|
func (sm *sessionManager) sign(payload string) string {
|
|
mac := hmac.New(sha256.New, sm.secret)
|
|
mac.Write([]byte(payload))
|
|
return base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func requireAdmin(sm *sessionManager) gin.HandlerFunc {
|
|
return func(c *gin.Context) {
|
|
claims, err := sm.claimsFromRequest(c)
|
|
if err != nil {
|
|
c.JSON(http.StatusUnauthorized, gin.H{"error": "admin login required"})
|
|
c.Abort()
|
|
return
|
|
}
|
|
c.Set("admin_claims", claims)
|
|
c.Next()
|
|
}
|
|
}
|