254 lines
10 KiB
Markdown
254 lines
10 KiB
Markdown
# Cognito Auth
|
|
|
|
A reusable Go package for AWS Cognito authentication and Permit.io authorization with Echo framework using PKCE flow.
|
|
|
|
## 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 with refresh token support
|
|
- Default home page with authentication status
|
|
- Simple integration with Echo framework
|
|
|
|
## Feature flag for Auth
|
|
To disable all of these features for all endpoints you must set
|
|
`DISABLE_AUTH=true` in your environment.
|
|
|
|
## Package details
|
|
For a detailed explanation of all of the files in this
|
|
package see the [summary](./summary.md) file.
|
|
|
|
## Configuration
|
|
|
|
The package reads configuration from environment variables:
|
|
|
|
### AWS Cognito (Authentication)
|
|
- `COGNITO_CLIENT_ID`: Your AWS Cognito App Client ID
|
|
- `COGNITO_CLIENT_SECRET`: Your AWS Cognito App Client Secret
|
|
- `COGNITO_USER_POOL_ID`: Your AWS Cognito User Pool ID
|
|
- `COGNITO_DOMAIN`: Your AWS Cognito domain
|
|
- `AWS_REGION`: AWS region where your Cognito User Pool is located (default: us-east-2)
|
|
- `COGNITO_REGION`: AWS region for Cognito service (default: us-east-2)
|
|
|
|
### Permit.io (Authorization)
|
|
- `PERMIT_IO_API_KEY`: Your Permit.io API key for accessing the PDP
|
|
- `PERMIT_IO_PDP_URL`: URL of your Permit.io Policy Decision Point (default: `http://localhost:7766`)
|
|
|
|
### General
|
|
- `DEBUG`: Set to "true" for debug logging
|
|
|
|
### Environment Variable Loading
|
|
Environment variables are loaded in the following hierarchy (first found wins):
|
|
1. **System environment variables**
|
|
2. **devbox.json "env" section** - Development environment defaults
|
|
3. **.env file** - Local overrides and secrets (loaded via `env_from` in devbox.json)
|
|
|
|
## Feature flags
|
|
|
|
### Disable Authentication
|
|
You can disable all of the auth features with the following environment variable:
|
|
`DISABLE_AUTH=true`
|
|
|
|
### Test Mode (JWT Validation Bypass)
|
|
For testing purposes only, you can bypass JWT signature validation with:
|
|
`NO_JWT_VALIDATION=true`
|
|
|
|
**Warning**: This flag should ONLY be used in testing environments. When enabled:
|
|
- JWT signature validation is bypassed
|
|
- Token expiration is ignored
|
|
- Unsigned test tokens can be used for authorization testing
|
|
- The system logs warnings when this mode is active
|
|
|
|
This allows testing of authorization logic without requiring valid AWS Cognito tokens.
|
|
|
|
## Middleware Architecture
|
|
|
|
### Middleware Chain Order
|
|
The authentication system uses two middleware components that must be registered in the correct order:
|
|
|
|
1. **`JWTAuthMiddleware`** (First)
|
|
- Converts JWT tokens from cookies to Authorization headers
|
|
- Skips processing for public paths (`/login`, `/logout`, `/home`, `/swagger/*`)
|
|
- Handles logout by clearing auth cookies
|
|
- **Does NOT perform authentication** - only prepares the request
|
|
- Passes requests to the next middleware in the chain
|
|
|
|
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
|
|
- **Thread-safe**: Uses `sync.RWMutex` for concurrent access protection
|
|
- **Automatic cleanup**: Clients are reused across requests to the same PDP
|
|
|
|
## Startup Validation
|
|
|
|
The system performs comprehensive validation at startup to ensure proper configuration:
|
|
|
|
### Configuration Validation (`ValidatePermitIOConfiguration`)
|
|
1. **Skip validation**: If `DISABLE_AUTH=true` (case-insensitive)
|
|
2. **PDP connectivity test**: HTTP health check to `/healthy` endpoint
|
|
3. **API key validation**: Quick permission check to verify API key validity
|
|
4. **Error handling**: Application halts if validation fails
|
|
|
|
### Validation Process
|
|
- **PDP URL**: Uses `PERMIT_IO_PDP_URL` or defaults to `http://localhost:7766`
|
|
- **API key**: Requires `PERMIT_IO_API_KEY` environment variable
|
|
- **Health check**: Tests connectivity with 10-second timeout
|
|
- **Permission test**: Validates API key with dummy authorization request
|
|
|
|
## Error Handling
|
|
|
|
### Authentication vs Authorization Errors
|
|
|
|
**Authentication Errors** (No valid JWT token):
|
|
```json
|
|
{
|
|
"error": "Authorization required",
|
|
"message": "This endpoint requires authentication. Please obtain a valid JWT token.",
|
|
"login_url": "/login"
|
|
}
|
|
```
|
|
- **Status Code**: `401 Unauthorized`
|
|
- **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
|
|
{
|
|
"error": "Insufficient permissions",
|
|
"message": "Access denied by authorization service",
|
|
"user": "user-subject-id",
|
|
"resource": "document",
|
|
"action": "get"
|
|
}
|
|
```
|
|
- **Status Code**: `403 Forbidden`
|
|
- **When**: Valid JWT but Permit.io denies access
|
|
|
|
### Error Response Format
|
|
All error responses return JSON with consistent structure:
|
|
- **error**: Brief error identifier
|
|
- **message**: Human-readable description
|
|
- **Additional fields**: Context-specific information (user, resource, action, etc.)
|
|
|
|
## Testing
|
|
|
|
### Test Mode JWT Handling
|
|
When `NO_JWT_VALIDATION=true` is set:
|
|
- **Pretty-printed JWTs**: Handles formatted JSON payloads with newlines/tabs
|
|
- **2-part JWT format**: Automatically handles `header.payload` format (adds empty signature)
|
|
- **Unsigned tokens**: Bypasses signature verification for testing
|
|
- **Flexible parsing**: More permissive token parsing for development
|
|
|
|
### Running Integration Tests
|
|
```bash
|
|
# Requires local PDP and API key
|
|
task test:permitio
|
|
|
|
# Or directly with verbose output
|
|
go test -tags=permitio -parallel 2 -count=1 -v ./internal/cognitoauth
|
|
```
|
|
|
|
### Test Environment Requirements
|
|
- **Local PDP**: Permit.io Policy Decision Point running on port 7766
|
|
- **API key**: Valid `PERMIT_IO_API_KEY` in environment or `.env` file
|
|
- **Test data**: Configured users, resources, and permissions in Permit.io
|
|
|
|
## Authorization System
|
|
|
|
### Permit.io Integration
|
|
This package uses Permit.io for dynamic authorization instead of static group-based permissions. The system:
|
|
|
|
1. **Authenticates** users via AWS Cognito (JWT tokens)
|
|
2. **Authorizes** requests via Permit.io Policy Decision Point (PDP)
|
|
3. **Maps** HTTP routes to Permit.io resources automatically
|
|
4. **Converts** HTTP methods to Permit.io actions
|
|
|
|
### Automatic Resource Mapping
|
|
The system automatically maps HTTP routes to Permit.io resources using the following logic:
|
|
|
|
- **Extracts the first path segment** from the route as the resource name
|
|
- **Removes leading slashes** to match Permit.io naming conventions
|
|
- **Ignores path parameters and nested segments**
|
|
|
|
#### Examples:
|
|
- `/document` → `document`
|
|
- `/document/123` → `document`
|
|
- `/document/foo/123` → `document`
|
|
- `/api/v1/users` → `api`
|
|
- `/query-endpoint/run` → `query-endpoint`
|
|
|
|
### HTTP Method to Action Mapping
|
|
HTTP methods are mapped to lowercase Permit.io actions:
|
|
|
|
- `GET` → `get`
|
|
- `POST` → `post`
|
|
- `PUT` → `put`
|
|
- `PATCH` → `patch`
|
|
- `DELETE` → `delete`
|
|
|
|
### Authorization Flow
|
|
1. User makes request to protected endpoint
|
|
2. Middleware extracts JWT token and validates it with Cognito
|
|
3. User subject (`sub` claim) is extracted from JWT
|
|
4. Route is mapped to resource name (first path segment)
|
|
5. HTTP method is mapped to action name
|
|
6. Permit.io PDP is queried: `CheckPermission(user, action, resource)`
|
|
7. Request is allowed/denied based on PDP response |