Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .well-known/.htaccess
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# .htaccess for .well-known OAuth endpoints
# Handle OAuth discovery endpoints with proper routing

RewriteEngine On

# Handle oauth-protected-resource requests (with or without trailing path)
RewriteRule ^oauth-protected-resource(/.*)?$ oauth-protected-resource/index.php [L,QSA]

# Handle oauth-authorization-server requests (with or without trailing path)
RewriteRule ^oauth-authorization-server(/.*)?$ oauth-authorization-server/index.php [L,QSA]

# Handle jwks requests (with or without trailing path)
RewriteRule ^jwks(/.*)?$ jwks/index.php [L,QSA]

# Handle docs requests (with or without trailing path)
RewriteRule ^docs(/.*)?$ docs/index.php [L,QSA]


# Set proper content type for OAuth metadata
<IfModule mod_headers.c>
Header always set Content-Type "application/json; charset=utf-8"
Header always set Cache-Control "no-cache, no-store, must-revalidate"
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization"
</IfModule>

# Security headers
<IfModule mod_headers.c>
Header always set X-Content-Type-Options nosniff
Header always set X-Frame-Options DENY
Header always set X-XSS-Protection "1; mode=block"
</IfModule>

# Handle preflight requests
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ - [R=200,L]
124 changes: 124 additions & 0 deletions .well-known/docs/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
<?php
/**
* OAuth Resource Documentation endpoint
* Location: /.well-known/docs
*/

// Handle CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
http_response_code(200);
exit;
}

// Only allow GET requests
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
header('Allow: GET, OPTIONS');
exit;
}

