diff --git a/cmd/cognito_test/readme.md b/cmd/cognito_test/readme.md index ed7fc767..9ce74674 100644 --- a/cmd/cognito_test/readme.md +++ b/cmd/cognito_test/readme.md @@ -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 diff --git a/docs/README.md b/docs/README.md index a6201f5f..de73918c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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 diff --git a/internal/cognitoauth/auth.go b/internal/cognitoauth/auth.go index bfb1fd4d..d1058728 100644 --- a/internal/cognitoauth/auth.go +++ b/internal/cognitoauth/auth.go @@ -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 { diff --git a/internal/cognitoauth/handler.go b/internal/cognitoauth/handler.go index 2cba70a1..fbd99ac4 100644 --- a/internal/cognitoauth/handler.go +++ b/internal/cognitoauth/handler.go @@ -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") diff --git a/internal/cognitoauth/jwks.go b/internal/cognitoauth/jwks.go index 21897edf..64a1d1f6 100644 --- a/internal/cognitoauth/jwks.go +++ b/internal/cognitoauth/jwks.go @@ -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) diff --git a/internal/cognitoauth/middleware.go b/internal/cognitoauth/middleware.go index f9bf3675..3d996f69 100644 --- a/internal/cognitoauth/middleware.go +++ b/internal/cognitoauth/middleware.go @@ -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 { diff --git a/internal/cognitoauth/permission_utils.go b/internal/cognitoauth/permission_utils.go index 8740f9a1..4e2fe5a1 100644 --- a/internal/cognitoauth/permission_utils.go +++ b/internal/cognitoauth/permission_utils.go @@ -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 "" -// } -// if len(s) <= 8 { -// return "****" -// } -// return s[:4] + "..." + s[len(s)-4:] -//} diff --git a/internal/cognitoauth/token.go b/internal/cognitoauth/token.go index ca447f4e..bd9a2f73 100644 --- a/internal/cognitoauth/token.go +++ b/internal/cognitoauth/token.go @@ -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{} diff --git a/internal/serviceconfig/auth/config.go b/internal/serviceconfig/auth/config.go index d31ddba0..9c7221d0 100644 --- a/internal/serviceconfig/auth/config.go +++ b/internal/serviceconfig/auth/config.go @@ -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 }