OAuth 2.1 with PKCE¶
TDB Enterprise ships a built-in OAuth 2.1 authorization server. This lets Cursor, Claude and ChatGPT custom connectors, and any other MCP client that supports OAuth 2.1 authenticate with TDB without manual token management.
Implemented specifications:
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
- RFC 8414 — Authorization Server Metadata
- RFC 7591 — Dynamic Client Registration
- PKCE S256 (required —
plainis rejected)
Required environment variables¶
The values below are for signing in with TDB's own admin password. To sign people in through your identity provider instead, see Single sign-on and SCIM.
TDB_JWT_SECRET=<64-char hex> # Required — signs the access tokens
TDB_ADMIN_USER=admin # Required — OAuth login credentials
TDB_ADMIN_PASSWORD=<password> # Required
# Required if TDB sits behind a reverse proxy or non-localhost URL
TDB_SERVER_URL=https://tdb.yourcompany.com
If TDB_SERVER_URL is not set, TDB derives the base URL from the incoming request.
Set it explicitly when running behind nginx, Caddy, or a load balancer.
Discovery endpoints¶
MCP clients discover the authorization server automatically. No manual configuration is needed beyond pointing the client at the TDB base URL.
| Endpoint | Spec | Description |
|---|---|---|
GET /.well-known/oauth-protected-resource |
RFC 9728 | Resource server metadata |
GET /.well-known/oauth-authorization-server |
RFC 8414 | Authorization server metadata |
# Verify discovery is working
curl http://localhost:8000/.well-known/oauth-authorization-server | python -m json.tool
Expected output:
{
"issuer": "http://localhost:8000",
"authorization_endpoint": "http://localhost:8000/oauth/authorize",
"token_endpoint": "http://localhost:8000/oauth/token",
"registration_endpoint": "http://localhost:8000/oauth/register",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["none"],
"authorization_response_iss_parameter_supported": true
}
From enterprise 0.18.0, the authorization redirect carries iss with the issuer
above (RFC 9207), so a client using several authorization servers can tell which
one issued a code. A 401 from /v1/mcp names the protected-resource metadata
with an absolute URL, built from TDB_SERVER_URL when it is set.
Connecting Claude Desktop¶
Which route works depends on where TDB runs, because Claude Desktop's custom connectors are reached from Anthropic's cloud, not from your computer.
TDB on your machine or a private network: use the TDB extension (tdb.mcpb), which
connects with an API key, not OAuth. See
Claude Desktop setup →. A url entry in
claude_desktop_config.json does not work: that file is for local servers Claude Desktop
starts itself.
TDB at a public HTTPS URL: add it as a custom connector.
- Set
TDB_SERVER_URLto the public URL, for examplehttps://tdb.example.com, so the discovery documents and the401challenge name it. - In Claude Desktop, open Settings → Connectors → Add custom connector and enter
https://tdb.example.com/v1/mcp. - Claude registers itself by dynamic registration, or by its
client metadata document if you set
TDB_OAUTH_CIMD_HOSTS=claude.ai. Log in withTDB_ADMIN_USER/TDB_ADMIN_PASSWORDwhen the browser opens.
Tokens expire after TDB_JWT_EXPIRE_MINUTES (default 60 minutes).
Not yet tested
The custom-connector route has not been run against TDB yet. The extension route has (Windows, community 0.12.0).
Connecting Cursor¶
-
Open Cursor → Settings → MCP → Add Server
-
Set the server URL to
http://localhost:8000/v1/mcp -
Cursor runs the OAuth 2.1 discovery and PKCE flow from your machine, so
localhostworks. The flow has been tested with Cursor's redirect URI but not yet inside Cursor.
Other clients¶
VS Code, JetBrains IDEs, Windsurf, and Cline are not confirmed to support the
WWW-Authenticate OAuth discovery flow yet — connect them with a static API key
via the Authorization header instead. See
Connect an IDE / AI Tool → for config and
per-client caveats (notably JetBrains, which has no documented way to attach a
header to a remote MCP server without a stdio bridge).
Manual OAuth flow (testing)¶
You can walk through the full OAuth flow manually with curl:
Step 1 — Register a client (dynamic registration):
curl -X POST http://localhost:8000/oauth/register \
-H "Content-Type: application/json" \
-d '{
"client_name": "My Test Client",
"redirect_uris": ["http://localhost:9999/callback"]
}'
Response:
{
"client_id": "abc123...",
"client_name": "My Test Client",
"redirect_uris": ["http://localhost:9999/callback"],
"grant_types": ["authorization_code"],
"response_types": ["code"],
"token_endpoint_auth_method": "none"
}
Step 2 — Generate a PKCE code verifier and challenge:
# Generate a random code verifier (43–128 chars, URL-safe)
CODE_VERIFIER=$(python3 -c "
import secrets, base64
v = secrets.token_urlsafe(48)
print(v)
")
# Compute the S256 code challenge
CODE_CHALLENGE=$(python3 -c "
import hashlib, base64, sys
v = '$CODE_VERIFIER'
digest = hashlib.sha256(v.encode('ascii')).digest()
print(base64.urlsafe_b64encode(digest).rstrip(b'=').decode())
")
echo "Verifier: $CODE_VERIFIER"
echo "Challenge: $CODE_CHALLENGE"
Step 3 — Open the authorization URL in a browser:
http://localhost:8000/oauth/authorize
?response_type=code
&client_id=abc123...
&redirect_uri=http://localhost:9999/callback
&code_challenge=<CODE_CHALLENGE>
&code_challenge_method=S256
&state=random-state-value
Log in with TDB_ADMIN_USER / TDB_ADMIN_PASSWORD. You'll be redirected to
http://localhost:9999/callback?code=<auth_code>&iss=http%3A%2F%2Flocalhost%3A8000&state=random-state-value.
The iss parameter (enterprise 0.18.0 and later) names the issuer; check that it
equals the issuer in the authorization server metadata.
Step 4 — Exchange the code for an access token:
curl -X POST http://localhost:8000/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=<auth_code>" \
-d "client_id=abc123..." \
-d "redirect_uri=http://localhost:9999/callback" \
-d "code_verifier=$CODE_VERIFIER"
Response:
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "bearer",
"expires_in": 3600
}
Step 5 — Use the access token on MCP:
curl -X POST http://localhost:8000/v1/mcp \
-H "Authorization: Bearer eyJhbGci..." \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "query_source",
"arguments": {"sql": "SELECT COUNT(*) FROM data"}
}
}'
This runs against the first registered source, and data is the table name of a
CSV source. With a database source, use a real table name and pass
"source_name".
Client ID Metadata Documents¶
From enterprise 0.18.0, and off unless enabled. A client may use an HTTPS URL
as its client_id instead of registering first. TDB fetches that URL, reads the
client's name and redirect URIs from the JSON document there, and continues the
flow as for a registered client. Dynamic registration stays available either way.
Enable it by listing the hosts whose documents may be fetched:
The authorization server metadata then advertises
client_id_metadata_document_supported: true.
Enabling this makes TDB fetch a URL that a client chose, from inside your network. Every fetch is guarded:
- the host must be in
TDB_OAUTH_CIMD_HOSTS; - the URL must be
https, with a path, and no credentials, query, fragment or./..segments; - every address the host resolves to must be public. Private, loopback, link-local and cloud-metadata addresses are refused. TDB connects to the address it checked, and TLS still verifies the certificate against the hostname, so a name that re-resolves cannot redirect the fetch inward;
- redirects are refused, and the fetch has a 5-second deadline and a 64 KiB size limit;
- the document's
client_idmust equal its URL, it must describe a public client (noclient_secret,token_endpoint_auth_methodnone), and itsredirect_urisfollow the rules below.
Documents are cached for 5 minutes, and refusals for 30 seconds. A refusal is
logged as oauth_cimd_refused with the reason. TDB connects directly and does
not use an egress proxy, so this cannot work in an air-gapped deployment.
Redirect URI restrictions¶
For security, TDB only allows redirect URIs that meet one of these criteria:
https://— any HTTPS URI (required for production)http://localhost— loopback only (required for native app flows like Cursor)
Plain http:// to a non-localhost host is rejected. This follows
OAuth 2.1 BCP.
Security notes¶
- PKCE is required for all authorization code flows —
plainchallenge method is rejected. - Authorization codes expire after 10 minutes and are single-use.
- Code exchange is atomic (uses
BEGIN IMMEDIATEtransaction) — replay attacks are not possible. - All login forms use constant-time credential comparison with a 200 ms brute-force throttle.