functionally complete

needs cleanup next
This commit is contained in:
jay brown
2025-07-18 10:40:05 -07:00
parent 94718b3d1f
commit b56c67f807
7 changed files with 614 additions and 15 deletions
+37 -6
View File
@@ -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(&currentURL).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")
})
}
+5
View File
@@ -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
+10
View File
@@ -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=
+4 -1
View File
@@ -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
+54 -5
View File
@@ -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)