This commit is contained in:
jay brown
2025-04-24 13:28:49 -07:00
parent e55585bae1
commit 525d3e0b7b
9 changed files with 305 additions and 67 deletions
+3 -7
View File
@@ -2,12 +2,8 @@
All cognito related research code here. Not suitable for production. Only for confirming things
work before placing them in the library.
## Jwtstuff
Contains just code for low level jwt operations.
Generate the private and public keys then run `go run main.go` to execute the jwtstuff code.
## cognito_test
This is the harness to demonstrate running all of the cognito auth and RBAC in a simple standalone
main.go. This is standalong documentation for the library contained in `internal/cognitoauth`.
running main will print out the token which you can decode with https://fusionauth.io/dev-tools/jwt-decoder manually.
# Generation of test private and public key for testing
openssl genpkey -algorithm RSA -out private_key.pem
openssl rsa -pubout -in private_key.pem -out public_key.pem
+2
View File
@@ -136,4 +136,6 @@ It will identify the type of event and the location of said event, and forward t
It supports the document initialisation and document text processing stages. Events may be pushed to the DOCINIT queue.
## cognito.auth.harness
Generated by Doczy
+39 -4
View File
@@ -14,7 +14,13 @@ import (
"github.com/lestrrat-go/jwx/v2/jwt"
)
// RegisterRoutes registers all authentication-related routes to the Echo engine
// RegisterRoutes registers all authentication-related routes to the Echo engine.
// It configures login and callback endpoints, and applies JWT authentication middleware.
// If the DISABLE_AUTH environment variable is set to "true", authentication is bypassed.
//
// Parameters:
// - e: The Echo instance to register routes on
// - config: Configuration provider for authentication settings
func RegisterRoutes(e *echo.Echo, config auth.ConfigProvider) {
// check the auth feature flag
fmt.Println("Registering routes for Auth.")
@@ -54,7 +60,16 @@ func RegisterRoutes(e *echo.Echo, config auth.ConfigProvider) {
e.Use(TokenValidationMiddleware(config))
}
// GetTokenFromRequest extracts the token from the request
// GetTokenFromRequest extracts the JWT token from the HTTP request.
// It first attempts to retrieve the token from the Authorization header
// in Bearer token format. If not found, it then tries to retrieve the token
// from the "auth_token" cookie. Returns an empty string if no token is found.
//
// Parameters:
// - c: The Echo context containing the HTTP request
//
// Returns:
// - string: The extracted JWT token, or an empty string if not found
func GetTokenFromRequest(c echo.Context) string {
// First try from Authorization header
authHeader := c.Request().Header.Get("Authorization")
@@ -71,7 +86,17 @@ func GetTokenFromRequest(c echo.Context) string {
return ""
}
// GetUserInfo gets user information from the context
// GetUserInfo retrieves user information from the Echo context.
// It first attempts to retrieve user info directly from the context.
// If not found there, it tries to extract and parse the JWT token,
// then extract user information from the token claims.
//
// Parameters:
// - c: The Echo context containing the HTTP request and context values
//
// Returns:
// - UserInfo: Structure containing username, email, and groups
// - bool: True if user information was successfully retrieved, false otherwise
func GetUserInfo(c echo.Context) (UserInfo, bool) {
// Try to get user info from context
userInfo, ok := c.Get("user_info").(UserInfo)
@@ -101,7 +126,17 @@ func GetUserInfo(c echo.Context) (UserInfo, bool) {
return userInfo, true
}
// RequireGroups creates a middleware that checks if the user is in any of the specified groups
// RequireGroups creates a middleware that enforces group-based authorization.
// It checks if the authenticated user belongs to at least one of the specified groups.
// If the user is not authenticated, it returns a 401 Unauthorized response.
// If the user is authenticated but doesn't belong to any of the required groups,
// it returns a 403 Forbidden response with detailed information.
//
// Parameters:
// - groups: Variable number of group names that grant access
//
// Returns:
// - echo.MiddlewareFunc: Middleware function for Echo framework
func RequireGroups(groups ...string) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
+52 -8
View File
@@ -16,8 +16,22 @@ import (
"github.com/labstack/echo/v4"
)
// initiateLoginWithPKCE generates PKCE code challenge and redirects to Cognito
// This function starts the OAuth authorization flow with PKCE
// initiateLoginWithPKCE initiates the OAuth 2.0 authorization flow with PKCE (Proof Key for Code Exchange)
// for Amazon Cognito authentication.
//
// This function:
// 1. Generates a random state parameter to prevent CSRF attacks
// 2. Creates a code verifier and its corresponding code challenge for PKCE
// 3. Stores the PKCE parameters in a session for later verification
// 4. Builds the authorization URL with all required OAuth and PKCE parameters
// 5. Redirects the user to the Cognito login page
//
// Parameters:
// - c: The Echo context for handling the HTTP request and response
// - config: Configuration provider that supplies auth-related settings
//
// Returns:
// - An error if any step of the process fails, or nil on successful redirect
func initiateLoginWithPKCE(c echo.Context, config auth.ConfigProvider) error {
// Generate random state parameter to prevent CSRF
state, err := generateRandomString(32)
@@ -63,9 +77,24 @@ func initiateLoginWithPKCE(c echo.Context, config auth.ConfigProvider) error {
return c.Redirect(http.StatusFound, authURL)
}
// exchangeCodeForTokensWithPKCE exchanges the authorization code for tokens using PKCE
// Called during the OAuth callback flow to get tokens from the authorization code
// Makes an HTTP request to Cognito's token endpoint to perform this exchange
// exchangeCodeForTokensWithPKCE exchanges an authorization code for OAuth tokens using PKCE.
//
// This function performs the second part of the OAuth 2.0 authorization code flow with PKCE:
// 1. Builds a token request with the authorization code, code verifier, and other required parameters
// 2. Sends the request to the Cognito token endpoint
// 3. Processes the response to extract the tokens
//
// It handles both confidential clients (with a client secret) and public clients (without a secret)
// by adapting the authentication method used with the token endpoint.
//
// Parameters:
// - authCode: The authorization code received from the OAuth provider (Cognito)
// - codeVerifier: The original code verifier that was used to generate the code challenge
// - config: Configuration provider that supplies auth-related settings
//
// Returns:
// - A pointer to a TokenResponse containing the ID token, access token, and refresh token
// - An error if the token exchange fails for any reason
func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.ConfigProvider) (*TokenResponse, error) {
data := url.Values{}
data.Set("grant_type", "authorization_code")
@@ -118,9 +147,24 @@ func exchangeCodeForTokensWithPKCE(authCode, codeVerifier string, config auth.Co
return &tokenResponse, nil
}
// handleOAuthCallback handles the OAuth 2.0 authorization code flow callback with PKCE
// Called when Cognito redirects back to our application with an authorization code
// Exchanges the code for tokens, verifies them, and redirects to home page with token in cookie
// handleOAuthCallback processes the callback from Cognito after a user has authenticated.
//
// This function completes the OAuth 2.0 authorization code flow with PKCE by:
// 1. Extracting and validating the authorization code and state parameters
// 2. Retrieving the stored code verifier associated with the state parameter
// 3. Exchanging the authorization code for tokens using the code verifier
// 4. Verifying the received tokens using the JSON Web Key Set (JWKS)
// 5. Extracting user information and group memberships from the tokens
// 6. Checking if the user has the required permissions based on their group memberships
// 7. Setting the access token as a cookie and redirecting to the home page if authorized
//
// Parameters:
// - c: The Echo context for handling the HTTP request and response
// - config: Configuration provider that supplies auth-related settings and permissions
//
// Returns:
// - Various HTTP status codes and error messages based on the outcome of each step
// - Redirects to the home page on successful authentication and authorization
func handleOAuthCallback(c echo.Context, config auth.ConfigProvider) error {
// Extract the authorization code and state
code := c.QueryParam("code")
+50 -7
View File
@@ -10,14 +10,38 @@ import (
"github.com/lestrrat-go/jwx/v2/jwk"
)
// Time constants for JWKS operations
const (
JWKS_CACHE_TTL = 24 * time.Hour
// JWKS_CACHE_TTL is the time-to-live duration for the JWKS cache (24 hours)
JWKS_CACHE_TTL = 24 * time.Hour
// JWKS_CLIENT_TIMEOUT is the timeout duration for HTTP requests to fetch JWKS (10 seconds)
JWKS_CLIENT_TIMEOUT = 10 * time.Second
)
// GetJWKS returns the cached JWKS or fetches a new one if needed
// This function implements caching logic to minimize external requests
// Used during token validation to get the key set for signature verification
// GetJWKS retrieves a JSON Web Key Set (JWKS) from the specified URL, using a
// thread-safe caching mechanism to minimize external requests.
//
// It implements a double-checked locking pattern to efficiently handle concurrent
// access to the cached JWKS. It first attempts to retrieve a valid cached key set
// using a read lock. If the cache is empty or expired, it acquires a write lock
// to update the cache, but does a second check to avoid redundant fetches in case
// another goroutine has already updated the cache.
//
// The retrieved JWKS is cached for JWKS_CACHE_TTL duration (24 hours by default).
// This cache duration can be adjusted based on the security requirements and the
// key rotation policy of the identity provider.
//
// Parameters:
// - jwksURL: The URL from which to fetch the JWKS (typically provided by the Cognito or OAuth provider)
// - logger: A structured logger for recording debug and error information during the JWKS retrieval process
//
// Returns:
// - jwk.Set: The parsed JSON Web Key Set containing the public keys used for token verification
// - error: An error if the JWKS cannot be fetched or parsed, nil otherwise
//
// This function is primarily used during token validation to get the key set
// needed for signature verification of JWTs issued by the identity provider.
func GetJWKS(jwksURL string, logger *slog.Logger) (jwk.Set, error) {
// Try to use cached JWKS first (read lock)
jwksCache.mutex.RLock()
@@ -55,9 +79,28 @@ func GetJWKS(jwksURL string, logger *slog.Logger) (jwk.Set, error) {
return keySet, nil
}
// fetchJWKS retrieves the JSON Web Key Set from Cognito
// Used to get the public keys needed to verify token signatures
// Makes an HTTP request to the JWKS URL and returns the raw JSON
// fetchJWKS makes an HTTP request to retrieve the JSON Web Key Set (JWKS) from
// the specified URL, typically from a Cognito or other OAuth identity provider.
//
// This function handles the actual HTTP communication with the JWKS endpoint,
// using a timeout-configured HTTP client to ensure the request doesn't hang
// indefinitely. It performs a standard GET request and reads the raw JSON
// response containing the public keys.
//
// The HTTP client is configured with JWKS_CLIENT_TIMEOUT (10 seconds by default)
// to prevent long-running requests. This timeout can be adjusted based on network
// conditions and reliability requirements.
//
// Parameters:
// - jwksURL: The URL endpoint from which to fetch the JWKS
//
// Returns:
// - string: The raw JSON string containing the JWKS data if successful
// - error: An error if the HTTP request fails, the response cannot be read,
// or any other error occurs during the fetching process
//
// This function is typically called by GetJWKS when the cached JWKS has expired
// or is not available, and should not be called directly in most cases.
func fetchJWKS(jwksURL string) (string, error) {
client := &http.Client{Timeout: JWKS_CLIENT_TIMEOUT}
req, err := http.NewRequest("GET", jwksURL, nil)
+35 -4
View File
@@ -11,9 +11,24 @@ import (
"github.com/labstack/echo/v4"
)
// TokenValidationMiddleware verifies JWT tokens efficiently
// This is the primary middleware that handles both authentication and authorization
// Applied to all routes to enforce security requirements
// TokenValidationMiddleware verifies JWT tokens and enforces authentication and authorization.
//
// This middleware performs several critical security functions:
// - Validates JWT tokens and their signatures against the JWKS from the identity provider
// - Extracts and validates claims from the token
// - Extracts user groups and stores them in the context
// - Checks authorization based on the user's group memberships
// - Enforces route-specific permissions
//
// The middleware skips authentication for public paths and handles special cases
// like OAuth callbacks and login initiation. For protected routes, it enforces
// both authentication (valid token) and authorization (correct group membership).
//
// Parameters:
// - config: A ConfigProvider implementation that supplies all necessary auth configuration
//
// Returns:
// - echo.MiddlewareFunc: An Echo middleware function that can be added to the middleware chain
func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
@@ -109,7 +124,23 @@ func TokenValidationMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
}
}
// JWTAuthMiddleware checks for JWT token in cookies and adds it to the Authorization header
// JWTAuthMiddleware extracts JWT tokens from cookies and adds them to the Authorization header.
//
// This middleware serves as a bridge between cookie-based and header-based authentication:
// - Checks for auth_token cookie and transfers it to the Authorization header
// - Skips processing for login and callback paths that handle authentication
// - Manages cookie deletion during logout operations
// - Redirects unauthenticated requests to the login path
//
// This middleware should be registered before TokenValidationMiddleware in the middleware
// chain to ensure the JWT token is properly placed in the Authorization header before
// validation occurs.
//
// Parameters:
// - config: A ConfigProvider implementation that supplies auth configuration values
//
// Returns:
// - echo.MiddlewareFunc: An Echo middleware function that can be added to the middleware chain
func JWTAuthMiddleware(config auth.ConfigProvider) echo.MiddlewareFunc {
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) error {
+39 -18
View File
@@ -6,9 +6,23 @@ import (
"strings"
)
// matchRoute checks if a request path matches a route pattern
// Converts Echo route patterns to regex for matching
// Supports parameter patterns like '/users/:id' and wildcard patterns
// matchRoute determines if a request path matches a route pattern by converting
// Echo-style route patterns to regular expressions for matching.
//
// The function supports several pattern formats:
// - Exact path matching (e.g., "/users/profile")
// - Parameter patterns (e.g., "/users/:id" matches "/users/123")
// - Wildcard patterns (e.g., "/static/*" matches any path starting with "/static/")
//
// Parameters:
// - requestPath: The actual HTTP request path to check
// - routePattern: The Echo-style route pattern to match against
//
// Returns:
// - bool: true if the requestPath matches the routePattern, false otherwise
//
// Note that if the conversion of the route pattern to a regular expression fails,
// the function will return false.
func matchRoute(requestPath, routePattern string) bool {
// Convert Echo route pattern to regex pattern
// e.g., "/users/:id" to "/users/([^/]+)"
@@ -43,9 +57,28 @@ func matchRoute(requestPath, routePattern string) bool {
return regex.MatchString(requestPath)
}
// checkPermissions checks if the user has the required permissions
// Used by both the main token validation middleware and the OAuth callback handler
// Returns true if authorized, false if not, along with the required groups
// checkPermissions verifies if a user has the necessary permissions to access a specific
// path based on their group memberships and the route permission configuration.
//
// This function is used by both the main token validation middleware and the OAuth
// callback handler to implement route-based access control. It works by:
// 1. Finding a matching route pattern in the routePermissions map
// 2. Extracting the required groups for that route
// 3. Checking if the user belongs to any of the required groups
//
// If no matching route pattern is found, access is allowed by default.
// If a matching pattern is found but no groups are required, access is allowed.
// Otherwise, the user must belong to at least one of the required groups.
//
// Parameters:
// - path: The HTTP request path to check permissions for
// - userGroups: A slice of group names that the user belongs to
// - routePermissions: A map where keys are route patterns and values are slices of required group names
// - logger: A structured logger for recording debug and authorization information
//
// Returns:
// - bool: true if the user is authorized, false otherwise
// - []string: The list of required groups for the matching route (may be empty)
func checkPermissions(path string, userGroups []string, routePermissions map[string][]string, logger *slog.Logger) (bool, []string) {
// Find matching route pattern
var requiredGroups []string
@@ -83,15 +116,3 @@ func checkPermissions(path string, userGroups []string, routePermissions map[str
return false, requiredGroups
}
//// Helper function to mask sensitive values
//// Used for logging sensitive information like client IDs
//func maskString(s string) string {
// if s == "" {
// return "<not set>"
// }
// if len(s) <= 8 {
// return "****"
// }
// return s[:4] + "..." + s[len(s)-4:]
//}
+85 -14
View File
@@ -18,9 +18,20 @@ import (
const PKCE_SESSION_TTL = 5 * time.Minute
// verifyToken verifies the JWT token and returns its claims
// Used during both OAuth callback and subsequent API requests with bearer token
// Verifies signature, expiration, issuer, and other JWT claims
// verifyToken verifies the JWT token and returns its claims.
//
// This function validates the provided JWT token against a JWK key set,
// verifying signature, expiration, issuer, and other standard JWT claims.
// It's used during both OAuth callback and subsequent API requests with bearer tokens.
//
// Parameters:
// - tokenString: The raw JWT token string to verify
// - keySet: The JWK set containing keys to verify the token signature
// - config: Configuration provider containing auth settings like region and user pool ID
//
// Returns:
// - map[string]interface{}: The verified token claims as a map
// - error: Error if token verification fails for any reason
func verifyToken(tokenString string, keySet jwk.Set, config auth.ConfigProvider) (map[string]interface{}, error) {
region := config.GetAuthRegion()
issuer := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s", region, config.GetAuthUserPoolID())
@@ -46,9 +57,18 @@ func verifyToken(tokenString string, keySet jwk.Set, config auth.ConfigProvider)
return claims, nil
}
// GetUserGroups extracts user groups from the token claims
// Attempts to find groups in different claim names depending on Cognito setup
// Returns an array of group names or an error if no groups found
// GetUserGroups extracts user groups from the token claims.
//
// This function attempts to find groups in different claim names depending
// on the Cognito setup. It checks several common claim names where group
// information might be stored, handling different data formats.
//
// Parameters:
// - claims: A map of token claims extracted from a verified JWT token
//
// Returns:
// - []string: An array of group names that the user belongs to
// - error: Error if no groups are found in any of the expected claim fields
func GetUserGroups(claims map[string]interface{}) ([]string, error) {
// The claim containing the groups might have different names depending on your Cognito setup
// Common names are "cognito:groups", "groups", or a custom attribute
@@ -74,8 +94,17 @@ func GetUserGroups(claims map[string]interface{}) ([]string, error) {
return nil, errors.New("no groups found in token claims")
}
// createCodeChallenge creates a code challenge from a code verifier
// Implements S256 code challenge method as specified in PKCE RFC 7636
// createCodeChallenge creates a code challenge from a code verifier.
//
// This function implements the S256 code challenge method as specified in
// PKCE (Proof Key for Code Exchange) RFC 7636. It creates a SHA256 hash
// of the verifier and encodes it as base64url without padding.
//
// Parameters:
// - verifier: The PKCE code verifier string to transform
//
// Returns:
// - string: The generated code challenge for use in the authorization request
func createCodeChallenge(verifier string) string {
// Create SHA256 hash of the verifier
hash := sha256.Sum256([]byte(verifier))
@@ -86,8 +115,19 @@ func createCodeChallenge(verifier string) string {
return challenge
}
// generateRandomString creates a cryptographically secure random string
// Used for generating state parameter and code verifier
// generateRandomString creates a cryptographically secure random string.
//
// This function generates a cryptographically secure random byte sequence
// of the specified length and encodes it as a URL-safe base64 string.
// It's used for generating the state parameter and code verifier in the
// OAuth/PKCE flow.
//
// Parameters:
// - length: The desired length of the random string
//
// Returns:
// - string: The generated random string
// - error: Error if random bytes generation fails
func generateRandomString(length int) (string, error) {
b := make([]byte, length)
if _, err := rand.Read(b); err != nil {
@@ -96,8 +136,17 @@ func generateRandomString(length int) (string, error) {
return base64.RawURLEncoding.EncodeToString(b)[:length], nil
}
// storePKCESession stores the PKCE session for later verification
// In a production environment, this should be replaced with a proper session store
// storePKCESession stores the PKCE session for later verification.
//
// This function creates and stores a new PKCE session in the in-memory session map,
// associating it with the provided state parameter. It also performs cleanup of
// expired sessions. In a production environment, this should be replaced with
// a proper persistent session store.
//
// Parameters:
// - state: The OAuth state parameter that serves as the session identifier
// - codeVerifier: The PKCE code verifier to store for later validation
// - logger: A structured logger for recording debug information
func storePKCESession(state, codeVerifier string, logger *slog.Logger) {
pkceSessionMap.Lock()
defer pkceSessionMap.Unlock()
@@ -120,7 +169,19 @@ func storePKCESession(state, codeVerifier string, logger *slog.Logger) {
}
}
// getCodeVerifier retrieves and validates the code verifier for a state
// getCodeVerifier retrieves and validates the code verifier for a state.
//
// This function looks up a PKCE session by its state parameter and returns
// the associated code verifier if the session exists and hasn't expired.
// It's used during the token exchange step of the OAuth/PKCE flow.
//
// Parameters:
// - state: The OAuth state parameter that serves as the session identifier
// - logger: A structured logger for recording debug information
//
// Returns:
// - string: The code verifier associated with the state
// - error: Error if no session is found or if the session has expired
func getCodeVerifier(state string, logger *slog.Logger) (string, error) {
pkceSessionMap.RLock()
defer pkceSessionMap.RUnlock()
@@ -137,7 +198,17 @@ func getCodeVerifier(state string, logger *slog.Logger) (string, error) {
return session.CodeVerifier, nil
}
// ExtractUserInfo extracts user information from JWT claims
// ExtractUserInfo extracts user information from JWT claims.
//
// This function parses a verified JWT claims map and extracts relevant user
// information such as username, email, and group memberships. It creates and
// returns a UserInfo struct with the extracted data.
//
// Parameters:
// - claims: A map of token claims extracted from a verified JWT token
//
// Returns:
// - UserInfo: A struct containing the extracted user information
func ExtractUserInfo(claims map[string]interface{}) UserInfo {
userInfo := UserInfo{}
-5
View File
@@ -172,11 +172,6 @@ func (c *CognitoConfig) PrettyPrintDebugOnly() {
}
}
//// SetRoutePermissions sets the route permissions map
//func (c *CognitoConfig) SetRoutePermissions(permissions map[string][]string) {
// c.AuthRoutePermissions = permissions
//}
func (c *CognitoConfig) GetAuthRegion() string {
return c.AuthRegion
}