PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
kcenon::pacs::web::auth::oauth2_middleware Class Reference

OAuth 2.0 middleware for DICOMweb endpoint authorization. More...

#include <oauth2_middleware.h>

Collaboration diagram for kcenon::pacs::web::auth::oauth2_middleware:
Collaboration graph

Public Member Functions

 oauth2_middleware (const oauth2_config &config)
 Construct middleware with OAuth 2.0 configuration.
 
void set_jwks_provider (std::shared_ptr< jwks_provider > provider)
 Set the JWKS provider for signature verification.
 
void set_access_control_manager (std::shared_ptr< security::access_control_manager > manager)
 Set the access control manager for RBAC integration.
 
std::optional< auth_result > authenticate (const crow::request &req, crow::response &res) const
 Authenticate a request using OAuth 2.0 Bearer token.
 
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.
 
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.
 
bool enabled () const noexcept
 Check if OAuth 2.0 is enabled.
 
const jwt_validator & validator () const noexcept
 Get the underlying JWT validator.
 

Private Member Functions

std::optional< std::string_view > extract_bearer_token (const crow::request &req) const
 Extract Bearer token from Authorization header.
 
bool verify_signature (const jwt_token &token) const
 Verify token signature using JWKS keys.
 

Static Private Member Functions

static void set_unauthorized (crow::response &res, std::string_view message)
 Set 401 Unauthorized response.
 
static void set_forbidden (crow::response &res, std::string_view message)
 Set 403 Forbidden response.
 

Private Attributes

oauth2_config config_
 
jwt_validator validator_
 
std::shared_ptr< jwks_provider > jwks_provider_
 
std::shared_ptr< security::access_control_manager > security_manager_
 

Detailed Description

OAuth 2.0 middleware for DICOMweb endpoint authorization.

When OAuth 2.0 is enabled, the middleware:

  1. Extracts the Bearer token from the Authorization header
  2. Decodes and validates the JWT (issuer, audience, expiration)
  3. Verifies the token signature using JWKS keys
  4. Checks required scopes for the DICOMweb operation
  5. Creates a user_context for the downstream endpoint

When OAuth 2.0 is disabled, the middleware is a no-op and the system falls back to the existing X-User-ID header-based authentication.

Example:
config.enabled = true;
config.issuer = "https://auth.example.com";
oauth2_middleware middleware(config);
middleware.set_jwks_provider(jwks);
// In endpoint handler:
auto result = middleware.authenticate(req, res);
if (!result) return res; // Error response already set
if (!middleware.require_scope(result->claims, res, dicomweb_scopes::read)) {
return res; // 403 Forbidden
}
OAuth 2.0 middleware for DICOMweb endpoint authorization.
OAuth 2.0 configuration for DICOMweb authorization.
bool enabled
Enable OAuth 2.0 authorization (disabled by default for backward compat)
std::string issuer
Expected token issuer (iss claim). Empty = skip issuer validation.

Definition at line 111 of file oauth2_middleware.h.

Constructor & Destructor Documentation

◆ oauth2_middleware()

kcenon::pacs::web::auth::oauth2_middleware::oauth2_middleware ( const oauth2_config & config)
explicit

Construct middleware with OAuth 2.0 configuration.

Parameters
configThe OAuth 2.0 configuration

Definition at line 32 of file oauth2_middleware.cpp.

Member Function Documentation

◆ authenticate()

std::optional< auth_result > kcenon::pacs::web::auth::oauth2_middleware::authenticate ( const crow::request & req,
crow::response & res ) const
nodiscard

Authenticate a request using OAuth 2.0 Bearer token.

Extracts the Bearer token, validates JWT claims, verifies the signature, and creates an auth_result containing both the user_context and validated claims. On failure, sets an appropriate error response (401 or 403).

Parameters
reqThe HTTP request
resThe HTTP response (set on error)
Returns
auth_result on success, std::nullopt on failure

Definition at line 45 of file oauth2_middleware.cpp.

46 {
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}
jwt_error validate_claims(const jwt_claims &claims) const
Validate JWT claims against configuration.
std::pair< jwt_token, jwt_error > decode(std::string_view token_string) const
Decode a JWT token string into its components.
static void set_unauthorized(crow::response &res, std::string_view message)
Set 401 Unauthorized response.
std::optional< std::string_view > extract_bearer_token(const crow::request &req) const
Extract Bearer token from Authorization header.
bool verify_signature(const jwt_token &token) const
Verify token signature using JWKS keys.
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.
bool allow_unknown_users
Allow unknown OAuth users not found in RBAC to access as Viewer When false (default): unknown users r...

References kcenon::pacs::web::auth::oauth2_config::allow_unknown_users, config_, kcenon::pacs::web::auth::jwt_validator::decode(), extract_bearer_token(), kcenon::pacs::security::User::id, kcenon::pacs::web::auth::jwt_error_message(), kcenon::pacs::web::auth::none, security_manager_, set_unauthorized(), kcenon::pacs::web::auth::jwt_validator::validate_claims(), validator_, verify_signature(), and kcenon::pacs::security::Viewer.

Here is the call graph for this function:

◆ enabled()

bool kcenon::pacs::web::auth::oauth2_middleware::enabled ( ) const
nodiscardnoexcept

Check if OAuth 2.0 is enabled.

Definition at line 138 of file oauth2_middleware.cpp.

138 {
139 return config_.enabled;
140}

References config_, and kcenon::pacs::web::auth::oauth2_config::enabled.

◆ extract_bearer_token()

std::optional< std::string_view > kcenon::pacs::web::auth::oauth2_middleware::extract_bearer_token ( const crow::request & req) const
nodiscardprivate

Extract Bearer token from Authorization header.

