131 lines
3.3 KiB
Go
131 lines
3.3 KiB
Go
package auth
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
type Middleware struct {
|
|
jwtSecret []byte
|
|
db *sqlx.DB
|
|
}
|
|
|
|
func NewMiddleware(jwtSecret string, db *sqlx.DB) *Middleware {
|
|
return &Middleware{jwtSecret: []byte(jwtSecret), db: db}
|
|
}
|
|
|
|
func (m *Middleware) RequireAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
token := extractBearerToken(r)
|
|
if token == "" {
|
|
http.Error(w, `{"error":"missing authorization token"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
userID, err := m.verifyJWT(token)
|
|
if err != nil {
|
|
http.Error(w, `{"error":"invalid token"}`, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
ctx := ContextWithUserID(r.Context(), userID)
|
|
<<<<<<< HEAD
|
|
// Tenant resolution is handled by TenantResolver middleware for scoped routes.
|
|
// Tenant management routes handle their own access control.
|
|
||||||| 82878df
|
|
|
|
// Resolve tenant and role from user_tenants
|
|
var membership struct {
|
|
TenantID uuid.UUID `db:"tenant_id"`
|
|
Role string `db:"role"`
|
|
}
|
|
err = m.db.GetContext(r.Context(), &membership,
|
|
"SELECT tenant_id, role FROM user_tenants WHERE user_id = $1 LIMIT 1", userID)
|
|
if err != nil {
|
|
http.Error(w, "no tenant found for user", http.StatusForbidden)
|
|
return
|
|
}
|
|
ctx = ContextWithTenantID(ctx, membership.TenantID)
|
|
ctx = ContextWithUserRole(ctx, membership.Role)
|
|
|
|
=======
|
|
|
|
// Resolve tenant from user_tenants
|
|
var tenantID uuid.UUID
|
|
err = m.db.GetContext(r.Context(), &tenantID,
|
|
"SELECT tenant_id FROM user_tenants WHERE user_id = $1 LIMIT 1", userID)
|
|
if err != nil {
|
|
http.Error(w, "no tenant found for user", http.StatusForbidden)
|
|
return
|
|
}
|
|
ctx = ContextWithTenantID(ctx, tenantID)
|
|
|
|
// Capture IP and user-agent for audit logging
|
|
ip := r.Header.Get("X-Forwarded-For")
|
|
if ip == "" {
|
|
ip = r.RemoteAddr
|
|
}
|
|
ctx = ContextWithRequestInfo(ctx, ip, r.UserAgent())
|
|
|
|
>>>>>>> mai/knuth/p0-audit-trail-append
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
func (m *Middleware) verifyJWT(tokenStr string) (uuid.UUID, error) {
|
|
parsedToken, err := jwt.Parse(tokenStr, func(t *jwt.Token) (interface{}, error) {
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
|
}
|
|
return m.jwtSecret, nil
|
|
})
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("parsing JWT: %w", err)
|
|
}
|
|
|
|
if !parsedToken.Valid {
|
|
return uuid.Nil, fmt.Errorf("invalid JWT token")
|
|
}
|
|
|
|
claims, ok := parsedToken.Claims.(jwt.MapClaims)
|
|
if !ok {
|
|
return uuid.Nil, fmt.Errorf("extracting JWT claims")
|
|
}
|
|
|
|
if exp, ok := claims["exp"].(float64); ok {
|
|
if time.Now().Unix() > int64(exp) {
|
|
return uuid.Nil, fmt.Errorf("JWT token has expired")
|
|
}
|
|
}
|
|
|
|
sub, ok := claims["sub"].(string)
|
|
if !ok {
|
|
return uuid.Nil, fmt.Errorf("missing sub claim in JWT")
|
|
}
|
|
|
|
userID, err := uuid.Parse(sub)
|
|
if err != nil {
|
|
return uuid.Nil, fmt.Errorf("invalid user ID format: %w", err)
|
|
}
|
|
|
|
return userID, nil
|
|
}
|
|
|
|
func extractBearerToken(r *http.Request) string {
|
|
auth := r.Header.Get("Authorization")
|
|
if auth == "" {
|
|
return ""
|
|
}
|
|
parts := strings.SplitN(auth, " ", 2)
|
|
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
|
|
return ""
|
|
}
|
|
return parts[1]
|
|
}
|