Skip to content

Commit 36c59e3

Browse files
committed
Bearer auth demo + docs; CTest wiring; extend server tests; ASCII OAuth 2.1 flows
1 parent 39ebb24 commit 36c59e3

12 files changed

Lines changed: 1012 additions & 35 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ add_library(mcp_cpp STATIC
5151
src/mcp/Server.cpp
5252
src/mcp/auth/OAuthClient.cpp
5353
src/mcp/auth/OAuth2ClientCredentialsAuth.cpp
54+
src/mcp/auth/ServerAuth.cpp
5455
)
5556
add_library(mcp::cpp ALIAS mcp_cpp)
5657

docs/api/server.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,102 @@ Notes:
123123
- Client parsing: handled in [src/mcp/Client.cpp](../../src/mcp/Client.cpp) `parseServerCapabilities()`.
124124
- Tests: [tests/test_capabilities_logging.cpp](../../tests/test_capabilities_logging.cpp).
125125

126+
## Server-side Bearer authentication (HTTP acceptor)
127+
128+
Header: [include/mcp/auth/ServerAuth.hpp](../../include/mcp/auth/ServerAuth.hpp)
129+
130+
The HTTP server acceptor can enforce Bearer authentication for incoming POSTs to `rpcPath` and `notifyPath` when explicitly enabled.
131+
132+
- API
133+
- `void HTTPServer::SetBearerAuth(mcp::auth::ITokenVerifier& verifier, const mcp::auth::RequireBearerTokenOptions& opts)`
134+
- `mcp::auth::ITokenVerifier` verifies the bearer token and populates `TokenInfo` (scopes, expiration, extra).
135+
- `mcp::auth::RequireBearerTokenOptions` controls enforcement:
136+
- `resourceMetadataUrl`: included in `WWW-Authenticate: Bearer resource_metadata=<url>` on 401/403.
137+
- `requiredScopes`: all listed scopes must be present; otherwise 403.
138+
139+
- Behavior
140+
- Authorization header must be `Authorization: Bearer <token>` (scheme case-insensitive).
141+
- On missing/invalid token → 401 Unauthorized.
142+
- On missing expiration or expired token → 401 Unauthorized.
143+
- On insufficient scope → 403 Forbidden.
144+
- When `resourceMetadataUrl` is provided, responses include `WWW-Authenticate: Bearer resource_metadata=<url>`.
145+
- On success, the verified token is available during handler execution via `mcp::auth::currentTokenInfo()`.
146+
- When `SetBearerAuth(...)` is not called, no authentication is enforced (backward compatible).
147+
148+
- Example (bridge to Server::HandleJSONRPC)
149+
150+
```cpp
151+
#include "mcp/HTTPServer.hpp"
152+
#include "mcp/Server.h"
153+
#include "mcp/auth/ServerAuth.hpp"
154+
155+
mcp::ServerFactory sf;
156+
auto server = sf.CreateServer({"Demo","1.0.0"});
157+
158+
mcp::HTTPServerFactory hf;
159+
auto acceptor = hf.CreateTransportAcceptor("http://127.0.0.1:9443");
160+
161+
struct MyVerifier : mcp::auth::ITokenVerifier {
162+
bool Verify(const std::string& token, mcp::auth::TokenInfo& out, std::string& err) override {
163+
if (token != "demo") { err = "invalid token"; return false; }
164+
out.scopes = {"tools:invoke"};
165+
out.expiration = std::chrono::system_clock::now() + std::chrono::minutes(5);
166+
return true;
167+
}
168+
} verifier;
169+
170+
mcp::auth::RequireBearerTokenOptions opts;
171+
opts.resourceMetadataUrl = "https://auth.example.com/rs";
172+
opts.requiredScopes = {"tools:invoke"};
173+
174+
auto* http = dynamic_cast<mcp::HTTPServer*>(acceptor.get());
175+
if (http) { http->SetBearerAuth(verifier, opts); }
176+
177+
acceptor->SetRequestHandler([&server](const mcp::JSONRPCRequest& req){ return server->HandleJSONRPC(req); });
178+
acceptor->Start().get();
179+
```
180+
181+
- Accessing token info in handlers
182+
- Inside server/tool handlers, use `mcp::auth::currentTokenInfo()` to read the request’s token metadata.
183+
184+
- Demo (run_demo.sh)
185+
- The demo wiring supports environment toggles to exercise this flow end-to-end:
186+
- `MCP_HTTP_REQUIRE_BEARER=1` — enable server-side Bearer for the HTTP demo.
187+
- `MCP_HTTP_RESOURCE_METADATA_URL=https://auth.example.com/rs` — advertised in `WWW-Authenticate`.
188+
- `MCP_HTTP_DEMO_TOKEN=demo` — client token to accept (demo verifier).
189+
- `MCP_HTTP_DEMO_SCOPES=s1,s2` — scopes attached by the demo verifier (default `s1,s2`).
190+
- `MCP_HTTP_REQUIRED_SCOPES=s1` — required scopes to enforce (optional).
191+
- The CTest target `HTTPDemo.BearerAuth` first verifies an unauthorized probe (expects 401 + `WWW-Authenticate`) and then runs the client with a bearer token to observe a successful 200 path.
192+
193+
### Flow (ASCII)
194+
195+
```
196+
+---------+ +------------------+
197+
| Client | | HTTPServer |
198+
+---------+ +------------------+
199+
| POST /mcp/rpc (no Authorization) |
200+
|---------------------------------------------------------->|
201+
| |
202+
|<----------------------------------------------------------|
203+
| 401 Unauthorized |
204+
| WWW-Authenticate: Bearer resource_metadata=<url> |
205+
| |
206+
| POST /mcp/rpc |
207+
|(Authorization: Bearer <token with bad scope>) |
208+
|---------------------------------------------------------->|
209+
| |
210+
|<----------------------------------------------------------|
211+
| 403 Forbidden |
212+
| WWW-Authenticate: Bearer resource_metadata=<url> |
213+
| |
214+
| POST /mcp/rpc |
215+
|(Authorization: Bearer <valid token>) |
216+
|---------------------------------------------------------->|
217+
| |
218+
|<----------------------------------------------------------|
219+
| 200 OK (JSON-RPC response) |
220+
```
221+
126222
## Tools
127223
- void RegisterTool(const std::string& name, ToolHandler handler)
128224
- void RegisterTool(const Tool& tool, ToolHandler handler)

