Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 48 additions & 2 deletions src/ros2_medkit_gateway/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,22 @@ find_package(ros2_medkit_msgs REQUIRED)
find_package(PkgConfig REQUIRED)
pkg_check_modules(cpp_httplib REQUIRED IMPORTED_TARGET cpp-httplib)

# Find OpenSSL (required by jwt-cpp for RS256)
find_package(OpenSSL REQUIRED)

# Fetch jwt-cpp header-only library
include(FetchContent)
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)
Comment on lines +45 to +54
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.

# Include directories
include_directories(include)

Expand All @@ -54,6 +70,12 @@ add_library(gateway_lib STATIC
src/operation_manager.cpp
src/configuration_manager.cpp
src/fault_manager.cpp
# Auth module (subfolder)
src/auth/auth_config.cpp
src/auth/auth_models.cpp
src/auth/auth_manager.cpp
src/auth/auth_middleware.cpp
src/auth/auth_requirement_policy.cpp
)

ament_target_dependencies(gateway_lib
Expand All @@ -70,6 +92,9 @@ target_link_libraries(gateway_lib
PkgConfig::cpp_httplib
nlohmann_json::nlohmann_json
yaml-cpp::yaml-cpp
OpenSSL::SSL
OpenSSL::Crypto
jwt-cpp::jwt-cpp
)

# Apply coverage flags to library
Expand Down Expand Up @@ -106,13 +131,22 @@ install(PROGRAMS scripts/get_type_schema.py
if(BUILD_TESTING)
# Linting and code quality checks
find_package(ament_lint_auto REQUIRED)
find_package(ament_cmake_clang_tidy REQUIRED)

# Use custom clang-format and clang-tidy configs from repo root
set(ament_cmake_clang_format_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-format")
set(ament_cmake_clang_tidy_CONFIG_FILE "${CMAKE_CURRENT_SOURCE_DIR}/../../.clang-tidy")

# Exclude uncrustify (conflicts with clang-format) and cpplint (conflicts with clang-format include order)
list(APPEND AMENT_LINT_AUTO_EXCLUDE ament_cmake_uncrustify ament_cmake_cpplint)
# Limit clang-tidy to only report issues from our source files (not FetchContent deps)
set(ament_cmake_clang_tidy_HEADER_FILTER "^${CMAKE_CURRENT_SOURCE_DIR}/(include|src|test)/")

# Exclude linters that don't work well with FetchContent dependencies:
# - uncrustify/cpplint: conflicts with clang-format
# - copyright/lint_cmake: flags generated/fetched files in install/
list(APPEND AMENT_LINT_AUTO_EXCLUDE
ament_cmake_uncrustify
ament_cmake_cpplint
)
ament_lint_auto_find_test_dependencies()

# Add GTest
Expand All @@ -121,6 +155,9 @@ if(BUILD_TESTING)
ament_add_gtest(test_gateway_node test/test_gateway_node.cpp)
target_link_libraries(test_gateway_node gateway_lib)

ament_add_gtest(test_auth_manager test/test_auth_manager.cpp)
target_link_libraries(test_auth_manager gateway_lib)

ament_add_gtest(test_operation_manager test/test_operation_manager.cpp)
target_link_libraries(test_operation_manager gateway_lib)

Expand All @@ -131,6 +168,8 @@ if(BUILD_TESTING)
if(ENABLE_COVERAGE)
target_compile_options(test_gateway_node PRIVATE --coverage -O0 -g)
target_link_options(test_gateway_node PRIVATE --coverage)
target_compile_options(test_auth_manager PRIVATE --coverage -O0 -g)
target_link_options(test_auth_manager PRIVATE --coverage)
target_compile_options(test_operation_manager PRIVATE --coverage -O0 -g)
target_link_options(test_operation_manager PRIVATE --coverage)
target_compile_options(test_configuration_manager PRIVATE --coverage -O0 -g)
Expand Down Expand Up @@ -160,6 +199,13 @@ if(BUILD_TESTING)
TIMEOUT 60
)

# Add authentication integration tests
add_launch_test(
test/test_auth.test.py
TARGET test_auth
TIMEOUT 120
)

# Demo automotive nodes
add_executable(demo_engine_temp_sensor
test/demo_nodes/engine_temp_sensor.cpp
Expand Down
168 changes: 168 additions & 0 deletions src/ros2_medkit_gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,92 @@ curl -X DELETE "http://localhost:8080/api/v1/components/long_calibration/operati
}
```

### Authentication Endpoints

#### POST /api/v1/auth/authorize

Authenticate using OAuth2 client credentials flow. Returns access and refresh tokens.

**Example (JSON):**
```bash
curl -X POST http://localhost:8080/api/v1/auth/authorize \
-H "Content-Type: application/json" \
-d '{
"grant_type": "client_credentials",
"client_id": "my_client",
"client_secret": "my_secret"
}'
```

**Example (Form URL-encoded):**
```bash
curl -X POST http://localhost:8080/api/v1/auth/authorize \
-H "Content-Type: application/x-www-form-urlencoded" \
-d 'grant_type=client_credentials&client_id=my_client&client_secret=my_secret'
```

**Response (200 OK):**
```json
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4...",
"scope": "admin"
}
```

**Response (401 Unauthorized - Invalid Credentials):**
```json
{
"error": "invalid_client",
"error_description": "Invalid client credentials"
}
```

#### POST /api/v1/auth/token

Refresh an access token using a refresh token.

**Example:**
```bash
curl -X POST http://localhost:8080/api/v1/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "refresh_token",
"refresh_token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."
}'
```

**Response (200 OK):**
```json
{
"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.
"scope": "admin"
}
```

#### POST /api/v1/auth/revoke

Revoke a refresh token to prevent further use.

**Example:**
```bash
curl -X POST http://localhost:8080/api/v1/auth/revoke \
-H "Content-Type: application/json" \
-d '{"token": "dGhpcyBpcyBhIHJlZnJlc2ggdG9rZW4..."}'
```

**Response (200 OK):**
```json
{
"status": "revoked"
}
```

### Configurations Endpoints

#### GET /api/v1/components/{component_id}/configurations
Expand Down Expand Up @@ -616,6 +702,30 @@ Cross-Origin Resource Sharing (CORS) settings for browser-based clients. CORS is
| `cors.allow_credentials` | bool | `false` | Allow credentials (cookies, auth headers). Cannot be `true` with wildcard origin. |
| `cors.max_age_seconds` | int | `86400` | How long browsers cache preflight response (24 hours default) |

#### Authentication Configuration

JWT-based authentication with Role-Based Access Control (RBAC). Authentication is **disabled by default** for backward compatibility.

| Parameter | Type | Default | Description |
| ----------------------------------- | -------- | --------------------- | --------------------------------------------------------------------------- |
| `auth.enabled` | bool | `false` | Enable/disable authentication. Set to `true` to require auth. |
| `auth.jwt_secret` | string | (required if enabled) | Secret key for HS256 signing. Must be at least 32 characters. |
| `auth.jwt_algorithm` | string | `HS256` | JWT signing algorithm: `HS256` (symmetric) or `RS256` (asymmetric). |
| `auth.token_expiry_seconds` | int | `3600` | Access token lifetime in seconds (range: 60-86400). |
| `auth.refresh_token_expiry_seconds` | int | `86400` | Refresh token lifetime in seconds (range: 300-604800). |
| `auth.require_auth_for` | string | `write` | Auth requirement: `none`, `write` (POST/PUT/DELETE only), or `all`. |
| `auth.issuer` | string | `ros2_medkit_gateway` | JWT issuer claim for token validation. |
| `auth.clients` | string[] | `[]` | Client credentials in format `client_id:client_secret:role`. |

**Roles and Permissions:**

| Role | Read (GET) | Operations (POST) | Configurations (PUT/DELETE) | Faults (DELETE) |
| ------------ | ---------- | ----------------- | --------------------------- | --------------- |
| `viewer` | ✅ | ❌ | ❌ | ❌ |
| `operator` | ✅ | ✅ | ❌ | ❌ |
| `configurator` | ✅ | ✅ | ✅ | ❌ |
| `admin` | ✅ | ✅ | ✅ | ✅ |

### Configuration Examples

**Change port via command line:**
Expand Down Expand Up @@ -650,6 +760,64 @@ cors:

> ⚠️ **Security Note:** Using `["*"]` as `allowed_origins` is not recommended for production. When `allow_credentials` is `true`, wildcard origins will cause the application to fail to start with an exception.

### Authentication Configuration Examples

**Enable authentication with write-only protection (recommended for development):**
```yaml
auth:
enabled: true
jwt_secret: "your_secret_key_at_least_32_chars_long"
jwt_algorithm: "HS256"
token_expiry_seconds: 3600
refresh_token_expiry_seconds: 86400
require_auth_for: "write" # GET requests work without auth
issuer: "ros2_medkit_gateway"
clients:
- "admin:admin_secret:admin"
- "operator:operator_secret:operator"
- "viewer:viewer_secret:viewer"
```

**Full protection (all endpoints require authentication):**
```yaml
auth:
enabled: true
jwt_secret: "your_production_secret_minimum_32_chars"
jwt_algorithm: "HS256"
token_expiry_seconds: 1800 # 30 minutes for tighter security
refresh_token_expiry_seconds: 43200 # 12 hours
require_auth_for: "all" # All endpoints require valid token
issuer: "production_gateway"
clients:
- "dashboard:dashboard_secret_key:admin"
- "monitoring:monitoring_secret:viewer"
```

**Usage with curl:**
```bash
# 1. Get access token
TOKEN=$(curl -s -X POST http://localhost:8080/api/v1/auth/authorize \
-H "Content-Type: application/json" \
-d '{"grant_type":"client_credentials","client_id":"admin","client_secret":"admin_secret"}' \
| jq -r '.access_token')

