-
Notifications
You must be signed in to change notification settings - Fork 2
[#89] JWT authentication and authorization #96
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
Implement comprehensive authentication and authorization system for the ros2_medkit gateway using JSON Web Tokens (JWT) with Role-Based Access Control (RBAC). Features: - OAuth2 client_credentials flow for authentication - JWT tokens with HS256/RS256 signing algorithms - Refresh token support with revocation capability - Four-tier RBAC: viewer, operator, configurator, admin - Configurable auth requirements (none/write/all) - Pre-routing middleware for endpoint protection New endpoints: - POST /api/v1/auth/authorize - Client authentication - POST /api/v1/auth/token - Token refresh - POST /api/v1/auth/revoke - Token revocation Dependencies: - jwt-cpp v0.7.0 (fetched via CMake FetchContent) - OpenSSL (system package) Auth is disabled by default for backward compatibility. Enable via `auth.enabled: true` in gateway_params.yaml.
Reorganize authentication into auth/ subfolder following SRP principles and implement reviewer feedback items for improved security and code quality. - Move auth files to auth/ subfolder (auth_config, auth_models, auth_manager) - Extract AuthMiddleware class from REST server for separation of concerns - Introduce IAuthRequirementPolicy interface with Strategy pattern - Add factory pattern for creating auth requirement policies - DRY parse_request() helper for JSON/form-urlencoded content types - Add convenience header auth/auth.hpp for single-include usage - Token type enforcement via JWT 'typ' claim (access vs refresh) - Client state validation on every request (disabled clients rejected) - Thread-safe RNG using thread_local for token ID generation - RS256 fail-fast validation at startup (validate key files exist) - Refresh token revocation propagates to associated access tokens - Add SYSTEM flag to jwt-cpp FetchContent to suppress warnings - Disable jwt-cpp examples and tests (JWT_CPP_BUILD_EXAMPLES=OFF) - Configure clang-tidy HEADER_FILTER to exclude _deps folder
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR implements comprehensive JWT authentication and authorization for the ros2_medkit gateway with Role-Based Access Control (RBAC). The system supports both HS256 (symmetric) and RS256 (asymmetric) JWT signing algorithms, provides OAuth2-compliant token endpoints, and includes four permission levels: VIEWER, OPERATOR, CONFIGURATOR, and ADMIN.
Key Changes:
- JWT-based authentication with access and refresh tokens following OAuth2 client credentials flow
- Role-Based Access Control (RBAC) with four hierarchical roles and fine-grained endpoint permissions
- Three authentication endpoints:
/auth/authorize,/auth/token,/auth/revoke
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
src/auth/auth_manager.cpp |
Core authentication logic including JWT generation/validation, token lifecycle management, and RBAC authorization checks |
src/auth/auth_middleware.cpp |
HTTP authentication middleware for Bearer token extraction and validation |
src/auth/auth_config.cpp |
Configuration builder and role permission definitions |
src/auth/auth_models.cpp |
Data structures and OAuth2 request/response models with URL-encoded form parsing |
src/auth/auth_requirement_policy.cpp |
Strategy pattern implementation for configurable authentication requirements |
src/rest_server.cpp |
Integration of auth middleware into pre-routing handler and three new auth endpoints |
src/gateway_node.cpp |
ROS 2 parameter configuration for authentication settings and client registration |
test/test_auth_manager.cpp |
Comprehensive unit tests covering authentication, authorization, token validation, and RBAC |
test/test_auth.test.py |
Integration tests verifying end-to-end authentication flows and role-based access control |
README.md |
Documentation for authentication configuration, endpoints, and usage examples |
config/gateway_params.yaml |
Default authentication configuration with inline documentation |
CMakeLists.txt |
Added jwt-cpp and OpenSSL dependencies via FetchContent |
| // Generate UUID-like string using thread-local RNG for thread safety | ||
| thread_local std::random_device rd; | ||
| thread_local std::mt19937_64 gen(rd()); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The thread_local random_device is initialized only once per thread and never reseeded. According to the C++ standard, std::random_device should not be used for seeding generators in a thread_local context because it may produce the same initial state across threads on some implementations. Consider using std::seed_seq with std::chrono or removing the thread_local from random_device.
| // Generate UUID-like string using thread-local RNG for thread safety | |
| thread_local std::random_device rd; | |
| thread_local std::mt19937_64 gen(rd()); | |
| // Generate UUID-like string using thread-local RNG for thread safety. | |
| // Seed the PRNG once per thread using a local random_device and time-based seed_seq | |
| thread_local std::mt19937_64 gen = []() { | |
| std::random_device rd; | |
| auto now = static_cast<uint64_t>( | |
| std::chrono::high_resolution_clock::now().time_since_epoch().count()); | |
| std::seed_seq seq{ | |
| rd(), | |
| static_cast<uint32_t>(now & 0xFFFFFFFFu), | |
| static_cast<uint32_t>((now >> 32) & 0xFFFFFFFFu)}; | |
| return std::mt19937_64(seq); | |
| }(); |
| bool AuthManager::matches_path(const std::string & pattern, const std::string & path) { | ||
| // Simple wildcard matching | ||
| // * matches any single path segment | ||
| // ** matches any number of path segments (including none) | ||
| // Pattern: /api/v1/components/*/data | ||
| // Path: /api/v1/components/engine/data | ||
| // Pattern: /api/v1/** | ||
| // Path: /api/v1/components/engine/data/temperature (matches) | ||
|
|
||
| if (pattern == path) { | ||
| return true; | ||
| } | ||
|
|
||
| // Convert pattern to regex | ||
| std::string regex_pattern; | ||
| regex_pattern.reserve(pattern.size() * 2); | ||
|
|
||
| for (size_t i = 0; i < pattern.size(); ++i) { | ||
| char c = pattern[i]; | ||
| if (c == '*') { | ||
| // Check for ** (multi-segment wildcard) | ||
| if (i + 1 < pattern.size() && pattern[i + 1] == '*') { | ||
| regex_pattern += ".*"; // Match anything including slashes | ||
| ++i; // Skip the second * | ||
| } else { | ||
| regex_pattern += "[^/]+"; // Match any non-slash characters (single segment) | ||
| } | ||
| } else { | ||
| switch (c) { | ||
| case '.': | ||
| case '[': | ||
| case ']': | ||
| case '(': | ||
| case ')': | ||
| case '{': | ||
| case '}': | ||
| case '\\': | ||
| case '^': | ||
| case '$': | ||
| case '|': | ||
| case '?': | ||
| case '+': | ||
| regex_pattern += '\\'; | ||
| regex_pattern += c; | ||
| break; | ||
| default: | ||
| regex_pattern += c; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Anchor the pattern | ||
| regex_pattern = "^" + regex_pattern + "$"; | ||
|
|
||
| try { | ||
| std::regex re(regex_pattern); | ||
| return std::regex_match(path, re); | ||
| } catch (const std::regex_error &) { | ||
| return false; | ||
| } | ||
| } |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The matches_path function creates and compiles a new regex on every call, which is inefficient for frequently called authorization checks. Consider caching compiled regex patterns or using a more efficient wildcard matching algorithm for performance.
| // Revoke token | ||
| bool revoked = auth_manager_->revoke_refresh_token(token); | ||
|
|
||
| // Per OAuth2 spec, always return 200 even if token wasn't found | ||
| json response = {{"status", revoked ? "revoked" : "not_found"}}; |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to OAuth2 RFC 6749 Section 5.2, error responses should return appropriate HTTP status codes. The revoke endpoint always returns 200 even for tokens that weren't found. While the response includes status "not_found", RFC 7009 (Token Revocation) specifies that the endpoint should return 200 OK regardless of whether the token existed, so this is correct. However, returning "not_found" in the body may leak information about token validity. Consider returning only "status": "revoked" for all cases to prevent token enumeration.
| // Revoke token | |
| bool revoked = auth_manager_->revoke_refresh_token(token); | |
| // Per OAuth2 spec, always return 200 even if token wasn't found | |
| json response = {{"status", revoked ? "revoked" : "not_found"}}; | |
| // Revoke token (do not reveal whether it existed to avoid token enumeration) | |
| auth_manager_->revoke_refresh_token(token); | |
| // Per OAuth2 RFC 7009, always return 200 and do not indicate token validity | |
| json response = {{"status", "revoked"}}; |
| response.access_token = access_token; | ||
| response.token_type = "Bearer"; | ||
| response.expires_in = config_.token_expiry_seconds; | ||
| response.scope = role_to_string(record->role); |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The refresh_token field in TokenResponse is documented as returning a new refresh token on refresh, but the implementation at line 199-203 does not include a refresh_token in the response (it's optional and not set). This means clients cannot get a new refresh token when refreshing. Consider either updating the documentation to clarify that refresh tokens are not rotated, or implement refresh token rotation for better security.
| response.scope = role_to_string(record->role); | |
| response.scope = role_to_string(record->role); | |
| // Return the existing refresh token so clients can continue to use it | |
| response.refresh_token = refresh_token; |
| "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", | ||
| "token_type": "Bearer", | ||
| "expires_in": 3600, | ||
| "refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...", |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The README example shows "refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4..." in the response for POST /auth/token, but based on the implementation, the refresh endpoint does not return a new refresh_token (only access_token). This documentation is inconsistent with the actual implementation and should be corrected to avoid confusing API consumers.
| "refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...", |
| FetchContent_Declare( | ||
| jwt-cpp | ||
| GIT_REPOSITORY https://github.com/Thalhammer/jwt-cpp.git | ||
| GIT_TAG v0.7.0 | ||
| SYSTEM # Treat as system headers to suppress warnings | ||
| ) | ||
| # Disable jwt-cpp warnings by treating its headers as SYSTEM | ||
| set(JWT_CPP_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE) | ||
| set(JWT_CPP_BUILD_TESTS OFF CACHE BOOL "" FORCE) | ||
| FetchContent_MakeAvailable(jwt-cpp) |
Copilot
AI
Dec 27, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The FetchContent_Declare for jwt-cpp pulls third-party code directly from GitHub using only a mutable tag (v0.7.0), which allows upstream changes (or compromise) to silently alter the code executed in your build environment. This creates a supply chain risk where an attacker controlling or tampering with the Thalhammer/jwt-cpp repository or tag could inject arbitrary code during your build, potentially compromising build artifacts or secrets. To mitigate this, pin the dependency to an immutable identifier (e.g., a specific commit SHA or verified release archive hash) and, where possible, add integrity checks (checksums/signatures) for the downloaded content.
Pull Request
Summary
Implements comprehensive JWT authentication and authorization system for the ros2_medkit gateway with Role-Based Access Control (RBAC).
Issue
Link the related issue (required):
Type
Testing
How was this tested / how should reviewers verify it?
Unit & Integration Tests
Checklist