Skip to content

Conversation

@bburda
Copy link
Collaborator

@bburda bburda commented Dec 27, 2025

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

  • Bug fix
  • New feature or tests
  • Breaking change
  • Documentation only

Testing

How was this tested / how should reviewers verify it?

Unit & Integration Tests


Checklist

  • Breaking changes are clearly described (and announced in docs / changelog if needed)
  • Tests were added or updated if needed
  • Docs were updated if behavior or public API changed

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
@bburda bburda added this to the MS5: Fleet & Advanced Features milestone Dec 27, 2025
@bburda bburda self-assigned this Dec 27, 2025
@bburda bburda added the enhancement New feature or request label Dec 27, 2025
@bburda bburda changed the title JWT authentication and authorization [#89] JWT authentication and authorization Dec 27, 2025
@bburda bburda marked this pull request as ready for review December 27, 2025 20:58
Copilot AI review requested due to automatic review settings December 27, 2025 20:58
Copy link
Contributor

Copilot AI left a 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

Comment on lines +514 to +516
// Generate UUID-like string using thread-local RNG for thread safety
thread_local std::random_device rd;
thread_local std::mt19937_64 gen(rd());
Copy link

Copilot AI Dec 27, 2025

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.

Suggested change
// 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);
}();

Copilot uses AI. Check for mistakes.
Comment on lines +534 to +594
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;
}
}
Copy link

Copilot AI Dec 27, 2025

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.

Copilot uses AI. Check for mistakes.
Comment on lines +2222 to +2226
// 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"}};
Copy link

Copilot AI Dec 27, 2025

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.

Suggested change
// 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"}};

Copilot uses AI. Check for mistakes.
response.access_token = access_token;
response.token_type = "Bearer";
response.expires_in = config_.token_expiry_seconds;
response.scope = role_to_string(record->role);
Copy link

Copilot AI Dec 27, 2025

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.

Suggested change
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;

Copilot uses AI. Check for mistakes.
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...",
Copy link

Copilot AI Dec 27, 2025

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.

Suggested change
"refresh_token": "bmV3IHJlZnJlc2ggdG9rZW4...",

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +54
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)
Copy link

Copilot AI Dec 27, 2025

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authentication & Authorization with JWT and RBAC

2 participants