PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
oauth2_middleware.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13// IMPORTANT: Include Crow FIRST before any PACS headers
14#include "crow.h"
15
17
23
24#include <string>
25
27
28// =============================================================================
29// oauth2_middleware Implementation
30// =============================================================================
31
33 : config_(config), validator_(config) {}
34
36 std::shared_ptr<jwks_provider> provider) {
37 jwks_provider_ = std::move(provider);
38}
39
41 std::shared_ptr<security::access_control_manager> manager) {
42 security_manager_ = std::move(manager);
43}
44
45std::optional<auth_result> oauth2_middleware::authenticate(
46 const crow::request& req, crow::response& res) const {
47
48 // Extract Bearer token from Authorization header
49 auto bearer = extract_bearer_token(req);
50 if (!bearer) {
51 set_unauthorized(res, "Missing or invalid Authorization header");
52 return std::nullopt;
53 }
54
55 // Decode JWT
56 auto [token, decode_err] = validator_.decode(*bearer);
57 if (decode_err != jwt_error::none) {
58 set_unauthorized(res, std::string(jwt_error_message(decode_err)));
59 return std::nullopt;
60 }
61
62 // Validate claims (issuer, audience, expiration)
63 auto claims_err = validator_.validate_claims(token.claims);
64 if (claims_err != jwt_error::none) {
65 set_unauthorized(res, std::string(jwt_error_message(claims_err)));
66 return std::nullopt;
67 }
68
69 // Verify signature using JWKS
70 if (!verify_signature(token)) {
71 set_unauthorized(res, "Token signature verification failed");
72 return std::nullopt;
73 }
74
75 // Create user_context from JWT subject
76 // Try to find existing user in RBAC system, or create an OAuth-based context
77 security::User user;
78 user.id = token.claims.sub;
79 user.username = token.claims.sub;
80 user.active = true;
81
82 // If we have an access control manager, try to look up the user
84 auto user_result = security_manager_->get_user(token.claims.sub);
85 if (user_result.is_ok()) {
86 user = user_result.unwrap();
87 } else if (config_.allow_unknown_users) {
88 // Backward compatibility: grant Viewer role to unknown OAuth users
89 user.roles = {security::Role::Viewer};
90 } else {
91 set_unauthorized(res, "Unknown user: not registered in access control system");
92 return std::nullopt;
93 }
94 } else if (config_.allow_unknown_users) {
95 user.roles = {security::Role::Viewer};
96 } else {
97 set_unauthorized(res, "Unknown user: no access control manager configured");
98 return std::nullopt;
99 }
100
101 auto ctx = security::user_context(std::move(user), token.claims.jti);
102 ctx.set_source_ip(std::string(req.remote_ip_address));
103 ctx.touch();
104
105 return auth_result{std::move(ctx), std::move(token.claims)};
106}
107
109 const jwt_claims& claims,
110 crow::response& res,
111 std::string_view required_scope) const {
112
113 if (!jwt_validator::has_scope(claims, required_scope)) {
114 set_forbidden(res,
115 "Insufficient scope: requires " + std::string(required_scope));
116 return false;
117 }
118 return true;
119}
120
122 const jwt_claims& claims,
123 crow::response& res,
124 const std::vector<std::string>& required_scopes) const {
125
126 if (!jwt_validator::has_any_scope(claims, required_scopes)) {
127 std::string scope_list;
128 for (size_t i = 0; i < required_scopes.size(); ++i) {
129 if (i > 0) scope_list += " or ";
130 scope_list += required_scopes[i];
131 }
132 set_forbidden(res, "Insufficient scope: requires " + scope_list);
133 return false;
134 }
135 return true;
136}
137
138bool oauth2_middleware::enabled() const noexcept {
139 return config_.enabled;
140}
141
143 return validator_;
144}
145
146// =============================================================================
147// Private Helpers
148// =============================================================================
149
150std::optional<std::string_view> oauth2_middleware::extract_bearer_token(
151 const crow::request& req) const {
152
153 auto auth_header = req.get_header_value("Authorization");
154 if (auth_header.empty()) return std::nullopt;
155
156 std::string_view header_view(auth_header);
157
158 // Check for "Bearer " prefix (case-insensitive for "Bearer")
159 constexpr std::string_view bearer_prefix = "Bearer ";
160 if (header_view.size() <= bearer_prefix.size()) return std::nullopt;
161
162 // Case-sensitive check per RFC 6750
163 if (header_view.substr(0, bearer_prefix.size()) != bearer_prefix) {
164 return std::nullopt;
165 }
166
167 auto token = header_view.substr(bearer_prefix.size());
168 if (token.empty()) return std::nullopt;
169
170 return token;
171}
172
174 if (!jwks_provider_) return false;
175
176 // Try to get key by kid from the token header
177 std::optional<jwk_key> key;
178 if (!token.header.kid.empty()) {
179 key = jwks_provider_->get_key(token.header.kid);
180 }
181
182 // Fall back to algorithm-based lookup
183 if (!key) {
184 key = jwks_provider_->get_key_by_alg(token.header.alg);
185 }
186
187 if (!key) return false;
188
189 // Verify based on algorithm
190 if (token.header.alg == "RS256") {
191 return validator_.verify_rs256(token, key->pem);
192 } else if (token.header.alg == "ES256") {
193 return validator_.verify_es256(token, key->pem);
194 }
195
196 return false;
197}
198
199void oauth2_middleware::set_unauthorized(crow::response& res,
200 std::string_view message) {
201 res.code = 401;
202 res.add_header("Content-Type", "application/json");
203 res.add_header("WWW-Authenticate", "Bearer");
204 res.body = make_error_json("UNAUTHORIZED", message);
205}
206
207void oauth2_middleware::set_forbidden(crow::response& res,
208 std::string_view message) {
209 res.code = 403;
210 res.add_header("Content-Type", "application/json");
211 res.body = make_error_json("FORBIDDEN", message);
212}
213
214} // namespace kcenon::pacs::web::auth
Core RBAC logic.
Represents the security context for a user session.
bool verify_es256(const jwt_token &token, std::string_view public_key_pem) const
Verify ES256 (ECDSA-SHA256) signature.
static bool has_any_scope(const jwt_claims &claims, const std::vector< std::string > &scopes) noexcept
Check if token has any of the specified scopes.
jwt_error validate_claims(const jwt_claims &claims) const
Validate JWT claims against configuration.
static bool has_scope(const jwt_claims &claims, std::string_view scope) noexcept
Check if token has a specific scope.
bool verify_rs256(const jwt_token &token, std::string_view public_key_pem) const
Verify RS256 (RSA-SHA256) signature.
std::pair< jwt_token, jwt_error > decode(std::string_view token_string) const
Decode a JWT token string into its components.
bool enabled() const noexcept
Check if OAuth 2.0 is enabled.
std::optional< auth_result > authenticate(const crow::request &req, crow::response &res) const
Authenticate a request using OAuth 2.0 Bearer token.
std::shared_ptr< jwks_provider > jwks_provider_
bool require_any_scope(const jwt_claims &claims, crow::response &res, const std::vector< std::string > &required_scopes) const
Check if the request has any of the required scopes.
static void set_unauthorized(crow::response &res, std::string_view message)
Set 401 Unauthorized response.
bool require_scope(const jwt_claims &claims, crow::response &res, std::string_view required_scope) const
Check if the authenticated request has a required scope.
std::optional< std::string_view > extract_bearer_token(const crow::request &req) const
Extract Bearer token from Authorization header.
void set_jwks_provider(std::shared_ptr< jwks_provider > provider)
Set the JWKS provider for signature verification.
bool verify_signature(const jwt_token &token) const
Verify token signature using JWKS keys.
void set_access_control_manager(std::shared_ptr< security::access_control_manager > manager)
Set the access control manager for RBAC integration.
oauth2_middleware(const oauth2_config &config)
Construct middleware with OAuth 2.0 configuration.
static void set_forbidden(crow::response &res, std::string_view message)
Set 403 Forbidden response.
const jwt_validator & validator() const noexcept
Get the underlying JWT validator.
std::shared_ptr< security::access_control_manager > security_manager_
@ Viewer
Read-only access to studies.
std::string_view jwt_error_message(jwt_error error) noexcept
Get human-readable description for a JWT error.
std::string make_error_json(std::string_view code, std::string_view message)
Create JSON error response body with details.
Definition rest_types.h:79
OAuth 2.0 middleware for DICOMweb endpoint authorization.
Common types and utilities for REST API.
Role definitions for RBAC.
Represents a user in the system.
Definition user.h:26
Result of a successful OAuth 2.0 authentication.
Decoded JWT claims (payload)
std::string kid
Key ID for key selection.
std::string alg
Algorithm (RS256, ES256)
Decoded JWT token with raw segments for signature verification.
OAuth 2.0 configuration for DICOMweb authorization.
bool enabled
Enable OAuth 2.0 authorization (disabled by default for backward compat)
bool allow_unknown_users
Allow unknown OAuth users not found in RBAC to access as Viewer When false (default): unknown users r...
User definition for RBAC.
User context for session-based access control.