cxxmcp 1.1.6
C++ MCP SDK
Loading...
Searching...
No Matches
http_token_endpoint.hpp
Go to the documentation of this file.
1// Copyright (c) 2025 [caomengxuan666]
2
3#pragma once
4
5#include <chrono>
6#include <cstdint>
7#include <functional>
8#include <nlohmann/json.hpp>
9#include <optional>
10#include <string>
11#include <utility>
12
15
18
19namespace mcp::auth {
20
23 std::string method = "POST";
24 std::string url;
25 HeaderMap headers;
26 std::string body;
27};
28
31 std::function<core::Result<OAuthHttpResponse>(const OAuthHttpRequest&)>;
32
40 public:
42 TimePoint now = SystemClock::now())
43 : post_(std::move(post)), now_(now) {}
44
45 core::Result<TokenSet> exchange_authorization_code(
46 const TokenExchangeRequest& request) override {
47 if (request.authorization_server.token_endpoint.empty()) {
48 return mcp::core::unexpected(make_oauth_error(
49 OAuthErrorCode::kTokenExchangeFailed, "token endpoint is required"));
50 }
51
52 auto form = FormBuilder{};
53 form.add("grant_type", "authorization_code");
54 form.add("code", request.authorization_code);
55 form.add("redirect_uri", request.client.redirect_uri);
56 form.add("client_id", request.client.client_id);
57 HeaderMap headers;
58 apply_client_auth(&form, &headers, request.client);
59 form.add("code_verifier", request.state.pkce.code_verifier);
60 if (!request.resource.empty()) {
61 form.add("resource", request.resource);
62 }
63 append_additional_parameters(&form, request.additional_parameters);
64
65 auto response = post_form(request.authorization_server.token_endpoint,
66 std::move(form).body(), std::move(headers));
67 if (!response.has_value()) {
68 return mcp::core::unexpected(response.error());
69 }
70 return parse_token_set(*response, OAuthErrorCode::kTokenExchangeFailed);
71 }
72
73 core::Result<TokenRefreshResult> refresh_access_token(
74 const TokenRefreshRequest& request) override {
75 if (request.authorization_server.token_endpoint.empty()) {
76 return mcp::core::unexpected(make_oauth_error(
77 OAuthErrorCode::kTokenRefreshFailed, "token endpoint is required"));
78 }
79
80 auto form = FormBuilder{};
81 form.add("grant_type", "refresh_token");
82 form.add("refresh_token", request.refresh_token);
83 form.add("client_id", request.client.client_id);
84 HeaderMap headers;
85 apply_client_auth(&form, &headers, request.client);
86 if (!request.resource.empty()) {
87 form.add("resource", request.resource);
88 }
89 const auto scope = detail::join_scopes(request.scopes);
90 if (!scope.empty()) {
91 form.add("scope", scope);
92 }
93 append_additional_parameters(&form, request.additional_parameters);
94
95 auto response = post_form(request.authorization_server.token_endpoint,
96 std::move(form).body(), std::move(headers));
97 if (!response.has_value()) {
98 return mcp::core::unexpected(response.error());
99 }
100 auto token_set =
101 parse_token_set(*response, OAuthErrorCode::kTokenRefreshFailed);
102 if (!token_set.has_value()) {
103 return mcp::core::unexpected(token_set.error());
104 }
105 TokenRefreshResult result;
106 result.refresh_token_rotated = token_set->refresh_token.has_value();
107 result.token_set = std::move(*token_set);
108 return result;
109 }
110
111 core::Result<TokenSet> exchange_client_credentials(
112 const TokenClientCredentialsRequest& request) override {
113 if (request.authorization_server.token_endpoint.empty()) {
114 return mcp::core::unexpected(
115 make_oauth_error(OAuthErrorCode::kClientCredentialsFailed,
116 "token endpoint is required"));
117 }
118
119 auto form = FormBuilder{};
120 form.add("grant_type", "client_credentials");
121 form.add("client_id", request.credentials.client_id);
122 OAuthClientConfig client;
123 client.client_id = request.credentials.client_id;
124 client.client_secret = request.credentials.client_secret;
125 client.metadata = request.credentials.metadata;
126 HeaderMap headers;
127 apply_client_auth(&form, &headers, client);
128 form.add("resource", request.credentials.resource);
129 const auto scope = detail::join_scopes(request.credentials.scopes);
130 if (!scope.empty()) {
131 form.add("scope", scope);
132 }
133 append_additional_parameters(&form, request.additional_parameters);
134
135 auto response = post_form(request.authorization_server.token_endpoint,
136 std::move(form).body(), std::move(headers));
137 if (!response.has_value()) {
138 return mcp::core::unexpected(response.error());
139 }
140 return parse_token_set(*response, OAuthErrorCode::kClientCredentialsFailed);
141 }
142
143 core::Result<TokenSet> exchange_token_grant(
144 const AuthorizationServerMetadata& authorization_server,
145 const OAuthClientConfig& client, const MetadataMap& parameters,
146 OAuthErrorCode error_code = OAuthErrorCode::kTokenExchangeFailed) {
147 if (authorization_server.token_endpoint.empty()) {
148 return mcp::core::unexpected(
149 make_oauth_error(error_code, "token endpoint is required"));
150 }
151 if (parameters.find("grant_type") == parameters.end()) {
152 return mcp::core::unexpected(
153 make_oauth_error(error_code, "grant_type is required"));
154 }
155
156 auto form = FormBuilder{};
157 if (!client.client_id.empty()) {
158 form.add("client_id", client.client_id);
159 }
160 HeaderMap headers;
161 apply_client_auth(&form, &headers, client);
162 append_additional_parameters(&form, parameters);
163
164 auto response = post_form(authorization_server.token_endpoint,
165 std::move(form).body(), std::move(headers));
166 if (!response.has_value()) {
167 return mcp::core::unexpected(response.error());
168 }
169 return parse_token_set(*response, error_code);
170 }
171
172 private:
173 using Json = nlohmann::json;
174
175 class FormBuilder {
176 public:
177 void add(const std::string& name, const std::string& value) {
178 if (!body_.empty()) {
179 body_.push_back('&');
180 }
181 body_.append(detail::oauth_url_encode(name));
182 body_.push_back('=');
183 body_.append(detail::oauth_url_encode(value));
184 }
185
186 std::string body() && { return std::move(body_); }
187
188 private:
189 std::string body_;
190 };
191
192 static void append_additional_parameters(FormBuilder* form,
193 const MetadataMap& parameters) {
194 for (const auto& parameter : parameters) {
195 form->add(parameter.first, parameter.second);
196 }
197 }
198
199 static std::string client_auth_method(const OAuthClientConfig& client) {
200 const auto iter = client.metadata.find("token_endpoint_auth_method");
201 if (iter != client.metadata.end() && !iter->second.empty()) {
202 return iter->second;
203 }
204 return client.client_secret.has_value() ? "client_secret_post" : "none";
205 }
206
207 static std::string base64_encode(std::string_view input) {
208 static constexpr char alphabet[] =
209 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
210 std::string output;
211 output.reserve(((input.size() + 2) / 3) * 4);
212 std::size_t index = 0;
213 while (index + 3 <= input.size()) {
214 const auto b0 = static_cast<unsigned char>(input[index++]);
215 const auto b1 = static_cast<unsigned char>(input[index++]);
216 const auto b2 = static_cast<unsigned char>(input[index++]);
217 output.push_back(alphabet[b0 >> 2]);
218 output.push_back(alphabet[((b0 & 0x03) << 4) | (b1 >> 4)]);
219 output.push_back(alphabet[((b1 & 0x0F) << 2) | (b2 >> 6)]);
220 output.push_back(alphabet[b2 & 0x3F]);
221 }
222 const auto remaining = input.size() - index;
223 if (remaining == 1) {
224 const auto b0 = static_cast<unsigned char>(input[index]);
225 output.push_back(alphabet[b0 >> 2]);
226 output.push_back(alphabet[(b0 & 0x03) << 4]);
227 output.push_back('=');
228 output.push_back('=');
229 } else if (remaining == 2) {
230 const auto b0 = static_cast<unsigned char>(input[index]);
231 const auto b1 = static_cast<unsigned char>(input[index + 1]);
232 output.push_back(alphabet[b0 >> 2]);
233 output.push_back(alphabet[((b0 & 0x03) << 4) | (b1 >> 4)]);
234 output.push_back(alphabet[(b1 & 0x0F) << 2]);
235 output.push_back('=');
236 }
237 return output;
238 }
239
240 static void apply_client_auth(FormBuilder* form, HeaderMap* headers,
241 const OAuthClientConfig& client) {
242 if (form == nullptr || headers == nullptr || !client.client_secret) {
243 return;
244 }
245 const auto method = client_auth_method(client);
246 if (method == "client_secret_basic") {
247 const auto credentials = detail::oauth_url_encode(client.client_id) +
248 ":" +
249 detail::oauth_url_encode(*client.client_secret);
250 headers->emplace("Authorization", "Basic " + base64_encode(credentials));
251 return;
252 }
253 if (method == "client_secret_post") {
254 form->add("client_secret", *client.client_secret);
255 }
256 }
257
258 core::Result<OAuthHttpResponse> post_form(std::string url, std::string body,
259 HeaderMap headers = {}) const {
260 if (!post_) {
261 return mcp::core::unexpected(
262 make_oauth_error(OAuthErrorCode::kTokenExchangeUnavailable,
263 "OAuth token HTTP POST endpoint is not configured"));
264 }
265
266 OAuthHttpRequest request;
267 request.url = std::move(url);
268 request.headers.emplace("Accept", "application/json");
269 request.headers.emplace("Content-Type",
270 "application/x-www-form-urlencoded");
271 for (auto& header : headers) {
272 request.headers[std::move(header.first)] = std::move(header.second);
273 }
274 request.body = std::move(body);
275
276 auto response = post_(request);
277 if (!response.has_value()) {
278 return mcp::core::unexpected(response.error());
279 }
280 return *response;
281 }
282
283 core::Result<TokenSet> parse_token_set(const OAuthHttpResponse& response,
284 OAuthErrorCode error_code) const {
285 if (response.status_code < 200 || response.status_code >= 300) {
286 return mcp::core::unexpected(make_oauth_error(
287 error_code, "OAuth token endpoint returned non-success HTTP status",
288 std::to_string(response.status_code)));
289 }
290
291 Json value;
292 try {
293 value = Json::parse(response.body);
294 } catch (const Json::exception& error) {
295 return mcp::core::unexpected(make_oauth_error(
296 error_code, "OAuth token endpoint returned invalid JSON",
297 error.what()));
298 }
299 if (!value.is_object()) {
300 return mcp::core::unexpected(make_oauth_error(
301 error_code, "OAuth token endpoint response must be an object"));
302 }
303
304 const auto access_token = optional_string(value, "access_token");
305 if (!access_token.has_value() || access_token->empty()) {
306 return mcp::core::unexpected(make_oauth_error(
307 error_code, "OAuth token endpoint response requires access_token"));
308 }
309
310 TokenSet token_set;
311 token_set.access_token = *access_token;
312 token_set.token_type =
313 optional_string(value, "token_type").value_or("Bearer");
314 token_set.refresh_token = optional_string(value, "refresh_token");
315 if (auto scope = optional_string(value, "scope"); scope.has_value()) {
316 token_set.scopes = detail::split_scopes(*scope);
317 }
318 if (auto expires_in = optional_integer(value, "expires_in");
319 expires_in.has_value() && *expires_in > 0) {
320 token_set.expires_at = now_ + std::chrono::seconds(*expires_in);
321 }
322 token_set.metadata = extension_metadata(value);
323 return token_set;
324 }
325
326 static std::optional<std::string> optional_string(const Json& value,
327 const char* key) {
328 const auto iter = value.find(key);
329 if (iter == value.end() || iter->is_null()) {
330 return std::nullopt;
331 }
332 if (iter->is_string()) {
333 return iter->get<std::string>();
334 }
335 return std::nullopt;
336 }
337
338 static std::optional<std::int64_t> optional_integer(const Json& value,
339 const char* key) {
340 const auto iter = value.find(key);
341 if (iter == value.end() || iter->is_null()) {
342 return std::nullopt;
343 }
344 if (iter->is_number_integer() || iter->is_number_unsigned()) {
345 return iter->get<std::int64_t>();
346 }
347 return std::nullopt;
348 }
349
350 static MetadataMap extension_metadata(const Json& value) {
351 MetadataMap result;
352 for (auto iter = value.begin(); iter != value.end(); ++iter) {
353 const auto& key = iter.key();
354 if (key == "access_token" || key == "token_type" ||
355 key == "refresh_token" || key == "expires_in" || key == "scope") {
356 continue;
357 }
358 if (iter->is_string()) {
359 result.emplace(key, iter->get<std::string>());
360 } else if (iter->is_boolean()) {
361 result.emplace(key, iter->get<bool>() ? "true" : "false");
362 } else if (iter->is_number() || iter->is_object() || iter->is_array()) {
363 result.emplace(key, iter->dump());
364 }
365 }
366 return result;
367 }
368
369 OAuthHttpPost post_;
370 TimePoint now_;
371};
372
373} // namespace mcp::auth
Default OAuth token endpoint implementation over an injected HTTP POST.
Definition http_token_endpoint.hpp:39
Token exchange and refresh network boundary.
Definition lifecycle.hpp:293
Default HTTP metadata endpoint parser for OAuth discovery.
std::function< core::Result< OAuthHttpResponse >(const OAuthHttpRequest &)> OAuthHttpPost
Application or SDK transport adapter used for token POST calls.
Definition http_token_endpoint.hpp:31
OAuth authorization lifecycle contracts and lightweight state logic.
OAuthErrorCode
OAuth lifecycle error codes used inside the stable "auth" category.
Definition lifecycle.hpp:33
core::Error make_oauth_error(OAuthErrorCode code, std::string message, std::string detail={})
Build an auth-category lifecycle error.
Definition lifecycle.hpp:50
tl::expected< T, Error > Result
Alias for the SDK result type.
Definition result.hpp:64
RFC 8414 OAuth authorization server metadata.
Definition metadata.hpp:28
Public OAuth client configuration used by lifecycle helpers.
Definition types.hpp:41
Transport-neutral HTTP request used by the auth token endpoint.
Definition http_token_endpoint.hpp:22
Transport-neutral HTTP response used by the auth metadata endpoint.
Definition http_metadata_endpoint.hpp:19
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
Token refresh result, including optional refresh-token rotation.
Definition token.hpp:36
OAuth access and refresh token state owned by the application.
Definition token.hpp:20