partial working

This commit is contained in:
jay brown
2025-04-11 16:46:05 -07:00
parent ba6f624299
commit 61accd4dd4
7 changed files with 100 additions and 88 deletions
+10 -7
View File
@@ -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 {
+11 -11
View File
@@ -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 {
+40 -40
View File
@@ -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())
}
+19 -19
View File
@@ -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
+3 -3
View File
@@ -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(
+17 -1
View File
@@ -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
-7
View File
@@ -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
}