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

JWT token validator for OAuth 2.0 Bearer tokens. More...

#include <jwt_validator.h>

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

Public Member Functions

 jwt_validator (const oauth2_config &config)
 Construct validator with OAuth 2.0 configuration.
 
std::pair< jwt_token, jwt_error > decode (std::string_view token_string) const
 Decode a JWT token string into its components.
 
jwt_error validate_claims (const jwt_claims &claims) const
 Validate JWT claims against configuration.
 
bool verify_rs256 (const jwt_token &token, std::string_view public_key_pem) const
 Verify RS256 (RSA-SHA256) signature.
 
bool verify_es256 (const jwt_token &token, std::string_view public_key_pem) const
 Verify ES256 (ECDSA-SHA256) signature.
 
const oauth2_config & config () const noexcept
 Get the OAuth 2.0 configuration.
 

Static Public Member Functions

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

Private Attributes

oauth2_config config_
 

Detailed Description

JWT token validator for OAuth 2.0 Bearer tokens.

Provides JWT decoding, claim validation, and signature verification. Signature verification requires OpenSSL (PACS_WITH_DIGITAL_SIGNATURES).

Example:
config.issuer = "https://auth.example.com";
config.audience = "pacs-api";
jwt_validator validator(config);
auto [token, decode_err] = validator.decode(bearer_token);
if (decode_err != jwt_error::none) { ... }
auto claims_err = validator.validate_claims(token.claims);
if (claims_err != jwt_error::none) { ... }
if (!validator.verify_rs256(token, public_key_pem)) { ... }
JWT token validator for OAuth 2.0 Bearer tokens.
const oauth2_config & config() const noexcept
Get the OAuth 2.0 configuration.
OAuth 2.0 configuration for DICOMweb authorization.
std::string audience
Expected audience (aud claim). Empty = skip audience validation.
std::string issuer
Expected token issuer (iss claim). Empty = skip issuer validation.

Definition at line 124 of file jwt_validator.h.

Constructor & Destructor Documentation

◆ jwt_validator()

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

Construct validator with OAuth 2.0 configuration.

Parameters
configThe OAuth 2.0 configuration

Definition at line 178 of file jwt_validator.cpp.

Member Function Documentation

◆ config()

const oauth2_config & kcenon::pacs::web::auth::jwt_validator::config ( ) const
nodiscardnoexcept

Get the OAuth 2.0 configuration.

Definition at line 481 of file jwt_validator.cpp.

481 {
482 return config_;
483}

References config_.

◆ decode()

std::pair< jwt_token, jwt_error > kcenon::pacs::web::auth::jwt_validator::decode ( std::string_view token_string) const
nodiscard

Decode a JWT token string into its components.

Splits the token, decodes Base64url segments, and parses JSON. Does NOT verify the signature.

Parameters
token_stringThe raw JWT string (header.payload.signature)
Returns
Pair of decoded token and error code

Definition at line 181 of file jwt_validator.cpp.

