functionally complete
needs cleanup next
This commit is contained in:
@@ -3,6 +3,7 @@ package cognitoauth
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
@@ -162,28 +163,40 @@ func GetUserSubject(c echo.Context) (string, bool) {
|
||||
// - accessTokenExpiresAt: Expiration time for the access token
|
||||
// - config: Configuration provider for auth settings
|
||||
func setTokenCookies(c echo.Context, tokens *TokenResponse, accessTokenExpiresAt time.Time, config auth.ConfigProvider) {
|
||||
// Log key cookie setting information for debugging
|
||||
log.Printf("DEBUG: setTokenCookies - Setting cookies: access_token_len=%d, refresh_token_len=%d",
|
||||
len(tokens.AccessToken), len(tokens.RefreshToken))
|
||||
|
||||
// Set access token cookie (shorter expiration)
|
||||
// Ensure proper UTC time handling
|
||||
accessTokenExpiresUTC := accessTokenExpiresAt.UTC()
|
||||
accessTokenCookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: tokens.AccessToken,
|
||||
Path: "/",
|
||||
Expires: accessTokenExpiresAt,
|
||||
Expires: accessTokenExpiresUTC,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
SameSite: http.SameSiteLaxMode, // Modern browser compatibility
|
||||
}
|
||||
c.SetCookie(accessTokenCookie)
|
||||
|
||||
// Set refresh token cookie (longer expiration - 30 days)
|
||||
// Use UTC time and ensure proper expiration
|
||||
refreshTokenExpires := time.Now().UTC().Add(30 * 24 * time.Hour)
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: tokens.RefreshToken,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||
Expires: refreshTokenExpires,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
SameSite: http.SameSiteLaxMode, // Modern browser compatibility
|
||||
}
|
||||
c.SetCookie(refreshTokenCookie)
|
||||
|
||||
log.Printf("DEBUG: setTokenCookies - Cookies set successfully")
|
||||
|
||||
config.GetAuthLogger().Debug("Token cookies set",
|
||||
"access_token_expires", accessTokenExpiresAt,
|
||||
"refresh_token_expires", refreshTokenCookie.Expires)
|
||||
@@ -198,9 +211,13 @@ func setTokenCookies(c echo.Context, tokens *TokenResponse, accessTokenExpiresAt
|
||||
// - c: The Echo context for setting cookies
|
||||
// - config: Configuration provider for auth settings
|
||||
func clearTokenCookies(c echo.Context, config auth.ConfigProvider) {
|
||||
pastTime := time.Now().Add(-1 * time.Hour)
|
||||
// Log when clearTokenCookies is called for debugging
|
||||
log.Printf("DEBUG: clearTokenCookies - Clearing auth cookies for path: %s", c.Request().URL.Path)
|
||||
|
||||
// Clear access token cookie
|
||||
// Use UTC time for consistent cookie clearing
|
||||
pastTime := time.Now().UTC().Add(-1 * time.Hour)
|
||||
|
||||
// Clear access token cookie - must match original attributes exactly
|
||||
accessTokenCookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
@@ -208,10 +225,11 @@ func clearTokenCookies(c echo.Context, config auth.ConfigProvider) {
|
||||
Expires: pastTime,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
SameSite: http.SameSiteLaxMode, // Must match original
|
||||
}
|
||||
c.SetCookie(accessTokenCookie)
|
||||
|
||||
// Clear refresh token cookie
|
||||
// Clear refresh token cookie - must match original attributes exactly
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: "",
|
||||
@@ -219,9 +237,11 @@ func clearTokenCookies(c echo.Context, config auth.ConfigProvider) {
|
||||
Expires: pastTime,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
SameSite: http.SameSiteLaxMode, // Must match original
|
||||
}
|
||||
c.SetCookie(refreshTokenCookie)
|
||||
|
||||
log.Printf("DEBUG: clearTokenCookies - Auth cookies cleared successfully")
|
||||
config.GetAuthLogger().Debug("Token cookies cleared")
|
||||
}
|
||||
|
||||
@@ -233,10 +253,21 @@ func clearTokenCookies(c echo.Context, config auth.ConfigProvider) {
|
||||
// Returns:
|
||||
// - string: The refresh token value, or empty string if not found
|
||||
func getRefreshTokenFromCookie(c echo.Context) string {
|
||||
// Check if we have cookies
|
||||
cookies := c.Cookies()
|
||||
log.Printf("DEBUG: getRefreshTokenFromCookie - Found %d cookies for %s", len(cookies), c.Request().URL.Path)
|
||||
|
||||
refreshCookie, err := c.Cookie("refresh_token")
|
||||
if err != nil || refreshCookie.Value == "" {
|
||||
if err != nil {
|
||||
log.Printf("DEBUG: getRefreshTokenFromCookie - Failed to get refresh_token cookie: %v", err)
|
||||
return ""
|
||||
}
|
||||
if refreshCookie.Value == "" {
|
||||
log.Printf("DEBUG: getRefreshTokenFromCookie - refresh_token cookie is empty")
|
||||
return ""
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: getRefreshTokenFromCookie - Found refresh_token cookie, length: %d", len(refreshCookie.Value))
|
||||
return refreshCookie.Value
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ func setupEnvironment(t *testing.T) {
|
||||
"COGNITO_CLIENT_SECRET": os.Getenv("COGNITO_CLIENT_SECRET"),
|
||||
"COGNITO_USER_POOL_ID": os.Getenv("COGNITO_USER_POOL_ID"),
|
||||
"COGNITO_CLIENT_ID": os.Getenv("COGNITO_CLIENT_ID"),
|
||||
"DEBUG": "true",
|
||||
"DEBUG": "false",
|
||||
"HEADLESS": os.Getenv("HEADLESS"),
|
||||
}
|
||||
|
||||
@@ -209,9 +209,10 @@ func TestCognitoAuthFlow(t *testing.T) {
|
||||
chromedp.ListenTarget(ctx, func(ev interface{}) {
|
||||
switch e := ev.(type) {
|
||||
case *network.EventRequestWillBeSent:
|
||||
t.Logf("Request: %s %s", e.Request.Method, e.Request.URL)
|
||||
t.Logf("Request: %s ", e.Request.Method)
|
||||
case *network.EventResponseReceived:
|
||||
t.Logf("Response: %s %s (status: %d)", e.Response.URL, e.Response.MimeType, e.Response.Status)
|
||||
//t.Logf("Response: %s %s (status: %d)", e.Response.URL, e.Response.MimeType, e.Response.Status)
|
||||
t.Logf("Response: (status: %d)", e.Response.Status)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,500 @@
|
||||
package cognitotest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"queryorchestration/internal/cognitoauth"
|
||||
"queryorchestration/internal/serviceconfig/auth"
|
||||
|
||||
"github.com/chromedp/cdproto/network"
|
||||
"github.com/chromedp/chromedp"
|
||||
)
|
||||
|
||||
// setupEnvironmentForRefresh ensures all required environment variables are set for refresh testing
|
||||
func setupEnvironmentForRefresh(t *testing.T) {
|
||||
// Define all required environment variables with test values
|
||||
envVars := map[string]string{
|
||||
"USER": os.Getenv("USER"),
|
||||
"PASSWORD": os.Getenv("PASSWORD"),
|
||||
"COGNITO_DOMAIN": os.Getenv("COGNITO_DOMAIN"),
|
||||
"AWS_REGION": os.Getenv("AWS_REGION"),
|
||||
"COGNITO_CLIENT_SECRET": os.Getenv("COGNITO_CLIENT_SECRET"),
|
||||
"COGNITO_USER_POOL_ID": os.Getenv("COGNITO_USER_POOL_ID"),
|
||||
"COGNITO_CLIENT_ID": os.Getenv("COGNITO_CLIENT_ID"),
|
||||
"DEBUG": "false",
|
||||
"HEADLESS": os.Getenv("HEADLESS"),
|
||||
}
|
||||
|
||||
// Verify all required variables are set
|
||||
for key, value := range envVars {
|
||||
if value == "" {
|
||||
// HEADLESS is optional with a default of "true"
|
||||
if key == "HEADLESS" {
|
||||
os.Setenv(key, "true")
|
||||
continue
|
||||
}
|
||||
t.Fatalf("Required environment variable %s is not set", key)
|
||||
}
|
||||
// Set the environment variable for the test
|
||||
os.Setenv(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
// setupTestServerForRefresh creates and configures an Echo server for refresh testing
|
||||
func setupTestServerForRefresh(t *testing.T) (*http.Server, *auth.CognitoConfig) {
|
||||
// Create a new Echo instance
|
||||
e := echo.New()
|
||||
|
||||
// Add basic middleware
|
||||
e.Use(middleware.Logger())
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
// Initialize logger
|
||||
logHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
|
||||
Level: slog.LevelDebug,
|
||||
})
|
||||
logger := slog.New(logHandler)
|
||||
|
||||
// Create auth config with correct base URL that matches Cognito settings
|
||||
baseURL := "http://localhost:8080"
|
||||
config := auth.InitializeConfig(baseURL, logger)
|
||||
|
||||
// Set route permissions based on test requirements
|
||||
routePermissions := map[string][]string{
|
||||
"/query": {"exporters", "uploaders", "querybuilders"},
|
||||
}
|
||||
config.SetAuthRoutePermissions(routePermissions)
|
||||
|
||||
// Register Cognito auth routes and middleware
|
||||
cognitoauth.RegisterRoutes(e, config)
|
||||
|
||||
// Add test endpoints
|
||||
e.GET("/home", func(c echo.Context) error {
|
||||
// Check if user is authenticated
|
||||
userInfo, authenticated := cognitoauth.GetUserInfo(c)
|
||||
|
||||
response := map[string]interface{}{
|
||||
"authenticated": authenticated,
|
||||
}
|
||||
|
||||
if authenticated {
|
||||
response["username"] = userInfo.Username
|
||||
response["email"] = userInfo.Email
|
||||
response["groups"] = userInfo.Groups
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, response)
|
||||
})
|
||||
|
||||
e.GET("/query", func(c echo.Context) error {
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"status": "success",
|
||||
"message": "You have access to the query endpoint",
|
||||
})
|
||||
})
|
||||
|
||||
// Create server with fixed address to match Cognito config
|
||||
server := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: e,
|
||||
ReadHeaderTimeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// Log the redirect URL for debugging
|
||||
logger.Info("Using redirect URI", "uri", config.GetAuthRedirectURI())
|
||||
|
||||
return server, config
|
||||
}
|
||||
|
||||
// parseJWTTokenExpiration extracts the expiration time from a JWT token without verification
|
||||
func parseJWTTokenExpiration(tokenString string) (time.Time, error) {
|
||||
// Split JWT into parts
|
||||
parts := strings.Split(tokenString, ".")
|
||||
if len(parts) != 3 {
|
||||
return time.Time{}, fmt.Errorf("invalid JWT format")
|
||||
}
|
||||
|
||||
// Decode payload (second part)
|
||||
payload := parts[1]
|
||||
// Add padding if needed
|
||||
for len(payload)%4 != 0 {
|
||||
payload += "="
|
||||
}
|
||||
|
||||
decoded, err := base64.URLEncoding.DecodeString(payload)
|
||||
if err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to decode JWT payload: %v", err)
|
||||
}
|
||||
|
||||
// Parse JSON payload
|
||||
var claims map[string]interface{}
|
||||
if err := json.Unmarshal(decoded, &claims); err != nil {
|
||||
return time.Time{}, fmt.Errorf("failed to parse JWT claims: %v", err)
|
||||
}
|
||||
|
||||
// Extract expiration time
|
||||
exp, ok := claims["exp"]
|
||||
if !ok {
|
||||
return time.Time{}, fmt.Errorf("no exp claim found in JWT")
|
||||
}
|
||||
|
||||
// Convert to time.Time
|
||||
switch expValue := exp.(type) {
|
||||
case float64:
|
||||
return time.Unix(int64(expValue), 0), nil
|
||||
default:
|
||||
return time.Time{}, fmt.Errorf("invalid exp claim type")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAutoRefreshFlow runs the complete end-to-end test for auto refresh functionality
|
||||
func TestAutoRefreshFlow(t *testing.T) {
|
||||
if os.Getenv("ENABLE_REFRESH_TESTS") != "true" {
|
||||
t.Skip("Skipping auto refresh tests - set ENABLE_REFRESH_TESTS=true to run")
|
||||
}
|
||||
|
||||
// Skip if running in CI environment or short test mode
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration test in short mode")
|
||||
}
|
||||
|
||||
// Setup test environment
|
||||
setupEnvironmentForRefresh(t)
|
||||
|
||||
// Get credentials from environment
|
||||
username := os.Getenv("USER")
|
||||
password := os.Getenv("PASSWORD")
|
||||
|
||||
// Setup test server
|
||||
server, config := setupTestServerForRefresh(t)
|
||||
|
||||
// Start the server in a goroutine
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
t.Logf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Ensure server shuts down at the end of the test
|
||||
defer func() {
|
||||
ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := server.Shutdown(ctx); err != nil {
|
||||
t.Logf("Server shutdown error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// Give the server time to start
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
// Log important URLs for debugging
|
||||
baseURL := "http://localhost:8080"
|
||||
t.Logf("Server URL: %s", baseURL)
|
||||
t.Logf("Login URL: %s%s", baseURL, config.GetAuthLoginPath())
|
||||
t.Logf("Callback URL: %s", config.GetAuthRedirectURI())
|
||||
|
||||
// Get headless mode from environment variable
|
||||
headless := true
|
||||
if os.Getenv("HEADLESS") == "false" {
|
||||
headless = false
|
||||
}
|
||||
|
||||
// Setup Chrome options
|
||||
opts := append(chromedp.DefaultExecAllocatorOptions[:],
|
||||
chromedp.Flag("headless", headless),
|
||||
chromedp.Flag("disable-gpu", true),
|
||||
chromedp.Flag("no-sandbox", true),
|
||||
chromedp.Flag("disable-dev-shm-usage", true),
|
||||
chromedp.Flag("host-blocking-patterns", "*prometheus*"),
|
||||
chromedp.Flag("disable-cache", true),
|
||||
)
|
||||
|
||||
allocCtx, cancel := chromedp.NewExecAllocator(t.Context(), opts...)
|
||||
defer cancel()
|
||||
|
||||
// Create Chrome context with extended timeout for token expiration testing
|
||||
ctx, cancel := chromedp.NewContext(allocCtx, chromedp.WithLogf(log.Printf))
|
||||
defer cancel()
|
||||
ctx, cancel = context.WithTimeout(ctx, 15*time.Minute) // Extended timeout for token expiration
|
||||
defer cancel()
|
||||
|
||||
// Enable request interception for debugging
|
||||
if err := chromedp.Run(ctx, network.Enable()); err != nil {
|
||||
t.Fatalf("Failed to enable network: %v", err)
|
||||
}
|
||||
|
||||
// Store cookies to extract JWT tokens
|
||||
var cookies []*network.Cookie
|
||||
|
||||
// Phase 1: Initial Authentication
|
||||
t.Run("Initial Authentication", func(t *testing.T) {
|
||||
// Navigate to login page
|
||||
loginURL := fmt.Sprintf("%s%s", baseURL, config.GetAuthLoginPath())
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(loginURL),
|
||||
chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to navigate to login page")
|
||||
|
||||
// Submit login credentials
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.WaitVisible(`input[name="username"]`, chromedp.ByQuery),
|
||||
chromedp.Clear(`input[name="username"]`, chromedp.ByQuery),
|
||||
chromedp.SendKeys(`input[name="username"]`, username, chromedp.ByQuery),
|
||||
chromedp.Click(`button[type="submit"], button.amplify-button--primary, input[type="Submit"]`, chromedp.ByQuery),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to submit username")
|
||||
|
||||
// Enter password
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.WaitVisible(`input[name="password"]`, chromedp.ByQuery),
|
||||
chromedp.Clear(`input[name="password"]`, chromedp.ByQuery),
|
||||
chromedp.SendKeys(`input[name="password"]`, password, chromedp.ByQuery),
|
||||
chromedp.Sleep(1*time.Second),
|
||||
chromedp.Click(`button[type="submit"], button.amplify-button--primary, input[type="Submit"]`, chromedp.ByQuery),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to submit password")
|
||||
|
||||
// Wait for redirection to home page
|
||||
err = chromedp.Run(ctx, chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
startTime := time.Now()
|
||||
timeout := 45 * time.Second
|
||||
|
||||
for {
|
||||
if time.Since(startTime) > timeout {
|
||||
return fmt.Errorf("timed out waiting for redirect")
|
||||
}
|
||||
|
||||
var currentURL string
|
||||
if err := chromedp.Location(¤tURL).Do(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if strings.Contains(currentURL, "/home") {
|
||||
return nil
|
||||
}
|
||||
|
||||
if strings.Contains(currentURL, "/login-callback") {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}))
|
||||
assert.NoError(t, err, "Failed to wait for redirect after login")
|
||||
|
||||
// Get cookies
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Sleep(2*time.Second),
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
cookies, err = network.GetCookies().Do(ctx)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to get cookies")
|
||||
|
||||
t.Log("✅ Initial authentication completed successfully")
|
||||
})
|
||||
|
||||
// Phase 2: Extract and Log Token Expiration
|
||||
var authToken string
|
||||
var tokenExpiration time.Time
|
||||
t.Run("Extract Token and Log Expiration", func(t *testing.T) {
|
||||
for _, cookie := range cookies {
|
||||
if cookie.Name == "auth_token" {
|
||||
authToken = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
assert.NotEmpty(t, authToken, "JWT token cookie not found")
|
||||
|
||||
var err error
|
||||
tokenExpiration, err = parseJWTTokenExpiration(authToken)
|
||||
assert.NoError(t, err, "Failed to parse token expiration")
|
||||
|
||||
t.Logf("✅ Token extracted successfully")
|
||||
t.Logf("📅 Token expires at: %s", tokenExpiration.Format(time.RFC3339))
|
||||
t.Logf("⏰ Current time: %s", time.Now().Format(time.RFC3339))
|
||||
t.Logf("⌛ Time until expiration: %s", time.Until(tokenExpiration))
|
||||
t.Logf("🔄 Auto refresh should trigger when token has < 5 minutes remaining")
|
||||
})
|
||||
|
||||
// Phase 3: Wait for Token Expiration
|
||||
t.Run("Wait for Token Expiration", func(t *testing.T) {
|
||||
// Calculate when the token will expire (plus 1 second buffer)
|
||||
timeToWait := time.Until(tokenExpiration) + 1*time.Second
|
||||
|
||||
// If token expires in more than 10 minutes, something might be wrong
|
||||
if timeToWait > 10*time.Minute {
|
||||
t.Logf("⚠️ Token expires in %s, which seems longer than expected for a 5-minute token", timeToWait)
|
||||
t.Logf("🤔 Proceeding anyway, but you may want to check Cognito token settings")
|
||||
}
|
||||
|
||||
if timeToWait > 0 {
|
||||
t.Logf("⏳ Waiting %s for token to expire...", timeToWait)
|
||||
|
||||
// Wait in chunks and log progress
|
||||
start := time.Now()
|
||||
for timeToWait > 0 {
|
||||
if timeToWait > 30*time.Second {
|
||||
time.Sleep(30 * time.Second)
|
||||
timeToWait = time.Until(tokenExpiration) + 1*time.Second
|
||||
elapsed := time.Since(start)
|
||||
t.Logf("⏳ Still waiting... elapsed: %s, remaining: %s", elapsed.Round(time.Second), timeToWait.Round(time.Second))
|
||||
} else {
|
||||
time.Sleep(timeToWait)
|
||||
timeToWait = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("✅ Wait period completed - token should now be expired")
|
||||
t.Logf("⏰ Current time: %s", time.Now().Format(time.RFC3339))
|
||||
})
|
||||
|
||||
// Phase 4: Test Auto Refresh
|
||||
t.Run("Test Auto Refresh Functionality", func(t *testing.T) {
|
||||
// Get cookies before the request
|
||||
var cookiesBefore []*network.Cookie
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var err error
|
||||
cookiesBefore, err = network.GetCookies().Do(ctx)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to get cookies before refresh test")
|
||||
|
||||
// Log all cookies for debugging
|
||||
cookieNames := make([]string, 0, len(cookiesBefore))
|
||||
for _, cookie := range cookiesBefore {
|
||||
cookieNames = append(cookieNames, cookie.Name)
|
||||
}
|
||||
t.Logf("🍪 Cookies before refresh test: %v", cookieNames)
|
||||
|
||||
var originalAuthToken string
|
||||
for _, cookie := range cookiesBefore {
|
||||
if cookie.Name == "auth_token" {
|
||||
originalAuthToken = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if originalAuthToken == "" {
|
||||
t.Logf("⚠️ No auth token found in cookies before refresh test")
|
||||
} else {
|
||||
t.Logf("🔍 Original token (first 20 chars): %s...", originalAuthToken[:20])
|
||||
}
|
||||
|
||||
// Make a request to a protected endpoint to trigger auto refresh
|
||||
queryURL := fmt.Sprintf("%s/query", baseURL)
|
||||
var queryResponseBody string
|
||||
|
||||
t.Logf("🚀 Making request to %s to trigger auto refresh...", queryURL)
|
||||
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.Navigate(queryURL),
|
||||
chromedp.Sleep(5*time.Second), // Give time for auto refresh to complete
|
||||
chromedp.OuterHTML("body", &queryResponseBody),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to access protected endpoint")
|
||||
|
||||
// Get cookies after the request
|
||||
var cookiesAfter []*network.Cookie
|
||||
err = chromedp.Run(ctx,
|
||||
chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
var err error
|
||||
cookiesAfter, err = network.GetCookies().Do(ctx)
|
||||
return err
|
||||
}),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to get cookies after refresh test")
|
||||
|
||||
// Log all cookies after refresh for debugging
|
||||
cookieNamesAfter := make([]string, 0, len(cookiesAfter))
|
||||
for _, cookie := range cookiesAfter {
|
||||
cookieNamesAfter = append(cookieNamesAfter, cookie.Name)
|
||||
}
|
||||
t.Logf("🍪 Cookies after refresh test: %v", cookieNamesAfter)
|
||||
|
||||
// Log the response body for debugging
|
||||
t.Logf("📄 Response body: %s", queryResponseBody)
|
||||
|
||||
// Check if we got a successful response (not a redirect to login)
|
||||
assert.Contains(t, queryResponseBody, "success", "Auto refresh should have succeeded and allowed access to protected endpoint")
|
||||
|
||||
// Find the new auth token
|
||||
var newAuthToken string
|
||||
for _, cookie := range cookiesAfter {
|
||||
if cookie.Name == "auth_token" {
|
||||
newAuthToken = cookie.Value
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
assert.NotEmpty(t, newAuthToken, "Auth token should be present after successful auto refresh")
|
||||
|
||||
if newAuthToken != "" {
|
||||
// Verify the token was refreshed (tokens should be different)
|
||||
if originalAuthToken == newAuthToken {
|
||||
t.Logf("⚠️ Token appears to be the same - auto refresh may not have occurred")
|
||||
t.Logf("🤔 This could happen if the token wasn't actually expired yet")
|
||||
} else {
|
||||
t.Logf("✅ Token was refreshed successfully!")
|
||||
if len(newAuthToken) >= 20 {
|
||||
t.Logf("🔍 New token (first 20 chars): %s...", newAuthToken[:20])
|
||||
} else {
|
||||
t.Logf("🔍 New token: %s", newAuthToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse new token expiration
|
||||
if newAuthToken != "" {
|
||||
newExpiration, err := parseJWTTokenExpiration(newAuthToken)
|
||||
if err == nil {
|
||||
t.Logf("📅 New token expires at: %s", newExpiration.Format(time.RFC3339))
|
||||
t.Logf("⌛ New token valid for: %s", time.Until(newExpiration))
|
||||
|
||||
// Verify new token has a later expiration than original
|
||||
if newExpiration.After(tokenExpiration) {
|
||||
t.Logf("✅ New token expiration is later than original - auto refresh confirmed!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("🎉 Auto refresh test completed successfully!")
|
||||
})
|
||||
|
||||
// Phase 5: Verify Continued Access to Protected Endpoint
|
||||
t.Run("Verify Continued Access After Refresh", func(t *testing.T) {
|
||||
// Make another request to a protected endpoint to ensure the refreshed token works
|
||||
queryURL := fmt.Sprintf("%s/query", baseURL)
|
||||
var secondResponseBody string
|
||||
|
||||
err := chromedp.Run(ctx,
|
||||
chromedp.Navigate(queryURL),
|
||||
chromedp.Sleep(3*time.Second),
|
||||
chromedp.OuterHTML("body", &secondResponseBody),
|
||||
)
|
||||
assert.NoError(t, err, "Failed to access protected endpoint after refresh")
|
||||
|
||||
assert.Contains(t, secondResponseBody, "success", "Should continue to have access to protected endpoints after auto refresh")
|
||||
t.Logf("✅ Continued access to protected endpoint verified")
|
||||
})
|
||||
}
|
||||
@@ -20,6 +20,7 @@ require (
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.1.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.2 // indirect
|
||||
@@ -31,12 +32,16 @@ require (
|
||||
github.com/mailru/easyjson v0.9.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/permitio/permit-golang v1.2.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/segmentio/asm v1.2.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/crypto v0.36.0 // indirect
|
||||
golang.org/x/net v0.36.0 // indirect
|
||||
golang.org/x/oauth2 v0.27.0 // indirect
|
||||
golang.org/x/sys v0.31.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
|
||||
@@ -19,6 +19,8 @@ github.com/gobwas/ws v1.1.0 h1:7RFti/xnNkMJnrK7D1yQ/iCIB5OrrY/54/H930kIbHA=
|
||||
github.com/gobwas/ws v1.1.0/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
@@ -48,6 +50,8 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw=
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/permitio/permit-golang v1.2.5 h1:5XdT5ziFjmWh+GJ2WsCuv4qfEDXdKltknZ7iGnzdpFM=
|
||||
github.com/permitio/permit-golang v1.2.5/go.mod h1:U3ytJkUh6mH7dPiBt7cWbVVsRSxAiJtnuL7FFhbDk8s=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
@@ -62,10 +66,16 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||
golang.org/x/net v0.36.0 h1:vWF2fRbw4qslQsQzgFqZff+BItCvGFQqKzKIzx1rmoA=
|
||||
golang.org/x/net v0.36.0/go.mod h1:bFmbeoIPfrw4sMHNhb4J9f6+tPziuGjq7Jk/38fxi1I=
|
||||
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
|
||||
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||
|
||||
@@ -9,9 +9,12 @@ It's not enough to simply add the Go package - you need the actual browser binar
|
||||
|
||||
This is why this module must not pollute the top level modules go.mod. If it did, then all of the docker containers
|
||||
for this entire project would have to have a massive amount of chrome dependencies added instead of just being
|
||||
able to run on a scrath container.
|
||||
able to run on a scratch container.
|
||||
|
||||
To run the tests just run `./test.sh` from this directory.
|
||||
Also the local permit.io pdp must also be running.
|
||||
This can be started by running `demo.sh` in
|
||||
`/cmd/cognito_test/permit_test/permit.harness`
|
||||
|
||||
## Obtaining a reusable JWT token
|
||||
If you observe the log output of running test.sh and look for
|
||||
|
||||
@@ -160,12 +160,55 @@ func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
|
||||
|
||||
// Extract JWT token from header or cookie
|
||||
tokenStr, err := extractToken(c, config)
|
||||
|
||||
// If token extraction fails, try refresh before returning 401
|
||||
if err != nil {
|
||||
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(),
|
||||
})
|
||||
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.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(),
|
||||
})
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -195,6 +238,12 @@ func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
|
||||
})
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
Reference in New Issue
Block a user