refresh token test
This commit is contained in:
@@ -1,3 +1,25 @@
|
|||||||
|
// Package cognitotest provides end-to-end integration tests for the Cognito authentication system.
|
||||||
|
//
|
||||||
|
// This package contains comprehensive tests that validate the complete authentication flow,
|
||||||
|
// including the critical auto-refresh token functionality. The tests use real browser automation
|
||||||
|
// via chromedp to simulate actual user interactions and test the full authentication lifecycle.
|
||||||
|
//
|
||||||
|
// Key Features Tested:
|
||||||
|
// - Full OAuth/PKCE authentication flow with AWS Cognito
|
||||||
|
// - Automatic token refresh when tokens expire
|
||||||
|
// - Cookie-based token storage and management
|
||||||
|
// - Integration with Permit.io for authorization
|
||||||
|
// - Real browser interaction using Chrome/Chromium
|
||||||
|
// - Natural token expiration testing (5-minute tokens)
|
||||||
|
//
|
||||||
|
// Test Requirements:
|
||||||
|
// - Valid AWS Cognito configuration
|
||||||
|
// - Test user credentials
|
||||||
|
// - Chrome/Chromium browser available
|
||||||
|
// - ENABLE_REFRESH_TESTS=true environment variable
|
||||||
|
//
|
||||||
|
// The tests are designed to catch regressions in the authentication system and ensure
|
||||||
|
// that users can maintain authenticated sessions without manual re-authentication.
|
||||||
package cognitotest
|
package cognitotest
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -24,7 +46,34 @@ import (
|
|||||||
"github.com/chromedp/chromedp"
|
"github.com/chromedp/chromedp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setupEnvironmentForRefresh ensures all required environment variables are set for refresh testing
|
// setupEnvironmentForRefresh ensures all required environment variables are set for refresh testing.
|
||||||
|
//
|
||||||
|
// This function validates that all necessary Cognito configuration and test credentials
|
||||||
|
// are available in the environment. It's a prerequisite step that must pass before any
|
||||||
|
// authentication testing can begin.
|
||||||
|
//
|
||||||
|
// Required Environment Variables:
|
||||||
|
// - USER: Test user email/username for Cognito authentication
|
||||||
|
// - PASSWORD: Test user password for Cognito authentication
|
||||||
|
// - COGNITO_DOMAIN: AWS Cognito hosted UI domain URL
|
||||||
|
// - AWS_REGION: AWS region where Cognito user pool is located
|
||||||
|
// - COGNITO_CLIENT_SECRET: OAuth client secret for Cognito app client
|
||||||
|
// - COGNITO_USER_POOL_ID: Cognito user pool identifier
|
||||||
|
// - COGNITO_CLIENT_ID: Cognito app client identifier
|
||||||
|
// - HEADLESS: Optional, controls if browser runs in headless mode (defaults to "true")
|
||||||
|
// - DEBUG: Set to "false" for cleaner test output
|
||||||
|
//
|
||||||
|
// The function will fail the test immediately if any required variable is missing,
|
||||||
|
// ensuring tests don't run with incomplete configuration.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - t: Testing context for logging and test control
|
||||||
|
//
|
||||||
|
// Behavior:
|
||||||
|
// - Validates all required environment variables are present
|
||||||
|
// - Sets HEADLESS to "true" if not specified
|
||||||
|
// - Calls t.Fatalf() if any required variable is missing
|
||||||
|
// - Re-exports all variables to ensure they're available to subprocesses
|
||||||
func setupEnvironmentForRefresh(t *testing.T) {
|
func setupEnvironmentForRefresh(t *testing.T) {
|
||||||
// Define all required environment variables with test values
|
// Define all required environment variables with test values
|
||||||
envVars := map[string]string{
|
envVars := map[string]string{
|
||||||
@@ -54,7 +103,42 @@ func setupEnvironmentForRefresh(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// setupTestServerForRefresh creates and configures an Echo server for refresh testing
|
// setupTestServerForRefresh creates and configures an Echo server for refresh testing.
|
||||||
|
//
|
||||||
|
// This function sets up a complete web server with authentication middleware that
|
||||||
|
// mimics the production environment. It's essential for testing the auto-refresh
|
||||||
|
// functionality because it provides the protected endpoints that trigger token
|
||||||
|
// refresh when accessed with expired tokens.
|
||||||
|
//
|
||||||
|
// Server Configuration:
|
||||||
|
// - Listens on localhost:8080 (matches Cognito redirect URI configuration)
|
||||||
|
// - Uses Echo web framework with logging and recovery middleware
|
||||||
|
// - Registers complete Cognito auth middleware stack
|
||||||
|
// - Configures Permit.io authorization for protected routes
|
||||||
|
// - Sets up structured logging at debug level for detailed test output
|
||||||
|
//
|
||||||
|
// Test Endpoints Created:
|
||||||
|
// - /home: Public endpoint that shows authentication status and user info
|
||||||
|
// - /query: Protected endpoint requiring "exporters", "uploaders", or "querybuilders" role
|
||||||
|
// - All Cognito auth routes: /login, /login-callback, /logout
|
||||||
|
//
|
||||||
|
// The /query endpoint is crucial for auto-refresh testing because:
|
||||||
|
// - It requires valid authentication (triggers token validation)
|
||||||
|
// - It's protected by Permit.io authorization (requires valid user context)
|
||||||
|
// - It returns a success response when auth succeeds
|
||||||
|
// - It's the endpoint we hit to trigger auto-refresh when tokens expire
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - t: Testing context for logging and test control
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *http.Server: Configured HTTP server ready to start
|
||||||
|
// - *auth.CognitoConfig: Auth configuration used by the server
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// - Server must be started in a goroutine: go server.ListenAndServe()
|
||||||
|
// - Server should be gracefully shutdown after test: server.Shutdown(ctx)
|
||||||
|
// - Config contains auth paths needed for browser navigation
|
||||||
func setupTestServerForRefresh(t *testing.T) (*http.Server, *auth.CognitoConfig) {
|
func setupTestServerForRefresh(t *testing.T) (*http.Server, *auth.CognitoConfig) {
|
||||||
// Create a new Echo instance
|
// Create a new Echo instance
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
@@ -120,7 +204,46 @@ func setupTestServerForRefresh(t *testing.T) (*http.Server, *auth.CognitoConfig)
|
|||||||
return server, config
|
return server, config
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseJWTTokenExpiration extracts the expiration time from a JWT token without verification
|
// parseJWTTokenExpiration extracts the expiration time from a JWT token without verification.
|
||||||
|
//
|
||||||
|
// This function performs client-side JWT parsing to extract the token's expiration time
|
||||||
|
// without requiring signature verification or external dependencies. It's specifically
|
||||||
|
// designed for testing scenarios where we need to know when a token expires so we can
|
||||||
|
// wait for natural expiration and test auto-refresh functionality.
|
||||||
|
//
|
||||||
|
// JWT Structure Parsing:
|
||||||
|
// - Expects standard JWT format: header.payload.signature
|
||||||
|
// - Extracts and base64-decodes the payload (middle section)
|
||||||
|
// - Parses JSON payload to access claims
|
||||||
|
// - Specifically looks for the "exp" (expiration) claim
|
||||||
|
//
|
||||||
|
// Key Features:
|
||||||
|
// - No signature verification (testing purposes only)
|
||||||
|
// - Handles base64 URL encoding with proper padding
|
||||||
|
// - Converts Unix timestamp to Go time.Time
|
||||||
|
// - Robust error handling for malformed tokens
|
||||||
|
//
|
||||||
|
// Security Note:
|
||||||
|
// This function is for testing only and should never be used in production
|
||||||
|
// code where token signature verification is required for security.
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - tokenString: Raw JWT token string (header.payload.signature format)
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - time.Time: Token expiration time in UTC
|
||||||
|
// - error: Error if token is malformed or missing expiration claim
|
||||||
|
//
|
||||||
|
// Error Conditions:
|
||||||
|
// - Invalid JWT format (not 3 parts separated by dots)
|
||||||
|
// - Failed base64 decoding of payload
|
||||||
|
// - Invalid JSON in payload
|
||||||
|
// - Missing "exp" claim
|
||||||
|
// - Invalid expiration value type (not a number)
|
||||||
|
//
|
||||||
|
// Usage in Testing:
|
||||||
|
// This enables the test to calculate exactly how long to wait for token
|
||||||
|
// expiration, allowing for precise timing of auto-refresh scenarios.
|
||||||
func parseJWTTokenExpiration(tokenString string) (time.Time, error) {
|
func parseJWTTokenExpiration(tokenString string) (time.Time, error) {
|
||||||
// Split JWT into parts
|
// Split JWT into parts
|
||||||
parts := strings.Split(tokenString, ".")
|
parts := strings.Split(tokenString, ".")
|
||||||
@@ -161,7 +284,75 @@ func parseJWTTokenExpiration(tokenString string) (time.Time, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAutoRefreshFlow runs the complete end-to-end test for auto refresh functionality
|
// TestAutoRefreshFlow runs the complete end-to-end test for auto refresh functionality.
|
||||||
|
//
|
||||||
|
// This is the main integration test that validates the entire auto-refresh token lifecycle
|
||||||
|
// using real browser automation and natural token expiration. It's designed to catch
|
||||||
|
// regressions in the authentication system that could break seamless user sessions.
|
||||||
|
//
|
||||||
|
// Test Flow Overview:
|
||||||
|
// 1. Environment Setup: Validates all required configuration
|
||||||
|
// 2. Server Setup: Creates test server with auth middleware
|
||||||
|
// 3. Browser Setup: Configures Chrome with appropriate options
|
||||||
|
// 4. Initial Authentication: Complete OAuth/PKCE login flow
|
||||||
|
// 5. Token Extraction: Parses JWT to determine expiration time
|
||||||
|
// 6. Expiration Wait: Waits for natural token expiration (up to 5+ minutes)
|
||||||
|
// 7. Auto-Refresh Test: Triggers refresh by accessing protected endpoint
|
||||||
|
// 8. Verification: Confirms new token and continued access
|
||||||
|
//
|
||||||
|
// Key Testing Aspects:
|
||||||
|
// - Uses real AWS Cognito for authentication (not mocked)
|
||||||
|
// - Uses real browser (Chrome/Chromium) for authentic user experience
|
||||||
|
// - Tests natural token expiration (5-minute tokens from Cognito)
|
||||||
|
// - Validates cookie-based token storage and retrieval
|
||||||
|
// - Tests non-rotating refresh token handling
|
||||||
|
// - Verifies Permit.io authorization integration
|
||||||
|
// - Confirms seamless user experience (no re-authentication required)
|
||||||
|
//
|
||||||
|
// Browser Automation Details:
|
||||||
|
// - Supports both headless and visible browser modes
|
||||||
|
// - Handles Cognito's hosted UI authentication forms
|
||||||
|
// - Waits for OAuth redirects and callback handling
|
||||||
|
// - Captures and analyzes HTTP cookies for token storage
|
||||||
|
// - Tests protected endpoint access before and after refresh
|
||||||
|
//
|
||||||
|
// Time Requirements:
|
||||||
|
// - Test duration: 5-10 minutes (depends on token expiration time)
|
||||||
|
// - Extended timeout: 15 minutes to handle longer token lifecycles
|
||||||
|
// - Progress logging: Shows wait progress every 30 seconds
|
||||||
|
//
|
||||||
|
// Test Phases (Subtests):
|
||||||
|
// 1. "Initial Authentication": Complete login flow and cookie capture
|
||||||
|
// 2. "Extract Token and Log Expiration": JWT parsing and timing calculation
|
||||||
|
// 3. "Wait for Token Expiration": Patient wait for natural expiration
|
||||||
|
// 4. "Test Auto Refresh Functionality": Core auto-refresh validation
|
||||||
|
// 5. "Verify Continued Access After Refresh": Confirms sustained access
|
||||||
|
//
|
||||||
|
// Success Criteria:
|
||||||
|
// - User successfully authenticates with Cognito
|
||||||
|
// - JWT tokens are properly stored in HTTP cookies
|
||||||
|
// - Token expiration time is correctly parsed and tracked
|
||||||
|
// - Auto-refresh triggers when accessing protected endpoints with expired tokens
|
||||||
|
// - New tokens are generated and stored without user intervention
|
||||||
|
// - Protected endpoints remain accessible after token refresh
|
||||||
|
// - Token expiration times are extended after refresh
|
||||||
|
//
|
||||||
|
// Failure Detection:
|
||||||
|
// - Missing or invalid authentication tokens
|
||||||
|
// - Failed access to protected endpoints
|
||||||
|
// - Redirect to login page (indicates failed auto-refresh)
|
||||||
|
// - Same token before and after refresh attempt
|
||||||
|
// - Earlier expiration time after refresh attempt
|
||||||
|
//
|
||||||
|
// Environment Requirements:
|
||||||
|
// - ENABLE_REFRESH_TESTS=true (test is skipped otherwise)
|
||||||
|
// - Valid Cognito configuration (domain, client ID, secret, etc.)
|
||||||
|
// - Test user credentials (USER, PASSWORD environment variables)
|
||||||
|
// - Chrome/Chromium browser available on system
|
||||||
|
// - Network access to AWS Cognito services
|
||||||
|
//
|
||||||
|
// This test is crucial for ensuring users don't experience authentication
|
||||||
|
// interruptions during long sessions, maintaining a smooth user experience.
|
||||||
func TestAutoRefreshFlow(t *testing.T) {
|
func TestAutoRefreshFlow(t *testing.T) {
|
||||||
if os.Getenv("ENABLE_REFRESH_TESTS") != "true" {
|
if os.Getenv("ENABLE_REFRESH_TESTS") != "true" {
|
||||||
t.Skip("Skipping auto refresh tests - set ENABLE_REFRESH_TESTS=true to run")
|
t.Skip("Skipping auto refresh tests - set ENABLE_REFRESH_TESTS=true to run")
|
||||||
@@ -241,6 +432,9 @@ func TestAutoRefreshFlow(t *testing.T) {
|
|||||||
var cookies []*network.Cookie
|
var cookies []*network.Cookie
|
||||||
|
|
||||||
// Phase 1: Initial Authentication
|
// Phase 1: Initial Authentication
|
||||||
|
// This phase performs the complete OAuth/PKCE authentication flow using the browser.
|
||||||
|
// It navigates to the Cognito hosted UI, submits credentials, handles redirects,
|
||||||
|
// and captures authentication cookies containing JWT tokens.
|
||||||
t.Run("Initial Authentication", func(t *testing.T) {
|
t.Run("Initial Authentication", func(t *testing.T) {
|
||||||
// Navigate to login page
|
// Navigate to login page
|
||||||
loginURL := fmt.Sprintf("%s%s", baseURL, config.GetAuthLoginPath())
|
loginURL := fmt.Sprintf("%s%s", baseURL, config.GetAuthLoginPath())
|
||||||
@@ -312,6 +506,9 @@ func TestAutoRefreshFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Phase 2: Extract and Log Token Expiration
|
// Phase 2: Extract and Log Token Expiration
|
||||||
|
// This phase extracts the JWT access token from cookies and parses it to determine
|
||||||
|
// the exact expiration time. This information is crucial for calculating how long
|
||||||
|
// to wait before testing the auto-refresh functionality.
|
||||||
var authToken string
|
var authToken string
|
||||||
var tokenExpiration time.Time
|
var tokenExpiration time.Time
|
||||||
t.Run("Extract Token and Log Expiration", func(t *testing.T) {
|
t.Run("Extract Token and Log Expiration", func(t *testing.T) {
|
||||||
@@ -335,6 +532,9 @@ func TestAutoRefreshFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Phase 3: Wait for Token Expiration
|
// Phase 3: Wait for Token Expiration
|
||||||
|
// This phase waits for the natural expiration of the JWT access token.
|
||||||
|
// It calculates the exact wait time and provides progress updates during the wait.
|
||||||
|
// This is essential for testing real-world token expiration scenarios.
|
||||||
t.Run("Wait for Token Expiration", func(t *testing.T) {
|
t.Run("Wait for Token Expiration", func(t *testing.T) {
|
||||||
// Calculate when the token will expire (plus 1 second buffer)
|
// Calculate when the token will expire (plus 1 second buffer)
|
||||||
timeToWait := time.Until(tokenExpiration) + 1*time.Second
|
timeToWait := time.Until(tokenExpiration) + 1*time.Second
|
||||||
@@ -368,6 +568,9 @@ func TestAutoRefreshFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Phase 4: Test Auto Refresh
|
// Phase 4: Test Auto Refresh
|
||||||
|
// This is the core phase that tests the auto-refresh functionality.
|
||||||
|
// It attempts to access a protected endpoint with an expired token, expecting
|
||||||
|
// the middleware to automatically refresh the token and grant access.
|
||||||
t.Run("Test Auto Refresh Functionality", func(t *testing.T) {
|
t.Run("Test Auto Refresh Functionality", func(t *testing.T) {
|
||||||
// Get cookies before the request
|
// Get cookies before the request
|
||||||
var cookiesBefore []*network.Cookie
|
var cookiesBefore []*network.Cookie
|
||||||
@@ -482,6 +685,8 @@ func TestAutoRefreshFlow(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Phase 5: Verify Continued Access to Protected Endpoint
|
// Phase 5: Verify Continued Access to Protected Endpoint
|
||||||
|
// This final phase confirms that the auto-refreshed token works for subsequent
|
||||||
|
// requests, ensuring sustained access to protected resources without user intervention.
|
||||||
t.Run("Verify Continued Access After Refresh", func(t *testing.T) {
|
t.Run("Verify Continued Access After Refresh", func(t *testing.T) {
|
||||||
// Make another request to a protected endpoint to ensure the refreshed token works
|
// Make another request to a protected endpoint to ensure the refreshed token works
|
||||||
queryURL := fmt.Sprintf("%s/query", baseURL)
|
queryURL := fmt.Sprintf("%s/query", baseURL)
|
||||||
|
|||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# start the queryAPI service and the
|
||||||
|
|
||||||
|
|
||||||
|
# Auto Refresh Token Test Script
|
||||||
|
# This script sets up the environment and runs the auto refresh token test
|
||||||
|
|
||||||
|
export USER=betot75403@isorax.com
|
||||||
|
export PASSWORD=qqQQ11!!
|
||||||
|
export COGNITO_DOMAIN=https://us-east-21y6po8rr8.auth.us-east-2.amazoncognito.com
|
||||||
|
export AWS_REGION=us-east-2
|
||||||
|
export COGNITO_CLIENT_SECRET=<get from jay>
|
||||||
|
export COGNITO_USER_POOL_ID=us-east-2_1y6po8rR8
|
||||||
|
export COGNITO_CLIENT_ID=552cqkf3640t39ncehkmgpce31
|
||||||
|
export DEBUG=true
|
||||||
|
export HEADLESS=false
|
||||||
|
export ENABLE_REFRESH_TESTS=true
|
||||||
|
export PERMIT_IO_API_KEY=permit_key_<get from jay>
|
||||||
|
|
||||||
|
echo "🚀 Starting Auto Refresh Token Test"
|
||||||
|
echo "⚠️ This test will wait for token expiration (up to 5+ minutes)"
|
||||||
|
echo "🔧 Make sure Cognito tokens are configured with 5 minute expiration"
|
||||||
|
echo "🌐 Browser will open in non-headless mode for debugging"
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
# Run the auto refresh test with extended timeout for token expiration
|
||||||
|
go test -run TestAutoRefreshFlow -timeout 15m -v
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "✅ Auto refresh test completed"
|
||||||
+208
-114
@@ -124,6 +124,203 @@ func performPermitIOAuthorization(c echo.Context, requestPath string, config aut
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HTTPError represents an HTTP error response with status code and response data
|
||||||
|
type HTTPError struct {
|
||||||
|
StatusCode int
|
||||||
|
Response interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *HTTPError) Error() string {
|
||||||
|
return fmt.Sprintf("HTTP %d error", e.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// attemptTokenRefresh handles the complete token refresh flow including non-rotating token preservation.
|
||||||
|
//
|
||||||
|
// This function consolidates the duplicate refresh logic that was scattered throughout the middleware.
|
||||||
|
// It attempts to refresh tokens using the provided refresh token and handles the case where Cognito
|
||||||
|
// returns empty refresh tokens (non-rotating tokens).
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - c: The Echo context for setting cookies and headers
|
||||||
|
// - refreshToken: The refresh token to use for getting new tokens
|
||||||
|
// - config: Configuration provider for auth settings
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - *TokenResponse: New tokens if refresh succeeds
|
||||||
|
// - error: Error if refresh fails
|
||||||
|
func attemptTokenRefresh(c echo.Context, refreshToken string, config auth.ConfigProvider) (*TokenResponse, error) {
|
||||||
|
config.GetAuthLogger().Debug("Attempting token refresh with refresh token")
|
||||||
|
|
||||||
|
newTokens, err := refreshTokens(refreshToken, config)
|
||||||
|
if err != nil {
|
||||||
|
config.GetAuthLogger().Warn("Token refresh failed", "error", err)
|
||||||
|
clearTokenCookies(c, config)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle non-rotating refresh tokens - preserve original if new one is empty
|
||||||
|
if newTokens.RefreshToken == "" {
|
||||||
|
config.GetAuthLogger().Debug("Refresh response missing refresh token, preserving original")
|
||||||
|
newTokens.RefreshToken = refreshToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update cookies with new tokens
|
||||||
|
newExpiresAt := time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
|
||||||
|
setTokenCookies(c, newTokens, newExpiresAt, config)
|
||||||
|
|
||||||
|
// Update the Authorization header for this request
|
||||||
|
c.Request().Header.Set("Authorization", "Bearer "+newTokens.AccessToken)
|
||||||
|
|
||||||
|
config.GetAuthLogger().Debug("Token refresh successful, proceeding with new token")
|
||||||
|
return newTokens, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// acquireValidToken gets a valid access token through extraction or refresh.
|
||||||
|
//
|
||||||
|
// This function handles the complete token acquisition flow:
|
||||||
|
// - Attempts to extract token from header/cookie
|
||||||
|
// - Falls back to refresh if no token found
|
||||||
|
// - Checks for expiration and refreshes if needed
|
||||||
|
// - Returns appropriate HTTP error responses
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - c: The Echo context for cookies, headers, and responses
|
||||||
|
// - config: Configuration provider for auth settings
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - string: Valid access token
|
||||||
|
// - error: HTTP error response if token acquisition fails
|
||||||
|
func acquireValidToken(c echo.Context, config auth.ConfigProvider) (string, error) {
|
||||||
|
// Extract JWT token from header or cookie
|
||||||
|
tokenStr, err := extractToken(c, config)
|
||||||
|
|
||||||
|
// If token extraction fails, try refresh before returning 401
|
||||||
|
if err != nil {
|
||||||
|
config.GetAuthLogger().Debug("No access token found, checking for refresh token")
|
||||||
|
config.GetAuthLogger().Debug("extractToken error", "error", err)
|
||||||
|
|
||||||
|
// Check if we have a refresh token
|
||||||
|
config.GetAuthLogger().Debug("About to call getRefreshTokenFromCookie")
|
||||||
|
refreshToken := getRefreshTokenFromCookie(c, config)
|
||||||
|
config.GetAuthLogger().Debug("getRefreshTokenFromCookie returned", "token_length", len(refreshToken))
|
||||||
|
|
||||||
|
if refreshToken == "" {
|
||||||
|
config.GetAuthLogger().Debug("No refresh token found, redirecting to login")
|
||||||
|
return "", &HTTPError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Response: map[string]interface{}{
|
||||||
|
"error": "Authorization required",
|
||||||
|
"message": "This endpoint requires authentication. Please obtain a valid JWT token.",
|
||||||
|
"login_url": config.GetAuthLoginPath(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to refresh tokens
|
||||||
|
newTokens, err := attemptTokenRefresh(c, refreshToken, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", &HTTPError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Response: map[string]interface{}{
|
||||||
|
"error": "Session expired",
|
||||||
|
"message": "Unable to refresh your session. Please log in again.",
|
||||||
|
"login_url": config.GetAuthLoginPath(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use the new token for this request
|
||||||
|
tokenStr = newTokens.AccessToken
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is expired and attempt refresh if needed
|
||||||
|
if isTokenExpired(tokenStr) {
|
||||||
|
config.GetAuthLogger().Debug("Access token expired, attempting refresh")
|
||||||
|
|
||||||
|
refreshToken := getRefreshTokenFromCookie(c, config)
|
||||||
|
if refreshToken == "" {
|
||||||
|
config.GetAuthLogger().Debug("No refresh token found, redirecting to login")
|
||||||
|
clearTokenCookies(c, config)
|
||||||
|
return "", &HTTPError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Response: map[string]interface{}{
|
||||||
|
"error": "Session expired",
|
||||||
|
"message": "Your session has expired. Please log in again.",
|
||||||
|
"login_url": config.GetAuthLoginPath(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to refresh tokens
|
||||||
|
newTokens, err := attemptTokenRefresh(c, refreshToken, config)
|
||||||
|
if err != nil {
|
||||||
|
return "", &HTTPError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Response: map[string]interface{}{
|
||||||
|
"error": "Session expired",
|
||||||
|
"message": "Unable to refresh your session. Please log in again.",
|
||||||
|
"login_url": config.GetAuthLoginPath(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the token string for subsequent verification
|
||||||
|
tokenStr = newTokens.AccessToken
|
||||||
|
config.GetAuthLogger().Info("Token refreshed successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// verifyTokenAndSetContext verifies token signature and populates request context.
|
||||||
|
//
|
||||||
|
// This function handles token verification and context setup:
|
||||||
|
// - Retrieves JWKS for token signature verification (unless in test mode)
|
||||||
|
// - Verifies the token against the key set
|
||||||
|
// - Extracts claims and user info
|
||||||
|
// - Populates the Echo context with user information
|
||||||
|
//
|
||||||
|
// Parameters:
|
||||||
|
// - c: The Echo context for setting context values and responses
|
||||||
|
// - tokenStr: The JWT token string to verify
|
||||||
|
// - config: Configuration provider for auth settings
|
||||||
|
//
|
||||||
|
// Returns:
|
||||||
|
// - map[string]interface{}: Token claims if verification succeeds
|
||||||
|
// - error: HTTP error response if verification fails
|
||||||
|
func verifyTokenAndSetContext(c echo.Context, tokenStr string, config auth.ConfigProvider) (map[string]interface{}, error) {
|
||||||
|
// Get JWKS (skip in test mode)
|
||||||
|
var keySet jwk.Set
|
||||||
|
var err error
|
||||||
|
if !config.GetNoJWTValidation() {
|
||||||
|
keySet, err = GetJWKS(config.GetAuthJwksURL(), config.GetAuthLogger())
|
||||||
|
if err != nil {
|
||||||
|
config.GetAuthLogger().Error("Failed to get JWKS", "error", err)
|
||||||
|
return nil, &HTTPError{
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
Response: map[string]string{"error": "Failed to verify token"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token (keySet can be nil in test mode)
|
||||||
|
claims, err := verifyToken(tokenStr, keySet, config)
|
||||||
|
if err != nil {
|
||||||
|
config.GetAuthLogger().Warn("Token verification failed", "error", err)
|
||||||
|
return nil, &HTTPError{
|
||||||
|
StatusCode: http.StatusUnauthorized,
|
||||||
|
Response: map[string]string{"error": "Invalid token"},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user info in context
|
||||||
|
c.Set("user_claims", claims)
|
||||||
|
userInfo := ExtractUserInfo(claims)
|
||||||
|
c.Set("user_info", userInfo)
|
||||||
|
|
||||||
|
return claims, nil
|
||||||
|
}
|
||||||
|
|
||||||
// TokenValidationMiddleware verifies JWT tokens and enforces authentication and authorization.
|
// TokenValidationMiddleware verifies JWT tokens and enforces authentication and authorization.
|
||||||
//
|
//
|
||||||
// This middleware performs several critical security functions:
|
// This middleware performs several critical security functions:
|
||||||
@@ -158,127 +355,24 @@ func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract JWT token from header or cookie
|
// Acquire valid token (handles extraction, refresh, expiration)
|
||||||
tokenStr, err := extractToken(c, config)
|
tokenStr, err := acquireValidToken(c, config)
|
||||||
|
|
||||||
// If token extraction fails, try refresh before returning 401
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config.GetAuthLogger().Debug("No access token found, checking for refresh token")
|
if httpErr, ok := err.(*HTTPError); ok {
|
||||||
config.GetAuthLogger().Debug("extractToken error", "error", err)
|
return c.JSON(httpErr.StatusCode, httpErr.Response)
|
||||||
|
|
||||||
// Check if we have a refresh token
|
|
||||||
config.GetAuthLogger().Debug("About to call getRefreshTokenFromCookie")
|
|
||||||
refreshToken := getRefreshTokenFromCookie(c, config)
|
|
||||||
config.GetAuthLogger().Debug("getRefreshTokenFromCookie returned", "token_length", len(refreshToken))
|
|
||||||
|
|
||||||
if refreshToken == "" {
|
|
||||||
config.GetAuthLogger().Debug("No refresh token found, redirecting to login")
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
|
|
||||||
"error": "Authorization required",
|
|
||||||
"message": "This endpoint requires authentication. Please obtain a valid JWT token.",
|
|
||||||
"login_url": config.GetAuthLoginPath(),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
return err
|
||||||
// Attempt to refresh tokens
|
|
||||||
config.GetAuthLogger().Debug("Attempting token refresh with refresh token")
|
|
||||||
newTokens, err := refreshTokens(refreshToken, config)
|
|
||||||
if err != nil {
|
|
||||||
config.GetAuthLogger().Warn("Token refresh failed", "error", err)
|
|
||||||
clearTokenCookies(c, config)
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
|
|
||||||
"error": "Session expired",
|
|
||||||
"message": "Unable to refresh your session. Please log in again.",
|
|
||||||
"login_url": config.GetAuthLoginPath(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle non-rotating refresh tokens - preserve original if new one is empty
|
|
||||||
if newTokens.RefreshToken == "" {
|
|
||||||
config.GetAuthLogger().Debug("Refresh response missing refresh token, preserving original")
|
|
||||||
newTokens.RefreshToken = refreshToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cookies with new tokens
|
|
||||||
newExpiresAt := time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
|
|
||||||
setTokenCookies(c, newTokens, newExpiresAt, config)
|
|
||||||
|
|
||||||
// Use the new token for this request
|
|
||||||
tokenStr = newTokens.AccessToken
|
|
||||||
config.GetAuthLogger().Debug("Token refresh successful, proceeding with new token")
|
|
||||||
|
|
||||||
// Update the Authorization header for this request
|
|
||||||
c.Request().Header.Set("Authorization", "Bearer "+tokenStr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if token is expired and attempt refresh if needed
|
// Verify token and populate context
|
||||||
if isTokenExpired(tokenStr) {
|
_, err = verifyTokenAndSetContext(c, tokenStr, config)
|
||||||
config.GetAuthLogger().Debug("Access token expired, attempting refresh")
|
|
||||||
|
|
||||||
refreshToken := getRefreshTokenFromCookie(c, config)
|
|
||||||
if refreshToken == "" {
|
|
||||||
config.GetAuthLogger().Debug("No refresh token found, redirecting to login")
|
|
||||||
clearTokenCookies(c, config)
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
|
|
||||||
"error": "Session expired",
|
|
||||||
"message": "Your session has expired. Please log in again.",
|
|
||||||
"login_url": config.GetAuthLoginPath(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attempt to refresh tokens
|
|
||||||
newTokens, err := refreshTokens(refreshToken, config)
|
|
||||||
if err != nil {
|
|
||||||
config.GetAuthLogger().Warn("Token refresh failed", "error", err)
|
|
||||||
clearTokenCookies(c, config)
|
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]interface{}{
|
|
||||||
"error": "Session expired",
|
|
||||||
"message": "Unable to refresh your session. Please log in again.",
|
|
||||||
"login_url": config.GetAuthLoginPath(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle non-rotating refresh tokens - preserve original if new one is empty
|
|
||||||
if newTokens.RefreshToken == "" {
|
|
||||||
config.GetAuthLogger().Debug("Refresh response missing refresh token, preserving original")
|
|
||||||
newTokens.RefreshToken = refreshToken
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update cookies with new tokens
|
|
||||||
newExpiresAt := time.Now().Add(time.Duration(newTokens.ExpiresIn) * time.Second)
|
|
||||||
setTokenCookies(c, newTokens, newExpiresAt, config)
|
|
||||||
|
|
||||||
// Update the token string for subsequent verification
|
|
||||||
tokenStr = newTokens.AccessToken
|
|
||||||
|
|
||||||
// Update the Authorization header for this request
|
|
||||||
c.Request().Header.Set("Authorization", "Bearer "+tokenStr)
|
|
||||||
|
|
||||||
config.GetAuthLogger().Info("Token refreshed successfully")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get JWKS (skip in test mode)
|
|
||||||
var keySet jwk.Set
|
|
||||||
if !config.GetNoJWTValidation() {
|
|
||||||
keySet, err = GetJWKS(config.GetAuthJwksURL(), config.GetAuthLogger())
|
|
||||||
if err != nil {
|
|
||||||
config.GetAuthLogger().Error("Failed to get JWKS", "error", err)
|
|
||||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to verify token"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify token (keySet can be nil in test mode)
|
|
||||||
claims, err := verifyToken(tokenStr, keySet, config)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
config.GetAuthLogger().Warn("Token verification failed", "error", err)
|
if httpErr, ok := err.(*HTTPError); ok {
|
||||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid token"})
|
return c.JSON(httpErr.StatusCode, httpErr.Response)
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store user info in context
|
|
||||||
c.Set("user_claims", claims)
|
|
||||||
userInfo := ExtractUserInfo(claims)
|
|
||||||
c.Set("user_info", userInfo)
|
|
||||||
|
|
||||||
// Perform Permit.io authorization
|
// Perform Permit.io authorization
|
||||||
if err := performPermitIOAuthorization(c, requestPath, config); err != nil {
|
if err := performPermitIOAuthorization(c, requestPath, config); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in New Issue
Block a user