cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
lifecycle.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 <cstdint>
9#include <iomanip>
10#include <memory>
11#include <optional>
12#include <sstream>
13#include <string>
14#include <string_view>
15#include <utility>
16#include <vector>
17
20#include "cxxmcp/auth/pkce.hpp"
22#include "cxxmcp/auth/token.hpp"
23#include "cxxmcp/auth/types.hpp"
26
29
30namespace mcp::auth {
31
33enum class OAuthErrorCode {
34 kInvalidRequest = 1,
35 kAuthorizationRequired = 2,
36 kAuthorizationPending = 3,
37 kTokenExchangeUnavailable = 4,
38 kTokenExchangeFailed = 5,
39 kTokenRefreshFailed = 6,
40 kInsufficientScope = 7,
41 kMetadataDiscoveryFailed = 8,
42 kClientRegistrationUnavailable = 9,
43 kClientRegistrationFailed = 10,
44 kClientMetadataDocumentUnsupported = 11,
45 kClientMetadataDocumentInvalid = 12,
46 kClientCredentialsFailed = 13,
47};
48
50inline core::Error make_oauth_error(OAuthErrorCode code, std::string message,
51 std::string detail = {}) {
52 return core::Error{static_cast<int>(code), std::move(message),
53 std::move(detail), std::string(AuthErrorCategory)};
54}
55
58 std::string resource;
59 std::string issuer;
60 std::string client_id;
61 MetadataMap attributes;
62};
63
66 std::string client_id;
67 std::optional<TokenSet> token_set;
68 ScopeList granted_scopes;
69 std::optional<TimePoint> token_received_at;
70 MetadataMap metadata;
71};
72
75 public:
76 virtual ~CredentialStore() = default;
77
79 const CredentialKey& key) = 0;
80 virtual core::Result<core::Unit> save(const CredentialKey& key,
81 StoredCredentials credentials) = 0;
82 virtual core::Result<core::Unit> clear(const CredentialKey& key) = 0;
83};
84
87 public:
89 const CredentialKey& key) override {
90 const auto iter = find_entry(key);
91 if (iter == entries_.end()) {
92 return std::optional<StoredCredentials>{};
93 }
94 return iter->second;
95 }
96
98 StoredCredentials credentials) override {
99 auto iter = find_entry(key);
100 if (iter == entries_.end()) {
101 entries_.emplace_back(key, std::move(credentials));
102 } else {
103 iter->second = std::move(credentials);
104 }
105 return core::Unit{};
106 }
107
108 core::Result<core::Unit> clear(const CredentialKey& key) override {
109 auto iter = find_entry(key);
110 if (iter != entries_.end()) {
111 entries_.erase(iter);
112 }
113 return core::Unit{};
114 }
115
116 private:
117 using Entry = std::pair<CredentialKey, StoredCredentials>;
118
119 static bool matches(const MetadataMap& lhs, const MetadataMap& rhs) {
120 if (lhs.size() != rhs.size()) {
121 return false;
122 }
123 bool equal = true;
124 auto lhs_iter = lhs.begin();
125 auto rhs_iter = rhs.begin();
126 for (; lhs_iter != lhs.end(); ++lhs_iter, ++rhs_iter) {
127 equal = constant_time_string_equal(lhs_iter->first, rhs_iter->first) &
128 constant_time_string_equal(lhs_iter->second, rhs_iter->second) &
129 equal;
130 }
131 return equal;
132 }
133
134 static bool matches(const CredentialKey& lhs, const CredentialKey& rhs) {
135 return constant_time_string_equal(lhs.resource, rhs.resource) &
136 constant_time_string_equal(lhs.issuer, rhs.issuer) &
137 constant_time_string_equal(lhs.client_id, rhs.client_id) &
138 matches(lhs.attributes, rhs.attributes);
139 }
140
141 std::vector<Entry>::iterator find_entry(const CredentialKey& key) {
142 for (auto iter = entries_.begin(); iter != entries_.end(); ++iter) {
143 if (matches(iter->first, key)) {
144 return iter;
145 }
146 }
147 return entries_.end();
148 }
149
150 std::vector<Entry>::const_iterator find_entry(
151 const CredentialKey& key) const {
152 for (auto iter = entries_.begin(); iter != entries_.end(); ++iter) {
153 if (matches(iter->first, key)) {
154 return iter;
155 }
156 }
157 return entries_.end();
158 }
159
160 std::vector<Entry> entries_;
161};
162
165 std::string state;
166 PkceChallenge pkce;
167 TimePoint created_at = SystemClock::now();
168 std::string resource;
169 std::string client_id;
170 std::string redirect_uri;
171 ScopeList requested_scopes;
172 MetadataMap metadata;
173};
174
175inline constexpr std::chrono::seconds kDefaultAuthorizationStateTtl{60};
176
179 public:
180 virtual ~StateStore() = default;
181
182 virtual core::Result<core::Unit> save(std::string state,
183 StoredAuthorizationState value) = 0;
185 const std::string& state) = 0;
186 virtual core::Result<core::Unit> remove(const std::string& state) = 0;
187};
188
190class InMemoryStateStore final : public StateStore {
191 public:
192 core::Result<core::Unit> save(std::string state,
193 StoredAuthorizationState value) override {
194 auto iter = find_entry(state);
195 if (iter == entries_.end()) {
196 entries_.emplace_back(std::move(state), std::move(value));
197 } else {
198 iter->second = std::move(value);
199 }
200 return core::Unit{};
201 }
202
204 const std::string& state) override {
205 const auto iter = find_entry(state);
206 if (iter == entries_.end()) {
207 return std::optional<StoredAuthorizationState>{};
208 }
209 return iter->second;
210 }
211
212 core::Result<core::Unit> remove(const std::string& state) override {
213 auto iter = find_entry(state);
214 if (iter != entries_.end()) {
215 entries_.erase(iter);
216 }
217 return core::Unit{};
218 }
219
220 private:
221 using Entry = std::pair<std::string, StoredAuthorizationState>;
222
223 std::vector<Entry>::iterator find_entry(const std::string& state) {
224 for (auto iter = entries_.begin(); iter != entries_.end(); ++iter) {
225 if (constant_time_string_equal(iter->first, state)) {
226 return iter;
227 }
228 }
229 return entries_.end();
230 }
231
232 std::vector<Entry>::const_iterator find_entry(
233 const std::string& state) const {
234 for (auto iter = entries_.begin(); iter != entries_.end(); ++iter) {
235 if (constant_time_string_equal(iter->first, state)) {
236 return iter;
237 }
238 }
239 return entries_.end();
240 }
241
242 std::vector<Entry> entries_;
243};
244
247 OAuthClientConfig client;
248 AuthorizationServerMetadata authorization_server;
249 std::string resource;
250 ScopeList scopes;
251 PkceChallenge pkce;
252 std::string state;
253 MetadataMap additional_parameters;
254};
255
258 std::string url;
260};
261
264 OAuthClientConfig client;
265 AuthorizationServerMetadata authorization_server;
266 std::string resource;
267 std::string authorization_code;
269 MetadataMap additional_parameters;
270};
271
274 OAuthClientConfig client;
275 AuthorizationServerMetadata authorization_server;
276 std::string resource;
277 std::string refresh_token;
278 ScopeList scopes;
279 MetadataMap additional_parameters;
280};
281
284 ClientCredentialsConfig credentials;
285 AuthorizationServerMetadata authorization_server;
286 MetadataMap additional_parameters;
287};
288
294 public:
295 virtual ~OAuthTokenEndpoint() = default;
296
297 virtual core::Result<TokenSet> exchange_authorization_code(
298 const TokenExchangeRequest& request) = 0;
299 virtual core::Result<TokenRefreshResult> refresh_access_token(
300 const TokenRefreshRequest& request) = 0;
301 virtual core::Result<TokenSet> exchange_client_credentials(
302 const TokenClientCredentialsRequest& request) = 0;
303};
304
307 kUnauthorized,
308 kAuthorizationPending,
309 kAuthorized,
310};
311
314 bool auto_upgrade = true;
315 std::uint32_t max_upgrade_attempts = 3;
316};
317
320 kProceed,
321 kAuthorizationRequired,
322 kScopeUpgradeRequired,
323};
324
327 AuthResponseAction action = AuthResponseAction::kProceed;
328 std::optional<WwwAuthenticateChallenge> challenge;
329 ScopeList required_scopes;
330 std::optional<std::string> resource_metadata_url;
331 std::optional<std::string> error_description;
332};
333
336 AuthResponseDecision decision;
337 bool should_retry = false;
338 std::optional<std::string> bearer_token;
339};
340
343 std::string url;
344 HeaderMap headers;
345};
346
352 public:
353 virtual ~OAuthMetadataEndpoint() = default;
354
356 fetch_protected_resource_metadata(const MetadataFetchRequest& request) = 0;
357
359 fetch_authorization_server_metadata(const MetadataFetchRequest& request) = 0;
360};
361
364 StringList protected_resource_metadata_urls;
365 StringList authorization_server_metadata_urls;
366};
367
370 HeaderMap headers;
371 StringList authorization_server_metadata_urls;
372};
373
376 ScopeList www_authenticate_scopes;
377 ScopeList protected_resource_scopes;
378 ScopeList authorization_server_scopes;
379 ScopeList default_scopes;
380};
381
382namespace detail {
383
384inline std::string oauth_url_encode(std::string_view value) {
385 std::ostringstream out;
386 out << std::uppercase << std::hex;
387 for (const auto raw_ch : value) {
388 const auto ch = static_cast<unsigned char>(raw_ch);
389 if (std::isalnum(ch) != 0 || ch == '-' || ch == '_' || ch == '.' ||
390 ch == '~') {
391 out << static_cast<char>(ch);
392 } else {
393 out << '%' << std::setw(2) << std::setfill('0') << static_cast<int>(ch);
394 }
395 }
396 return out.str();
397}
398
399inline std::string join_scopes(const ScopeList& scopes) {
400 std::string joined;
401 for (const auto& scope : scopes) {
402 if (scope.empty()) {
403 continue;
404 }
405 if (!joined.empty()) {
406 joined.push_back(' ');
407 }
408 joined.append(scope);
409 }
410 return joined;
411}
412
413inline ScopeList split_scopes(std::string_view scopes) {
414 ScopeList result;
415 std::size_t pos = 0;
416 while (pos < scopes.size()) {
417 while (pos < scopes.size() &&
418 std::isspace(static_cast<unsigned char>(scopes[pos])) != 0) {
419 ++pos;
420 }
421 const auto begin = pos;
422 while (pos < scopes.size() &&
423 std::isspace(static_cast<unsigned char>(scopes[pos])) == 0) {
424 ++pos;
425 }
426 if (begin != pos) {
427 result.emplace_back(scopes.substr(begin, pos - begin));
428 }
429 }
430 return result;
431}
432
433inline std::string pkce_method_name(PkceCodeChallengeMethod method) {
434 switch (method) {
435 case PkceCodeChallengeMethod::kS256:
436 return "S256";
437 }
438 return "S256";
439}
440
441inline void append_query_param(std::string* url, const std::string& name,
442 const std::string& value) {
443 url->push_back(url->find('?') == std::string::npos ? '?' : '&');
444 url->append(oauth_url_encode(name));
445 url->push_back('=');
446 url->append(oauth_url_encode(value));
447}
448
449inline bool has_scope(const ScopeList& scopes, const std::string& scope) {
450 return std::find(scopes.begin(), scopes.end(), scope) != scopes.end();
451}
452
453inline void append_unique(StringList* values, std::string value) {
454 if (value.empty()) {
455 return;
456 }
457 if (std::find(values->begin(), values->end(), value) == values->end()) {
458 values->push_back(std::move(value));
459 }
460}
461
462inline std::string strip_query_and_fragment(std::string url) {
463 const auto fragment = url.find('#');
464 if (fragment != std::string::npos) {
465 url.erase(fragment);
466 }
467 const auto query = url.find('?');
468 if (query != std::string::npos) {
469 url.erase(query);
470 }
471 return url;
472}
473
474inline std::string origin_from_url(std::string_view url) {
475 const auto scheme = url.find("://");
476 if (scheme == std::string_view::npos) {
477 return {};
478 }
479 const auto authority_start = scheme + 3;
480 const auto path_start = url.find('/', authority_start);
481 if (path_start == std::string_view::npos) {
482 return std::string(url);
483 }
484 return std::string(url.substr(0, path_start));
485}
486
487inline std::string path_from_url(std::string_view url) {
488 const auto scheme = url.find("://");
489 if (scheme == std::string_view::npos) {
490 return {};
491 }
492 const auto authority_start = scheme + 3;
493 const auto path_start = url.find('/', authority_start);
494 if (path_start == std::string_view::npos) {
495 return "/";
496 }
497 auto path = std::string(url.substr(path_start));
498 const auto query = path.find('?');
499 if (query != std::string::npos) {
500 path.erase(query);
501 }
502 const auto fragment = path.find('#');
503 if (fragment != std::string::npos) {
504 path.erase(fragment);
505 }
506 return path.empty() ? std::string("/") : path;
507}
508
509inline bool url_has_fragment(std::string_view url) {
510 return url.find('#') != std::string_view::npos;
511}
512
513inline std::string url_scheme(std::string_view url) {
514 const auto scheme_end = url.find("://");
515 if (scheme_end == std::string_view::npos) {
516 return {};
517 }
518 return std::string(url.substr(0, scheme_end));
519}
520
521inline bool url_has_userinfo(std::string_view url) {
522 const auto scheme_end = url.find("://");
523 if (scheme_end == std::string_view::npos) {
524 return false;
525 }
526 const auto authority_start = scheme_end + 3;
527 const auto authority_end = url.find_first_of("/?#", authority_start);
528 const auto authority =
529 url.substr(authority_start, authority_end == std::string_view::npos
530 ? std::string_view::npos
531 : authority_end - authority_start);
532 return authority.find('@') != std::string_view::npos;
533}
534
535inline std::string url_host(std::string_view url) {
536 const auto scheme_end = url.find("://");
537 if (scheme_end == std::string_view::npos) {
538 return {};
539 }
540 const auto authority_start = scheme_end + 3;
541 const auto authority_end = url.find_first_of("/?#", authority_start);
542 auto authority =
543 url.substr(authority_start, authority_end == std::string_view::npos
544 ? std::string_view::npos
545 : authority_end - authority_start);
546 const auto userinfo = authority.rfind('@');
547 if (userinfo != std::string_view::npos) {
548 authority.remove_prefix(userinfo + 1);
549 }
550 if (authority.empty()) {
551 return {};
552 }
553 if (authority.front() == '[') {
554 const auto bracket = authority.find(']');
555 if (bracket == std::string_view::npos) {
556 return {};
557 }
558 return std::string(authority.substr(1, bracket - 1));
559 }
560 const auto port = authority.find(':');
561 return std::string(authority.substr(0, port));
562}
563
564inline bool url_uses_https(std::string_view url) {
565 return ascii_iequals(url_scheme(url), "https");
566}
567
568inline bool url_uses_loopback_http(std::string_view url) {
569 if (!ascii_iequals(url_scheme(url), "http")) {
570 return false;
571 }
572 const auto host = url_host(url);
573 return ascii_iequals(host, "localhost") || host == "127.0.0.1" ||
574 host == "::1";
575}
576
577inline bool redirect_uri_is_secure(std::string_view url) {
578 return url_uses_https(url) || url_uses_loopback_http(url);
579}
580
581inline bool metadata_discovery_url_is_safe(std::string_view url) {
582 if (url.empty() || url_has_fragment(url) || url_has_userinfo(url)) {
583 return false;
584 }
585 if (!url_uses_https(url) && !url_uses_loopback_http(url)) {
586 return false;
587 }
588 if (url_host(url).empty()) {
589 return false;
590 }
591 if (url_contains_dot_segment(path_from_url(url))) {
592 return false;
593 }
594 return true;
595}
596
597inline std::string trim_leading_slash(std::string value) {
598 while (!value.empty() && value.front() == '/') {
599 value.erase(value.begin());
600 }
601 return value;
602}
603
604inline std::optional<std::string> header_value(const HeaderMap& headers,
605 std::string_view name) {
606 for (const auto& entry : headers) {
607 if (ascii_iequals(entry.first, name)) {
608 return entry.second;
609 }
610 }
611 return std::nullopt;
612}
613
614} // namespace detail
615
622 const std::string& resource_url,
623 std::optional<std::string> challenged_resource_metadata_url =
624 std::nullopt) {
625 StringList urls;
626 if (challenged_resource_metadata_url.has_value() &&
627 detail::metadata_discovery_url_is_safe(
628 *challenged_resource_metadata_url)) {
629 detail::append_unique(&urls, detail::strip_query_and_fragment(std::move(
630 *challenged_resource_metadata_url)));
631 }
632
633 if (!detail::metadata_discovery_url_is_safe(resource_url)) {
634 return urls;
635 }
636 const auto normalized_resource =
637 detail::strip_query_and_fragment(resource_url);
638 const auto origin = detail::origin_from_url(normalized_resource);
639 if (origin.empty()) {
640 return urls;
641 }
642
643 const auto path = detail::path_from_url(normalized_resource);
644 if (path != "/") {
645 detail::append_unique(&urls, origin +
646 "/.well-known/oauth-protected-resource/" +
647 detail::trim_leading_slash(path));
648 }
649 detail::append_unique(&urls,
650 origin + "/.well-known/oauth-protected-resource");
651 return urls;
652}
653
656 const std::string& issuer_or_base_url) {
657 StringList urls;
658 if (!detail::metadata_discovery_url_is_safe(issuer_or_base_url)) {
659 return urls;
660 }
661 const auto normalized = detail::strip_query_and_fragment(issuer_or_base_url);
662 const auto origin = detail::origin_from_url(normalized);
663 if (origin.empty()) {
664 return urls;
665 }
666
667 const auto path = detail::path_from_url(normalized);
668 if (path != "/") {
669 detail::append_unique(&urls,
670 origin + "/.well-known/oauth-authorization-server/" +
671 detail::trim_leading_slash(path));
672 detail::append_unique(&urls,
673 normalized + "/.well-known/openid-configuration");
674 }
675 detail::append_unique(&urls,
676 origin + "/.well-known/oauth-authorization-server");
677 detail::append_unique(&urls, origin + "/.well-known/openid-configuration");
678 return urls;
679}
680
683 const std::string& resource_url,
684 const AuthResponseDecision& decision = AuthResponseDecision{}) {
685 MetadataDiscoveryPlan plan;
686 plan.protected_resource_metadata_urls =
687 build_protected_resource_metadata_urls(resource_url,
688 decision.resource_metadata_url);
689 return plan;
690}
691
697 public:
699 : endpoint_(endpoint) {}
700
702 const std::string& resource_url,
703 const AuthResponseDecision& decision = AuthResponseDecision{},
704 MetadataDiscoveryOptions options = {}) {
705 const auto plan = build_metadata_discovery_plan(resource_url, decision);
706 if (plan.protected_resource_metadata_urls.empty()) {
707 return mcp::core::unexpected(make_oauth_error(
708 OAuthErrorCode::kInvalidRequest,
709 "resource URL cannot produce metadata candidates", resource_url));
710 }
711
712 std::optional<core::Error> last_error;
713 std::optional<ProtectedResourceMetadata> protected_resource;
714 for (const auto& url : plan.protected_resource_metadata_urls) {
715 auto fetched = endpoint_.fetch_protected_resource_metadata(
716 MetadataFetchRequest{url, options.headers});
717 if (fetched.has_value()) {
718 protected_resource = std::move(*fetched);
719 break;
720 }
721 last_error = fetched.error();
722 }
723 if (!protected_resource.has_value()) {
724 return mcp::core::unexpected(make_oauth_error(
725 OAuthErrorCode::kMetadataDiscoveryFailed,
726 "protected resource metadata discovery failed",
727 last_error.has_value() ? last_error->message : std::string{}));
728 }
729
731 result.protected_resource = std::move(*protected_resource);
732
733 StringList authorization_urls = options.authorization_server_metadata_urls;
734 for (const auto& server : result.protected_resource.authorization_servers) {
735 for (auto candidate : build_authorization_server_metadata_urls(server)) {
736 detail::append_unique(&authorization_urls, std::move(candidate));
737 }
738 }
739 if (authorization_urls.empty()) {
740 return result;
741 }
742
743 last_error.reset();
744 for (const auto& url : authorization_urls) {
745 auto fetched = endpoint_.fetch_authorization_server_metadata(
746 MetadataFetchRequest{url, options.headers});
747 if (fetched.has_value()) {
748 result.authorization_server = std::move(*fetched);
749 return result;
750 }
751 last_error = fetched.error();
752 }
753
754 return mcp::core::unexpected(make_oauth_error(
755 OAuthErrorCode::kMetadataDiscoveryFailed,
756 "authorization server metadata discovery failed",
757 last_error.has_value() ? last_error->message : std::string{}));
758 }
759
760 private:
761 OAuthMetadataEndpoint& endpoint_;
762};
763
769 const ScopeSelectionContext& context) {
770 if (!context.www_authenticate_scopes.empty()) {
771 return context.www_authenticate_scopes;
772 }
773 if (!context.protected_resource_scopes.empty()) {
774 return context.protected_resource_scopes;
775 }
776 if (!context.authorization_server_scopes.empty()) {
777 return context.authorization_server_scopes;
778 }
779 return context.default_scopes;
780}
781
785 ScopeList& scopes, const AuthorizationServerMetadata& metadata) {
786 if (scopes.empty()) {
787 return;
788 }
789 const auto has_offline =
790 std::find(scopes.begin(), scopes.end(), "offline_access") != scopes.end();
791 if (has_offline) {
792 return;
793 }
794 const auto supports_offline =
795 std::find(metadata.scopes_supported.begin(),
796 metadata.scopes_supported.end(),
797 "offline_access") != metadata.scopes_supported.end();
798 if (supports_offline) {
799 scopes.push_back("offline_access");
800 }
801}
802
809 const AuthorizationServerMetadata& metadata,
810 std::string_view token_endpoint_auth_method = "client_secret_post") {
811 if (metadata.token_endpoint.empty()) {
812 return mcp::core::unexpected(
813 make_oauth_error(OAuthErrorCode::kClientCredentialsFailed,
814 "authorization server has no token endpoint"));
815 }
816
817 const auto& grants = metadata.grant_types_supported;
818 if (!grants.empty() && std::find(grants.begin(), grants.end(),
819 "client_credentials") == grants.end()) {
820 return mcp::core::unexpected(make_oauth_error(
821 OAuthErrorCode::kClientCredentialsFailed,
822 "authorization server does not advertise client_credentials grant"));
823 }
824
825 const auto& methods = metadata.token_endpoint_auth_methods_supported;
826 if (methods.empty()) {
827 return core::Unit{};
828 }
829
830 const auto has_method =
831 std::find(methods.begin(), methods.end(), token_endpoint_auth_method) !=
832 methods.end();
833 if (!has_method) {
834 return mcp::core::unexpected(make_oauth_error(
835 OAuthErrorCode::kClientCredentialsFailed,
836 "authorization server does not support requested client credentials "
837 "authentication method: " +
838 std::string(token_endpoint_auth_method)));
839 }
840
841 return core::Unit{};
842}
843
846 AuthorizationUrlResult authorization;
847 std::string redirect_uri;
848
849 const std::string& authorization_url() const { return authorization.url; }
850 const std::string& state() const { return authorization.state.state; }
851 const ScopeList& requested_scopes() const {
852 return authorization.state.requested_scopes;
853 }
854};
855
863 PkceChallenge pkce;
864 std::string state;
865 MetadataMap additional_authorization_parameters;
866};
867
870 const AuthorizationUrlRequest& request) {
871 if (request.authorization_server.authorization_endpoint.empty()) {
872 return mcp::core::unexpected(make_oauth_error(
873 OAuthErrorCode::kInvalidRequest, "authorization endpoint is required"));
874 }
875 if (request.client.client_id.empty()) {
876 return mcp::core::unexpected(make_oauth_error(
877 OAuthErrorCode::kInvalidRequest, "client_id is required"));
878 }
879 if (request.client.redirect_uri.empty()) {
880 return mcp::core::unexpected(make_oauth_error(
881 OAuthErrorCode::kInvalidRequest, "redirect_uri is required"));
882 }
883 if (!detail::url_uses_https(
884 request.authorization_server.authorization_endpoint) &&
885 !detail::url_uses_loopback_http(
886 request.authorization_server.authorization_endpoint)) {
887 return mcp::core::unexpected(make_oauth_error(
888 OAuthErrorCode::kInvalidRequest,
889 "authorization endpoint must use HTTPS or loopback HTTP"));
890 }
891 if (!request.authorization_server.token_endpoint.empty() &&
892 !detail::url_uses_https(request.authorization_server.token_endpoint) &&
893 !detail::url_uses_loopback_http(
894 request.authorization_server.token_endpoint)) {
895 return mcp::core::unexpected(
896 make_oauth_error(OAuthErrorCode::kInvalidRequest,
897 "token endpoint must use HTTPS or loopback HTTP"));
898 }
899 if (!detail::redirect_uri_is_secure(request.client.redirect_uri)) {
900 return mcp::core::unexpected(
901 make_oauth_error(OAuthErrorCode::kInvalidRequest,
902 "redirect_uri must use HTTPS or loopback HTTP"));
903 }
904 if (request.state.empty()) {
905 return mcp::core::unexpected(
906 make_oauth_error(OAuthErrorCode::kInvalidRequest, "state is required"));
907 }
908 if (request.pkce.code_challenge.empty() ||
909 request.pkce.code_verifier.empty()) {
910 return mcp::core::unexpected(
911 make_oauth_error(OAuthErrorCode::kInvalidRequest,
912 "PKCE verifier and challenge are required"));
913 }
914 auto url = request.authorization_server.authorization_endpoint;
915 detail::append_query_param(&url, "response_type", "code");
916 detail::append_query_param(&url, "client_id", request.client.client_id);
917 detail::append_query_param(&url, "redirect_uri", request.client.redirect_uri);
918 detail::append_query_param(&url, "state", request.state);
919 detail::append_query_param(&url, "code_challenge",
920 request.pkce.code_challenge);
921 detail::append_query_param(&url, "code_challenge_method",
922 detail::pkce_method_name(request.pkce.method));
923 if (!request.resource.empty()) {
924 detail::append_query_param(&url, "resource", request.resource);
925 }
926 const auto scope = detail::join_scopes(request.scopes);
927 if (!scope.empty()) {
928 detail::append_query_param(&url, "scope", scope);
929 }
930 for (const auto& parameter : request.additional_parameters) {
931 detail::append_query_param(&url, parameter.first, parameter.second);
932 }
933
935 stored.state = request.state;
936 stored.pkce = request.pkce;
937 stored.resource = request.resource;
938 stored.client_id = request.client.client_id;
939 stored.redirect_uri = request.client.redirect_uri;
940 stored.requested_scopes = request.scopes;
941
942 return AuthorizationUrlResult{std::move(url), std::move(stored)};
943}
944
947 const HttpResponseMetadata& response) {
948 AuthResponseDecision decision;
949 if (response.status_code != 401 && response.status_code != 403) {
950 return decision;
951 }
952
953 const auto header =
954 detail::header_value(response.headers, "WWW-Authenticate");
955 if (!header.has_value()) {
956 decision.action = response.status_code == 401
957 ? AuthResponseAction::kAuthorizationRequired
958 : AuthResponseAction::kScopeUpgradeRequired;
959 return decision;
960 }
961
962 auto parsed = parse_www_authenticate(*header);
963 if (!parsed.has_value()) {
964 return mcp::core::unexpected(parsed.error());
965 }
966
967 for (const auto& challenge : *parsed) {
968 if (!challenge.bearer()) {
969 continue;
970 }
971 decision.challenge = challenge;
972 decision.resource_metadata_url = resource_metadata_url(challenge);
973 const auto description = challenge.parameter("error_description");
974 if (!description.empty()) {
975 decision.error_description = description;
976 }
977 const auto scope =
978 challenge.parameter(std::string(WwwAuthenticateScopeParam));
979 if (!scope.empty()) {
980 decision.required_scopes = detail::split_scopes(scope);
981 }
982 decision.action = insufficient_scope(challenge)
983 ? AuthResponseAction::kScopeUpgradeRequired
984 : AuthResponseAction::kAuthorizationRequired;
985 return decision;
986 }
987
988 decision.action = response.status_code == 401
989 ? AuthResponseAction::kAuthorizationRequired
990 : AuthResponseAction::kScopeUpgradeRequired;
991 return decision;
992}
993
999 public:
1000 AuthorizationManager() = default;
1001
1002 AuthorizationManager(std::string resource,
1004 OAuthClientConfig client)
1005 : resource_(std::move(resource)),
1006 metadata_(std::move(metadata)),
1007 client_(std::move(client)) {}
1008
1009 void set_resource(std::string resource) { resource_ = std::move(resource); }
1010 void set_authorization_server_metadata(AuthorizationServerMetadata metadata) {
1011 metadata_ = std::move(metadata);
1012 }
1013 void configure_client(OAuthClientConfig client) {
1014 client_ = std::move(client);
1015 }
1016 void set_credential_store(std::shared_ptr<CredentialStore> store) {
1017 credential_store_ = std::move(store);
1018 }
1019 void set_state_store(std::shared_ptr<StateStore> store) {
1020 state_store_ = std::move(store);
1021 }
1022 void set_token_endpoint(std::shared_ptr<OAuthTokenEndpoint> endpoint) {
1023 token_endpoint_ = std::move(endpoint);
1024 }
1025 void set_client_registration_endpoint(
1026 std::shared_ptr<OAuthClientRegistrationEndpoint> endpoint) {
1027 registration_endpoint_ = std::move(endpoint);
1028 }
1029 void set_scope_upgrade_config(ScopeUpgradeConfig config) {
1030 scope_upgrade_config_ = config;
1031 }
1032 void set_authorization_state_ttl(std::chrono::seconds ttl) {
1033 authorization_state_ttl_ = ttl;
1034 }
1035
1036 OAuthLifecycleState lifecycle_state() const { return state_; }
1037 const OAuthClientConfig& client_config() const { return client_; }
1038 const ScopeList& current_scopes() const { return current_scopes_; }
1039 std::chrono::seconds authorization_state_ttl() const {
1040 return authorization_state_ttl_;
1041 }
1042 std::uint32_t scope_upgrade_attempts() const {
1043 return scope_upgrade_attempts_;
1044 }
1045
1046 CredentialKey credential_key() const {
1047 return CredentialKey{resource_, metadata_.issuer, client_.client_id, {}};
1048 }
1049
1050 core::Result<OAuthClientConfig> configure_client_id(
1051 std::string client_id, std::string redirect_uri = {},
1052 ScopeList scopes = {}) {
1053 if (client_id.empty()) {
1054 return mcp::core::unexpected(make_oauth_error(
1055 OAuthErrorCode::kInvalidRequest, "client_id is required"));
1056 }
1057 if (redirect_uri.empty()) {
1058 redirect_uri = client_.redirect_uri;
1059 }
1060 if (redirect_uri.empty()) {
1061 return mcp::core::unexpected(make_oauth_error(
1062 OAuthErrorCode::kInvalidRequest, "redirect_uri is required"));
1063 }
1064 OAuthClientConfig config;
1065 config.client_id = std::move(client_id);
1066 config.client_secret = client_.client_secret;
1067 config.redirect_uri = std::move(redirect_uri);
1068 config.scopes = std::move(scopes);
1069 configure_client(config);
1070 return client_;
1071 }
1072
1073 core::Result<OAuthClientConfig> configure_client_id_metadata_url(
1074 std::string client_id_metadata_url, std::string redirect_uri,
1075 ScopeList scopes = {}) {
1076 if (!supports_client_id_metadata_document(metadata_)) {
1077 return mcp::core::unexpected(make_oauth_error(
1078 OAuthErrorCode::kClientMetadataDocumentUnsupported,
1079 "authorization server does not advertise Client ID Metadata "
1080 "Document support"));
1081 }
1082 if (!is_valid_client_id_metadata_url(client_id_metadata_url)) {
1083 return mcp::core::unexpected(make_oauth_error(
1084 OAuthErrorCode::kClientMetadataDocumentInvalid,
1085 "client_id metadata document URL must be HTTPS with a non-root path",
1086 client_id_metadata_url));
1087 }
1088 return configure_client_id(std::move(client_id_metadata_url),
1089 std::move(redirect_uri), std::move(scopes));
1090 }
1091
1092 core::Result<OAuthClientConfig> register_client(
1093 ClientRegistrationOptions options, HeaderMap headers = {}) {
1094 if (!registration_endpoint_) {
1095 return mcp::core::unexpected(make_oauth_error(
1096 OAuthErrorCode::kClientRegistrationUnavailable,
1097 "dynamic client registration endpoint is not configured"));
1098 }
1099 if (!metadata_.registration_endpoint.has_value() ||
1100 metadata_.registration_endpoint->empty()) {
1101 return mcp::core::unexpected(make_oauth_error(
1102 OAuthErrorCode::kClientRegistrationUnavailable,
1103 "authorization server metadata has no registration endpoint"));
1104 }
1105
1106 auto registration = build_client_registration_request(options);
1107 if (!registration.has_value()) {
1108 return mcp::core::unexpected(registration.error());
1109 }
1110
1112 request.registration_endpoint = *metadata_.registration_endpoint;
1113 request.headers = std::move(headers);
1114 request.registration = std::move(*registration);
1115
1116 auto response = registration_endpoint_->register_client(request);
1117 if (!response.has_value()) {
1118 return mcp::core::unexpected(make_oauth_error(
1119 OAuthErrorCode::kClientRegistrationFailed,
1120 "dynamic client registration failed", response.error().message));
1121 }
1122 if (response->client_id.empty()) {
1123 return mcp::core::unexpected(make_oauth_error(
1124 OAuthErrorCode::kClientRegistrationFailed,
1125 "dynamic client registration response did not include client_id"));
1126 }
1127
1129 *response, options.redirect_uri, std::move(options.scopes));
1130 configure_client(config);
1131 return client_;
1132 }
1133
1134 core::Result<OAuthClientConfig> configure_client_for_authorization(
1136 if (options.client_id_metadata_url.has_value() &&
1138 return configure_client_id_metadata_url(*options.client_id_metadata_url,
1139 std::move(options.redirect_uri),
1140 std::move(options.scopes));
1141 }
1142
1143 ClientRegistrationOptions registration_options;
1144 registration_options.client_name = std::move(options.client_name);
1145 registration_options.redirect_uri = std::move(options.redirect_uri);
1146 registration_options.scopes = std::move(options.scopes);
1147 registration_options.metadata = std::move(options.metadata);
1148 return register_client(std::move(registration_options),
1149 std::move(options.headers));
1150 }
1151
1152 core::Result<OAuthSession> start_session(
1154 if (client_.client_id.empty() ||
1155 client_.redirect_uri != request.client.redirect_uri) {
1156 auto configured = configure_client_for_authorization(request.client);
1157 if (!configured.has_value()) {
1158 return mcp::core::unexpected(configured.error());
1159 }
1160 }
1161
1162 auto authorization = start_authorization(
1163 request.client.scopes, std::move(request.pkce),
1164 std::move(request.state),
1165 std::move(request.additional_authorization_parameters));
1166 if (!authorization.has_value()) {
1167 return mcp::core::unexpected(authorization.error());
1168 }
1169 return OAuthSession{std::move(*authorization), client_.redirect_uri};
1170 }
1171
1172 core::Result<AuthorizationUrlResult> start_authorization(
1173 ScopeList scopes, PkceChallenge pkce, std::string state,
1174 MetadataMap additional_parameters = {}) {
1175 add_offline_access_if_supported(scopes, metadata_);
1177 request.client = client_;
1178 request.authorization_server = metadata_;
1179 request.resource = resource_;
1180 request.scopes = std::move(scopes);
1181 request.pkce = std::move(pkce);
1182 request.state = std::move(state);
1183 request.additional_parameters = std::move(additional_parameters);
1184
1185 auto result = build_authorization_url(request);
1186 if (!result.has_value()) {
1187 return mcp::core::unexpected(result.error());
1188 }
1189
1190 auto store_result = state_store().save(result->state.state, result->state);
1191 if (!store_result.has_value()) {
1192 return mcp::core::unexpected(store_result.error());
1193 }
1194 state_ = OAuthLifecycleState::kAuthorizationPending;
1195 return result;
1196 }
1197
1198 core::Result<TokenSet> exchange_authorization_code(
1199 std::string authorization_code, const std::string& state) {
1200 if (!token_endpoint_) {
1201 return mcp::core::unexpected(
1202 make_oauth_error(OAuthErrorCode::kTokenExchangeUnavailable,
1203 "OAuth token endpoint is not configured"));
1204 }
1205
1206 auto loaded_state = state_store().load(state);
1207 if (!loaded_state.has_value()) {
1208 return mcp::core::unexpected(loaded_state.error());
1209 }
1210 if (!loaded_state->has_value()) {
1211 return mcp::core::unexpected(
1212 make_oauth_error(OAuthErrorCode::kAuthorizationRequired,
1213 "authorization state was not found"));
1214 }
1215 auto remove_result = state_store().remove(state);
1216 if (!remove_result.has_value()) {
1217 return mcp::core::unexpected(remove_result.error());
1218 }
1219
1220 auto stored_state = std::move(**loaded_state);
1221 const auto now = SystemClock::now();
1222 if (authorization_state_ttl_ <= std::chrono::seconds::zero() ||
1223 stored_state.created_at + authorization_state_ttl_ <= now) {
1224 return mcp::core::unexpected(
1225 make_oauth_error(OAuthErrorCode::kAuthorizationRequired,
1226 "authorization state has expired"));
1227 }
1228
1229 TokenExchangeRequest request;
1230 request.client = client_;
1231 request.authorization_server = metadata_;
1232 request.resource = resource_;
1233 request.authorization_code = std::move(authorization_code);
1234 request.state = std::move(stored_state);
1235
1236 auto token_result = token_endpoint_->exchange_authorization_code(request);
1237 if (!token_result.has_value()) {
1238 return mcp::core::unexpected(token_result.error());
1239 }
1240
1241 current_scopes_ = token_result->scopes.empty()
1242 ? request.state.requested_scopes
1243 : token_result->scopes;
1244 scope_upgrade_attempts_ = 0;
1245 StoredCredentials credentials;
1246 credentials.client_id = client_.client_id;
1247 credentials.token_set = *token_result;
1248 credentials.granted_scopes = current_scopes_;
1249 credentials.token_received_at = SystemClock::now();
1250 auto save_result =
1251 credential_store().save(credential_key(), std::move(credentials));
1252 if (!save_result.has_value()) {
1253 return mcp::core::unexpected(save_result.error());
1254 }
1255
1256 state_ = OAuthLifecycleState::kAuthorized;
1257 return *token_result;
1258 }
1259
1260 core::Result<TokenRefreshResult> refresh_access_token() {
1261 if (!token_endpoint_) {
1262 return mcp::core::unexpected(
1263 make_oauth_error(OAuthErrorCode::kTokenExchangeUnavailable,
1264 "OAuth token endpoint is not configured"));
1265 }
1266
1267 auto loaded = credential_store().load(credential_key());
1268 if (!loaded.has_value()) {
1269 return mcp::core::unexpected(loaded.error());
1270 }
1271 if (!loaded->has_value() || !(**loaded).token_set.has_value()) {
1272 return mcp::core::unexpected(
1273 make_oauth_error(OAuthErrorCode::kAuthorizationRequired,
1274 "stored credentials are not available"));
1275 }
1276
1277 auto credentials = **loaded;
1278 const auto& current = *credentials.token_set;
1279 if (!current.refresh_token.has_value() || current.refresh_token->empty()) {
1280 return mcp::core::unexpected(
1281 make_oauth_error(OAuthErrorCode::kTokenRefreshFailed,
1282 "refresh token is not available"));
1283 }
1284
1285 TokenRefreshRequest request;
1286 request.client = client_;
1287 request.authorization_server = metadata_;
1288 request.resource = resource_;
1289 request.refresh_token = *current.refresh_token;
1290 request.scopes = credentials.granted_scopes;
1291
1292 auto refreshed = token_endpoint_->refresh_access_token(request);
1293 if (!refreshed.has_value()) {
1294 return mcp::core::unexpected(refreshed.error());
1295 }
1296 if (!refreshed->token_set.refresh_token.has_value()) {
1297 refreshed->token_set.refresh_token = current.refresh_token;
1298 refreshed->refresh_token_rotated = false;
1299 } else {
1300 refreshed->refresh_token_rotated =
1301 refreshed->token_set.refresh_token != current.refresh_token;
1302 }
1303
1304 current_scopes_ = refreshed->token_set.scopes.empty()
1305 ? credentials.granted_scopes
1306 : refreshed->token_set.scopes;
1307 credentials.token_set = refreshed->token_set;
1308 credentials.granted_scopes = current_scopes_;
1309 credentials.token_received_at = SystemClock::now();
1310 auto save_result =
1311 credential_store().save(credential_key(), std::move(credentials));
1312 if (!save_result.has_value()) {
1313 return mcp::core::unexpected(save_result.error());
1314 }
1315
1316 state_ = OAuthLifecycleState::kAuthorized;
1317 return *refreshed;
1318 }
1319
1320 core::Result<std::string> get_access_token(
1321 std::chrono::seconds refresh_skew = std::chrono::seconds(30)) {
1322 auto loaded = credential_store().load(credential_key());
1323 if (!loaded.has_value()) {
1324 return mcp::core::unexpected(loaded.error());
1325 }
1326 if (!loaded->has_value() || !(**loaded).token_set.has_value()) {
1327 return mcp::core::unexpected(
1328 make_oauth_error(OAuthErrorCode::kAuthorizationRequired,
1329 "stored credentials are not available"));
1330 }
1331
1332 const auto& token_set = *(**loaded).token_set;
1333 if (!token_set.expires_at.has_value() ||
1334 *token_set.expires_at > SystemClock::now() + refresh_skew) {
1335 return token_set.access_token;
1336 }
1337 if (!token_set.refresh_token.has_value()) {
1338 return mcp::core::unexpected(make_oauth_error(
1339 OAuthErrorCode::kAuthorizationRequired,
1340 "access token is expired and no refresh token is available"));
1341 }
1342
1343 auto refreshed = refresh_access_token();
1344 if (!refreshed.has_value()) {
1345 return mcp::core::unexpected(refreshed.error());
1346 }
1347 return refreshed->token_set.access_token;
1348 }
1349
1350 core::Result<OAuthRefreshRetryResult> refresh_after_unauthorized_response(
1351 const HttpResponseMetadata& response) {
1352 auto decision = analyze_auth_response(response);
1353 if (!decision.has_value()) {
1354 return mcp::core::unexpected(decision.error());
1355 }
1356
1358 retry.decision = std::move(*decision);
1359 if (response.status_code != 401 ||
1360 retry.decision.action != AuthResponseAction::kAuthorizationRequired) {
1361 return retry;
1362 }
1363
1364 auto refreshed = refresh_access_token();
1365 if (!refreshed.has_value()) {
1366 return mcp::core::unexpected(refreshed.error());
1367 }
1368
1369 retry.should_retry = true;
1370 retry.bearer_token = refreshed->token_set.access_token;
1371 return retry;
1372 }
1373
1379 ClientCredentialsConfig config) {
1380 if (!token_endpoint_) {
1381 return mcp::core::unexpected(
1382 make_oauth_error(OAuthErrorCode::kTokenExchangeUnavailable,
1383 "OAuth token endpoint is not configured"));
1384 }
1385 if (config.client_id.empty()) {
1386 return mcp::core::unexpected(make_oauth_error(
1387 OAuthErrorCode::kInvalidRequest, "client_id is required"));
1388 }
1389 if (config.client_secret.empty()) {
1390 return mcp::core::unexpected(make_oauth_error(
1391 OAuthErrorCode::kInvalidRequest, "client_secret is required"));
1392 }
1393 if (config.resource.empty()) {
1394 return mcp::core::unexpected(make_oauth_error(
1395 OAuthErrorCode::kInvalidRequest, "resource is required"));
1396 }
1397
1398 const auto method_iter = config.metadata.find("token_endpoint_auth_method");
1399 const auto token_endpoint_auth_method =
1400 method_iter == config.metadata.end() || method_iter->second.empty()
1401 ? std::string_view("client_secret_post")
1402 : std::string_view(method_iter->second);
1403 auto validation = validate_client_credentials_metadata(
1404 metadata_, token_endpoint_auth_method);
1405 if (!validation.has_value()) {
1406 return mcp::core::unexpected(validation.error());
1407 }
1408
1409 add_offline_access_if_supported(config.scopes, metadata_);
1410
1412 request.credentials = std::move(config);
1413 request.authorization_server = metadata_;
1414
1415 auto token_result = token_endpoint_->exchange_client_credentials(request);
1416 if (!token_result.has_value()) {
1417 return mcp::core::unexpected(token_result.error());
1418 }
1419
1420 client_.client_id = request.credentials.client_id;
1421 client_.client_secret = request.credentials.client_secret;
1422 resource_ = request.credentials.resource;
1423
1424 current_scopes_ = token_result->scopes.empty() ? request.credentials.scopes
1425 : token_result->scopes;
1426 scope_upgrade_attempts_ = 0;
1427
1428 StoredCredentials credentials;
1429 credentials.client_id = request.credentials.client_id;
1430 credentials.token_set = *token_result;
1431 credentials.granted_scopes = current_scopes_;
1432 credentials.token_received_at = SystemClock::now();
1433 auto save_result =
1434 credential_store().save(credential_key(), std::move(credentials));
1435 if (!save_result.has_value()) {
1436 return mcp::core::unexpected(save_result.error());
1437 }
1438
1439 state_ = OAuthLifecycleState::kAuthorized;
1440 return *token_result;
1441 }
1442
1443 bool can_attempt_scope_upgrade() const {
1444 return scope_upgrade_config_.auto_upgrade &&
1445 scope_upgrade_attempts_ < scope_upgrade_config_.max_upgrade_attempts;
1446 }
1447
1448 core::Result<AuthorizationUrlResult> request_scope_upgrade(
1449 const WwwAuthenticateChallenge& challenge, PkceChallenge pkce,
1450 std::string state, MetadataMap additional_parameters = {}) {
1451 if (!insufficient_scope(challenge)) {
1452 return mcp::core::unexpected(make_oauth_error(
1453 OAuthErrorCode::kInsufficientScope,
1454 "WWW-Authenticate challenge is not insufficient_scope"));
1455 }
1456 if (!can_attempt_scope_upgrade()) {
1457 return mcp::core::unexpected(
1458 make_oauth_error(OAuthErrorCode::kInsufficientScope,
1459 "scope upgrade attempts are exhausted"));
1460 }
1461
1462 const auto required_scope =
1463 challenge.parameter(std::string(WwwAuthenticateScopeParam));
1464 const auto upgraded_scopes =
1465 compute_scope_union(current_scopes_, required_scope);
1466 ++scope_upgrade_attempts_;
1467 return start_authorization(upgraded_scopes, std::move(pkce),
1468 std::move(state),
1469 std::move(additional_parameters));
1470 }
1471
1472 static ScopeList compute_scope_union(const ScopeList& current,
1473 std::string_view required_scope) {
1474 ScopeList result = current;
1475 for (const auto& scope : detail::split_scopes(required_scope)) {
1476 if (!detail::has_scope(result, scope)) {
1477 result.push_back(scope);
1478 }
1479 }
1480 return result;
1481 }
1482
1483 private:
1484 CredentialStore& credential_store() {
1485 if (!credential_store_) {
1486 credential_store_ = std::make_shared<InMemoryCredentialStore>();
1487 }
1488 return *credential_store_;
1489 }
1490
1491 StateStore& state_store() {
1492 if (!state_store_) {
1493 state_store_ = std::make_shared<InMemoryStateStore>();
1494 }
1495 return *state_store_;
1496 }
1497
1498 std::string resource_;
1499 AuthorizationServerMetadata metadata_;
1500 OAuthClientConfig client_;
1501 std::shared_ptr<CredentialStore> credential_store_;
1502 std::shared_ptr<StateStore> state_store_;
1503 std::shared_ptr<OAuthTokenEndpoint> token_endpoint_;
1504 std::shared_ptr<OAuthClientRegistrationEndpoint> registration_endpoint_;
1505 ScopeUpgradeConfig scope_upgrade_config_;
1506 OAuthLifecycleState state_ = OAuthLifecycleState::kUnauthorized;
1507 ScopeList current_scopes_;
1508 std::uint32_t scope_upgrade_attempts_ = 0;
1509 std::chrono::seconds authorization_state_ttl_ = kDefaultAuthorizationStateTtl;
1510};
1511
1512} // namespace mcp::auth
Shared lightweight value types for cxxmcp auth contracts.
Transport-neutral OAuth authorization lifecycle manager.
Definition lifecycle.hpp:998
core::Result< TokenSet > authenticate_client_credentials(ClientCredentialsConfig config)
SEP-1046: authenticate using the Client Credentials grant.
Definition lifecycle.hpp:1378
Application-provided OAuth credential persistence boundary.
Definition lifecycle.hpp:74
Non-persistent credential store for tests and simple clients.
Definition lifecycle.hpp:86
Non-persistent authorization state store.
Definition lifecycle.hpp:190
Execute protected-resource and authorization-server discovery.
Definition lifecycle.hpp:696
OAuth metadata network boundary.
Definition lifecycle.hpp:351
Token exchange and refresh network boundary.
Definition lifecycle.hpp:293
Application-provided authorization state persistence boundary.
Definition lifecycle.hpp:178
Constant-time comparison helpers for auth secrets and lookup keys.
bool constant_time_string_equal(std::string_view lhs, std::string_view rhs) noexcept
Compare two strings without data-dependent early exit.
Definition constant_time.hpp:17
core::Result< AuthResponseDecision > analyze_auth_response(const HttpResponseMetadata &response)
Analyze status and WWW-Authenticate metadata for OAuth next action.
Definition lifecycle.hpp:946
OAuthLifecycleState
Runtime state for the interactive OAuth lifecycle.
Definition lifecycle.hpp:306
AuthResponseAction
Parsed client action from HTTP auth response metadata.
Definition lifecycle.hpp:319
core::Result< AuthorizationUrlResult > build_authorization_url(const AuthorizationUrlRequest &request)
Build an OAuth authorization URL without performing network I/O.
Definition lifecycle.hpp:869
StringList build_authorization_server_metadata_urls(const std::string &issuer_or_base_url)
Build RFC 8414 authorization-server metadata candidates.
Definition lifecycle.hpp:655
OAuthErrorCode
OAuth lifecycle error codes used inside the stable "auth" category.
Definition lifecycle.hpp:33
core::Result< core::Unit > validate_client_credentials_metadata(const AuthorizationServerMetadata &metadata, std::string_view token_endpoint_auth_method="client_secret_post")
SEP-1046: validate that the authorization server supports client credentials authentication.
Definition lifecycle.hpp:808
void add_offline_access_if_supported(ScopeList &scopes, const AuthorizationServerMetadata &metadata)
SEP-2207: auto-append offline_access when the authorization server advertises it in scopes_supported ...
Definition lifecycle.hpp:784
MetadataDiscoveryPlan build_metadata_discovery_plan(const std::string &resource_url, const AuthResponseDecision &decision=AuthResponseDecision{})
Build the metadata discovery plan from a resource URL and auth hint.
Definition lifecycle.hpp:682
core::Error make_oauth_error(OAuthErrorCode code, std::string message, std::string detail={})
Build an auth-category lifecycle error.
Definition lifecycle.hpp:50
ScopeList select_authorization_scopes(const ScopeSelectionContext &context)
Select scopes using the RMCP priority order.
Definition lifecycle.hpp:768
StringList build_protected_resource_metadata_urls(const std::string &resource_url, std::optional< std::string > challenged_resource_metadata_url=std::nullopt)
Build RFC 9728 protected-resource metadata candidates.
Definition lifecycle.hpp:621
RFC 9728 and RFC 8414 metadata value models.
PKCE contracts for OAuth authorization-code flows.
Dynamic Client Registration and client metadata models.
core::Result< ClientRegistrationRequest > build_client_registration_request(const ClientRegistrationOptions &options)
Build the default MCP OAuth DCR request for a public client.
Definition registration.hpp:109
bool supports_client_id_metadata_document(const AuthorizationServerMetadata &metadata)
Whether authorization metadata advertises URL-based client_id docs.
Definition registration.hpp:204
bool is_valid_client_id_metadata_url(std::string_view url)
Validate an SEP-991-style HTTPS URL client_id.
Definition registration.hpp:211
OAuthClientConfig oauth_client_config_from_registration_response(const ClientRegistrationResponse &response, std::string redirect_uri, ScopeList scopes)
Convert a successful DCR response into the SDK OAuth client config.
Definition registration.hpp:131
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
Auth response decision derived from status and WWW-Authenticate.
Definition lifecycle.hpp:326
RFC 8414 OAuth authorization server metadata.
Definition metadata.hpp:28
Full client-id selection plus authorization URL request.
Definition lifecycle.hpp:861
Authorization URL construction input.
Definition lifecycle.hpp:246
Authorization URL plus the state that must be stored for callback.
Definition lifecycle.hpp:257
OAuth 2.0 Client Credentials flow configuration (SEP-1046).
Definition types.hpp:54
Options used when selecting a client_id before authorization.
Definition registration.hpp:98
Request sent through an application-provided DCR transport.
Definition registration.hpp:53
Options used to build a dynamic client registration request.
Definition registration.hpp:90
Stable key for OAuth credential storage.
Definition lifecycle.hpp:57
Transport-neutral HTTP response descriptor used by auth helpers.
Definition types.hpp:35
Options for metadata discovery execution.
Definition lifecycle.hpp:369
Candidate metadata URLs for an MCP Streamable HTTP resource.
Definition lifecycle.hpp:363
Complete discovery result for an MCP Streamable HTTP resource.
Definition metadata.hpp:50
Metadata fetch request routed through application/transport code.
Definition lifecycle.hpp:342
Public OAuth client configuration used by lifecycle helpers.
Definition types.hpp:41
Result of evaluating an HTTP auth response for one-shot retry.
Definition lifecycle.hpp:335
User-facing authorization session for an active code flow.
Definition lifecycle.hpp:845
PKCE verifier/challenge pair used by OAuth 2.1 authorization flows.
Definition pkce.hpp:19
Inputs for RMCP-style authorization scope selection.
Definition lifecycle.hpp:375
Scope upgrade policy used after insufficient_scope challenges.
Definition lifecycle.hpp:313
Stored one-time authorization state for OAuth code + PKCE flows.
Definition lifecycle.hpp:164
Stored OAuth credentials and granted scope bookkeeping.
Definition lifecycle.hpp:65
Token endpoint client credentials input (SEP-1046).
Definition lifecycle.hpp:283
Token endpoint exchange input. Implementations perform network I/O.
Definition lifecycle.hpp:263
Token endpoint refresh input. Implementations perform network I/O.
Definition lifecycle.hpp:273
Structured error returned by fallible SDK operations.
Definition result.hpp:35
OAuth token models and storage contracts.
WWW-Authenticate challenge models and parser boundary.
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