From 61accd4dd4c5ef0be2bfb7b2ad04fb0d4133ab4c Mon Sep 17 00:00:00 2001 From: jay brown Date: Fri, 11 Apr 2025 16:46:05 -0700 Subject: [PATCH] partial working --- cmd/queryAPI/main.go | 17 ++++--- internal/cognitoauth/auth.go | 22 ++++---- internal/cognitoauth/handler.go | 80 +++++++++++++++--------------- internal/cognitoauth/middleware.go | 38 +++++++------- internal/cognitoauth/token.go | 6 +-- internal/server/api/listener.go | 18 ++++++- internal/serviceconfig/common.go | 7 --- 7 files changed, 100 insertions(+), 88 deletions(-) diff --git a/cmd/queryAPI/main.go b/cmd/queryAPI/main.go index b534a9ab..3a825688 100644 --- a/cmd/queryAPI/main.go +++ b/cmd/queryAPI/main.go @@ -105,13 +105,16 @@ func main() { os.Exit(1) } - // note initialize cognito config here - // Initialize the rbac config here since only the API service needs it - //errorGettingAuthProvider := rbac.InitializeAuthProvider(&cfg.AuthConfig) - //if errorGettingAuthProvider != nil { - // slog.Error(errorGettingAuthProvider.Error()) - // os.Exit(1) - //} + // Authentication specific config. + // This may move to another function after refactoring is done. + // replace the path here with value from ENV + baseUrl := "http://localhost:8080" + errorInitializingAuthConfig := cfg.InitializeAuthConfig(baseUrl, cfg.GetLogger()) + // join initErr with errorInitializingAuthConfig + if errorInitializingAuthConfig != nil { + slog.Error(errorInitializingAuthConfig.Error()) + os.Exit(1) + } server, err := api.New(ctx, cfg) if err != nil { diff --git a/internal/cognitoauth/auth.go b/internal/cognitoauth/auth.go index f835da71..12ee2cf7 100644 --- a/internal/cognitoauth/auth.go +++ b/internal/cognitoauth/auth.go @@ -14,15 +14,15 @@ import ( ) // RegisterRoutes registers all authentication-related routes to the Echo engine -func RegisterRoutes(e *echo.Echo, config *auth.CognitoConfig) { +func RegisterRoutes(e *echo.Echo, config auth.ConfigProvider) { // Register the login route - the middleware will handle this - e.GET(config.AuthLoginPath, func(c echo.Context) error { + e.GET(config.GetAuthLoginPath(), func(c echo.Context) error { // This is handled by the middleware return nil }) // Register callback route - the middleware will handle this - e.GET(config.AuthCallbackPath, func(c echo.Context) error { + e.GET(config.GetAuthCallbackPath(), func(c echo.Context) error { // This handler will only be called for requests that don't have a code parameter // (those are handled directly in the middleware) @@ -39,12 +39,12 @@ func RegisterRoutes(e *echo.Echo, config *auth.CognitoConfig) { }) // Register logout route - e.GET(config.AuthLogoutPath, func(c echo.Context) error { + e.GET(config.GetAuthLogoutPath(), func(c echo.Context) error { return LogoutHandler(c, config) }) // Default home page handler if requested - e.GET(config.AuthHomePath, func(c echo.Context) error { + e.GET(config.GetAuthHomePath(), func(c echo.Context) error { return HomeHandler(c, config) }) @@ -56,20 +56,20 @@ func RegisterRoutes(e *echo.Echo, config *auth.CognitoConfig) { } // HomeHandler implements a simple home page that shows auth status and available endpoints -func HomeHandler(c echo.Context, config *auth.CognitoConfig) error { +func HomeHandler(c echo.Context, config auth.ConfigProvider) error { // Create a list of all registered routes var endpoints []string // Add the auth routes endpoints = append(endpoints, []string{ - config.AuthLoginPath, - config.AuthCallbackPath, - config.AuthHomePath, - config.AuthLogoutPath, + config.GetAuthLoginPath(), + config.GetAuthCallbackPath(), + config.GetAuthHomePath(), + config.GetAuthLogoutPath(), }...) // Add routes from the permission map - for route := range config.AuthRoutePermissions { + for route := range config.GetAuthRoutePermissions() { // Skip routes that are already in the list alreadyAdded := false for _, endpoint := range endpoints { diff --git a/internal/cognitoauth/handler.go b/internal/cognitoauth/handler.go index b65ad63a..a37b7cb1 100644 --- a/internal/cognitoauth/handler.go +++ b/internal/cognitoauth/handler.go @@ -18,18 +18,18 @@ import ( // initiateLoginWithPKCE generates PKCE code challenge and redirects to Cognito // This function starts the OAuth authorization flow with PKCE -func initiateLoginWithPKCE(c echo.Context, config *auth.CognitoConfig) error { +func initiateLoginWithPKCE(c echo.Context, config auth.ConfigProvider) error { // Generate random state parameter to prevent CSRF state, err := generateRandomString(32) if err != nil { - config.AuthLogger.Error("Failed to generate state parameter", "error", err) + config.GetAuthLogger().Error("Failed to generate state parameter", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Authentication initialization failed"}) } // Generate code verifier (random string between 43-128 chars) codeVerifier, err := generateRandomString(64) if err != nil { - config.AuthLogger.Error("Failed to generate code verifier", "error", err) + config.GetAuthLogger().Error("Failed to generate code verifier", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Authentication initialization failed"}) } @@ -37,26 +37,26 @@ func initiateLoginWithPKCE(c echo.Context, config *auth.CognitoConfig) error { codeChallenge := createCodeChallenge(codeVerifier) // Store in session for later verification (with expiration time) - storePKCESession(state, codeVerifier, config.AuthLogger) + storePKCESession(state, codeVerifier, config.GetAuthLogger()) // Build authorization URL with PKCE parameters params := url.Values{} - params.Set("client_id", config.AuthClientID) + params.Set("client_id", config.GetAuthClientID()) params.Set("response_type", "code") - params.Set("redirect_uri", config.AuthRedirectURI) + params.Set("redirect_uri", config.GetAuthRedirectURI()) params.Set("scope", "openid email profile") params.Set("state", state) params.Set("code_challenge", codeChallenge) params.Set("code_challenge_method", "S256") - authURL := fmt.Sprintf("%s?%s", config.AuthURL, params.Encode()) + authURL := fmt.Sprintf("%s?%s", config.GetAuthURL(), params.Encode()) if os.Getenv("DEBUG") == "true" { - config.AuthLogger.Debug("Initiating login with PKCE", + config.GetAuthLogger().Debug("Initiating login with PKCE", "code_verifier", codeVerifier, "code_challenge", codeChallenge, "state", state, - "redirect_uri", config.AuthRedirectURI) + "redirect_uri", config.GetAuthRedirectURI()) } // Redirect user to Cognito login page @@ -66,20 +66,20 @@ func initiateLoginWithPKCE(c echo.Context, config *auth.CognitoConfig) error { // exchangeCodeForTokensWithPKCE exchanges the authorization code for tokens using PKCE // Called during the OAuth callback flow to get tokens from the authorization code // Makes an HTTP request to Cognito's token endpoint to perform this exchange -func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.CognitoConfig) (*TokenResponse, error) { +func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.ConfigProvider) (*TokenResponse, error) { data := url.Values{} data.Set("grant_type", "authorization_code") - data.Set("client_id", config.AuthClientID) + data.Set("client_id", config.GetAuthClientID()) data.Set("code", authCode) - data.Set("redirect_uri", config.AuthRedirectURI) + data.Set("redirect_uri", config.GetAuthRedirectURI()) data.Set("code_verifier", codeVerifier) // Include code verifier for PKCE - config.AuthLogger.Debug("Token request details", - "url", config.AuthTokenURL, - "client_id", config.AuthClientID, - "redirect_uri", config.AuthRedirectURI) + config.GetAuthLogger().Debug("Token request details", + "url", config.GetAuthTokenURL(), + "client_id", config.GetAuthClientID(), + "redirect_uri", config.GetAuthRedirectURI()) - req, err := http.NewRequest("POST", config.AuthTokenURL, strings.NewReader(data.Encode())) + req, err := http.NewRequest("POST", config.GetAuthTokenURL(), strings.NewReader(data.Encode())) if err != nil { return nil, fmt.Errorf("failed to create token request: %w", err) } @@ -87,11 +87,11 @@ func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.Co req.Header.Add("Content-Type", "application/x-www-form-urlencoded") // Add Authorization header if client secret is provided - if config.AuthClientSecret != "" { - req.SetBasicAuth(config.AuthClientID, config.AuthClientSecret) - config.AuthLogger.Debug("Using Basic Auth authentication") + if config.GetAuthClientSecret() != "" { + req.SetBasicAuth(config.GetAuthClientID(), config.GetAuthClientSecret()) + config.GetAuthLogger().Debug("Using Basic Auth authentication") } else { - config.AuthLogger.Debug("Using public client authentication (no secret)") + config.GetAuthLogger().Debug("Using public client authentication (no secret)") } client := &http.Client{Timeout: 10 * time.Second} @@ -121,7 +121,7 @@ func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.Co // handleOAuthCallback handles the OAuth 2.0 authorization code flow callback with PKCE // Called when Cognito redirects back to our application with an authorization code // Exchanges the code for tokens, verifies them, and redirects to home page with token in cookie -func handleOAuthCallback(c echo.Context, config *auth.CognitoConfig) error { +func handleOAuthCallback(c echo.Context, config auth.ConfigProvider) error { // Extract the authorization code and state code := c.QueryParam("code") if code == "" { @@ -144,52 +144,52 @@ func handleOAuthCallback(c echo.Context, config *auth.CognitoConfig) error { } // Retrieve stored code verifier - codeVerifier, err := getCodeVerifier(state, config.AuthLogger) + codeVerifier, err := getCodeVerifier(state, config.GetAuthLogger()) if err != nil { - config.AuthLogger.Error("Failed to retrieve code verifier", "error", err, "state", state) + config.GetAuthLogger().Error("Failed to retrieve code verifier", "error", err, "state", state) return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid or expired session"}) } // Exchange the code for tokens using PKCE - tokens, err := exchangeCodeForTokensWithPKCE(code, codeVerifier, *config) + tokens, err := exchangeCodeForTokensWithPKCE(code, codeVerifier, config) if err != nil { - config.AuthLogger.Error("Failed to exchange code for tokens", "error", err) + config.GetAuthLogger().Error("Failed to exchange code for tokens", "error", err) return c.JSON(http.StatusUnauthorized, map[string]string{"error": err.Error()}) } // Get the JWKS for token verification - keySet, err := GetJWKS(config.AuthJwksURL, config.AuthLogger) + keySet, err := GetJWKS(config.GetAuthJwksURL(), config.GetAuthLogger()) if err != nil { - config.AuthLogger.Error("Failed to fetch JWKS", "error", err) + config.GetAuthLogger().Error("Failed to fetch JWKS", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to verify token"}) } // Verify ID token - idTokenClaims, err := verifyToken(tokens.IDToken, keySet, *config) + idTokenClaims, err := verifyToken(tokens.IDToken, keySet, config) if err != nil { - config.AuthLogger.Error("Failed to verify ID token", "error", err) + config.GetAuthLogger().Error("Failed to verify ID token", "error", err) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) } // Extract user groups userGroups, err := GetUserGroups(idTokenClaims) if err != nil { - config.AuthLogger.Warn("Failed to extract user groups", "error", err) + config.GetAuthLogger().Warn("Failed to extract user groups", "error", err) userGroups = []string{} // Initialize as empty array } // Debug logging for tokens if os.Getenv("DEBUG") == "true" { // Log decoded token for debugging - config.AuthLogger.Info("ID Token Claims", "claims", idTokenClaims) - config.AuthLogger.Info("Raw ID Token", "token", tokens.IDToken) - config.AuthLogger.Info("Raw Access Token", "token", tokens.AccessToken) + config.GetAuthLogger().Info("ID Token Claims", "claims", idTokenClaims) + config.GetAuthLogger().Info("Raw ID Token", "token", tokens.IDToken) + config.GetAuthLogger().Info("Raw Access Token", "token", tokens.AccessToken) } // Check authorization - authorized, requiredGroups := checkPermissions(config.AuthCallbackPath, userGroups, config.AuthRoutePermissions, config.AuthLogger) + authorized, requiredGroups := checkPermissions(config.GetAuthCallbackPath(), userGroups, config.GetAuthRoutePermissions(), config.GetAuthLogger()) if !authorized { - config.AuthLogger.Warn("OAuth callback - access denied", + config.GetAuthLogger().Warn("OAuth callback - access denied", "user", idTokenClaims["cognito:username"], "groups", userGroups, "required", requiredGroups) @@ -224,14 +224,14 @@ func handleOAuthCallback(c echo.Context, config *auth.CognitoConfig) error { // Extract username for display purposes (optional) username, _ := idTokenClaims["cognito:username"].(string) - config.AuthLogger.Info("User authenticated successfully", "username", username, "groups", userGroups) + config.GetAuthLogger().Info("User authenticated successfully", "username", username, "groups", userGroups) // Redirect to home page - return c.Redirect(http.StatusFound, config.AuthHomePath) + return c.Redirect(http.StatusFound, config.GetAuthHomePath()) } // LogoutHandler handles the logout process by clearing the auth cookie -func LogoutHandler(c echo.Context, config *auth.CognitoConfig) error { +func LogoutHandler(c echo.Context, config auth.ConfigProvider) error { // Clear the token cookie cookie := new(http.Cookie) cookie.Name = "auth_token" @@ -242,5 +242,5 @@ func LogoutHandler(c echo.Context, config *auth.CognitoConfig) error { c.SetCookie(cookie) // Redirect to home page - return c.Redirect(http.StatusFound, config.AuthHomePath) + return c.Redirect(http.StatusFound, config.GetAuthHomePath()) } diff --git a/internal/cognitoauth/middleware.go b/internal/cognitoauth/middleware.go index 1bbb1fc1..3c4dc277 100644 --- a/internal/cognitoauth/middleware.go +++ b/internal/cognitoauth/middleware.go @@ -13,24 +13,24 @@ import ( // TokenValidationMiddleware verifies JWT tokens efficiently // This is the primary middleware that handles both authentication and authorization // Applied to all routes to enforce security requirements -func TokenValidationMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { +func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { requestPath := c.Request().URL.Path - config.AuthLogger.Debug("Processing request", "path", requestPath, "method", c.Request().Method) + config.GetAuthLogger().Debug("Processing request", "path", requestPath, "method", c.Request().Method) // Skip token validation for specified public paths - publicPaths := []string{config.AuthHomePath, config.AuthLogoutPath} + publicPaths := []string{config.GetAuthHomePath(), config.GetAuthLogoutPath()} for _, path := range publicPaths { if requestPath == path { - config.AuthLogger.Debug("Skipping token validation for public path", "path", requestPath) + config.GetAuthLogger().Debug("Skipping token validation for public path", "path", requestPath) return next(c) } } // Special case for the OAuth callback path - if requestPath == config.AuthCallbackPath && (c.QueryParam("code") != "" || c.QueryParam("error") != "") { - config.AuthLogger.Debug("Handling OAuth callback", + if requestPath == config.GetAuthCallbackPath() && (c.QueryParam("code") != "" || c.QueryParam("error") != "") { + config.GetAuthLogger().Debug("Handling OAuth callback", "has_code", c.QueryParam("code") != "", "has_error", c.QueryParam("error") != "") // Skip token validation for OAuth callback - it will be handled by the callback handler @@ -38,14 +38,14 @@ func TokenValidationMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { } // Initialize login flow if this is a login request - if requestPath == config.AuthLoginPath { + if requestPath == config.GetAuthLoginPath() { return initiateLoginWithPKCE(c, config) } // Get authorization header authHeader := c.Request().Header.Get("Authorization") if authHeader == "" { - config.AuthLogger.Warn("No authorization header provided") + config.GetAuthLogger().Warn("No authorization header provided") return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authorization required"}) } @@ -56,23 +56,23 @@ func TokenValidationMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { } // Get JWKS (cached if possible) - keySet, err := GetJWKS(config.AuthJwksURL, config.AuthLogger) + keySet, err := GetJWKS(config.GetAuthJwksURL(), config.GetAuthLogger()) if err != nil { - config.AuthLogger.Error("Failed to get JWKS", "error", err) + config.GetAuthLogger().Error("Failed to get JWKS", "error", err) return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to verify token"}) } // Verify token - claims, err := verifyToken(tokenStr, keySet, *config) + claims, err := verifyToken(tokenStr, keySet, config) if err != nil { - config.AuthLogger.Warn("Token verification failed", "error", err) + config.GetAuthLogger().Warn("Token verification failed", "error", err) return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"}) } // Extract user groups userGroups, err := GetUserGroups(claims) if err != nil { - config.AuthLogger.Warn("Failed to extract user groups", "error", err) + config.GetAuthLogger().Warn("Failed to extract user groups", "error", err) userGroups = []string{} // Empty array if no groups found } @@ -85,9 +85,9 @@ func TokenValidationMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { c.Set("user_info", userInfo) // Check authorization - authorized, requiredGroups := checkPermissions(requestPath, userGroups, config.AuthRoutePermissions, config.AuthLogger) + authorized, requiredGroups := checkPermissions(requestPath, userGroups, config.GetAuthRoutePermissions(), config.GetAuthLogger()) if !authorized { - config.AuthLogger.Warn("Access denied", + config.GetAuthLogger().Warn("Access denied", "path", requestPath, "user", claims["cognito:username"], "groups", userGroups, @@ -109,16 +109,16 @@ func TokenValidationMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { } // JWTAuthMiddleware checks for JWT token in cookies and adds it to the Authorization header -func JWTAuthMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { +func JWTAuthMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc { return func(next echo.HandlerFunc) echo.HandlerFunc { return func(c echo.Context) error { // Skip for login and callback paths - if c.Path() == config.AuthLoginPath || c.Path() == config.AuthCallbackPath { + if c.Path() == config.GetAuthLoginPath() || c.Path() == config.GetAuthCallbackPath() { return next(c) } // Skip for home and logout as they should be accessible without auth - if c.Path() == config.AuthHomePath || c.Path() == config.AuthLogoutPath { + if c.Path() == config.GetAuthHomePath() || c.Path() == config.GetAuthLogoutPath() { return next(c) } @@ -133,7 +133,7 @@ func JWTAuthMiddleware(config *auth.CognitoConfig) echo.MiddlewareFunc { tokenCookie, err := c.Cookie("auth_token") if err != nil || tokenCookie.Value == "" { // No token cookie found - return c.Redirect(http.StatusFound, config.AuthLoginPath) + return c.Redirect(http.StatusFound, config.GetAuthLoginPath()) } // Add token to Authorization header diff --git a/internal/cognitoauth/token.go b/internal/cognitoauth/token.go index c6cfc45b..3b37e924 100644 --- a/internal/cognitoauth/token.go +++ b/internal/cognitoauth/token.go @@ -19,9 +19,9 @@ import ( // verifyToken verifies the JWT token and returns its claims // Used during both OAuth callback and subsequent API requests with bearer token // Verifies signature, expiration, issuer, and other JWT claims -func verifyToken(tokenString string, keySet jwk.Set, config auth.CognitoConfig) (map[string]interface{}, error) { - region := config.AuthRegion - issuer := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", region, config.AuthUserPoolID) +func verifyToken(tokenString string, keySet jwk.Set, config auth.ConfigProvider) (map[string]interface{}, error) { + region := config.GetAuthRegion() + issuer := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", region, config.GetAuthUserPoolID()) // Verify the token with the keySet verifiedToken, err := jwt.Parse( diff --git a/internal/server/api/listener.go b/internal/server/api/listener.go index 993463de..6045d7ae 100644 --- a/internal/server/api/listener.go +++ b/internal/server/api/listener.go @@ -9,6 +9,8 @@ import ( "strconv" "time" + "queryorchestration/internal/cognitoauth" + "queryorchestration/internal/server" "github.com/getkin/kin-openapi/openapi3" @@ -170,6 +172,21 @@ func New(ctx context.Context, cfg Config) (*Server, error) { } e := echo.New() + cfg.SetRouter(e) + + // auth start - may move to separate rbac function + routePermissions := map[string][]string{ + "/users": {"exporters", "uploaders", "querybuilders"}, + "/users/:id": {"exporters", "querybuilders"}, + "/orders": {"exporters"}, + "/reports/sales": {"exporters", "uploaders", "querybuilders"}, + "/settings": {"exporters"}, + "/api/inventory/update": {"exporters", "uploaders"}, + } + cfg.SetAuthRoutePermissions(routePermissions) + + cognitoauth.RegisterRoutes(cfg.GetRouter(), cfg) + // auth end e.Use(echoprometheus.NewMiddleware("echo")) e.GET("/metrics", echoprometheus.NewHandler()) @@ -181,7 +198,6 @@ func New(ctx context.Context, cfg Config) (*Server, error) { // Add debug middleware early to catch all requests e.Use(getDebugMiddleware(cfg.GetLogger())) - cfg.SetRouter(e) opnapi, err := cfg.RegisterHandlers() if err != nil { return nil, err diff --git a/internal/serviceconfig/common.go b/internal/serviceconfig/common.go index aaf9a91c..7ff7f457 100644 --- a/internal/serviceconfig/common.go +++ b/internal/serviceconfig/common.go @@ -159,13 +159,6 @@ func initializeConfigPrivate(cfg ConfigProvider) error { // Reinitialize with environment level cfg.SetDefaultLogger() - // replace the path here with value from ENV - baseUrl := "http://localhost:8080" - errorInitializingAuthConfig := cfg.InitializeAuthConfig(baseUrl, cfg.GetLogger()) - // join initErr with errorInitializingAuthConfig - if errorInitializingAuthConfig != nil { - initErr = fmt.Errorf("%w; %v", initErr, errorInitializingAuthConfig) - } return initErr }