# 2. Use token for protected endpoints
curl http://localhost:8080/api/v1/components/temp_sensor/data \
-H "Authorization: Bearer $TOKEN"

# 3. Set a parameter (requires operator+ role)
curl -X PUT http://localhost:8080/api/v1/components/temp_sensor/configurations/rate \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"value": 5.0}'
```

> ⚠️ **Security Notes:**
> - Store `jwt_secret` securely and never commit it to version control
> - Use environment variables or secure secret management in production
> - RS256 algorithm requires additional setup with public/private key files
> - Client secrets should be generated using cryptographically secure random strings

## Architecture

### Components
Expand Down
54 changes: 54 additions & 0 deletions src/ros2_medkit_gateway/config/gateway_params.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -71,3 +71,57 @@ ros2_medkit_gateway:
# - Returns metadata instantly for topics without publishers (no CLI timeout wait)
# This significantly improves UX when many topics are idle (e.g., robot waiting for commands)
# CLI is only used for publishing (ros2 topic pub)

# Authentication Configuration (REQ_INTEROP_086, REQ_INTEROP_087)
# JWT-based authentication with Role-Based Access Control (RBAC)
auth:
# Enable/disable authentication
# Default: false (disabled for local development)
enabled: false

# JWT signing secret (required when enabled)
# For HS256: The shared secret string
# For RS256: Path to the private key file (PEM format)
jwt_secret: ""

# Path to public key file for RS256 (required for RS256, optional for HS256)
jwt_public_key: ""

# JWT signing algorithm
# Options: "HS256" (symmetric, default), "RS256" (asymmetric)
jwt_algorithm: "HS256"

# Access token validity period in seconds
# Default: 3600 (1 hour)
token_expiry_seconds: 3600

# Refresh token validity period in seconds
# Default: 86400 (24 hours)
# Must be >= token_expiry_seconds
refresh_token_expiry_seconds: 86400

# When to require authentication
# Options:
# - "none": No authentication required (auth endpoints still available)
# - "write": Auth required for write operations (POST, PUT, DELETE)
# - "all": Auth required for all operations
# Default: "write"
require_auth_for: "write"

# JWT issuer claim
# Default: "ros2_medkit_gateway"
issuer: "ros2_medkit_gateway"

# Pre-configured clients for authentication
# Format: "client_id:client_secret:role"
# Roles: viewer, operator, configurator, admin
#
# Role permissions:
# - viewer: Read-only access (GET on areas, components, data, faults)
# - operator: Viewer + trigger operations, acknowledge faults, publish data
# - configurator: Operator + modify/reset configurations
# - admin: Full access including auth management
#
# Example:
# clients: ["admin:admin_secret_123:admin", "viewer:viewer_pass:viewer"]
clients: [""]
29 changes: 29 additions & 0 deletions src/ros2_medkit_gateway/include/ros2_medkit_gateway/auth/auth.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright 2025 bburda
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
* @file auth.hpp
* @brief Convenience header that includes all authentication components
*
* This header provides a single include for all authentication-related
* functionality in the ros2_medkit_gateway.
*/

#pragma once

#include "ros2_medkit_gateway/auth/auth_config.hpp"
#include "ros2_medkit_gateway/auth/auth_manager.hpp"
#include "ros2_medkit_gateway/auth/auth_middleware.hpp"
#include "ros2_medkit_gateway/auth/auth_models.hpp"
#include "ros2_medkit_gateway/auth/auth_requirement_policy.hpp"
Loading
Loading