182 {
183
184 jwt_token token;
185
186 // Split into 3 parts: header.payload.signature
187 auto dot1 = token_string.find('.');
188 if (dot1 == std::string_view::npos) {
189 return {token, jwt_error::malformed_token};
190 }
191
192 auto dot2 = token_string.find('.', dot1 + 1);
193 if (dot2 == std::string_view::npos) {
194 return {token, jwt_error::malformed_token};
195 }
196
197 // Check no additional dots
198 if (token_string.find('.', dot2 + 1) != std::string_view::npos) {
199 return {token, jwt_error::malformed_token};
200 }
201
202 auto header_b64 = token_string.substr(0, dot1);
203 auto payload_b64 = token_string.substr(dot1 + 1, dot2 - dot1 - 1);
204 auto signature_b64 = token_string.substr(dot2 + 1);
205
206 // Store header.payload for signature verification
207 token.header_payload = std::string(token_string.substr(0, dot2));
208
209 // Decode header
210 auto header_json = base64url_decode(header_b64);
211 if (!header_json) {
212 return {token, jwt_error::invalid_base64};
213 }
214
215 // Decode payload
216 auto payload_json = base64url_decode(payload_b64);
217 if (!payload_json) {
218 return {token, jwt_error::invalid_base64};
219 }
220
221 // Decode signature
222 auto sig_bytes = base64url_decode(signature_b64);
223 if (!sig_bytes) {
224 return {token, jwt_error::invalid_base64};
225 }
226 token.signature_bytes = std::move(*sig_bytes);
227
228 // Parse header JSON
229 auto header_rv = crow::json::load(*header_json);
230 if (!header_rv) {
231 return {token, jwt_error::invalid_json};
232 }
233
234 token.header.alg = json_string(header_rv, "alg");
235 token.header.typ = json_string(header_rv, "typ");
236 token.header.kid = json_string(header_rv, "kid");
237
238 if (token.header.alg.empty()) {
239 return {token, jwt_error::missing_required_claim};
240 }
241
242 // Block dangerous algorithms unconditionally (CVE: JWT alg:none attack)
243 static const std::set<std::string> kDangerousAlgorithms = {
244 "none", "None", "NONE", "HS256", "HS384", "HS512"};
245 if (kDangerousAlgorithms.count(token.header.alg)) {
246 return {token, jwt_error::unsupported_algorithm};
247 }
248
249 // Check algorithm is allowed
250 auto& allowed = config_.allowed_algorithms;
251 if (!allowed.empty()) {
252 bool alg_allowed = std::any_of(
253 allowed.begin(), allowed.end(),
254 [&](const std::string& a) { return a == token.header.alg; });
255 if (!alg_allowed) {
256 return {token, jwt_error::unsupported_algorithm};
257 }
258 }
259
260 // Parse payload JSON
261 auto payload_rv = crow::json::load(*payload_json);
262 if (!payload_rv) {
263 return {token, jwt_error::invalid_json};
264 }
265
266 token.claims.iss = json_string(payload_rv, "iss");
267 token.claims.sub = json_string(payload_rv, "sub");
268 token.claims.aud = json_string(payload_rv, "aud");
269 token.claims.exp = json_int64(payload_rv, "exp");
270 token.claims.iat = json_int64(payload_rv, "iat");
271 token.claims.nbf = json_int64(payload_rv, "nbf");
272 token.claims.jti = json_string(payload_rv, "jti");
273
274 // Parse scopes from "scope" claim (space-delimited string per RFC 6749)
275 auto scope_str = json_string(payload_rv, "scope");
276 if (!scope_str.empty()) {
277 token.claims.scopes = parse_scopes(scope_str);
278 }
279
280 return {token, jwt_error::none};
281}
@ invalid_base64
Base64url decoding failed.
@ malformed_token
Token doesn't have 3 dot-separated parts.
@ unsupported_algorithm
Algorithm not in allowed list.
@ missing_required_claim
Required claim is missing.
@ invalid_json
JSON parsing failed.
std::optional< std::string > base64url_decode(std::string_view input)
Decode a Base64url-encoded string (RFC 4648 Section 5)
std::vector< std::string > allowed_algorithms
Allowed signing algorithms (default: RS256, ES256)

References kcenon::pacs::web::auth::jwt_header::alg, kcenon::pacs::web::auth::oauth2_config::allowed_algorithms, kcenon::pacs::web::auth::jwt_claims::aud, kcenon::pacs::web::auth::base64url_decode(), kcenon::pacs::web::auth::jwt_token::claims, config_, kcenon::pacs::web::auth::jwt_claims::exp, kcenon::pacs::web::auth::jwt_token::header, kcenon::pacs::web::auth::jwt_token::header_payload, kcenon::pacs::web::auth::jwt_claims::iat, kcenon::pacs::web::auth::invalid_base64, kcenon::pacs::web::auth::invalid_json, kcenon::pacs::web::auth::jwt_claims::iss, kcenon::pacs::web::auth::jwt_claims::jti, kcenon::pacs::web::auth::jwt_header::kid, kcenon::pacs::web::auth::malformed_token, kcenon::pacs::web::auth::missing_required_claim, kcenon::pacs::web::auth::jwt_claims::nbf, kcenon::pacs::web::auth::none, kcenon::pacs::web::auth::jwt_claims::scopes, kcenon::pacs::web::auth::jwt_token::signature_bytes, kcenon::pacs::web::auth::jwt_claims::sub, kcenon::pacs::web::auth::jwt_header::typ, and kcenon::pacs::web::auth::unsupported_algorithm.

Referenced by kcenon::pacs::web::auth::oauth2_middleware::authenticate().

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

◆ has_any_scope()

bool kcenon::pacs::web::auth::jwt_validator::has_any_scope ( const jwt_claims & claims,
const std::vector< std::string > & scopes )
staticnodiscardnoexcept

Check if token has any of the specified scopes.

Parameters
claimsThe token claims
scopesList of acceptable scopes
Returns
true if at least one scope matches

Definition at line 471 of file jwt_validator.cpp.

