in progress
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"queryorchestration/internal/serviceconfig/auth"
|
||||
|
||||
@@ -148,3 +149,109 @@ func GetUserSubject(c echo.Context) (string, bool) {
|
||||
|
||||
return subject, true
|
||||
}
|
||||
|
||||
// setTokenCookies sets both access and refresh token cookies.
|
||||
//
|
||||
// This function creates HTTP-only cookies for both the access token and refresh token.
|
||||
// The access token cookie expires with the token, while the refresh token cookie
|
||||
// has a longer expiration (30 days) to allow for token refresh operations.
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Echo context for setting cookies
|
||||
// - tokens: TokenResponse containing both access and refresh tokens
|
||||
// - 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) {
|
||||
// Set access token cookie (shorter expiration)
|
||||
accessTokenCookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: tokens.AccessToken,
|
||||
Path: "/",
|
||||
Expires: accessTokenExpiresAt,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
}
|
||||
c.SetCookie(accessTokenCookie)
|
||||
|
||||
// Set refresh token cookie (longer expiration - 30 days)
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: tokens.RefreshToken,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
}
|
||||
c.SetCookie(refreshTokenCookie)
|
||||
|
||||
config.GetAuthLogger().Debug("Token cookies set",
|
||||
"access_token_expires", accessTokenExpiresAt,
|
||||
"refresh_token_expires", refreshTokenCookie.Expires)
|
||||
}
|
||||
|
||||
// clearTokenCookies removes both access and refresh token cookies.
|
||||
//
|
||||
// This function clears authentication cookies by setting them to empty values
|
||||
// with past expiration dates, effectively removing them from the client.
|
||||
//
|
||||
// Parameters:
|
||||
// - 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)
|
||||
|
||||
// Clear access token cookie
|
||||
accessTokenCookie := &http.Cookie{
|
||||
Name: "auth_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: pastTime,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
}
|
||||
c.SetCookie(accessTokenCookie)
|
||||
|
||||
// Clear refresh token cookie
|
||||
refreshTokenCookie := &http.Cookie{
|
||||
Name: "refresh_token",
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: pastTime,
|
||||
HttpOnly: true,
|
||||
Secure: isSecureContext(config),
|
||||
}
|
||||
c.SetCookie(refreshTokenCookie)
|
||||
|
||||
config.GetAuthLogger().Debug("Token cookies cleared")
|
||||
}
|
||||
|
||||
// getRefreshTokenFromCookie extracts the refresh token from the request cookies.
|
||||
//
|
||||
// Parameters:
|
||||
// - c: The Echo context containing the HTTP request
|
||||
//
|
||||
// Returns:
|
||||
// - string: The refresh token value, or empty string if not found
|
||||
func getRefreshTokenFromCookie(c echo.Context) string {
|
||||
refreshCookie, err := c.Cookie("refresh_token")
|
||||
if err != nil || refreshCookie.Value == "" {
|
||||
return ""
|
||||
}
|
||||
return refreshCookie.Value
|
||||
}
|
||||
|
||||
// isSecureContext determines if cookies should be marked as Secure.
|
||||
//
|
||||
// In production environments, cookies should be marked as Secure to ensure
|
||||
// they are only sent over HTTPS connections.
|
||||
//
|
||||
// Parameters:
|
||||
// - config: Configuration provider for auth settings
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if cookies should be marked as Secure
|
||||
func isSecureContext(config auth.ConfigProvider) bool {
|
||||
// For now, return false to match existing comment in original code
|
||||
// In production, this should return true based on environment detection
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -147,6 +147,69 @@ func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.Co
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
|
||||
// refreshTokens exchanges a refresh token for new access and refresh tokens.
|
||||
//
|
||||
// This function implements the OAuth 2.0 refresh token grant flow with AWS Cognito:
|
||||
// 1. Builds a token refresh request with the refresh token and client credentials
|
||||
// 2. Sends the request to the Cognito token endpoint
|
||||
// 3. Processes the response to extract the new tokens
|
||||
//
|
||||
// Parameters:
|
||||
// - refreshToken: The refresh token received during initial authentication
|
||||
// - config: Configuration provider that supplies auth-related settings
|
||||
//
|
||||
// Returns:
|
||||
// - A pointer to a TokenResponse containing new ID token, access token, and refresh token
|
||||
// - An error if the token refresh fails for any reason
|
||||
func refreshTokens(refreshToken string, config auth.ConfigProvider) (*TokenResponse, error) {
|
||||
data := url.Values{}
|
||||
data.Set("grant_type", "refresh_token")
|
||||
data.Set("client_id", config.GetAuthClientID())
|
||||
data.Set("refresh_token", refreshToken)
|
||||
|
||||
config.GetAuthLogger().Debug("Token refresh request details",
|
||||
"url", config.GetAuthTokenURL(),
|
||||
"client_id", config.GetAuthClientID())
|
||||
|
||||
req, err := http.NewRequest("POST", config.GetAuthTokenURL(), strings.NewReader(data.Encode()))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create token refresh request: %w", err)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
|
||||
|
||||
// Add Authorization header if client secret is provided
|
||||
if config.GetAuthClientSecret() != "" {
|
||||
req.SetBasicAuth(config.GetAuthClientID(), config.GetAuthClientSecret())
|
||||
config.GetAuthLogger().Debug("Using Basic Auth authentication for refresh")
|
||||
} else {
|
||||
config.GetAuthLogger().Debug("Using public client authentication for refresh (no secret)")
|
||||
}
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("token refresh request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read token refresh response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("token refresh endpoint returned status %d: %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var tokenResponse TokenResponse
|
||||
if err := json.Unmarshal(body, &tokenResponse); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token refresh response: %w", err)
|
||||
}
|
||||
|
||||
return &tokenResponse, nil
|
||||
}
|
||||
|
||||
// handleOAuthCallback processes the callback from Cognito after a user has authenticated.
|
||||
//
|
||||
// This function completes the OAuth 2.0 authorization code flow with PKCE by:
|
||||
@@ -231,16 +294,8 @@ func handleOAuthCallback(c echo.Context, config auth.ConfigProvider) error {
|
||||
// Calculate token expiration time based on token's expires_in value
|
||||
expiresAt := time.Now().Add(time.Duration(tokens.ExpiresIn) * time.Second)
|
||||
|
||||
// Set a cookie with the access token
|
||||
tokenCookie := new(http.Cookie)
|
||||
tokenCookie.Name = "auth_token"
|
||||
tokenCookie.Value = tokens.AccessToken
|
||||
tokenCookie.Path = "/"
|
||||
tokenCookie.Expires = expiresAt
|
||||
tokenCookie.HttpOnly = true // Not accessible via JavaScript
|
||||
// In production, set Secure to true
|
||||
// tokenCookie.Secure = true
|
||||
c.SetCookie(tokenCookie)
|
||||
// Set cookies with both access and refresh tokens
|
||||
setTokenCookies(c, tokens, expiresAt, config)
|
||||
|
||||
// Extract username for display purposes (optional)
|
||||
username, _ := idTokenClaims["cognito:username"].(string)
|
||||
|
||||
@@ -168,6 +168,46 @@ func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
|
||||
})
|
||||
}
|
||||
|
||||
// Check if token is expired and attempt refresh if needed
|
||||
if isTokenExpired(tokenStr) {
|
||||
config.GetAuthLogger().Debug("Access token expired, attempting refresh")
|
||||
|
||||
refreshToken := getRefreshTokenFromCookie(c)
|
||||
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(),
|
||||
})
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -226,15 +266,9 @@ func JWTAuthMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
// if logout then remove the auth cookie
|
||||
// if logout then remove both auth cookies
|
||||
if c.Path() == config.GetAuthLogoutPath() {
|
||||
cookie := new(http.Cookie)
|
||||
cookie.Name = "auth_token"
|
||||
cookie.Value = ""
|
||||
cookie.Path = "/"
|
||||
cookie.Expires = time.Now().Add(-1 * time.Hour) // Set expiry in the past
|
||||
cookie.HttpOnly = true
|
||||
c.SetCookie(cookie)
|
||||
clearTokenCookies(c, config)
|
||||
}
|
||||
|
||||
// Skip for home and logout as they should be accessible without auth
|
||||
|
||||
@@ -5,12 +5,13 @@ A reusable Go package for AWS Cognito authentication and Permit.io authorization
|
||||
## Features
|
||||
|
||||
- Complete OAuth 2.0 PKCE flow for AWS Cognito authentication
|
||||
- **Automatic JWT token refresh** for seamless user experience
|
||||
- JWT token validation and verification
|
||||
- Dynamic route-based authorization using Permit.io Policy Decision Point (PDP)
|
||||
- Automatic resource mapping from HTTP routes to Permit.io resources
|
||||
- HTTP method to action mapping for authorization checks
|
||||
- Middleware for token handling and permission enforcement
|
||||
- Cookie-based token storage
|
||||
- Cookie-based token storage with refresh token support
|
||||
- Default home page with authentication status
|
||||
- Simple integration with Echo framework
|
||||
|
||||
@@ -79,12 +80,47 @@ The authentication system uses two middleware components that must be registered
|
||||
|
||||
2. **`TokenValidationMiddleware`** (Second)
|
||||
- **Performs authentication**: Validates JWT tokens and extracts user claims
|
||||
- **Automatic token refresh**: Detects expired tokens and refreshes them transparently
|
||||
- **Performs authorization**: Checks permissions via Permit.io PDP
|
||||
- Returns JSON error responses for authentication/authorization failures
|
||||
- Sets user context for subsequent handlers
|
||||
|
||||
**Why order matters**: The first middleware prepares the request for token validation, while the second middleware performs the actual **authentication and authorization enforcement**.
|
||||
|
||||
### Automatic Token Refresh
|
||||
|
||||
The system provides seamless token refresh functionality to maintain user sessions without interruption:
|
||||
|
||||
#### How It Works
|
||||
1. **Proactive Refresh**: Tokens are automatically refreshed 5 minutes before expiration
|
||||
2. **Transparent Process**: Users never experience session interruptions
|
||||
3. **Dual Cookie Storage**: Both access tokens (short-lived) and refresh tokens (30-day) are stored as HTTP-only cookies
|
||||
4. **Fallback Handling**: When refresh fails, users are gracefully redirected to login
|
||||
|
||||
#### Token Storage Strategy
|
||||
- **Access Token Cookie**: `auth_token` - expires with the JWT token (typically 1 hour)
|
||||
- **Refresh Token Cookie**: `refresh_token` - longer expiration (30 days)
|
||||
- **Security**: Both cookies are HTTP-only and secure in production environments
|
||||
|
||||
#### Refresh Process Flow
|
||||
1. **Token Expiration Check**: Middleware checks if access token expires within 5 minutes
|
||||
2. **Refresh Token Retrieval**: Extracts refresh token from secure cookie
|
||||
3. **Token Exchange**: Uses OAuth 2.0 refresh grant to get new tokens from Cognito
|
||||
4. **Cookie Update**: Sets new access and refresh tokens in cookies
|
||||
5. **Request Continuation**: Processes the original request with fresh tokens
|
||||
|
||||
#### Error Handling
|
||||
- **Missing Refresh Token**: Redirects to login with "Session expired" message
|
||||
- **Invalid Refresh Token**: Clears cookies and redirects to login
|
||||
- **Cognito Service Issues**: Logs errors and falls back to re-authentication
|
||||
- **Network Errors**: Retries on next request, maintains user experience
|
||||
|
||||
#### Session Expiration Scenarios
|
||||
- **Access Token Expired + Valid Refresh Token**: Automatic refresh (transparent)
|
||||
- **Both Tokens Expired**: Redirect to login with clear messaging
|
||||
- **Refresh Token Invalid**: Clear cookies and redirect to login
|
||||
- **User Logout**: Both cookies are immediately cleared
|
||||
|
||||
### Client Caching
|
||||
The system implements efficient client caching for Permit.io connections:
|
||||
- **Singleton pattern**: One client instance per unique (apiKey, pdpURL) combination
|
||||
@@ -120,7 +156,18 @@ The system performs comprehensive validation at startup to ensure proper configu
|
||||
}
|
||||
```
|
||||
- **Status Code**: `401 Unauthorized`
|
||||
- **When**: Missing, invalid, or expired JWT tokens
|
||||
- **When**: Missing, invalid, or malformed JWT tokens
|
||||
|
||||
**Session Expiration Errors** (Token refresh failed):
|
||||
```json
|
||||
{
|
||||
"error": "Session expired",
|
||||
"message": "Your session has expired. Please log in again.",
|
||||
"login_url": "/login"
|
||||
}
|
||||
```
|
||||
- **Status Code**: `401 Unauthorized`
|
||||
- **When**: Access token expired and refresh token is missing or invalid
|
||||
|
||||
**Authorization Errors** (Valid token, insufficient permissions):
|
||||
```json
|
||||
|
||||
@@ -10,9 +10,9 @@ This package uses AWS Cognito for authentication and Permit.io for dynamic autho
|
||||
|
||||
- **auth.go**: Main exports for the package and middleware registration.
|
||||
- **handler.go**: HTTP handlers for login, callback, logout, and home.
|
||||
- **middleware.go**: Echo middleware for JWT extraction, validation, and Permit.io authorization enforcement.
|
||||
- **middleware.go**: Echo middleware for JWT extraction, validation, automatic token refresh, and Permit.io authorization enforcement.
|
||||
- **jwks.go**: Handles JWKS key fetching and JWT signature validation.
|
||||
- **token.go**: Token verification and management.
|
||||
- **token.go**: Token verification, expiration checking, and refresh token management.
|
||||
- **models.go**: Data models and types for tokens, users, etc.
|
||||
- **permitio.go**: Permit.io client integration and authorization logic.
|
||||
- **mapping.go**: Route-to-resource and HTTP method-to-action mapping.
|
||||
@@ -58,6 +58,7 @@ The cognito client must be setup to redirect back to /login-callback for PKCE fl
|
||||
6. **Request Flow**:
|
||||
- User requests a protected endpoint.
|
||||
- Middleware extracts JWT from cookies/headers and validates it.
|
||||
- **Token Refresh**: If token expires within 5 minutes, automatically refresh using stored refresh token.
|
||||
- Route is mapped to resource (first path segment) and HTTP method to action.
|
||||
- Permit.io PDP is queried: `CheckPermission(user_subject, action, resource)`.
|
||||
- If authorized, the request proceeds to the handler.
|
||||
@@ -167,4 +168,35 @@ sequenceDiagram
|
||||
Cognito->>AppServer: 14. Tokens response
|
||||
Note over AppServer: 15. Verify tokens<br/>Check permissions
|
||||
AppServer->>Browser: 16. Authentication successful
|
||||
```
|
||||
```
|
||||
|
||||
## Token Refresh Flow
|
||||
|
||||
The system implements automatic token refresh to maintain seamless user sessions:
|
||||
|
||||
### Token Storage Strategy
|
||||
- **Access Token**: Short-lived (1 hour), stored in `auth_token` cookie
|
||||
- **Refresh Token**: Long-lived (30 days), stored in `refresh_token` cookie
|
||||
- **Security**: Both cookies are HTTP-only and secure in production
|
||||
|
||||
### Automatic Refresh Process
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Browser
|
||||
participant Middleware as TokenValidationMiddleware
|
||||
participant Cognito
|
||||
|
||||
Browser->>Middleware: 1. API Request with expired token
|
||||
Note over Middleware: 2. Check token expiration<br/>(5 min buffer)
|
||||
Middleware->>Middleware: 3. Extract refresh token from cookie
|
||||
Middleware->>Cognito: 4. POST /oauth2/token<br/>grant_type=refresh_token
|
||||
Cognito->>Middleware: 5. New tokens response
|
||||
Note over Middleware: 6. Update cookies with new tokens<br/>Set Authorization header
|
||||
Middleware->>Browser: 7. Process original request<br/>Return response with new cookies
|
||||
```
|
||||
|
||||
### Key Benefits
|
||||
- **Transparent**: Users never experience session interruptions
|
||||
- **Proactive**: Tokens refreshed 5 minutes before expiration
|
||||
- **Secure**: HTTP-only cookies prevent XSS attacks
|
||||
- **Fallback**: Graceful degradation to login when refresh fails
|
||||
@@ -282,3 +282,41 @@ func ExtractUserInfo(claims map[string]interface{}) UserInfo {
|
||||
|
||||
return userInfo
|
||||
}
|
||||
|
||||
// isTokenExpired checks if a JWT token is expired or will expire soon.
|
||||
//
|
||||
// This function parses the JWT token without verification to extract the
|
||||
// expiration claim and checks if the token is expired or will expire within
|
||||
// a buffer period (5 minutes). This allows for proactive token refresh.
|
||||
// For malformed tokens, it returns false to let the normal validation handle them.
|
||||
//
|
||||
// Parameters:
|
||||
// - tokenString: The JWT token string to check
|
||||
//
|
||||
// Returns:
|
||||
// - bool: True if the token is expired or will expire soon, false otherwise
|
||||
func isTokenExpired(tokenString string) bool {
|
||||
if tokenString == "" {
|
||||
return false // Let normal validation handle empty tokens
|
||||
}
|
||||
|
||||
// Parse token without signature verification to get claims
|
||||
parsedToken, err := jwt.Parse(
|
||||
[]byte(tokenString),
|
||||
jwt.WithVerify(false), // Skip signature verification
|
||||
jwt.WithValidate(false), // Skip expiration validation (we'll check manually)
|
||||
)
|
||||
if err != nil {
|
||||
return false // Let normal validation handle malformed tokens
|
||||
}
|
||||
|
||||
// Extract expiration time
|
||||
exp := parsedToken.Expiration()
|
||||
if exp.IsZero() {
|
||||
return false // Let normal validation handle tokens without expiration
|
||||
}
|
||||
|
||||
// Check if token is expired or will expire within 5 minutes (buffer)
|
||||
bufferTime := 5 * time.Minute
|
||||
return time.Now().Add(bufferTime).After(exp)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user