docs/api/transport.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,45 @@ Notes:
174174
- Secrets are not logged. Prefer environment variables or secure config handling in your process to populate `clientSecret`.
175175
- The token endpoint response must include `access_token` and may include `expires_in` (seconds). When absent, a default 1‑hour lifetime is assumed.
176176

177+
##### Flow diagrams (ASCII)
178+
179+
- Static Bearer header
180+
181+
```
182+
+-----------+ +------------------+
183+
| Client | | Resource Server |
184+
+-----------+ +------------------+
185+
| Build HTTP request (JSON-RPC over HTTP)
186+
| Authorization: Bearer <token>
187+
|---------------------------------------------->
188+
| POST /mcp/rpc
189+
|
190+
| 200 OK / Error
191+
|<----------------------------------------------
192+
```
193+
194+
- OAuth 2.1 Client Credentials grant (token fetch + use)
195+
196+
```
197+
+-----------+ +--------------+ +------------------+
198+
| Client | | Auth Server | | Resource Server |
199+
+-----------+ +--------------+ +------------------+
200+
| POST /oauth2/token
201+
| (grant_type=client_credentials,
202+
| client_id, client_secret, scope?)
203+
|-------------------------->|
204+
|
205+
| 200 OK {access_token, expires_in, ...}
206+
|<---------------------------------|
207+
| Build HTTP request (JSON-RPC over HTTP) |
208+
| Authorization: Bearer <access_token> |
209+
|------------------------------------------------------------->|
210+
| POST /mcp/rpc
211+
|
212+
| 200 OK / Error
213+
|<-------------------------------------------------------------|
214+
```
215+
177216
#### Code-based authentication injection (IAuth)
178217

179218
You can also inject an auth provider programmatically using `IAuth`.