473 {
474
475 return std::any_of(scopes.begin(), scopes.end(),
476 [&claims](const std::string& scope) {
477 return has_scope(claims, scope);
478 });
479}

Referenced by kcenon::pacs::web::auth::oauth2_middleware::require_any_scope().

Here is the caller graph for this function:

◆ has_scope()

bool kcenon::pacs::web::auth::jwt_validator::has_scope ( const jwt_claims & claims,
std::string_view scope )
staticnodiscardnoexcept

Check if token has a specific scope.

Parameters
claimsThe token claims
scopeThe required scope
Returns
true if the scope is present

Definition at line 463 of file jwt_validator.cpp.

465 {
466
467 return std::any_of(claims.scopes.begin(), claims.scopes.end(),
468 [scope](const std::string& s) { return s == scope; });
469}

Referenced by kcenon::pacs::web::auth::oauth2_middleware::require_scope().

Here is the caller graph for this function:

◆ validate_claims()

jwt_error kcenon::pacs::web::auth::jwt_validator::validate_claims ( const jwt_claims & claims) const
nodiscard

Validate JWT claims against configuration.

Checks issuer, audience, expiration, and not-before claims.

Parameters
claimsThe decoded claims to validate
Returns
jwt_error::none if all claims are valid

Definition at line 283 of file jwt_validator.cpp.

283 {
284 auto now = current_timestamp();
285 auto skew = static_cast<std::int64_t>(config_.clock_skew_seconds);
286
287 // Check expiration
288 if (claims.exp > 0 && now > claims.exp + skew) {
290 }
291
292 // Check not-before
293 if (claims.nbf > 0 && now < claims.nbf - skew) {
295 }
296
297 // Check issuer
298 if (!config_.issuer.empty() && claims.iss != config_.issuer) {
300 }
301
302 // Check audience
303 if (!config_.audience.empty() && claims.aud != config_.audience) {
305 }
306
307 // Sub claim is typically required
308 if (claims.sub.empty()) {
310 }
311
312 return jwt_error::none;
313}
@ invalid_audience
Audience doesn't match expected value.
@ token_expired
Token has expired (exp claim)
@ invalid_issuer
Issuer doesn't match expected value.
@ token_not_yet_valid
Token not yet valid (nbf claim)
std::uint32_t clock_skew_seconds
Allowed clock skew in seconds for exp/nbf validation.

References kcenon::pacs::web::auth::jwt_claims::aud, kcenon::pacs::web::auth::oauth2_config::audience, kcenon::pacs::web::auth::oauth2_config::clock_skew_seconds, config_, kcenon::pacs::web::auth::jwt_claims::exp, kcenon::pacs::web::auth::invalid_audience, kcenon::pacs::web::auth::invalid_issuer, kcenon::pacs::web::auth::jwt_claims::iss, kcenon::pacs::web::auth::oauth2_config::issuer, kcenon::pacs::web::auth::missing_required_claim, kcenon::pacs::web::auth::jwt_claims::nbf, kcenon::pacs::web::auth::none, kcenon::pacs::web::auth::jwt_claims::sub, kcenon::pacs::web::auth::token_expired, and kcenon::pacs::web::auth::token_not_yet_valid.

Referenced by kcenon::pacs::web::auth::oauth2_middleware::authenticate().

Here is the caller graph for this function:

◆ verify_es256()

bool kcenon::pacs::web::auth::jwt_validator::verify_es256 ( const jwt_token & token,
std::string_view public_key_pem ) const
nodiscard

Verify ES256 (ECDSA-SHA256) signature.

Parameters
tokenThe decoded token with signature data
public_key_pemEC public key in PEM format
Returns
true if signature is valid

Definition at line 451 of file jwt_validator.cpp.

453 {
454 return false;
455}

Referenced by kcenon::pacs::web::auth::oauth2_middleware::verify_signature().

Here is the caller graph for this function:

◆ verify_rs256()

bool kcenon::pacs::web::auth::jwt_validator::verify_rs256 ( const jwt_token & token,
std::string_view public_key_pem ) const
nodiscard

Verify RS256 (RSA-SHA256) signature.

Parameters
tokenThe decoded token with signature data
public_key_pemRSA public key in PEM format
Returns
true if signature is valid

Definition at line 445 of file jwt_validator.cpp.

447 {
448 return false;
449}

Referenced by kcenon::pacs::web::auth::oauth2_middleware::verify_signature().

Here is the caller graph for this function:

Member Data Documentation

◆ config_

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

Definition at line 201 of file jwt_validator.h.

Referenced by config(), decode(), and validate_claims().


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