// Get the base URL
$scheme = $_SERVER['REQUEST_SCHEME'] ?? (($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https' : 'http');
$host = $_SERVER['HTTP_HOST'];
$baseUrl = $scheme . '://' . $host;

?>
<!DOCTYPE html>
<html>
<head>
<title>Formulize MCP Server - OAuth Documentation</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px; margin: 50px auto; padding: 20px;
line-height: 1.6; color: #333;
}
.endpoint { background: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; }
code { background: #e9ecef; padding: 2px 5px; border-radius: 3px; }
pre { background: #f8f9fa; padding: 15px; border-radius: 5px; overflow-x: auto; }
h1, h2 { color: #007acc; }
.note { background: #fff3cd; padding: 15px; border-left: 4px solid #ffc107; margin: 15px 0; }
</style>
</head>
<body>
<h1>🔐 Formulize MCP Server - OAuth 2.1 Documentation</h1>

<div class="note">
<strong>📋 OAuth 2.1 Compliance:</strong> This server implements OAuth 2.1 with PKCE, Resource Indicators (RFC 8707),
Protected Resource Metadata (RFC 9728), and Authorization Server Metadata (RFC 8414).
</div>

<h2>🔍 Discovery Endpoints</h2>
<div class="endpoint">
<strong>Protected Resource Metadata:</strong><br>
<code>GET <?php echo $baseUrl; ?>/.well-known/oauth-protected-resource</code>
</div>
<div class="endpoint">
<strong>Authorization Server Metadata:</strong><br>
<code>GET <?php echo $baseUrl; ?>/.well-known/oauth-authorization-server</code>
</div>

<h2>🚀 OAuth Endpoints</h2>
<div class="endpoint">
<strong>Authorization Endpoint:</strong><br>
<code>GET <?php echo $baseUrl; ?>/mcp?action=authorize</code><br>
<small>Parameters: client_id, redirect_uri, response_type=code, scope, state, code_challenge, code_challenge_method=S256, resource</small>
</div>
<div class="endpoint">
<strong>Token Endpoint:</strong><br>
<code>POST <?php echo $baseUrl; ?>/mcp?action=token</code><br>
<small>Parameters: grant_type=authorization_code, code, client_id, redirect_uri, code_verifier, resource</small>
</div>
<div class="endpoint">
<strong>Client Registration:</strong><br>
<code>POST <?php echo $baseUrl; ?>/mcp?action=register</code><br>
<small>Dynamic client registration (RFC 7591)</small>
</div>

<h2>🛡️ Security Features</h2>
<ul>
<li><strong>PKCE Required:</strong> All authorization flows must use PKCE for security</li>
<li><strong>Resource Indicators:</strong> Tokens are bound to specific resources (RFC 8707)</li>
<li><strong>Public Clients:</strong> No client secrets required</li>
<li><strong>Short-lived Tokens:</strong> Access tokens expire in 1 hour</li>
</ul>

<h2>🎯 Resource Binding</h2>
<p>This server requires the <code>resource</code> parameter in authorization and token requests to bind tokens to specific resources:</p>
<ul>
<li><code><?php echo $baseUrl; ?>/mcp</code> - Main MCP endpoint</li>
<li><code><?php echo $baseUrl; ?></code> - Base server resource</li>
</ul>

<h2>📝 Scopes</h2>
<ul>
<li><code>read</code> - Read access to data</li>
<li><code>write</code> - Write access to data</li>
<li><code>read_data</code> - Read form data and entries</li>
<li><code>write_data</code> - Create and modify form data and entries</li>
<li><code>claudeai</code> - Access to Claude AI integration</li>
</ul>

<h2>🔧 MCP Access</h2>
<div class="endpoint">
<strong>MCP Server:</strong><br>
<code>POST <?php echo $baseUrl; ?>/mcp</code><br>
<small>Include: <code>Authorization: Bearer {access_token}</code></small>
</div>

<h2>⚡ Example Flow</h2>
<pre>
1. GET /.well-known/oauth-protected-resource
2. GET /.well-known/oauth-authorization-server
3. GET /mcp?action=authorize&client_id=...&resource=<?php echo $baseUrl; ?>/mcp&...
4. POST /mcp?action=token (with code_verifier and resource)
5. POST /mcp (with Bearer token)
</pre>

<hr>
<p><small>🏠 <a href="<?php echo $baseUrl; ?>">Return to Formulize</a> | 🔍 <a href="<?php echo $baseUrl; ?>/mcp/health">Server Health</a></small></p>
</body>
</html>
59 changes: 59 additions & 0 deletions .well-known/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php
/**
* Catch-all handler for .well-known OAuth requests
* This handles any OAuth-related requests that might have extra path components
*/

// Get the request URI and clean it up
$requestUri = $_SERVER['REQUEST_URI'];
$parsedUrl = parse_url($requestUri);
$path = $parsedUrl['path'] ?? '';

// Handle CORS for all requests
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
http_response_code(200);
exit;
}

// Remove any trailing /mcp or other path components from well-known URLs
if (strpos($path, '/.well-known/oauth-protected-resource') !== false) {
// Redirect to the correct protected resource metadata endpoint
include dirname(__FILE__) . '/oauth-protected-resource/index.php';
exit;
}

if (strpos($path, '/.well-known/oauth-authorization-server') !== false) {
// Redirect to the correct authorization server metadata endpoint
include dirname(__FILE__) . '/oauth-authorization-server/index.php';
exit;
}

if (strpos($path, '/.well-known/jwks') !== false) {
// Redirect to the JWKS endpoint
include dirname(__FILE__) . '/jwks/index.php';
exit;
}

if (strpos($path, '/.well-known/docs') !== false) {
// Redirect to the documentation endpoint
include dirname(__FILE__) . '/docs/index.php';
exit;
}

// If we get here, it's an unknown .well-known request
http_response_code(404);
header('Content-Type: application/json');
echo json_encode([
'error' => 'not_found',
'error_description' => 'Unknown .well-known endpoint: ' . $path,
'available_endpoints' => [
'/.well-known/oauth-protected-resource',
'/.well-known/oauth-authorization-server',
'/.well-known/jwks',
'/.well-known/docs'
]
]);
?>
37 changes: 37 additions & 0 deletions .well-known/jwks/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php
/**
* JSON Web Key Set (JWKS) endpoint
* Location: /.well-known/jwks
*
* This is optional for Bearer token OAuth but included for completeness
*/

// Handle CORS
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
http_response_code(200);
exit;
}

// Only allow GET requests
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
header('Allow: GET, OPTIONS');
exit;
}

// For now, return empty JWKS since we're using bearer tokens, not JWT
$jwks = [
'keys' => []
];

// Set headers
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Cache-Control: public, max-age=3600'); // Cache for 1 hour

// Output the JWKS
echo json_encode($jwks, JSON_PRETTY_PRINT);
?>
94 changes: 94 additions & 0 deletions .well-known/oauth-authorization-server/index.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* OAuth 2.0 Authorization Server Metadata (RFC 8414)
* Location: /.well-known/oauth-authorization-server
*/

// Handle CORS - set headers before any other output
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
header('Access-Control-Max-Age: 3600');
// Set JSON content type
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-cache, no-store, must-revalidate');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
// Only allow GET requests
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
header('Allow: GET, OPTIONS');
exit;
}
// Get the server's base URL
function getServerBaseUrl() {
$scheme = $_SERVER['REQUEST_SCHEME'] ?? (($_SERVER['HTTPS'] ?? 'off') === 'on' ? 'https' : 'http');
$host = $_SERVER['HTTP_HOST'];
return $scheme . '://' . $host;
}

$baseUrl = getServerBaseUrl();

// RFC 8414 Authorization Server Metadata
$metadata = [
'issuer' => $baseUrl,
'authorization_endpoint' => $baseUrl . '/oauth/authorize',
'token_endpoint' => $baseUrl . '/oauth/token',
'registration_endpoint' => $baseUrl . '/oauth/register',
'scopes_supported' => [
'read',
'write',
'read_data',
'write_data',
'claudeai'
],
'response_types_supported' => [
'code'
],
'grant_types_supported' => [
'authorization_code'
],
'token_endpoint_auth_methods_supported' => [
'none' // Public clients only
],
'code_challenge_methods_supported' => [
'S256',
'plain'
],
'service_documentation' => $baseUrl . '/.well-known/docs',
'ui_locales_supported' => [
'en-US',
'en'
],
'op_policy_uri' => $baseUrl . '/privacy',
'op_tos_uri' => $baseUrl . '/terms',
'authorization_response_iss_parameter_supported' => false,

// RFC 8707 Resource Indicators support
'resource_parameter_supported' => true,

// RFC 7591 Dynamic Client Registration support
'registration_endpoint' => $baseUrl . '/oauth/register',

// PKCE support (required for OAuth 2.1)
'require_pushed_authorization_requests' => false,
'pushed_authorization_request_endpoint' => null,

// Additional security features
'tls_client_certificate_bound_access_tokens' => false,
'dpop_signing_alg_values_supported' => [],

// MCP-specific metadata
'mcp_server_info' => [
'name' => 'Formulize MCP Server',
'version' => '1.0',
'mcp_endpoint' => $baseUrl . '/mcp',
'health_endpoint' => $baseUrl . '/mcp/health',
'capabilities_endpoint' => $baseUrl . '/mcp/capabilities'
]
];

echo json_encode($metadata, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

Loading