Files
query-orchestration/internal/cognitoauth/middleware.go
T

189 lines
6.8 KiB
Go
Raw Normal View History

2025-04-08 16:27:06 -07:00
package cognitoauth
import (
"net/http"
2025-04-15 12:04:54 -07:00
"time"
2025-04-11 11:13:07 -07:00
"queryorchestration/internal/serviceconfig/auth"
2025-04-08 16:27:06 -07:00
"strings"
"github.com/labstack/echo/v4"
)
2025-04-24 13:28:49 -07:00
// TokenValidationMiddleware verifies JWT tokens and enforces authentication and authorization.
//
// This middleware performs several critical security functions:
// - Validates JWT tokens and their signatures against the JWKS from the identity provider
// - Extracts and validates claims from the token
// - Extracts user groups and stores them in the context
// - Checks authorization based on the user's group memberships
// - Enforces route-specific permissions
//
// The middleware skips authentication for public paths and handles special cases
// like OAuth callbacks and login initiation. For protected routes, it enforces
// both authentication (valid token) and authorization (correct group membership).
//
// Parameters:
// - config: A ConfigProvider implementation that supplies all necessary auth configuration
//
// Returns:
// - echo.MiddlewareFunc: An Echo middleware function that can be added to the middleware chain
2025-04-11 16:46:05 -07:00
func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
2025-04-08 16:27:06 -07:00
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
requestPath := c.Request().URL.Path
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Debug("Processing request", "path", requestPath, "method", c.Request().Method)
2025-04-08 16:27:06 -07:00
// Skip token validation for specified public paths
2025-04-11 16:46:05 -07:00
publicPaths := []string{config.GetAuthHomePath(), config.GetAuthLogoutPath()}
2025-04-08 16:27:06 -07:00
for _, path := range publicPaths {
if requestPath == path {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Debug("Skipping token validation for public path", "path", requestPath)
2025-04-08 16:27:06 -07:00
return next(c)
}
}
// Special case for the OAuth callback path
2025-04-11 16:46:05 -07:00
if requestPath == config.GetAuthCallbackPath() && (c.QueryParam("code") != "" || c.QueryParam("error") != "") {
config.GetAuthLogger().Debug("Handling OAuth callback",
2025-04-08 16:27:06 -07:00
"has_code", c.QueryParam("code") != "",
"has_error", c.QueryParam("error") != "")
// Skip token validation for OAuth callback - it will be handled by the callback handler
return handleOAuthCallback(c, config)
}
// Initialize login flow if this is a login request
2025-04-11 16:46:05 -07:00
if requestPath == config.GetAuthLoginPath() {
2025-04-08 16:27:06 -07:00
return initiateLoginWithPKCE(c, config)
}
// Get authorization header
authHeader := c.Request().Header.Get("Authorization")
if authHeader == "" {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Warn("No authorization header provided")
2025-04-08 16:27:06 -07:00
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authorization required"})
}
// Extract token
tokenStr := authHeader
if strings.HasPrefix(authHeader, "Bearer ") {
tokenStr = authHeader[7:]
}
// Get JWKS (cached if possible)
2025-04-11 16:46:05 -07:00
keySet, err := GetJWKS(config.GetAuthJwksURL(), config.GetAuthLogger())
2025-04-08 16:27:06 -07:00
if err != nil {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Error("Failed to get JWKS", "error", err)
2025-04-08 16:27:06 -07:00
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to verify token"})
}
// Verify token
2025-04-11 16:46:05 -07:00
claims, err := verifyToken(tokenStr, keySet, config)
2025-04-08 16:27:06 -07:00
if err != nil {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Warn("Token verification failed", "error", err)
2025-04-08 16:27:06 -07:00
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"})
}
// Extract user groups
userGroups, err := GetUserGroups(claims)
if err != nil {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Warn("Failed to extract user groups", "error", err)
2025-04-08 16:27:06 -07:00
userGroups = []string{} // Empty array if no groups found
}
// Store in context
c.Set("user_claims", claims)
c.Set("user_groups", userGroups)
// Create and store user info in context
userInfo := ExtractUserInfo(claims)
c.Set("user_info", userInfo)
// Check authorization
2025-04-11 16:46:05 -07:00
authorized, requiredGroups := checkPermissions(requestPath, userGroups, config.GetAuthRoutePermissions(), config.GetAuthLogger())
2025-04-08 16:27:06 -07:00
if !authorized {
2025-04-11 16:46:05 -07:00
config.GetAuthLogger().Warn("Access denied",
2025-04-08 16:27:06 -07:00
"path", requestPath,
"user", claims["cognito:username"],
"groups", userGroups,
"required", requiredGroups)
return c.JSON(http.StatusForbidden, map[string]interface{}{
"error": "Insufficient permissions",
"message": "User doesn't have the required group membership",
"username": claims["cognito:username"],
"groups": userGroups,
"required_groups": requiredGroups,
})
}
// User is authenticated and authorized
return next(c)
}
}
}
2025-04-24 13:28:49 -07:00
// JWTAuthMiddleware extracts JWT tokens from cookies and adds them to the Authorization header.
//
// This middleware serves as a bridge between cookie-based and header-based authentication:
// - Checks for auth_token cookie and transfers it to the Authorization header
// - Skips processing for login and callback paths that handle authentication
// - Manages cookie deletion during logout operations
// - Redirects unauthenticated requests to the login path
//
// This middleware should be registered before TokenValidationMiddleware in the middleware
// chain to ensure the JWT token is properly placed in the Authorization header before
// validation occurs.
//
// Parameters:
// - config: A ConfigProvider implementation that supplies auth configuration values
//
// Returns:
// - echo.MiddlewareFunc: An Echo middleware function that can be added to the middleware chain
2025-04-11 16:46:05 -07:00
func JWTAuthMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
2025-04-08 16:27:06 -07:00
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
// Skip for login and callback paths
2025-04-11 16:46:05 -07:00
if c.Path() == config.GetAuthLoginPath() || c.Path() == config.GetAuthCallbackPath() {
2025-04-08 16:27:06 -07:00
return next(c)
}
2025-04-15 12:04:54 -07:00
// if logout then remove the auth cookie
if c.Path() == config.GetAuthLogoutPath() {
cookie := new(http.Cookie)
cookie.Name = "auth_token"
cookie.Value = ""
cookie.Path = "/"
cookie.Expires = time.Now().Add(-1 * time.Hour) // Set expiry in the past
cookie.HttpOnly = true
c.SetCookie(cookie)
}
2025-04-08 16:27:06 -07:00
// Skip for home and logout as they should be accessible without auth
2025-04-11 16:46:05 -07:00
if c.Path() == config.GetAuthHomePath() || c.Path() == config.GetAuthLogoutPath() {
2025-04-08 16:27:06 -07:00
return next(c)
}
// Check if Authorization header is already set
authHeader := c.Request().Header.Get("Authorization")
if authHeader != "" && strings.HasPrefix(authHeader, "Bearer ") {
// Authorization header is already set, proceed to the next handler
return next(c)
}
// Try to get token from cookie
tokenCookie, err := c.Cookie("auth_token")
if err != nil || tokenCookie.Value == "" {
// No token cookie found
2025-04-11 16:46:05 -07:00
return c.Redirect(http.StatusFound, config.GetAuthLoginPath())
2025-04-08 16:27:06 -07:00
}
// Add token to Authorization header
c.Request().Header.Set("Authorization", "Bearer "+tokenCookie.Value)
return next(c)
}
}
}