examples/mcp_client/main.cpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,12 @@ int main(int argc, char** argv) {
9595
HTTPTransportFactory f;
9696
std::string url = getArgValue(argc, argv, "--url").value_or("http://127.0.0.1:9443");
9797
std::string cfg = parseHttpUrlToConfig(url);
98+
if (auto extra = getArgValue(argc, argv, "--httpcfg"); extra.has_value()) {
99+
if (!extra->empty()) {
100+
if (!cfg.empty() && cfg.back() != ';') { cfg += "; "; }
101+
cfg += *extra;
102+
}
103+
}
98104
transport = f.CreateTransport(cfg);
99105
} else {
100106
LOG_ERROR("Unknown --transport option: {} (expected stdio|shm|http)", transportKind);

examples/mcp_server/main.cpp

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include "mcp/SharedMemoryTransport.hpp"
1212
#include "mcp/HTTPServer.hpp"
1313
#include "mcp/Protocol.h"
14+
#include "mcp/auth/ServerAuth.hpp"
1415
#include <iostream>
1516
#include <cstddef>
1617
#include <chrono>
@@ -139,6 +140,69 @@ int main(int argc, char** argv) {
139140
LOG_INFO("HTTPServer listening at: {} (rpcPath=/mcp/rpc, notifyPath=/mcp/notify)", listen);
140141
HTTPServerFactory hf;
141142
auto acceptor = hf.CreateTransportAcceptor(listen);
143+
144+
// Optional: Configure server-side Bearer auth for demo via environment variables
145+
// MCP_HTTP_REQUIRE_BEARER=1 enables; MCP_HTTP_DEMO_TOKEN sets accepted token;
146+
// MCP_HTTP_RESOURCE_METADATA_URL sets metadata URL; MCP_HTTP_REQUIRED_SCOPES is comma-separated list (e.g., "s1,s2").
147+
{
148+
std::string req = GetEnvOrDefault("MCP_HTTP_REQUIRE_BEARER", "0");
149+
if (req == std::string("1")) {
150+
// Downcast to HTTPServer to access SetBearerAuth
151+
auto* httpSrv = dynamic_cast<HTTPServer*>(acceptor.get());
152+
if (httpSrv != nullptr) {
153+
struct DemoTokenVerifier : public mcp::auth::ITokenVerifier {
154+
std::string expected;
155+
std::vector<std::string> demoScopes;
156+
bool Verify(const std::string& token, mcp::auth::TokenInfo& outInfo, std::string& errorMessage) override {
157+
if (token != expected) {
158+
errorMessage = std::string("invalid token");
159+
return false;
160+
}
161+
outInfo.scopes = demoScopes;
162+
outInfo.expiration = std::chrono::system_clock::now() + std::chrono::minutes(5);
163+
return true;
164+
}
165+
};
166+
static DemoTokenVerifier verifier;
167+
verifier.expected = GetEnvOrDefault("MCP_HTTP_DEMO_TOKEN", "demo");
168+
// Default demo scopes allow common checks; adjustable via MCP_HTTP_DEMO_SCOPES
169+
std::string ds = GetEnvOrDefault("MCP_HTTP_DEMO_SCOPES", "s1,s2");
170+
verifier.demoScopes.clear();
171+
{
172+
std::size_t start = 0;
173+
while (start <= ds.size()) {
174+
std::size_t comma = ds.find(',', start);
175+
std::string item = (comma == std::string::npos) ? ds.substr(start) : ds.substr(start, comma - start);
176+
if (!item.empty()) { verifier.demoScopes.push_back(item); }
177+
if (comma == std::string::npos) { break; }
178+
start = comma + 1;
179+
}
180+
}
181+
mcp::auth::RequireBearerTokenOptions aopts;
182+
aopts.resourceMetadataUrl = GetEnvOrDefault("MCP_HTTP_RESOURCE_METADATA_URL", "");
183+
{
184+
std::string rs = GetEnvOrDefault("MCP_HTTP_REQUIRED_SCOPES", "");
185+
aopts.requiredScopes.clear();
186+
if (!rs.empty()) {
187+
std::size_t start = 0;
188+
while (start <= rs.size()) {
189+
std::size_t comma = rs.find(',', start);
190+
std::string item = (comma == std::string::npos) ? rs.substr(start) : rs.substr(start, comma - start);
191+
if (!item.empty()) { aopts.requiredScopes.push_back(item); }
192+
if (comma == std::string::npos) { break; }
193+
start = comma + 1;
194+
}
195+
}
196+
}
197+
httpSrv->SetBearerAuth(verifier, aopts);
198+
LOG_INFO("HTTP Bearer auth enabled for demo (requiredScopes={} resource_metadata={})",
199+
aopts.requiredScopes.size(), aopts.resourceMetadataUrl);
200+
} else {
201+
LOG_WARN("HTTP Bearer auth requested but acceptor is not HTTPServer");
202+
}
203+
}
204+
}
205+
142206
acceptor->SetRequestHandler([&server](const JSONRPCRequest& req) {
143207
return server->HandleJSONRPC(req);
144208
});

include/mcp/HTTPServer.hpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <memory>
1414
#include "mcp/Transport.h"
1515
#include "mcp/JSONRPCTypes.h"
16+
#include "mcp/auth/ServerAuth.hpp"
1617

1718
namespace mcp {
1819

@@ -78,6 +79,23 @@ namespace mcp {
7879
//==========================================================================================================
7980
void SetErrorHandler(ITransport::ErrorHandler handler) override;
8081

82+
//==========================================================================================================
83+
// SetBearerAuth
84+
// Purpose: Configure optional server-side Bearer authentication for HTTP requests.
85+
// Notes:
86+
// - Non-owning reference to an ITokenVerifier implementation; caller must ensure lifetime spans server use.
87+
// - When configured, incoming requests to rpcPath/notifyPath must include a valid
88+
// Authorization: Bearer <token> header. On failure, the server responds with 401/403 and
89+
// includes a WWW-Authenticate: Bearer resource_metadata=<url> header when a URL is provided.
90+
// - On success, the verified TokenInfo is made available for the duration of request handling
91+
// via mcp::auth::CurrentTokenInfo().
92+
// Args:
93+
// verifier: Token verifier to validate bearer tokens.
94+
// opts: Required scopes and resource metadata URL for WWW-Authenticate.
95+
//==========================================================================================================
96+
void SetBearerAuth(mcp::auth::ITokenVerifier& verifier,
97+
const mcp::auth::RequireBearerTokenOptions& opts);
98+
8199
private:
82100
class Impl;
83101
std::unique_ptr<Impl> pImpl;

include/mcp/auth/ServerAuth.hpp

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
//==========================================================================================================
2+
// SPDX-License-Identifier: MIT
3+
// Copyright (c) 2025 Vinny Parla
4+
// File: ServerAuth.hpp
5+
// Purpose: Server-side bearer authentication interfaces and helpers for HTTP server
6+
//==========================================================================================================
7+
8+
#pragma once
9+
10+
#include <string>
11+
#include <vector>
12+
#include <memory>
13+
#include <unordered_map>
14+
#include <chrono>
15+
16+
namespace mcp::auth {
17+
18+
//==========================================================================================================
19+
// TokenInfo
20+
// Purpose: Information extracted from a bearer token (scopes, expiration, and optional extra fields).
21+
//==========================================================================================================
22+
struct TokenInfo {
23+
std::vector<std::string> scopes;
24+
std::chrono::system_clock::time_point expiration;
25+
std::unordered_map<std::string, std::string> extra;
26+
};
27+
28+
//==========================================================================================================
29+
// ITokenVerifier
30+
// Purpose: Interface to validate a bearer token and populate TokenInfo when valid.
31+
// Returns: true on success (TokenInfo populated); false on failure (set errorMessage).
32+
//==========================================================================================================
33+
class ITokenVerifier {
34+
public:
35+
virtual ~ITokenVerifier() = default;
36+
virtual bool Verify(const std::string& token, TokenInfo& outInfo, std::string& errorMessage) = 0;
37+
};
38+
39+
//==========================================================================================================
40+
// RequireBearerTokenOptions
41+
// Purpose: Options controlling server-side bearer enforcement and resource metadata advertisement.
42+
// Fields:
43+
// resourceMetadataUrl: URL to include in WWW-Authenticate header when returning 401/403.
44+
// requiredScopes: All listed scopes must be present in the token.
45+
//==========================================================================================================
46+
struct RequireBearerTokenOptions {
47+
std::string resourceMetadataUrl;
48+
std::vector<std::string> requiredScopes;
49+
};
50+
51+
//==========================================================================================================
52+
// BearerCheckResult
53+
// Purpose: Result of checking a bearer Authorization header.
54+
// Fields:
55+
// ok: True if authorization passed.
56+
// httpStatus: HTTP status to use on failure (401 or 403).
57+
// errorMessage: Error message to include in payload.
58+
// includeWWWAuthenticate: Whether the caller should include a WWW-Authenticate header.
59+
//==========================================================================================================
60+
struct BearerCheckResult {
61+
bool ok{false};
62+
int httpStatus{401};
63+
std::string errorMessage;
64+
bool includeWWWAuthenticate{false};
65+
};
66+
67+
//==========================================================================================================
68+
// CheckBearerAuth
69+
// Purpose: Validate Authorization header using the supplied verifier and options.
70+
// Args:
71+
// authHeader: Value of the Authorization header (may be empty).
72+
// verifier: Token verifier (must not be null if enforcement is enabled).
73+
// opts: Enforcement options (required scopes and resource metadata URL).
74+
// outInfo: Populated on success with token information.
75+
// Returns:
76+
// BearerCheckResult indicating success or failure details.
77+
//==========================================================================================================
78+
BearerCheckResult CheckBearerAuth(
79+
const std::string& authHeader,
80+
ITokenVerifier& verifier,
81+
const RequireBearerTokenOptions& opts,
82+
TokenInfo& outInfo);
83+
84+
//==========================================================================================================
85+
// Per-request TokenInfo context accessors
86+
// Purpose: Provide access to the current request's TokenInfo for server handlers.
87+
//==========================================================================================================
88+
const TokenInfo* CurrentTokenInfo();
89+
90+
// RAII helper: sets the current TokenInfo for the lifetime of this object, then clears.
91+
class TokenInfoScope {
92+
public:
93+
explicit TokenInfoScope(const TokenInfo* info);
94+
~TokenInfoScope();
95+
private:
96+
const TokenInfo* prev{nullptr};
97+
};
98+
99+
} // namespace mcp::auth

0 commit comments

Comments
 (0)