cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
dpop.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <algorithm>
6#include <cctype>
7#include <chrono>
8#include <mutex>
9#include <optional>
10#include <string>
11#include <string_view>
12#include <unordered_map>
13#include <utility>
14
15#include "cxxmcp/auth/token.hpp"
16#include "cxxmcp/auth/types.hpp"
18
21
22namespace mcp::auth {
23
24namespace detail {
25
26inline core::Error dpop_error(std::string message, std::string detail = {}) {
27 return core::Error{1, std::move(message), std::move(detail),
28 std::string(AuthErrorCategory)};
29}
30
31inline std::string uppercase_ascii(std::string_view value) {
32 std::string output(value);
33 std::transform(
34 output.begin(), output.end(), output.begin(),
35 [](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
36 return output;
37}
38
39} // namespace detail
40
48 public:
49 SecureString() = default;
50 explicit SecureString(const char* value)
51 : value_(value == nullptr ? "" : value) {}
52 explicit SecureString(std::string value) : value_(std::move(value)) {}
53 explicit SecureString(std::string_view value) : value_(value) {}
54
55 SecureString(const SecureString&) = default;
56 SecureString& operator=(const SecureString&) = default;
57
58 SecureString(SecureString&& other) noexcept
59 : value_(std::move(other.value_)) {
60 other.zeroize();
61 }
62
63 SecureString& operator=(SecureString&& other) noexcept {
64 if (this == &other) {
65 return *this;
66 }
67 zeroize();
68 value_ = std::move(other.value_);
69 other.zeroize();
70 return *this;
71 }
72
73 SecureString& operator=(std::string value) {
74 reset(std::move(value));
75 return *this;
76 }
77
78 ~SecureString() { zeroize(); }
79
80 void reset(std::string value = {}) {
81 zeroize();
82 value_ = std::move(value);
83 }
84
85 std::string_view view() const noexcept { return value_; }
86 const std::string& str() const noexcept { return value_; }
87 bool empty() const noexcept { return value_.empty(); }
88 std::size_t size() const noexcept { return value_.size(); }
89
90 private:
91 void zeroize() noexcept {
92 if (value_.empty()) {
93 return;
94 }
95 volatile char* data = value_.empty() ? nullptr : &value_[0];
96 for (std::size_t index = 0; index < value_.size(); ++index) {
97 data[index] = '\0';
98 }
99 value_.clear();
100 }
101
102 std::string value_;
103};
104
109struct DpopKey {
110 std::string key_id;
111 std::string algorithm;
112 SecureString private_key_pem;
113};
114
117 HttpRequestTarget target;
118 DpopKey key;
119 std::optional<std::string> access_token;
120 std::optional<std::string> nonce;
121};
122
125 std::string jwt_id;
126 std::string method;
127 std::string url;
128 TimePoint issued_at;
129 std::optional<std::string> access_token_hash;
130 std::optional<std::string> nonce;
131 MetadataMap claims;
132};
133
139 public:
140 virtual ~DpopReplayCache() = default;
141
142 virtual core::Result<bool> remember_once(std::string jwt_id,
143 TimePoint expires_at,
144 TimePoint now) = 0;
145};
146
152 public:
153 core::Result<bool> remember_once(std::string jwt_id, TimePoint expires_at,
154 TimePoint now) override {
155 if (jwt_id.empty()) {
156 return mcp::core::unexpected(
157 detail::dpop_error("DPoP proof jti is required"));
158 }
159
160 std::lock_guard<std::mutex> lock(mutex_);
161 for (auto iter = seen_.begin(); iter != seen_.end();) {
162 if (iter->second <= now) {
163 iter = seen_.erase(iter);
164 } else {
165 ++iter;
166 }
167 }
168
169 const auto existing = seen_.find(jwt_id);
170 if (existing != seen_.end() && existing->second > now) {
171 return false;
172 }
173 seen_[std::move(jwt_id)] = expires_at;
174 return true;
175 }
176
177 private:
178 std::mutex mutex_;
179 std::unordered_map<std::string, TimePoint> seen_;
180};
181
184 TimePoint now = SystemClock::now();
185 std::chrono::seconds clock_skew_tolerance{300};
186 std::chrono::seconds replay_ttl{300};
187 bool case_sensitive_method = true;
188 std::optional<std::string> expected_access_token_hash;
189};
190
197 const DpopProofClaims& claims, const HttpRequestTarget& target,
198 const std::optional<std::string>& access_token,
199 const DpopClaimValidationOptions& options = {},
200 DpopReplayCache* replay_cache = nullptr) {
201 if (claims.jwt_id.empty()) {
202 return mcp::core::unexpected(
203 detail::dpop_error("DPoP proof jti is required"));
204 }
205 if (target.method.empty()) {
206 return mcp::core::unexpected(
207 detail::dpop_error("HTTP request method is required"));
208 }
209 if (target.url.empty()) {
211 detail::dpop_error("HTTP request URL is required"));
212 }
213 if (claims.method.empty()) {
215 detail::dpop_error("DPoP proof htm is required"));
216 }
217 if (claims.url.empty()) {
219 detail::dpop_error("DPoP proof htu is required"));
220 }
221
222 const bool method_matches = options.case_sensitive_method
223 ? claims.method == target.method
224 : detail::uppercase_ascii(claims.method) ==
225 detail::uppercase_ascii(target.method);
226 if (!method_matches) {
227 return mcp::core::unexpected(detail::dpop_error(
228 "DPoP proof htm does not match request method", claims.method));
229 }
230 if (claims.url != target.url) {
231 return mcp::core::unexpected(detail::dpop_error(
232 "DPoP proof htu does not match request URL", claims.url));
233 }
234
235 if (claims.issued_at > options.now + options.clock_skew_tolerance) {
237 detail::dpop_error("DPoP proof iat is too far in the future"));
238 }
239 if (claims.issued_at + options.clock_skew_tolerance < options.now) {
241 detail::dpop_error("DPoP proof iat is too old"));
242 }
243
244 if (access_token.has_value()) {
245 if (!claims.access_token_hash.has_value() ||
246 claims.access_token_hash->empty()) {
248 detail::dpop_error("DPoP proof ath is required"));
249 }
250 if (!options.expected_access_token_hash.has_value() ||
251 options.expected_access_token_hash->empty()) {
253 detail::dpop_error("expected DPoP access-token hash is required"));
254 }
255 if (*claims.access_token_hash != *options.expected_access_token_hash) {
257 detail::dpop_error("DPoP proof ath does not match access token"));
258 }
259 }
260
261 if (replay_cache != nullptr) {
262 const auto remembered = replay_cache->remember_once(
263 claims.jwt_id, options.now + options.replay_ttl, options.now);
264 if (!remembered.has_value()) {
265 return mcp::core::unexpected(remembered.error());
266 }
267 if (!*remembered) {
269 detail::dpop_error("DPoP proof replay detected"));
270 }
271 }
272
273 return core::Unit{};
274}
275
278 kAccessToken,
279 kIdToken,
280 kClientAssertion,
281 kDpopProof,
282};
283
290 std::string jwt;
291 JwtVerificationPurpose purpose = JwtVerificationPurpose::kAccessToken;
292 std::optional<std::string> issuer;
293 std::optional<std::string> audience;
294 std::optional<std::string> required_algorithm;
295 MetadataMap required_claims;
296 TimePoint now = SystemClock::now();
297};
298
301 std::string issuer;
302 std::string subject;
303 std::string audience;
304 std::optional<TimePoint> issued_at;
305 std::optional<TimePoint> expires_at;
306 MetadataMap claims;
307};
308
311 public:
312 virtual ~DpopSigner() = default;
313
314 virtual core::Result<std::string> sign(const DpopProofRequest& request) = 0;
315};
316
319 HttpRequestTarget target;
320 DpopKey key;
321 std::string access_token;
322 std::optional<std::string> nonce;
323 std::string authorization_scheme = "DPoP";
324};
325
328 HeaderMap headers;
329 std::string proof;
330};
331
338 DpopSigner& signer, DpopProofRequest request) {
339 if (request.target.method.empty()) {
340 return mcp::core::unexpected(
341 detail::dpop_error("HTTP request method is required"));
342 }
343 if (request.target.url.empty()) {
344 return mcp::core::unexpected(
345 detail::dpop_error("HTTP request URL is required"));
346 }
347
348 auto proof = signer.sign(request);
349 if (!proof.has_value()) {
350 return mcp::core::unexpected(proof.error());
351 }
352 if (proof->empty()) {
353 return mcp::core::unexpected(
354 detail::dpop_error("DPoP signer returned an empty proof"));
355 }
356
358 result.proof = std::move(*proof);
359 result.headers.emplace("DPoP", result.proof);
360 return result;
361}
362
365 DpopSigner& signer, DpopAuthorizationRequest request) {
366 if (request.access_token.empty()) {
367 return mcp::core::unexpected(
368 detail::dpop_error("DPoP access token is required"));
369 }
370 if (request.authorization_scheme.empty()) {
371 return mcp::core::unexpected(
372 detail::dpop_error("DPoP authorization scheme is required"));
373 }
374
375 DpopProofRequest proof_request;
376 proof_request.target = std::move(request.target);
377 proof_request.key = std::move(request.key);
378 proof_request.access_token = request.access_token;
379 proof_request.nonce = std::move(request.nonce);
380
381 auto headers = build_dpop_proof_headers(signer, std::move(proof_request));
382 if (!headers.has_value()) {
383 return mcp::core::unexpected(headers.error());
384 }
385 headers->headers.emplace("Authorization", request.authorization_scheme + " " +
386 request.access_token);
387 return headers;
388}
389
392 public:
393 virtual ~DpopVerifier() = default;
394
395 virtual core::Result<DpopProofClaims> verify(
396 const std::string& proof_jwt, const HttpRequestTarget& target,
397 const std::optional<std::string>& access_token) = 0;
398};
399
402 public:
403 virtual ~JwtVerifier() = default;
404
405 virtual core::Result<VerifiedJwtClaims> verify(
406 const JwtVerificationRequest& request) = 0;
407};
408
409} // namespace mcp::auth
Shared lightweight value types for cxxmcp auth contracts.
Replay cache boundary used by DPoP proof validators.
Definition dpop.hpp:138
DPoP proof construction boundary.
Definition dpop.hpp:310
DPoP proof verification boundary for server-side auth providers.
Definition dpop.hpp:391
Thread-safe in-memory replay cache for process-local DPoP validation.
Definition dpop.hpp:151
JWT verification boundary for access tokens and client assertions.
Definition dpop.hpp:401
Small owning string wrapper that zeroizes stored bytes on reset and destruction.
Definition dpop.hpp:47
core::Result< DpopAuthorizationHeaders > build_dpop_proof_headers(DpopSigner &signer, DpopProofRequest request)
Build only the DPoP proof header for an HTTP request.
Definition dpop.hpp:337
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
core::Result< DpopAuthorizationHeaders > build_dpop_authorization_headers(DpopSigner &signer, DpopAuthorizationRequest request)
Build Authorization and DPoP headers for a resource request.
Definition dpop.hpp:364
JwtVerificationPurpose
JWT verification purpose for OAuth/DPoP deployments.
Definition dpop.hpp:277
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
constexpr auto unexpected(E &&value)
Creates an unexpected result value for the active expected backend.
Definition result.hpp:24
Headers and proof produced for a DPoP-authorized request.
Definition dpop.hpp:327
Input for authorizing an HTTP resource request with DPoP.
Definition dpop.hpp:318
Options for validating verified DPoP claims against an HTTP request.
Definition dpop.hpp:183
Private key handle for DPoP proof generation.
Definition dpop.hpp:109
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
Input for signature- and claims-verified JWT validation.
Definition dpop.hpp:289
Claims returned only after JWT signature and claim validation.
Definition dpop.hpp:300
OAuth token models and storage contracts.