cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
www_auth.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <cctype>
6#include <optional>
7#include <string>
8#include <string_view>
9#include <utility>
10#include <vector>
11
12#include "cxxmcp/auth/types.hpp"
14
17
18namespace mcp::auth {
19
20inline constexpr core::StringConstant WwwAuthenticateResourceMetadataParam{
21 "resource_metadata"};
22inline constexpr core::StringConstant WwwAuthenticateErrorParam{"error"};
23inline constexpr core::StringConstant WwwAuthenticateScopeParam{"scope"};
24inline constexpr core::StringConstant WwwAuthenticateInsufficientScopeError{
25 "insufficient_scope"};
26
27namespace detail {
28
29inline bool is_http_token_char(char value) {
30 const auto ch = static_cast<unsigned char>(value);
31 return std::isalnum(ch) != 0 || value == '!' || value == '#' ||
32 value == '$' || value == '%' || value == '&' || value == '\'' ||
33 value == '*' || value == '+' || value == '-' || value == '.' ||
34 value == '^' || value == '_' || value == '`' || value == '|' ||
35 value == '~';
36}
37
38inline bool is_token68_char(char value) {
39 const auto ch = static_cast<unsigned char>(value);
40 return std::isalnum(ch) != 0 || value == '-' || value == '.' ||
41 value == '_' || value == '~' || value == '+' || value == '/' ||
42 value == '=';
43}
44
45inline void skip_ows(std::string_view input, std::size_t& pos) {
46 while (pos < input.size() && (input[pos] == ' ' || input[pos] == '\t')) {
47 ++pos;
48 }
49}
50
51inline std::string parse_token(std::string_view input, std::size_t& pos) {
52 const auto begin = pos;
53 while (pos < input.size() && is_http_token_char(input[pos])) {
54 ++pos;
55 }
56 return std::string(input.substr(begin, pos - begin));
57}
58
59inline std::string ascii_lower(std::string value) {
60 for (auto& ch : value) {
61 if (ch >= 'A' && ch <= 'Z') {
62 ch = static_cast<char>(ch - 'A' + 'a');
63 }
64 }
65 return value;
66}
67
68inline bool ascii_iequals(std::string_view lhs, std::string_view rhs) {
69 if (lhs.size() != rhs.size()) {
70 return false;
71 }
72 for (std::size_t index = 0; index < lhs.size(); ++index) {
73 char left = lhs[index];
74 char right = rhs[index];
75 if (left >= 'A' && left <= 'Z') {
76 left = static_cast<char>(left - 'A' + 'a');
77 }
78 if (right >= 'A' && right <= 'Z') {
79 right = static_cast<char>(right - 'A' + 'a');
80 }
81 if (left != right) {
82 return false;
83 }
84 }
85 return true;
86}
87
88inline core::Error www_auth_parse_error(std::string message) {
89 return core::Error{1, std::move(message), {}, std::string(AuthErrorCategory)};
90}
91
92inline core::Result<std::string> parse_quoted_string(std::string_view input,
93 std::size_t& pos) {
94 if (pos >= input.size() || input[pos] != '"') {
96 www_auth_parse_error("expected quoted WWW-Authenticate value"));
97 }
98 ++pos;
99
100 std::string result;
101 while (pos < input.size()) {
102 const auto ch = input[pos++];
103 if (ch == '"') {
104 return result;
105 }
106 if (ch == '\\') {
107 if (pos >= input.size()) {
109 www_auth_parse_error("unterminated WWW-Authenticate escape"));
110 }
111 result.push_back(input[pos++]);
112 continue;
113 }
114 result.push_back(ch);
115 }
116
118 www_auth_parse_error("unterminated WWW-Authenticate quoted value"));
119}
120
121inline bool comma_starts_parameter(std::string_view input, std::size_t pos) {
122 if (pos >= input.size() || input[pos] != ',') {
123 return false;
124 }
125 ++pos;
126 skip_ows(input, pos);
127 const auto name_begin = pos;
128 const auto name = parse_token(input, pos);
129 if (name.empty() || name_begin >= input.size()) {
130 return false;
131 }
132 skip_ows(input, pos);
133 if (pos >= input.size() || input[pos] != '=') {
134 return false;
135 }
136 ++pos;
137 skip_ows(input, pos);
138 return pos < input.size() &&
139 (input[pos] == '"' || is_http_token_char(input[pos]));
140}
141
142inline bool starts_parameter(std::string_view input, std::size_t pos) {
143 skip_ows(input, pos);
144 const auto name_begin = pos;
145 auto name = parse_token(input, pos);
146 if (name.empty()) {
147 return false;
148 }
149 skip_ows(input, pos);
150 if (name_begin >= input.size() || pos >= input.size() || input[pos] != '=') {
151 return false;
152 }
153 ++pos;
154 skip_ows(input, pos);
155 return pos < input.size() &&
156 (input[pos] == '"' || is_http_token_char(input[pos]));
157}
158
159} // namespace detail
160
163 std::string scheme;
165 std::optional<std::string> token68;
166 MetadataMap parameters;
167
168 std::string parameter(std::string key) const {
169 const auto iter = parameters.find(key);
170 if (iter != parameters.end()) {
171 return iter->second;
172 }
173 const auto normalized = detail::ascii_lower(std::move(key));
174 const auto normalized_iter = parameters.find(normalized);
175 return normalized_iter == parameters.end() ? std::string{}
176 : normalized_iter->second;
177 }
178
180 bool bearer() const { return detail::ascii_iequals(scheme, "Bearer"); }
181};
182
184inline std::optional<std::string> resource_metadata_url(
185 const WwwAuthenticateChallenge& challenge) {
186 const auto value =
187 challenge.parameter(std::string(WwwAuthenticateResourceMetadataParam));
188 if (value.empty()) {
189 return std::nullopt;
190 }
191 return value;
192}
193
195inline bool insufficient_scope(const WwwAuthenticateChallenge& challenge) {
196 return challenge.parameter(std::string(WwwAuthenticateErrorParam)) ==
197 WwwAuthenticateInsufficientScopeError;
198}
199
201inline std::optional<std::string> first_resource_metadata_url(
202 const std::vector<WwwAuthenticateChallenge>& challenges) {
203 for (const auto& challenge : challenges) {
204 auto value = resource_metadata_url(challenge);
205 if (value.has_value()) {
206 return value;
207 }
208 }
209 return std::nullopt;
210}
211
217 public:
218 virtual ~WwwAuthenticateParser() = default;
219
221 const std::string& header_value) const = 0;
222};
223
230 public:
232 const std::string& header_value) const override {
233 const std::string_view input(header_value);
234 std::vector<WwwAuthenticateChallenge> challenges;
235 std::size_t pos = 0;
236
237 while (true) {
238 detail::skip_ows(input, pos);
239 while (pos < input.size() && input[pos] == ',') {
240 ++pos;
241 detail::skip_ows(input, pos);
242 }
243 if (pos >= input.size()) {
244 break;
245 }
246
247 auto scheme = detail::parse_token(input, pos);
248 if (scheme.empty()) {
249 return mcp::core::unexpected(detail::www_auth_parse_error(
250 "expected WWW-Authenticate auth scheme"));
251 }
252
253 WwwAuthenticateChallenge challenge;
254 challenge.scheme = std::move(scheme);
255 detail::skip_ows(input, pos);
256
257 if (pos < input.size() && input[pos] != ',') {
258 if (!detail::starts_parameter(input, pos)) {
259 auto token68 = parse_token68(input, pos);
260 if (!token68.has_value()) {
261 return mcp::core::unexpected(token68.error());
262 }
263 challenge.token68 = std::move(*token68);
264 } else {
265 auto parsed = parse_parameters(input, pos, challenge);
266 if (!parsed.has_value()) {
267 return mcp::core::unexpected(parsed.error());
268 }
269 }
270 }
271
272 challenges.push_back(std::move(challenge));
273 if (pos >= input.size()) {
274 break;
275 }
276 if (input[pos] == ',') {
277 ++pos;
278 continue;
279 }
280 return mcp::core::unexpected(detail::www_auth_parse_error(
281 "expected comma after WWW-Authenticate challenge"));
282 }
283
284 return challenges;
285 }
286
287 private:
288 static core::Result<std::string> parse_token68(std::string_view input,
289 std::size_t& pos) {
290 const auto begin = pos;
291 while (pos < input.size() && detail::is_token68_char(input[pos])) {
292 ++pos;
293 }
294 if (begin == pos) {
295 return mcp::core::unexpected(
296 detail::www_auth_parse_error("expected WWW-Authenticate token68"));
297 }
298 const auto token68 = std::string(input.substr(begin, pos - begin));
299 detail::skip_ows(input, pos);
300 if (pos < input.size() && input[pos] != ',') {
301 return mcp::core::unexpected(detail::www_auth_parse_error(
302 "unexpected character after WWW-Authenticate token68"));
303 }
304 return token68;
305 }
306
307 static core::Result<core::Unit> parse_parameters(
308 std::string_view input, std::size_t& pos,
309 WwwAuthenticateChallenge& challenge) {
310 while (pos < input.size()) {
311 detail::skip_ows(input, pos);
312
313 const auto name = detail::parse_token(input, pos);
314 if (name.empty()) {
315 return mcp::core::unexpected(detail::www_auth_parse_error(
316 "expected WWW-Authenticate parameter name"));
317 }
318
319 detail::skip_ows(input, pos);
320 if (pos >= input.size() || input[pos] != '=') {
321 return mcp::core::unexpected(detail::www_auth_parse_error(
322 "expected '=' after WWW-Authenticate parameter name"));
323 }
324 ++pos;
325 detail::skip_ows(input, pos);
326
327 auto value = parse_parameter_value(input, pos);
328 if (!value.has_value()) {
329 return mcp::core::unexpected(value.error());
330 }
331 challenge.parameters[detail::ascii_lower(name)] = std::move(*value);
332
333 detail::skip_ows(input, pos);
334 if (pos >= input.size()) {
335 return core::Unit{};
336 }
337 if (input[pos] != ',') {
338 return mcp::core::unexpected(detail::www_auth_parse_error(
339 "expected comma after WWW-Authenticate parameter"));
340 }
341 if (!detail::comma_starts_parameter(input, pos)) {
342 return core::Unit{};
343 }
344 ++pos;
345 }
346
347 return core::Unit{};
348 }
349
350 static core::Result<std::string> parse_parameter_value(std::string_view input,
351 std::size_t& pos) {
352 if (pos >= input.size()) {
353 return mcp::core::unexpected(detail::www_auth_parse_error(
354 "expected WWW-Authenticate parameter value"));
355 }
356 if (input[pos] == '"') {
357 return detail::parse_quoted_string(input, pos);
358 }
359 auto value = detail::parse_token(input, pos);
360 if (value.empty()) {
361 return mcp::core::unexpected(detail::www_auth_parse_error(
362 "expected WWW-Authenticate token parameter value"));
363 }
364 return value;
365 }
366};
367
370parse_www_authenticate(const std::string& header_value) {
371 return DefaultWwwAuthenticateParser{}.parse(header_value);
372}
373
374} // namespace mcp::auth
Shared lightweight value types for cxxmcp auth contracts.
Default RFC-style parser for WWW-Authenticate challenge headers.
Definition www_auth.hpp:229
Parser boundary for HTTP authentication challenges.
Definition www_auth.hpp:216
Shared result and error primitives used by the public cxxmcp SDK.
std::monostate Unit
Success value for operations that only need to report failure.
Definition result.hpp:55
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
Parsed authentication challenge from a WWW-Authenticate header.
Definition www_auth.hpp:162
bool bearer() const
Returns true when this is a Bearer challenge.
Definition www_auth.hpp:180
std::optional< std::string > token68
token68 payload for schemes that use it instead of key-value parameters.
Definition www_auth.hpp:165
bool insufficient_scope(const WwwAuthenticateChallenge &challenge)
Returns true for an OAuth insufficient_scope challenge.
Definition www_auth.hpp:195
std::optional< std::string > resource_metadata_url(const WwwAuthenticateChallenge &challenge)
Returns the MCP OAuth protected-resource metadata URL, when present.
Definition www_auth.hpp:184
core::Result< std::vector< WwwAuthenticateChallenge > > parse_www_authenticate(const std::string &header_value)
Parse a WWW-Authenticate header with the default parser.
Definition www_auth.hpp:370
std::optional< std::string > first_resource_metadata_url(const std::vector< WwwAuthenticateChallenge > &challenges)
Returns the first resource_metadata parameter in parsed challenges.
Definition www_auth.hpp:201