cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
server_auth_provider.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <cctype>
6#include <functional>
7#include <optional>
8#include <string>
9#include <string_view>
10#include <unordered_map>
11#include <utility>
12
13#include "cxxmcp/auth/dpop.hpp"
15
18
19namespace mcp::auth {
20
21namespace server_auth_detail {
22
23inline std::string_view trim_ascii(std::string_view value) {
24 while (!value.empty() &&
25 std::isspace(static_cast<unsigned char>(value.front())) != 0) {
26 value.remove_prefix(1);
27 }
28 while (!value.empty() &&
29 std::isspace(static_cast<unsigned char>(value.back())) != 0) {
30 value.remove_suffix(1);
31 }
32 return value;
33}
34
35inline bool ascii_iequals(std::string_view lhs, std::string_view rhs) {
36 if (lhs.size() != rhs.size()) {
37 return false;
38 }
39 for (std::size_t index = 0; index < lhs.size(); ++index) {
40 const auto left = static_cast<unsigned char>(lhs[index]);
41 const auto right = static_cast<unsigned char>(rhs[index]);
42 if (std::tolower(left) != std::tolower(right)) {
43 return false;
44 }
45 }
46 return true;
47}
48
49inline std::optional<std::string_view> header_value(
50 const std::unordered_map<std::string, std::string>& headers,
51 std::string_view name) {
52 for (const auto& header : headers) {
53 if (ascii_iequals(header.first, name)) {
54 return header.second;
55 }
56 }
57 return std::nullopt;
58}
59
60inline std::optional<std::pair<std::string_view, std::string_view>>
61authorization_scheme_and_token(std::string_view authorization) {
62 authorization = trim_ascii(authorization);
63 const auto split = authorization.find_first_of(" \t\r\n");
64 if (split == std::string_view::npos) {
65 return std::nullopt;
66 }
67 auto scheme = authorization.substr(0, split);
68 authorization.remove_prefix(split);
69 const auto token = trim_ascii(authorization);
70 if (scheme.empty() || token.empty()) {
71 return std::nullopt;
72 }
73 return std::make_pair(scheme, token);
74}
75
76} // namespace server_auth_detail
77
80 std::optional<std::string> issuer;
81 std::optional<std::string> audience;
82 std::optional<std::string> required_algorithm;
83 MetadataMap required_claims;
85 bool require_dpop = false;
86};
87
94 public:
95 using AccessTokenHashFunction =
96 std::function<core::Result<std::string>(std::string_view access_token)>;
97
98 DpopBearerAuthProvider(JwtVerifier& jwt_verifier, DpopVerifier& dpop_verifier,
99 AccessTokenHashFunction access_token_hash,
100 DpopReplayCache* replay_cache = nullptr,
101 DpopAuthProviderOptions options = {})
102 : jwt_verifier_(jwt_verifier),
103 dpop_verifier_(dpop_verifier),
104 access_token_hash_(std::move(access_token_hash)),
105 replay_cache_(replay_cache),
106 options_(std::move(options)) {}
107
109 const server::AuthRequest& request) override {
110 const auto authorization =
111 server_auth_detail::header_value(request.headers, "Authorization");
112 if (!authorization.has_value()) {
113 return mcp::core::unexpected(
114 server::make_auth_error("missing authorization header"));
115 }
116
117 const auto parsed =
118 server_auth_detail::authorization_scheme_and_token(*authorization);
119 if (!parsed.has_value()) {
120 return mcp::core::unexpected(
121 server::make_auth_error("invalid authorization header"));
122 }
123
124 const auto scheme = parsed->first;
125 const auto access_token = parsed->second;
126 const bool dpop_scheme = server_auth_detail::ascii_iequals(scheme, "DPoP");
127 if (!dpop_scheme && !server_auth_detail::ascii_iequals(scheme, "Bearer")) {
128 return mcp::core::unexpected(
129 server::make_auth_error("unsupported authorization scheme"));
130 }
131
132 auto jwt = verify_access_token(access_token);
133 if (!jwt.has_value()) {
134 return mcp::core::unexpected(server::make_auth_error(
135 "access token verification failed", jwt.error().message));
136 }
137
138 const bool has_dpop_header =
139 server_auth_detail::header_value(request.headers, "DPoP").has_value();
140 if (options_.require_dpop || dpop_scheme || has_dpop_header) {
141 auto proof = verify_dpop_proof(request, access_token);
142 if (!proof.has_value()) {
143 return mcp::core::unexpected(proof.error());
144 }
145 }
146
147 server::AuthIdentity identity;
148 identity.subject = jwt->subject;
149 identity.claims = jwt_claims_to_identity(*jwt);
150 return identity;
151 }
152
153 private:
154 core::Result<VerifiedJwtClaims> verify_access_token(
155 std::string_view access_token) {
156 JwtVerificationRequest jwt_request;
157 jwt_request.jwt = std::string(access_token);
158 jwt_request.purpose = JwtVerificationPurpose::kAccessToken;
159 jwt_request.issuer = options_.issuer;
160 jwt_request.audience = options_.audience;
161 jwt_request.required_algorithm = options_.required_algorithm;
162 jwt_request.required_claims = options_.required_claims;
163 jwt_request.now = SystemClock::now();
164 return jwt_verifier_.verify(jwt_request);
165 }
166
167 core::Result<core::Unit> verify_dpop_proof(const server::AuthRequest& request,
168 std::string_view access_token) {
169 if (!request.http_method.has_value() || request.http_method->empty() ||
170 !request.http_url.has_value() || request.http_url->empty()) {
171 return mcp::core::unexpected(server::make_auth_error(
172 "DPoP authentication requires HTTP method and URL"));
173 }
174
175 const auto proof_header =
176 server_auth_detail::header_value(request.headers, "DPoP");
177 if (!proof_header.has_value() || proof_header->empty()) {
178 return mcp::core::unexpected(
179 server::make_auth_error("missing DPoP proof header"));
180 }
181 if (!access_token_hash_) {
182 return mcp::core::unexpected(server::make_auth_error(
183 "DPoP authentication requires an access-token hash function"));
184 }
185
186 const HttpRequestTarget target{*request.http_method, *request.http_url};
187 auto claims = dpop_verifier_.verify(
188 std::string(*proof_header), target,
189 std::optional<std::string>{std::string(access_token)});
190 if (!claims.has_value()) {
191 return mcp::core::unexpected(server::make_auth_error(
192 "DPoP proof verification failed", claims.error().message));
193 }
194
195 auto expected_hash = access_token_hash_(access_token);
196 if (!expected_hash.has_value()) {
197 return mcp::core::unexpected(server::make_auth_error(
198 "DPoP access-token hash failed", expected_hash.error().message));
199 }
200
201 auto claim_options = options_.dpop_claims;
202 claim_options.now = SystemClock::now();
203 claim_options.expected_access_token_hash = *expected_hash;
204 auto validated = validate_dpop_proof_claims(
205 *claims, target, std::optional<std::string>{std::string(access_token)},
206 claim_options, replay_cache_);
207 if (!validated.has_value()) {
208 return mcp::core::unexpected(server::make_auth_error(
209 "DPoP proof claims are invalid", validated.error().message));
210 }
211 return core::Unit{};
212 }
213
214 static std::unordered_map<std::string, std::string> jwt_claims_to_identity(
215 const VerifiedJwtClaims& claims) {
216 std::unordered_map<std::string, std::string> identity_claims;
217 if (!claims.issuer.empty()) {
218 identity_claims.emplace("iss", claims.issuer);
219 }
220 if (!claims.audience.empty()) {
221 identity_claims.emplace("aud", claims.audience);
222 }
223 for (const auto& claim : claims.claims) {
224 identity_claims.emplace(claim.first, claim.second);
225 }
226 return identity_claims;
227 }
228
229 JwtVerifier& jwt_verifier_;
230 DpopVerifier& dpop_verifier_;
231 AccessTokenHashFunction access_token_hash_;
232 DpopReplayCache* replay_cache_ = nullptr;
233 DpopAuthProviderOptions options_;
234};
235
236} // namespace mcp::auth
Server AuthProvider backed by injected JWT and DPoP verifiers.
Definition server_auth_provider.hpp:93
core::Result< server::AuthIdentity > authenticate(const server::AuthRequest &request) override
Authenticate a transport request.
Definition server_auth_provider.hpp:108
Replay cache boundary used by DPoP proof validators.
Definition dpop.hpp:138
DPoP proof verification boundary for server-side auth providers.
Definition dpop.hpp:391
JWT verification boundary for access tokens and client assertions.
Definition dpop.hpp:401
Abstract authentication provider used by server integrations.
Definition auth.hpp:117
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
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
Authentication extension points for server transports.
Options for DPoP-aware server authentication over verified tokens.
Definition server_auth_provider.hpp:79
Options for validating verified DPoP claims against an HTTP request.
Definition dpop.hpp:183
Input for signature- and claims-verified JWT validation.
Definition dpop.hpp:289
Authenticated principal and associated claims.
Definition auth.hpp:103
std::string subject
Stable principal identifier, such as a user id, service account, or token subject.
Definition auth.hpp:106
std::unordered_map< std::string, std::string > claims
Provider-specific claims copied by value into the identity.
Definition auth.hpp:108
Transport-neutral authentication input.
Definition auth.hpp:91
std::unordered_map< std::string, std::string > headers
Request headers or metadata supplied by the transport.
Definition auth.hpp:93
std::optional< std::string > http_url
Absolute HTTP request URL when supplied by an HTTP-based transport.
Definition auth.hpp:99
std::optional< std::string > http_method
HTTP request method when supplied by an HTTP-based transport.
Definition auth.hpp:97