5#include <openssl/bio.h>
6#include <openssl/evp.h>
7#include <openssl/pem.h>
8#include <openssl/rand.h>
15#include <nlohmann/json.hpp>
30namespace mcp::auth::openssl {
35 void operator()(BIO* value)
const noexcept { BIO_free(value); }
38using BioPtr = std::unique_ptr<BIO, BioDeleter>;
42 return core::unexpected(make_jose_error(
43 JoseErrorCode::kInvalidJwk,
"DPoP private key PEM is required"));
46 static_cast<std::size_t
>((std::numeric_limits<int>::max)())) {
47 return core::unexpected(make_jose_error(
48 JoseErrorCode::kInvalidJwk,
"DPoP private key PEM is too large"));
51 BioPtr bio(BIO_new_mem_buf(pem.data(),
static_cast<int>(pem.size())));
53 return core::unexpected(
54 make_jose_error(JoseErrorCode::kInvalidJwk,
55 "failed to allocate OpenSSL private-key BIO"));
58 EVP_PKEY* raw = PEM_read_bio_PrivateKey(bio.get(),
nullptr,
nullptr,
nullptr);
60 return core::unexpected(make_jose_error(
61 JoseErrorCode::kInvalidJwk,
"failed to parse DPoP private key PEM"));
63 return EvpPkeyPtr(raw);
66inline core::Result<std::string> random_jwt_id() {
67 std::vector<unsigned char> bytes(16);
68 if (RAND_bytes(bytes.data(),
static_cast<int>(bytes.size())) != 1) {
69 return core::unexpected(
70 make_jose_error(JoseErrorCode::kSignatureVerificationFailed,
71 "failed to generate DPoP jti"));
73 return base64url_encode(bytes);
76inline std::int64_t unix_seconds_from_time(TimePoint value) {
77 return std::chrono::duration_cast<std::chrono::seconds>(
78 value.time_since_epoch())
82inline TimePoint time_from_unix_seconds(std::int64_t seconds) {
83 return TimePoint(std::chrono::seconds(seconds));
86inline nlohmann::json public_jwk_to_json(
const JsonWebKey& jwk) {
88 value[
"kty"] = jwk.key_type;
89 if (jwk.algorithm.has_value()) {
90 value[
"alg"] = *jwk.algorithm;
92 if (jwk.key_id.has_value()) {
93 value[
"kid"] = *jwk.key_id;
95 if (jwk.curve.has_value()) {
96 value[
"crv"] = *jwk.curve;
98 if (jwk.x.has_value()) {
101 if (jwk.y.has_value()) {
104 if (jwk.modulus.has_value()) {
105 value[
"n"] = *jwk.modulus;
107 if (jwk.exponent.has_value()) {
108 value[
"e"] = *jwk.exponent;
113inline core::Result<std::string> sign_compact_jws(
114 EVP_PKEY* key, std::string_view algorithm,
const nlohmann::json& header,
115 const nlohmann::json& payload) {
116 const EVP_MD* digest = digest_for_jose_algorithm(algorithm);
117 if (digest ==
nullptr) {
118 return core::unexpected(
119 make_jose_error(JoseErrorCode::kUnsupportedJoseAlgorithm,
120 "unsupported DPoP signing algorithm"));
123 const std::string encoded_header = base64url_encode(header.dump());
124 const std::string encoded_payload = base64url_encode(payload.dump());
125 const std::string signing_input = encoded_header +
"." + encoded_payload;
127 EvpMdCtxPtr context(EVP_MD_CTX_new());
129 EVP_DigestSignInit(context.get(),
nullptr, digest,
nullptr, key) <= 0 ||
130 EVP_DigestSignUpdate(context.get(), signing_input.data(),
131 signing_input.size()) <= 0) {
132 return core::unexpected(
133 make_jose_error(JoseErrorCode::kSignatureVerificationFailed,
134 "failed to initialize DPoP proof signing"));
137 std::size_t signature_size = 0;
138 if (EVP_DigestSignFinal(context.get(),
nullptr, &signature_size) <= 0) {
139 return core::unexpected(
140 make_jose_error(JoseErrorCode::kSignatureVerificationFailed,
141 "failed to size DPoP proof signature"));
143 std::vector<unsigned char> signature(signature_size);
144 if (EVP_DigestSignFinal(context.get(), signature.data(), &signature_size) <=
146 return core::unexpected(
147 make_jose_error(JoseErrorCode::kSignatureVerificationFailed,
148 "failed to sign DPoP proof"));
150 signature.resize(signature_size);
152 if (is_ecdsa_jose_algorithm(algorithm)) {
153 auto raw = ecdsa_der_signature_to_raw(signature, algorithm);
154 if (!raw.has_value()) {
155 return core::unexpected(raw.error());
157 signature = std::move(*raw);
160 return signing_input +
"." + base64url_encode(signature);
163inline core::Result<nlohmann::json> parse_json_object_bytes(
164 const std::vector<unsigned char>& bytes, std::string message) {
166 auto parsed = nlohmann::json::parse(bytes.begin(), bytes.end());
167 if (!parsed.is_object()) {
168 return core::unexpected(make_jose_error(
169 JoseErrorCode::kJwtClaimValidationFailed, std::move(message)));
172 }
catch (
const nlohmann::json::parse_error& error) {
173 return core::unexpected(
174 make_jose_error(JoseErrorCode::kJwtClaimValidationFailed,
175 std::move(message), error.what()));
179inline std::optional<std::string> string_claim(
const nlohmann::json&
object,
181 const auto iter =
object.find(key);
182 if (iter ==
object.end() || iter->is_null() || !iter->is_string()) {
185 return iter->get<std::string>();
188inline std::optional<std::int64_t> numeric_date_claim(
189 const nlohmann::json&
object,
const char* key) {
190 const auto iter =
object.find(key);
191 if (iter ==
object.end() || iter->is_null()) {
194 if (iter->is_number_integer()) {
195 return iter->get<std::int64_t>();
197 if (iter->is_number_unsigned()) {
198 const auto value = iter->get<std::uint64_t>();
199 if (value <=
static_cast<std::uint64_t
>(
200 (std::numeric_limits<std::int64_t>::max)())) {
201 return static_cast<std::int64_t
>(value);
207inline MetadataMap dpop_claim_metadata(
const nlohmann::json& payload) {
209 for (
auto iter = payload.begin(); iter != payload.end(); ++iter) {
210 const std::string key = iter.key();
211 if (key ==
"jti" || key ==
"htm" || key ==
"htu" || key ==
"iat" ||
212 key ==
"ath" || key ==
"nonce") {
215 if (iter->is_string()) {
216 result.emplace(key, iter->get<std::string>());
217 }
else if (iter->is_boolean()) {
218 result.emplace(key, iter->get<
bool>() ?
"true" :
"false");
219 }
else if (iter->is_number() || iter->is_array() || iter->is_object()) {
220 result.emplace(key, iter->dump());
229 std::string private_key_pem;
230 std::string algorithm =
"ES256";
231 std::string client_id;
232 std::string audience;
233 std::optional<std::string> key_id;
234 std::optional<std::string> jwt_id;
235 TimePoint issued_at = SystemClock::now();
236 std::chrono::seconds lifetime = std::chrono::minutes(5);
241 if (request.client_id.empty()) {
242 return core::unexpected(make_jose_error(
243 JoseErrorCode::kJwtClaimValidationFailed,
"client_id is required"));
245 if (request.audience.empty()) {
246 return core::unexpected(make_jose_error(
247 JoseErrorCode::kJwtClaimValidationFailed,
"audience is required"));
250 auto private_key = detail::private_key_from_pem(request.private_key_pem);
251 if (!private_key.has_value()) {
252 return core::unexpected(private_key.error());
255 nlohmann::json header;
256 header[
"alg"] = request.algorithm;
257 if (request.key_id.has_value()) {
258 header[
"kid"] = *request.key_id;
261 core::Result<std::string> jwt_id =
262 request.jwt_id.has_value() ? core::Result<std::string>(*request.jwt_id)
263 : detail::random_jwt_id();
264 if (!jwt_id.has_value()) {
265 return core::unexpected(jwt_id.error());
268 const auto issued_at = detail::unix_seconds_from_time(request.issued_at);
269 const auto expires_at =
270 detail::unix_seconds_from_time(request.issued_at + request.lifetime);
272 nlohmann::json payload;
273 payload[
"iss"] = request.client_id;
274 payload[
"sub"] = request.client_id;
275 payload[
"aud"] = request.audience;
276 payload[
"jti"] = *jwt_id;
277 payload[
"iat"] = issued_at;
278 payload[
"exp"] = expires_at;
280 return detail::sign_compact_jws(private_key->get(), request.algorithm, header,
285 std::function<TimePoint()> now;
286 std::function<core::Result<std::string>()> jwt_id_generator;
292 : options_(std::move(options)) {}
295 if (request.target.method.empty()) {
296 return core::unexpected(
297 mcp::auth::detail::dpop_error(
"HTTP request method is required"));
299 if (request.target.url.empty()) {
300 return core::unexpected(
301 mcp::auth::detail::dpop_error(
"HTTP request URL is required"));
303 if (request.key.algorithm.empty()) {
304 return core::unexpected(
305 mcp::auth::detail::dpop_error(
"DPoP signing algorithm is required"));
309 detail::private_key_from_pem(request.key.private_key_pem.view());
310 if (!private_key.has_value()) {
311 return core::unexpected(private_key.error());
314 auto public_jwk = public_jwk_from_evp_pkey(
315 private_key->get(), request.key.algorithm,
316 request.key.key_id.empty()
317 ? std::optional<std::string>{}
318 : std::optional<std::string>{request.key.key_id});
319 if (!public_jwk.has_value()) {
320 return core::unexpected(public_jwk.error());
323 auto jwt_id = options_.jwt_id_generator ? options_.jwt_id_generator()
324 : detail::random_jwt_id();
325 if (!jwt_id.has_value()) {
326 return core::unexpected(jwt_id.error());
329 const TimePoint now = options_.now ? options_.now() : SystemClock::now();
330 nlohmann::json header;
331 header[
"typ"] =
"dpop+jwt";
332 header[
"alg"] = request.key.algorithm;
333 if (!request.key.key_id.empty()) {
334 header[
"kid"] = request.key.key_id;
336 header[
"jwk"] = detail::public_jwk_to_json(*public_jwk);
338 nlohmann::json payload;
339 payload[
"jti"] = *jwt_id;
340 payload[
"htm"] = request.target.method;
341 payload[
"htu"] = request.target.url;
342 payload[
"iat"] = detail::unix_seconds_from_time(now);
343 if (request.access_token.has_value()) {
345 if (!access_token_hash.has_value()) {
346 return core::unexpected(access_token_hash.error());
348 payload[
"ath"] = *access_token_hash;
350 if (request.nonce.has_value()) {
351 payload[
"nonce"] = *request.nonce;
354 return detail::sign_compact_jws(private_key->get(), request.key.algorithm,
364 bool validate_claims =
true;
370 : options_(std::move(options)) {}
374 const std::optional<std::string>& access_token)
override {
375 auto decoded = decode_compact_jws(proof_jwt);
376 if (!decoded.has_value()) {
377 return core::unexpected(decoded.error());
380 const auto& header = decoded->protected_header;
381 if (!header.type.has_value() || *header.type !=
"dpop+jwt") {
382 return core::unexpected(
383 make_jose_error(JoseErrorCode::kJwtClaimValidationFailed,
384 "DPoP proof typ must be dpop+jwt"));
386 const auto jwk_value = header.raw.find(
"jwk");
387 if (jwk_value == header.raw.end() || !jwk_value->is_object()) {
388 return core::unexpected(make_jose_error(
389 JoseErrorCode::kInvalidJwk,
"DPoP proof header jwk is required"));
393 if (!jwk.has_value()) {
394 return core::unexpected(jwk.error());
396 if (!jwk->algorithm.has_value()) {
397 jwk->algorithm = header.algorithm;
399 if (!jwk->key_id.has_value()) {
400 jwk->key_id = header.key_id;
404 verification_options.required_algorithm = header.algorithm;
405 verification_options.required_key_id = header.key_id;
407 verify_compact_jws_signature(proof_jwt, *jwk, verification_options);
408 if (!verified.has_value()) {
409 return core::unexpected(verified.error());
412 auto payload = detail::parse_json_object_bytes(
413 decoded->payload,
"DPoP proof payload must be a JSON object");
414 if (!payload.has_value()) {
415 return core::unexpected(payload.error());
418 auto claims = parse_claims(*payload);
419 if (!claims.has_value()) {
420 return core::unexpected(claims.error());
423 if (options_.validate_claims) {
424 auto claim_options = options_.claim_validation;
425 if (access_token.has_value()) {
427 if (!expected_hash.has_value()) {
428 return core::unexpected(expected_hash.error());
430 claim_options.expected_access_token_hash = *expected_hash;
433 claim_options,
nullptr);
434 if (!validated.has_value()) {
435 return core::unexpected(validated.error());
444 const nlohmann::json& payload) {
445 auto jwt_id = detail::string_claim(payload,
"jti");
446 auto method = detail::string_claim(payload,
"htm");
447 auto url = detail::string_claim(payload,
"htu");
448 auto issued_at = detail::numeric_date_claim(payload,
"iat");
449 if (!jwt_id.has_value() || jwt_id->empty()) {
450 return core::unexpected(
451 mcp::auth::detail::dpop_error(
"DPoP proof jti is required"));
453 if (!method.has_value() || method->empty()) {
454 return core::unexpected(
455 mcp::auth::detail::dpop_error(
"DPoP proof htm is required"));
457 if (!url.has_value() || url->empty()) {
458 return core::unexpected(
459 mcp::auth::detail::dpop_error(
"DPoP proof htu is required"));
461 if (!issued_at.has_value()) {
462 return core::unexpected(
463 mcp::auth::detail::dpop_error(
"DPoP proof iat is required"));
467 claims.jwt_id = std::move(*jwt_id);
468 claims.method = std::move(*method);
469 claims.url = std::move(*url);
470 claims.issued_at = detail::time_from_unix_seconds(*issued_at);
471 claims.access_token_hash = detail::string_claim(payload,
"ath");
472 claims.nonce = detail::string_claim(payload,
"nonce");
473 claims.claims = detail::dpop_claim_metadata(payload);
DPoP proof construction boundary.
Definition dpop.hpp:310
DPoP proof verification boundary for server-side auth providers.
Definition dpop.hpp:391
DPoP proof model and signing/verification boundaries.
core::Result< core::Unit > validate_dpop_proof_claims(const DpopProofClaims &claims, const HttpRequestTarget &target, const std::optional< std::string > &access_token, const DpopClaimValidationOptions &options={}, DpopReplayCache *replay_cache=nullptr)
Validate DPoP claims after JWT signature verification.
Definition dpop.hpp:196
OpenSSL conversion helpers for public JSON Web Keys.
core::Result< JsonWebKey > parse_json_web_key(const nlohmann::json &value)
Parse a public JWK from JSON without performing trust decisions.
Definition jwks.hpp:182
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
OpenSSL-backed SHA-256 helpers for optional auth crypto.
core::Result< std::string > dpop_access_token_hash(std::string_view access_token)
Compute the RFC 9449 DPoP ath value for an access token.
Definition sha256.hpp:29
Options for validating verified DPoP claims against an HTTP request.
Definition dpop.hpp:183
Parsed or verified DPoP proof claims.
Definition dpop.hpp:124
Input for constructing a DPoP proof JWT.
Definition dpop.hpp:116
Transport-neutral HTTP request descriptor used by auth helpers.
Definition types.hpp:29
Definition jws_verify.hpp:147