Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
146 lines
4.7 KiB
Go
146 lines
4.7 KiB
Go
package mw
|
|
|
|
import (
|
|
"context"
|
|
"log"
|
|
"net/http"
|
|
"slices"
|
|
"strings"
|
|
|
|
"crussell/auth"
|
|
)
|
|
|
|
type contextKey string
|
|
|
|
const (
|
|
UserIDKey contextKey = "user_id"
|
|
UserRoleKey contextKey = "user_role"
|
|
JTIKey contextKey = "jti"
|
|
)
|
|
|
|
// RequireAuth middleware - validates JWT and adds user info to context
|
|
func RequireAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
|
|
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "missing or invalid authorization header"})
|
|
return
|
|
}
|
|
|
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
|
if err != nil {
|
|
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid token"})
|
|
return
|
|
}
|
|
|
|
// Add user info and JTI to context
|
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
|
ctx = context.WithValue(ctx, UserRoleKey, role)
|
|
ctx = context.WithValue(ctx, JTIKey, jti)
|
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
|
|
// OptionalAuth middleware - extracts user info if token present, otherwise passes through
|
|
func OptionalAuth(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
|
|
tokenString := strings.TrimPrefix(authHeader, "Bearer ")
|
|
|
|
userID, role, jti, err := auth.VerifyToken(tokenString, r.Context())
|
|
if err != nil {
|
|
log.Printf("OptionalAuth: invalid token ignored")
|
|
} else {
|
|
ctx := context.WithValue(r.Context(), UserIDKey, userID)
|
|
ctx = context.WithValue(ctx, UserRoleKey, role)
|
|
ctx = context.WithValue(ctx, JTIKey, jti)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RequireRole middleware - checks if user has required role(s)
|
|
func RequireRole(allowedRoles ...string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role, ok := r.Context().Value(UserRoleKey).(string)
|
|
if !ok {
|
|
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "unauthorized"})
|
|
return
|
|
}
|
|
|
|
// Check if user has one of the allowed roles
|
|
hasRole := slices.Contains(allowedRoles, role)
|
|
|
|
if !hasRole {
|
|
RespondJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// RequireVerified middleware - only allows verified_email and admin
|
|
func RequireVerified(next http.Handler) http.Handler {
|
|
return RequireRole("verified_email", "admin")(next)
|
|
}
|
|
|
|
// RequireNonGuest middleware - blocks guest-role tokens from money routes.
|
|
// Guests are created with account_role='guest' (account_type is NOT checked —
|
|
// real guests are seeded with account_type='email'), and they have no login
|
|
// flow, so a legitimate guest never holds a JWT. This is a defense-in-depth
|
|
// guard: any token whose role claim is 'guest' (forged/minted guest tokens or
|
|
// future changes) is refused. A missing role is treated as not-guest and passes
|
|
// through — RequireAuth guarantees the role is present when this runs, matching
|
|
// the trust model of the in-handler isVerifiedRole checks.
|
|
func RequireNonGuest(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
role, ok := r.Context().Value(UserRoleKey).(string)
|
|
if ok && role == "guest" {
|
|
RespondJSON(w, http.StatusForbidden, map[string]string{"error": "forbidden"})
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
|
|
// RequireAdmin middleware - only allows admin and validates the user ID is present
|
|
// in context, so handlers don't need to re-check. The user ID is always set by
|
|
// RequireAuth before this runs, but this adds a defensive safety net.
|
|
func RequireAdmin(next http.Handler) http.Handler {
|
|
return RequireRole("admin")(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
uid, ok := r.Context().Value(UserIDKey).(string)
|
|
if !ok || uid == "" {
|
|
RespondJSON(w, http.StatusUnauthorized, map[string]string{"error": "Authentication required"})
|
|
return
|
|
}
|
|
next.ServeHTTP(w, r)
|
|
}))
|
|
}
|
|
|
|
// Helper functions to get user info from context
|
|
func GetUserID(ctx context.Context) (string, bool) {
|
|
userID, ok := ctx.Value(UserIDKey).(string)
|
|
return userID, ok
|
|
}
|
|
|
|
func GetUserRole(ctx context.Context) (string, bool) {
|
|
role, ok := ctx.Value(UserRoleKey).(string)
|
|
return role, ok
|
|
}
|
|
|
|
func GetJTI(ctx context.Context) (string, bool) {
|
|
jti, ok := ctx.Value(JTIKey).(string)
|
|
return jti, ok
|
|
}
|