Definition at line 150 of file oauth2_middleware.cpp.

151 {
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}

Referenced by authenticate().

Here is the caller graph for this function:

◆ require_any_scope()

bool kcenon::pacs::web::auth::oauth2_middleware::require_any_scope ( const jwt_claims & claims,
crow::response & res,
const std::vector< std::string > & required_scopes ) const
nodiscard

Check if the request has any of the required scopes.

Parameters
claimsJWT claims from a previously validated token
resThe HTTP response (set on failure)
required_scopesList of acceptable scopes
Returns
true if at least one scope is present

Definition at line 121 of file oauth2_middleware.cpp.

124 {
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}
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.
static void set_forbidden(crow::response &res, std::string_view message)
Set 403 Forbidden response.

References kcenon::pacs::web::auth::jwt_validator::has_any_scope(), and set_forbidden().

Here is the call graph for this function:

◆ require_scope()

bool kcenon::pacs::web::auth::oauth2_middleware::require_scope ( const jwt_claims & claims,
crow::response & res,
std::string_view required_scope ) const
nodiscard

Check if the authenticated request has a required scope.

Must be called after authenticate(). Returns false and sets 403 response if the required scope is missing.

Parameters
claimsJWT claims from a previously validated token
resThe HTTP response (set on failure)
required_scopeThe required OAuth scope
Returns
true if the scope is present

Definition at line 108 of file oauth2_middleware.cpp.

111 {
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}
static bool has_scope(const jwt_claims &claims, std::string_view scope) noexcept
Check if token has a specific scope.

References kcenon::pacs::web::auth::jwt_validator::has_scope(), and set_forbidden().

Here is the call graph for this function:

◆ set_access_control_manager()

void kcenon::pacs::web::auth::oauth2_middleware::set_access_control_manager ( std::shared_ptr< security::access_control_manager > manager)

Set the access control manager for RBAC integration.

Parameters
managerShared pointer to access control manager

Definition at line 40 of file oauth2_middleware.cpp.

41 {
42 security_manager_ = std::move(manager);
43}

References security_manager_.

◆ set_forbidden()

void kcenon::pacs::web::auth::oauth2_middleware::set_forbidden ( crow::response & res,
std::string_view message )
staticprivate

Set 403 Forbidden response.

Definition at line 207 of file oauth2_middleware.cpp.

208 {
209 res.code = 403;
210 res.add_header("Content-Type", "application/json");
211 res.body = make_error_json("FORBIDDEN", message);
212}
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

References kcenon::pacs::web::make_error_json().

Referenced by require_any_scope(), and require_scope().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ set_jwks_provider()

void kcenon::pacs::web::auth::oauth2_middleware::set_jwks_provider ( std::shared_ptr< jwks_provider > provider)

Set the JWKS provider for signature verification.

Parameters
providerShared pointer to JWKS provider

Definition at line 35 of file oauth2_middleware.cpp.

36 {
37 jwks_provider_ = std::move(provider);
38}
std::shared_ptr< jwks_provider > jwks_provider_

References jwks_provider_.

◆ set_unauthorized()

void kcenon::pacs::web::auth::oauth2_middleware::set_unauthorized ( crow::response & res,
std::string_view message )
staticprivate

Set 401 Unauthorized response.

Definition at line 199 of file oauth2_middleware.cpp.

200 {
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}

References kcenon::pacs::web::make_error_json().

Referenced by authenticate().

Here is the call graph for this function:
Here is the caller graph for this function:

◆ validator()

const jwt_validator & kcenon::pacs::web::auth::oauth2_middleware::validator ( ) const
nodiscardnoexcept

Get the underlying JWT validator.

Definition at line 142 of file oauth2_middleware.cpp.

142 {
143 return validator_;
144}

References validator_.

◆ verify_signature()

bool kcenon::pacs::web::auth::oauth2_middleware::verify_signature ( const jwt_token & token) const
nodiscardprivate

Verify token signature using JWKS keys.

Definition at line 173 of file oauth2_middleware.cpp.

173 {
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}
bool verify_es256(const jwt_token &token, std::string_view public_key_pem) const
Verify ES256 (ECDSA-SHA256) signature.
bool verify_rs256(const jwt_token &token, std::string_view public_key_pem) const
Verify RS256 (RSA-SHA256) signature.

References kcenon::pacs::web::auth::jwt_header::alg, kcenon::pacs::web::auth::jwt_token::header, jwks_provider_, kcenon::pacs::web::auth::jwt_header::kid, validator_, kcenon::pacs::web::auth::jwt_validator::verify_es256(), and kcenon::pacs::web::auth::jwt_validator::verify_rs256().

Referenced by authenticate().

Here is the call graph for this function:
Here is the caller graph for this function:

Member Data Documentation

◆ config_

oauth2_config kcenon::pacs::web::auth::oauth2_middleware::config_
private

Definition at line 187 of file oauth2_middleware.h.

Referenced by authenticate(), and enabled().

◆ jwks_provider_

std::shared_ptr<jwks_provider> kcenon::pacs::web::auth::oauth2_middleware::jwks_provider_
private

Definition at line 189 of file oauth2_middleware.h.

Referenced by set_jwks_provider(), and verify_signature().

◆ security_manager_

std::shared_ptr<security::access_control_manager> kcenon::pacs::web::auth::oauth2_middleware::security_manager_
private

Definition at line 190 of file oauth2_middleware.h.

Referenced by authenticate(), and set_access_control_manager().

◆ validator_

jwt_validator kcenon::pacs::web::auth::oauth2_middleware::validator_
private

Definition at line 188 of file oauth2_middleware.h.

Referenced by authenticate(), validator(), and verify_signature().


The documentation for this class was generated from the following files: