// errors.go provides error classification for AWS Cognito errors. // It maps AWS error types to appropriate HTTP status codes and sanitized error messages // suitable for returning in API responses without leaking sensitive information. package usermanagement import ( "errors" "fmt" "net/http" "strings" "github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider/types" "github.com/aws/smithy-go" ) // CognitoErrorType represents a machine-readable error classification. type CognitoErrorType string const ( // ErrTypeUserNotFound indicates the user does not exist in Cognito. ErrTypeUserNotFound CognitoErrorType = "UserNotFound" // ErrTypeAccessDenied indicates the caller does not have permission to perform the operation. ErrTypeAccessDenied CognitoErrorType = "AccessDenied" // ErrTypeNotAuthorized indicates the caller is not authorized (e.g., invalid credentials). ErrTypeNotAuthorized CognitoErrorType = "NotAuthorized" // ErrTypeInvalidParameter indicates an invalid parameter was provided. ErrTypeInvalidParameter CognitoErrorType = "InvalidParameter" // ErrTypeRateLimited indicates too many requests were made. ErrTypeRateLimited CognitoErrorType = "RateLimited" // ErrTypeServiceUnavailable indicates the Cognito service is temporarily unavailable. ErrTypeServiceUnavailable CognitoErrorType = "ServiceUnavailable" // ErrTypeServiceError indicates a general error communicating with Cognito. ErrTypeServiceError CognitoErrorType = "ServiceError" // ErrTypeUserExists indicates the user already exists in Cognito. ErrTypeUserExists CognitoErrorType = "UserExists" ) // CognitoErrorInfo contains classified error information suitable for HTTP responses. // The Message and Details fields are sanitized to avoid leaking sensitive information // like ARNs, internal paths, or request IDs. type CognitoErrorInfo struct { // HTTPStatus is the appropriate HTTP status code for this error type. HTTPStatus int // ErrorType is a machine-readable error classification. ErrorType CognitoErrorType // Message is a user-friendly error message safe for HTTP responses. Message string // Details provides additional error context (sanitized). Details string } // ClassifyCognitoError analyzes a Cognito error and returns appropriate HTTP response info. // The returned CognitoErrorInfo contains sanitized messages suitable for API responses. // // Parameters: // - err: The error returned from a Cognito operation // - email: The user's email address for context in error messages // // Returns: // - CognitoErrorInfo: Classified error information with appropriate HTTP status and messages func ClassifyCognitoError(err error, email string) CognitoErrorInfo { if err == nil { return CognitoErrorInfo{ HTTPStatus: http.StatusOK, ErrorType: "", Message: "", Details: "", } } // Check for specific Cognito SDK error types var userNotFoundErr *types.UserNotFoundException if errors.As(err, &userNotFoundErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusNotFound, ErrorType: ErrTypeUserNotFound, Message: fmt.Sprintf("User not found: %s", email), Details: "The specified user does not exist in Cognito", } } var notAuthorizedErr *types.NotAuthorizedException if errors.As(err, ¬AuthorizedErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusUnauthorized, ErrorType: ErrTypeNotAuthorized, Message: "Not authorized to perform this operation", Details: sanitizeErrorMessage(err.Error()), } } var invalidParamErr *types.InvalidParameterException if errors.As(err, &invalidParamErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusBadRequest, ErrorType: ErrTypeInvalidParameter, Message: "Invalid parameter in request", Details: sanitizeErrorMessage(err.Error()), } } var tooManyRequestsErr *types.TooManyRequestsException if errors.As(err, &tooManyRequestsErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusTooManyRequests, ErrorType: ErrTypeRateLimited, Message: "Too many requests to Cognito", Details: "Rate limit exceeded. Please retry after a short delay.", } } var internalErr *types.InternalErrorException if errors.As(err, &internalErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusServiceUnavailable, ErrorType: ErrTypeServiceUnavailable, Message: "Cognito service temporarily unavailable", Details: "Internal service error. Please retry later.", } } var usernameExistsErr *types.UsernameExistsException if errors.As(err, &usernameExistsErr) { return CognitoErrorInfo{ HTTPStatus: http.StatusConflict, ErrorType: ErrTypeUserExists, Message: fmt.Sprintf("User already exists: %s", email), Details: "A user with this email already exists in Cognito", } } // Check for generic AWS API errors (including AccessDeniedException) var apiErr smithy.APIError if errors.As(err, &apiErr) { return classifyAPIError(apiErr, email) } // Default: treat as a generic service communication error return CognitoErrorInfo{ HTTPStatus: http.StatusBadGateway, ErrorType: ErrTypeServiceError, Message: "Failed to communicate with Cognito", Details: sanitizeErrorMessage(err.Error()), } } // classifyAPIError handles generic AWS API errors that may not have specific SDK types. // // Parameters: // - apiErr: The AWS API error // - email: The user's email address for context in error messages // // Returns: // - CognitoErrorInfo: Classified error information func classifyAPIError(apiErr smithy.APIError, email string) CognitoErrorInfo { code := apiErr.ErrorCode() switch code { case "AccessDeniedException": return CognitoErrorInfo{ HTTPStatus: http.StatusForbidden, ErrorType: ErrTypeAccessDenied, Message: "Access denied to Cognito user pool", Details: "The service does not have permission to perform this Cognito operation", } case "UserNotFoundException": return CognitoErrorInfo{ HTTPStatus: http.StatusNotFound, ErrorType: ErrTypeUserNotFound, Message: fmt.Sprintf("User not found: %s", email), Details: "The specified user does not exist in Cognito", } case "NotAuthorizedException": return CognitoErrorInfo{ HTTPStatus: http.StatusUnauthorized, ErrorType: ErrTypeNotAuthorized, Message: "Not authorized to perform this operation", Details: sanitizeErrorMessage(apiErr.ErrorMessage()), } case "InvalidParameterException": return CognitoErrorInfo{ HTTPStatus: http.StatusBadRequest, ErrorType: ErrTypeInvalidParameter, Message: "Invalid parameter in request", Details: sanitizeErrorMessage(apiErr.ErrorMessage()), } case "TooManyRequestsException": return CognitoErrorInfo{ HTTPStatus: http.StatusTooManyRequests, ErrorType: ErrTypeRateLimited, Message: "Too many requests to Cognito", Details: "Rate limit exceeded. Please retry after a short delay.", } case "ServiceUnavailable", "InternalError", "InternalErrorException": return CognitoErrorInfo{ HTTPStatus: http.StatusServiceUnavailable, ErrorType: ErrTypeServiceUnavailable, Message: "Cognito service temporarily unavailable", Details: "Internal service error. Please retry later.", } case "UsernameExistsException": return CognitoErrorInfo{ HTTPStatus: http.StatusConflict, ErrorType: ErrTypeUserExists, Message: fmt.Sprintf("User already exists: %s", email), Details: "A user with this email already exists in Cognito", } default: return CognitoErrorInfo{ HTTPStatus: http.StatusBadGateway, ErrorType: ErrTypeServiceError, Message: "Failed to communicate with Cognito", Details: sanitizeErrorMessage(apiErr.ErrorMessage()), } } } // sanitizeErrorMessage removes sensitive information from error messages. // It strips out ARNs, request IDs, and internal resource names. // // Parameters: // - msg: The raw error message // // Returns: // - string: The sanitized error message func sanitizeErrorMessage(msg string) string { msg = removeARNs(msg) msg = removeRequestIDs(msg) msg = normalizeWhitespace(msg) return msg } // removeARNs replaces all AWS ARNs with [REDACTED]. func removeARNs(msg string) string { for { start := strings.Index(msg, "arn:aws:") if start == -1 { return msg } end := findEndOfToken(msg, start) msg = msg[:start] + "[REDACTED]" + msg[end:] } } // removeRequestIDs removes request ID patterns and their values from the message. func removeRequestIDs(msg string) string { patterns := []string{"requestid:", "request-id:", "request id:"} msgLower := strings.ToLower(msg) for _, pattern := range patterns { for { idx := strings.Index(msgLower, pattern) if idx == -1 { break } end := idx + len(pattern) end = skipWhitespace(msg, end) end = findEndOfToken(msg, end) msg = msg[:idx] + msg[end:] msgLower = strings.ToLower(msg) } } return msg } // findEndOfToken finds the end of a token (space, comma, or newline delimited). func findEndOfToken(msg string, start int) int { end := start for end < len(msg) && !isTokenDelimiter(msg[end]) { end++ } return end } // isTokenDelimiter returns true if the character is a token delimiter. func isTokenDelimiter(c byte) bool { return c == ' ' || c == ',' || c == '\n' || c == '\r' } // skipWhitespace advances past whitespace characters. func skipWhitespace(msg string, pos int) int { for pos < len(msg) && (msg[pos] == ' ' || msg[pos] == '\t') { pos++ } return pos } // normalizeWhitespace trims and collapses multiple spaces. func normalizeWhitespace(msg string) string { msg = strings.TrimSpace(msg) for strings.Contains(msg, " ") { msg = strings.ReplaceAll(msg, " ", " ") } return msg }