9#include <nlohmann/json.hpp>
23namespace mcp::auth::openssl {
27inline core::Error jwt_error(std::string message, std::string detail = {}) {
28 return make_jose_error(JoseErrorCode::kJwtClaimValidationFailed,
29 std::move(message), std::move(detail));
32inline TimePoint jwt_time_from_unix_seconds(std::int64_t seconds) {
33 return TimePoint(std::chrono::seconds(seconds));
36inline std::optional<std::int64_t> jwt_numeric_date(
37 const nlohmann::json&
object,
const char* key) {
38 const auto iter =
object.find(key);
39 if (iter ==
object.end() || iter->is_null()) {
42 if (iter->is_number_integer()) {
43 return iter->get<std::int64_t>();
45 if (iter->is_number_unsigned()) {
46 const auto value = iter->get<std::uint64_t>();
47 if (value <=
static_cast<std::uint64_t
>(
48 (std::numeric_limits<std::int64_t>::max)())) {
49 return static_cast<std::int64_t
>(value);
55inline std::optional<std::string> jwt_string_claim(
const nlohmann::json&
object,
57 const auto iter =
object.find(key);
58 if (iter ==
object.end() || iter->is_null() || !iter->is_string()) {
61 return iter->get<std::string>();
64inline std::string jwt_claim_to_metadata_value(
const nlohmann::json& value) {
65 if (value.is_string()) {
66 return value.get<std::string>();
68 if (value.is_boolean()) {
69 return value.get<
bool>() ?
"true" :
"false";
71 if (value.is_number() || value.is_array() || value.is_object()) {
77inline bool jwt_required_claim_matches(
const nlohmann::json& payload,
78 const std::string& key,
79 const std::string& expected) {
80 const auto iter = payload.find(key);
81 if (iter == payload.end() || iter->is_null()) {
84 return jwt_claim_to_metadata_value(*iter) == expected;
87inline bool jwt_audience_matches(
const nlohmann::json& payload,
88 const std::string& expected) {
89 const auto iter = payload.find(
"aud");
90 if (iter == payload.end() || iter->is_null()) {
93 if (iter->is_string()) {
94 return iter->get<std::string>() == expected;
96 if (iter->is_array()) {
97 for (
const auto& entry : *iter) {
98 if (entry.is_string() && entry.get<std::string>() == expected) {
106inline std::string jwt_verified_audience(
107 const nlohmann::json& payload,
const std::optional<std::string>& expected) {
108 if (expected.has_value()) {
111 const auto iter = payload.find(
"aud");
112 if (iter == payload.end() || iter->is_null()) {
115 if (iter->is_string()) {
116 return iter->get<std::string>();
118 if (iter->is_array()) {
119 for (
const auto& entry : *iter) {
120 if (entry.is_string()) {
121 return entry.get<std::string>();
128inline MetadataMap jwt_claims_metadata(
const nlohmann::json& payload) {
130 for (
auto iter = payload.begin(); iter != payload.end(); ++iter) {
131 const std::string key = iter.key();
132 if (key ==
"iss" || key ==
"sub" || key ==
"aud" || key ==
"exp" ||
133 key ==
"nbf" || key ==
"iat" || key ==
"jti") {
136 const auto value = jwt_claim_to_metadata_value(*iter);
137 if (!value.empty()) {
138 result.emplace(key, value);
144inline core::Result<nlohmann::json> parse_jwt_payload_json(
145 const std::vector<unsigned char>& payload) {
147 auto parsed = nlohmann::json::parse(payload.begin(), payload.end());
148 if (!parsed.is_object()) {
149 return core::unexpected(jwt_error(
"JWT payload must be a JSON object"));
152 }
catch (
const nlohmann::json::parse_error& error) {
153 return core::unexpected(
154 jwt_error(
"JWT payload is not valid JSON", error.what()));
158inline bool jwt_error_should_refresh_jwks(
const core::Error& error) {
159 const int metadata_error =
160 static_cast<int>(OAuthErrorCode::kMetadataDiscoveryFailed);
161 return error.code == metadata_error ||
162 error.code ==
static_cast<int>(JoseErrorCode::kInvalidJwk) ||
164 static_cast<int>(JoseErrorCode::kUnsupportedJoseAlgorithm) ||
166 static_cast<int>(JoseErrorCode::kSignatureVerificationFailed);
171inline core::Result<VerifiedJwtClaims> verify_jwt_with_jwks(
172 const JwtVerificationRequest& request,
const JsonWebKeySet& jwks) {
173 if (request.jwt.empty()) {
174 return core::unexpected(detail::jwt_error(
"JWT is required"));
177 auto decoded = decode_compact_jws(request.jwt);
178 if (!decoded.has_value()) {
179 return core::unexpected(decoded.error());
182 const auto& header = decoded->protected_header;
183 if (request.required_algorithm.has_value() &&
184 header.algorithm != *request.required_algorithm) {
185 return core::unexpected(
186 detail::jwt_error(
"JWT alg does not match required algorithm"));
189 JwkSelectionCriteria criteria;
190 criteria.key_id = header.key_id;
191 criteria.algorithm = header.algorithm;
192 criteria.public_key_use =
"sig";
194 if (!key.has_value()) {
195 criteria.public_key_use.reset();
198 if (!key.has_value()) {
199 return core::unexpected(key.error());
201 if (key->public_key_use.has_value() && *key->public_key_use !=
"sig") {
202 return core::unexpected(
203 detail::jwt_error(
"JWKS key is not intended for signatures"));
205 if (!key->key_operations.empty() &&
206 std::find(key->key_operations.begin(), key->key_operations.end(),
207 "verify") == key->key_operations.end()) {
208 return core::unexpected(
209 detail::jwt_error(
"JWKS key is not intended for verification"));
212 JwsVerificationOptions verification_options;
213 verification_options.required_algorithm = header.algorithm;
214 verification_options.required_key_id = header.key_id;
215 auto verified_signature =
216 verify_compact_jws_signature(request.jwt, *key, verification_options);
217 if (!verified_signature.has_value()) {
218 return core::unexpected(verified_signature.error());
221 auto payload = detail::parse_jwt_payload_json(decoded->payload);
222 if (!payload.has_value()) {
223 return core::unexpected(payload.error());
226 const auto expires_at = detail::jwt_numeric_date(*payload,
"exp");
227 if (!expires_at.has_value()) {
228 return core::unexpected(detail::jwt_error(
"JWT exp claim is required"));
230 if (detail::jwt_time_from_unix_seconds(*expires_at) <= request.now) {
231 return core::unexpected(detail::jwt_error(
"JWT has expired"));
234 const auto not_before = detail::jwt_numeric_date(*payload,
"nbf");
235 if (not_before.has_value() &&
236 detail::jwt_time_from_unix_seconds(*not_before) > request.now) {
237 return core::unexpected(detail::jwt_error(
"JWT nbf is in the future"));
240 const auto issued_at = detail::jwt_numeric_date(*payload,
"iat");
241 if (issued_at.has_value() &&
242 detail::jwt_time_from_unix_seconds(*issued_at) > request.now) {
243 return core::unexpected(detail::jwt_error(
"JWT iat is in the future"));
246 const auto issuer = detail::jwt_string_claim(*payload,
"iss");
247 if (request.issuer.has_value() &&
248 (!issuer.has_value() || *issuer != *request.issuer)) {
249 return core::unexpected(detail::jwt_error(
"JWT issuer does not match"));
252 if (request.audience.has_value() &&
253 !detail::jwt_audience_matches(*payload, *request.audience)) {
254 return core::unexpected(detail::jwt_error(
"JWT audience does not match"));
257 for (
const auto& required_claim : request.required_claims) {
258 if (!detail::jwt_required_claim_matches(*payload, required_claim.first,
259 required_claim.second)) {
260 return core::unexpected(detail::jwt_error(
261 "JWT required claim does not match", required_claim.first));
265 VerifiedJwtClaims claims;
266 claims.issuer = issuer.value_or(
"");
267 claims.subject = detail::jwt_string_claim(*payload,
"sub").value_or(
"");
268 claims.audience = detail::jwt_verified_audience(*payload, request.audience);
269 claims.expires_at = detail::jwt_time_from_unix_seconds(*expires_at);
270 if (issued_at.has_value()) {
271 claims.issued_at = detail::jwt_time_from_unix_seconds(*issued_at);
273 claims.claims = detail::jwt_claims_metadata(*payload);
283 return verify_jwt_with_jwks(request, jwks_);
291 std::string jwks_uri;
293 bool use_cache =
true;
294 bool refresh_on_key_or_signature_failure =
true;
308 : endpoint_(endpoint), cache_(cache), options_(std::move(options)) {}
312 if (options_.jwks_uri.empty()) {
313 return core::unexpected(
314 detail::jwt_error(
"JWKS URI is required for JWT verification"));
317 if (options_.use_cache && cache_ !=
nullptr) {
318 auto cached = cache_->load(options_.jwks_uri);
319 if (!cached.has_value()) {
320 return core::unexpected(cached.error());
322 if (cached->has_value()) {
323 auto verified = verify_jwt_with_jwks(request, **cached);
324 if (verified.has_value()) {
327 if (!options_.refresh_on_key_or_signature_failure ||
328 !detail::jwt_error_should_refresh_jwks(verified.error())) {
329 return core::unexpected(verified.error());
334 auto fetched = fetch_and_cache_jwks();
335 if (!fetched.has_value()) {
336 return core::unexpected(fetched.error());
338 return verify_jwt_with_jwks(request, *fetched);
344 fetch_request.jwks_uri = options_.jwks_uri;
345 fetch_request.headers = options_.headers;
346 auto fetched = endpoint_.fetch_jwks(fetch_request);
347 if (!fetched.has_value()) {
348 return core::unexpected(fetched.error());
350 if (cache_ !=
nullptr) {
351 auto saved = cache_->save(options_.jwks_uri, *fetched);
352 if (!saved.has_value()) {
353 return core::unexpected(saved.error());
Application-provided JWKS cache boundary.
Definition jwks.hpp:69
Application-provided JWKS retrieval boundary.
Definition jwks.hpp:60
JWT verification boundary for access tokens and client assertions.
Definition dpop.hpp:401
JWT verifier that fetches and caches JWKS through injected SDK boundaries.
Definition jwt.hpp:304
DPoP proof model and signing/verification boundaries.
JWKS value models, parsing, fetch, and cache contracts.
core::Result< JsonWebKey > select_json_web_key(const JsonWebKeySet &set, const JwkSelectionCriteria &criteria)
Select a single JWK by stable JWT header criteria.
Definition jwks.hpp:241
OpenSSL-backed compact JWS signature verification helpers.
Shared result and error primitives used by the public cxxmcp SDK.
tl::expected< T, Error > Result
Alias for the SDK result type.
Definition result.hpp:64
JSON Web Key Set value model.
Definition jwks.hpp:40
Request for retrieving a JWKS document.
Definition jwks.hpp:54
Input for signature- and claims-verified JWT validation.
Definition dpop.hpp:289