cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
jwt.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <algorithm>
6#include <chrono>
7#include <cstdint>
8#include <limits>
9#include <nlohmann/json.hpp>
10#include <optional>
11#include <string>
12#include <utility>
13#include <vector>
14
15#include "cxxmcp/auth/dpop.hpp"
16#include "cxxmcp/auth/jwks.hpp"
19
22
23namespace mcp::auth::openssl {
24
25namespace detail {
26
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));
30}
31
32inline TimePoint jwt_time_from_unix_seconds(std::int64_t seconds) {
33 return TimePoint(std::chrono::seconds(seconds));
34}
35
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()) {
40 return std::nullopt;
41 }
42 if (iter->is_number_integer()) {
43 return iter->get<std::int64_t>();
44 }
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);
50 }
51 }
52 return std::nullopt;
53}
54
55inline std::optional<std::string> jwt_string_claim(const nlohmann::json& object,
56 const char* key) {
57 const auto iter = object.find(key);
58 if (iter == object.end() || iter->is_null() || !iter->is_string()) {
59 return std::nullopt;
60 }
61 return iter->get<std::string>();
62}
63
64inline std::string jwt_claim_to_metadata_value(const nlohmann::json& value) {
65 if (value.is_string()) {
66 return value.get<std::string>();
67 }
68 if (value.is_boolean()) {
69 return value.get<bool>() ? "true" : "false";
70 }
71 if (value.is_number() || value.is_array() || value.is_object()) {
72 return value.dump();
73 }
74 return {};
75}
76
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()) {
82 return false;
83 }
84 return jwt_claim_to_metadata_value(*iter) == expected;
85}
86
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()) {
91 return false;
92 }
93 if (iter->is_string()) {
94 return iter->get<std::string>() == expected;
95 }
96 if (iter->is_array()) {
97 for (const auto& entry : *iter) {
98 if (entry.is_string() && entry.get<std::string>() == expected) {
99 return true;
100 }
101 }
102 }
103 return false;
104}
105
106inline std::string jwt_verified_audience(
107 const nlohmann::json& payload, const std::optional<std::string>& expected) {
108 if (expected.has_value()) {
109 return *expected;
110 }
111 const auto iter = payload.find("aud");
112 if (iter == payload.end() || iter->is_null()) {
113 return {};
114 }
115 if (iter->is_string()) {
116 return iter->get<std::string>();
117 }
118 if (iter->is_array()) {
119 for (const auto& entry : *iter) {
120 if (entry.is_string()) {
121 return entry.get<std::string>();
122 }
123 }
124 }
125 return {};
126}
127
128inline MetadataMap jwt_claims_metadata(const nlohmann::json& payload) {
129 MetadataMap result;
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") {
134 continue;
135 }
136 const auto value = jwt_claim_to_metadata_value(*iter);
137 if (!value.empty()) {
138 result.emplace(key, value);
139 }
140 }
141 return result;
142}
143
144inline core::Result<nlohmann::json> parse_jwt_payload_json(
145 const std::vector<unsigned char>& payload) {
146 try {
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"));
150 }
151 return parsed;
152 } catch (const nlohmann::json::parse_error& error) {
153 return core::unexpected(
154 jwt_error("JWT payload is not valid JSON", error.what()));
155 }
156}
157
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) ||
163 error.code ==
164 static_cast<int>(JoseErrorCode::kUnsupportedJoseAlgorithm) ||
165 error.code ==
166 static_cast<int>(JoseErrorCode::kSignatureVerificationFailed);
167}
168
169} // namespace detail
170
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"));
175 }
176
177 auto decoded = decode_compact_jws(request.jwt);
178 if (!decoded.has_value()) {
179 return core::unexpected(decoded.error());
180 }
181
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"));
187 }
188
189 JwkSelectionCriteria criteria;
190 criteria.key_id = header.key_id;
191 criteria.algorithm = header.algorithm;
192 criteria.public_key_use = "sig";
193 auto key = select_json_web_key(jwks, criteria);
194 if (!key.has_value()) {
195 criteria.public_key_use.reset();
196 key = select_json_web_key(jwks, criteria);
197 }
198 if (!key.has_value()) {
199 return core::unexpected(key.error());
200 }
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"));
204 }
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"));
210 }
211
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());
219 }
220
221 auto payload = detail::parse_jwt_payload_json(decoded->payload);
222 if (!payload.has_value()) {
223 return core::unexpected(payload.error());
224 }
225
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"));
229 }
230 if (detail::jwt_time_from_unix_seconds(*expires_at) <= request.now) {
231 return core::unexpected(detail::jwt_error("JWT has expired"));
232 }
233
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"));
238 }
239
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"));
244 }
245
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"));
250 }
251
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"));
255 }
256
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));
262 }
263 }
264
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);
272 }
273 claims.claims = detail::jwt_claims_metadata(*payload);
274 return claims;
275}
276
277class StaticJwksJwtVerifier final : public JwtVerifier {
278 public:
279 explicit StaticJwksJwtVerifier(JsonWebKeySet jwks) : jwks_(std::move(jwks)) {}
280
282 const JwtVerificationRequest& request) override {
283 return verify_jwt_with_jwks(request, jwks_);
284 }
285
286 private:
287 JsonWebKeySet jwks_;
288};
289
291 std::string jwks_uri;
292 HeaderMap headers;
293 bool use_cache = true;
294 bool refresh_on_key_or_signature_failure = true;
295};
296
305 public:
308 : endpoint_(endpoint), cache_(cache), options_(std::move(options)) {}
309
311 const JwtVerificationRequest& request) override {
312 if (options_.jwks_uri.empty()) {
313 return core::unexpected(
314 detail::jwt_error("JWKS URI is required for JWT verification"));
315 }
316
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());
321 }
322 if (cached->has_value()) {
323 auto verified = verify_jwt_with_jwks(request, **cached);
324 if (verified.has_value()) {
325 return verified;
326 }
327 if (!options_.refresh_on_key_or_signature_failure ||
328 !detail::jwt_error_should_refresh_jwks(verified.error())) {
329 return core::unexpected(verified.error());
330 }
331 }
332 }
333
334 auto fetched = fetch_and_cache_jwks();
335 if (!fetched.has_value()) {
336 return core::unexpected(fetched.error());
337 }
338 return verify_jwt_with_jwks(request, *fetched);
339 }
340
341 private:
342 core::Result<JsonWebKeySet> fetch_and_cache_jwks() {
343 JwksFetchRequest fetch_request;
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());
349 }
350 if (cache_ != nullptr) {
351 auto saved = cache_->save(options_.jwks_uri, *fetched);
352 if (!saved.has_value()) {
353 return core::unexpected(saved.error());
354 }
355 }
356 return *fetched;
357 }
358
359 JwksEndpoint& endpoint_;
360 JwksCache* cache_ = nullptr;
362};
363
364} // namespace mcp::auth::openssl
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