Cybersecurity-Projects/PROJECTS/advanced/monitor-the-situation-dashb.../backend/internal/middleware/auth.go

190 lines
4.1 KiB
Go

// AngelaMos | 2026
// auth.go
package middleware
import (
"context"
"errors"
"net/http"
"strings"
"github.com/carterperez-dev/monitor-the-situation/backend/internal/core"
)
const (
UserIDKey contextKey = "user_id"
UserRoleKey contextKey = "user_role"
UserTierKey contextKey = "user_tier"
ClaimsKey contextKey = "jwt_claims"
)
type TokenVerifier interface {
VerifyAccessToken(
ctx context.Context,
token string,
) (*AccessTokenClaims, error)
}
type AccessTokenClaims struct {
UserID string
Role string
Tier string
TokenVersion int
}
func Authenticator(verifier TokenVerifier) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := ExtractToken(r)
if token == "" {
core.JSONError(
w,
core.UnauthorizedError("missing authorization token"),
)
return
}
claims, err := verifier.VerifyAccessToken(r.Context(), token)
if err != nil {
handleAuthError(w, err)
return
}
ctx := r.Context()
ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
ctx = context.WithValue(ctx, UserTierKey, claims.Tier)
ctx = context.WithValue(ctx, ClaimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
func OptionalAuth(verifier TokenVerifier) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := ExtractToken(r)
if token != "" {
claims, err := verifier.VerifyAccessToken(r.Context(), token)
if err == nil {
ctx := r.Context()
ctx = context.WithValue(ctx, UserIDKey, claims.UserID)
ctx = context.WithValue(ctx, UserRoleKey, claims.Role)
ctx = context.WithValue(ctx, UserTierKey, claims.Tier)
ctx = context.WithValue(ctx, ClaimsKey, claims)
r = r.WithContext(ctx)
}
}
next.ServeHTTP(w, r)
})
}
}
func RequireRole(roles ...string) func(http.Handler) http.Handler {
roleSet := make(map[string]struct{}, len(roles))
for _, role := range roles {
roleSet[role] = struct{}{}
}
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userRole := GetUserRole(r.Context())
if userRole == "" {
core.JSONError(
w,
core.UnauthorizedError("authentication required"),
)
return
}
if _, ok := roleSet[userRole]; !ok {
core.JSONError(
w,
core.ForbiddenError("insufficient permissions"),
)
return
}
next.ServeHTTP(w, r)
})
}
}
func RequireAdmin(next http.Handler) http.Handler {
return RequireRole("admin")(next)
}
func ExtractToken(r *http.Request) string {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return ""
}
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
return ""
}
return strings.TrimSpace(parts[1])
}
func handleAuthError(w http.ResponseWriter, err error) {
if core.IsAppError(err) {
core.JSONError(w, err)
return
}
switch {
case errors.Is(err, core.ErrTokenExpired):
core.JSONError(w, core.TokenExpiredError())
case errors.Is(err, core.ErrTokenRevoked):
core.JSONError(w, core.TokenRevokedError())
case errors.Is(err, core.ErrTokenInvalid):
core.JSONError(w, core.TokenInvalidError())
default:
core.JSONError(w, core.TokenInvalidError())
}
}
func GetUserID(ctx context.Context) string {
if id, ok := ctx.Value(UserIDKey).(string); ok {
return id
}
return ""
}
func GetUserRole(ctx context.Context) string {
if role, ok := ctx.Value(UserRoleKey).(string); ok {
return role
}
return ""
}
func GetUserTier(ctx context.Context) string {
if tier, ok := ctx.Value(UserTierKey).(string); ok {
return tier
}
return ""
}
func GetClaims(ctx context.Context) *AccessTokenClaims {
if claims, ok := ctx.Value(ClaimsKey).(*AccessTokenClaims); ok {
return claims
}
return nil
}
func IsAuthenticated(ctx context.Context) bool {
return GetUserID(ctx) != ""
}
func IsAdmin(ctx context.Context) bool {
return GetUserRole(ctx) == "admin"
}