├── src ├── auto-client │ ├── .env.example │ ├── package.json │ ├── mcp-api.js │ ├── discovery.js │ ├── index.js │ ├── auth-flow.js │ └── package-lock.json ├── mcp-server │ ├── .env.example │ ├── package.json │ ├── resource-metadata.js │ ├── token-validator.js │ └── index.js ├── client │ ├── package.json │ ├── .env.example │ ├── mcp-api.js │ ├── discovery.js │ ├── index.js │ ├── auth-flow.js │ └── package-lock.json └── shared │ └── config.js ├── docs ├── mcp-oauth-architecture.mermaid ├── mcp-oauth-sequence.mermaid ├── mcp-oauth-sequence-dcr.mermaid ├── dcr-security-recommendations.md ├── architecture-guide.md └── setup-guide.md ├── LICENSE ├── package.json ├── .gitignore ├── scripts ├── cleanup.js └── deploy.js ├── README.md └── infrastructure └── cloudformation └── mcp-oauth-stack.yml /src/auto-client/.env.example: -------------------------------------------------------------------------------- 1 | # Auto Client Configuration 2 | AUTO_CLIENT_PORT=3002 3 | AUTO_CLIENT_URL=http://localhost:3002 4 | 5 | # MCP Server URL 6 | MCP_SERVER_URL=http://localhost:3001 7 | 8 | # Dynamic Client Registration Endpoint 9 | DCR_ENDPOINT=https://your-api-gateway-url.execute-api.region.amazonaws.com/v1/register 10 | -------------------------------------------------------------------------------- /src/mcp-server/.env.example: -------------------------------------------------------------------------------- 1 | # Server Configuration 2 | PORT=3001 3 | BASE_URL=http://localhost:3001 4 | 5 | # Cognito Configuration (for token validation) 6 | COGNITO_REGION=us-east-1 7 | COGNITO_USER_POOL_ID=your-user-pool-id 8 | COGNITO_CLIENT_ID=your-client-id 9 | COGNITO_CLIENT_SECRET=your-client-secret 10 | 11 | # Dynamic Client Registration Endpoint 12 | DCR_ENDPOINT=https://your-api-gateway-url.execute-api.region.amazonaws.com/v1/register 13 | -------------------------------------------------------------------------------- /src/client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito-client", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "Empires Security Labs", 10 | "license": "MIT", 11 | "description": "", 12 | "dependencies": { 13 | "axios": "^1.9.0", 14 | "crypto": "^1.0.1", 15 | "dotenv": "^16.5.0", 16 | "express": "^5.1.0", 17 | "express-session": "^1.18.1", 18 | "open": "^10.1.1" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/mcp-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito-mcp-server", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "Empires Security Labs", 10 | "license": "MIT", 11 | "description": "", 12 | "dependencies": { 13 | "axios": "^1.9.0", 14 | "cors": "^2.8.5", 15 | "dotenv": "^16.5.0", 16 | "express": "^5.1.0", 17 | "jsonwebtoken": "^9.0.2", 18 | "jwk-to-pem": "^2.0.7" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/client/.env.example: -------------------------------------------------------------------------------- 1 | # Client Configuration 2 | PORT=3000 3 | CLIENT_URL=http://localhost:3000 4 | 5 | # MCP Server URL (for discovery) 6 | MCP_SERVER_URL=http://localhost:3001 7 | 8 | # Cognito Configuration (for OAuth flow) 9 | COGNITO_REGION=us-east-1 10 | COGNITO_USER_POOL_ID=your-user-pool-id 11 | COGNITO_CLIENT_ID=your-client-id 12 | COGNITO_CLIENT_SECRET=your-client-secret 13 | COGNITO_DOMAIN=https://your-domain.auth.region.amazoncognito.com 14 | 15 | # OAuth Configuration 16 | OAUTH_REDIRECT_URI=http://localhost:3000/callback 17 | OAUTH_SCOPE=openid profile email 18 | -------------------------------------------------------------------------------- /src/auto-client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito-auto-client", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "Empires Security Labs", 10 | "license": "MIT", 11 | "description": "Auto-discovery client with Dynamic Client Registration for MCP OAuth 2.1", 12 | "dependencies": { 13 | "axios": "^1.9.0", 14 | "crypto": "^1.0.1", 15 | "dotenv": "^16.5.0", 16 | "express": "^5.1.0", 17 | "express-session": "^1.18.1" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/client/mcp-api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const config = require('../shared/config'); 3 | 4 | async function getMcpData(accessToken) { 5 | try { 6 | const response = await axios.get(`${config.mcpServer.baseUrl}/v1/contexts`, { 7 | headers: { 8 | 'Authorization': `Bearer ${accessToken}`, 9 | 'MCP-Protocol-Version': '2025-06-18' 10 | } 11 | }); 12 | 13 | return response.data; 14 | } catch (error) { 15 | console.error('Error calling MCP API:', error.message); 16 | throw error; 17 | } 18 | } 19 | 20 | module.exports = { 21 | getMcpData 22 | }; 23 | -------------------------------------------------------------------------------- /src/auto-client/mcp-api.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const config = require('../shared/config'); 3 | 4 | async function getMcpData(accessToken) { 5 | try { 6 | const response = await axios.get(`${config.mcpServer.baseUrl}/v1/contexts`, { 7 | headers: { 8 | 'Authorization': `Bearer ${accessToken}`, 9 | 'MCP-Protocol-Version': '2025-06-18' 10 | } 11 | }); 12 | 13 | return response.data; 14 | } catch (error) { 15 | console.error('Error calling MCP API:', error.message); 16 | throw error; 17 | } 18 | } 19 | 20 | module.exports = { 21 | getMcpData 22 | }; 23 | -------------------------------------------------------------------------------- /docs/mcp-oauth-architecture.mermaid: -------------------------------------------------------------------------------- 1 | flowchart LR 2 | subgraph subGraph0["User Session"] 3 | A["Client"] 4 | end 5 | subgraph subGraph1["MCP Infrastructure"] 6 | B["MCP Server - Resource Server"] 7 | end 8 | subgraph subGraph2["AWS Infrastructure"] 9 | C["AWS Cognito - Authorization Server"] 10 | end 11 | A -- "1 - No Token Request" --> B 12 | B -- "2- 401 Unauthorized + PRM URL" --> A 13 | A -- "3- Discover Authorization Server" --> C 14 | A -- "4- OAuth Authorization Flow (PKCE)" --> C 15 | C -- "5- Access Token" --> A 16 | A -- "6- Request with Bearer Token" --> B 17 | B -- "7- Return Protected Resource" --> A 18 | -------------------------------------------------------------------------------- /src/mcp-server/resource-metadata.js: -------------------------------------------------------------------------------- 1 | const config = require('../shared/config'); 2 | 3 | function createResourceMetadata() { 4 | // Expose generic OAuth authorization server endpoint 5 | // The server will proxy this to the actual Cognito endpoints 6 | const genericAuthServerUrl = `${config.mcpServer.baseUrl}/.well-known/oauth-authorization-server`; 7 | 8 | return { 9 | resource: config.mcpServer.baseUrl, 10 | authorization_servers: [ 11 | genericAuthServerUrl 12 | ], 13 | bearer_methods_supported: [ 14 | "header" 15 | ], 16 | scopes_supported: [ 17 | "openid", 18 | "profile", 19 | "email", 20 | "mcp-api/read", 21 | "mcp-api/write" 22 | ], 23 | resource_documentation: `${config.mcpServer.baseUrl}/docs` 24 | }; 25 | } 26 | 27 | module.exports = { 28 | createResourceMetadata 29 | }; 30 | -------------------------------------------------------------------------------- /docs/mcp-oauth-sequence.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 3 | participant MCP_Server as MCP Server (Resource Server) 4 | participant AWS_Cognito as AWS Cognito (Authorization Server) 5 | 6 | Client->>MCP_Server: Request without token 7 | MCP_Server-->>Client: 401 Unauthorized + WWW-Authenticate (resource_metadata URL) 8 | 9 | Client->>MCP_Server: GET /.well-known/oauth-protected-resource 10 | MCP_Server-->>Client: Resource Metadata with Authorization Server URL 11 | 12 | Client->>AWS_Cognito: Start OAuth 2.1 Authorization Code Flow (PKCE) 13 | AWS_Cognito-->>Client: Authorization Code (via redirect) 14 | 15 | Client->>AWS_Cognito: Exchange Authorization Code + Code Verifier for Access Token 16 | AWS_Cognito-->>Client: Access Token 17 | 18 | Client->>MCP_Server: Request with Bearer Token 19 | MCP_Server-->>Client: Protected Resource Response -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Empires 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito", 3 | "version": "1.0.0", 4 | "description": "Demo of MCP OAuth2.1 integration with AWS Cognito", 5 | "main": "index.js", 6 | "scripts": { 7 | "install:client": "cd src/client && npm install", 8 | "install:auto-client": "cd src/auto-client && npm install", 9 | "install:server": "cd src/mcp-server && npm install", 10 | "install:all": "npm install && npm run install:client && npm run install:auto-client && npm run install:server", 11 | "deploy": "node scripts/deploy.js", 12 | "cleanup": "node scripts/cleanup.js", 13 | "start:server": "cd src/mcp-server && node index.js", 14 | "start:client": "cd src/client && node index.js", 15 | "start:auto-client": "cd src/auto-client && node index.js", 16 | "dev": "concurrently \"npm run start:server\" \"npm run start:client\" \"npm run start:auto-client\"" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/empires-security/mcp-oauth2-aws-cognito.git" 21 | }, 22 | "keywords": [ 23 | "mcp", 24 | "oauth", 25 | "aws", 26 | "cognito", 27 | "dynamic-client-registration" 28 | ], 29 | "author": "Empires Security Labs", 30 | "license": "MIT", 31 | "bugs": { 32 | "url": "https://github.com/empires-security/mcp-oauth2-aws-cognito/issues" 33 | }, 34 | "homepage": "https://github.com/empires-security/mcp-oauth2-aws-cognito#readme", 35 | "dependencies": { 36 | "@aws-sdk/client-cloudformation": "^3.798.0", 37 | "dotenv": "^16.5.0" 38 | }, 39 | "devDependencies": { 40 | "concurrently": "^9.1.2" 41 | } 42 | } -------------------------------------------------------------------------------- /docs/mcp-oauth-sequence-dcr.mermaid: -------------------------------------------------------------------------------- 1 | sequenceDiagram 2 | participant Client 3 | participant AutoClient as Auto-Discovery Client 4 | participant MCP_Server as MCP Server (RS) 5 | participant API_Gateway as API Gateway (DCR) 6 | participant Lambda as Lambda Functions 7 | participant DynamoDB as DynamoDB Registry 8 | participant AWS_Cognito as AWS Cognito (AS) 9 | 10 | %% Standard OAuth Flow 11 | Client->>MCP_Server: Request without token 12 | MCP_Server-->>Client: 401 Unauthorized + WWW-Authenticate 13 | Client->>MCP_Server: GET /.well-known/oauth-protected-resource 14 | MCP_Server-->>Client: Resource Metadata with AS URL 15 | Client->>AWS_Cognito: Start OAuth 2.1 Flow (PKCE) 16 | AWS_Cognito-->>Client: Authorization Code (via redirect) 17 | Client->>AWS_Cognito: Exchange Code + Code Verifier for Token 18 | AWS_Cognito-->>Client: Access Token 19 | Client->>MCP_Server: Request with Bearer Token 20 | MCP_Server-->>Client: Protected Resource Response 21 | 22 | %% Dynamic Client Registration Flow 23 | AutoClient->>MCP_Server: Request without token 24 | MCP_Server-->>AutoClient: 401 Unauthorized + WWW-Authenticate 25 | AutoClient->>MCP_Server: GET /.well-known/oauth-protected-resource 26 | MCP_Server-->>AutoClient: Resource Metadata with AS URL 27 | 28 | %% DCR-specific steps 29 | AutoClient->>API_Gateway: POST /register (redirect_uris, scopes, etc.) 30 | API_Gateway->>Lambda: Invoke Registration Lambda 31 | Lambda->>AWS_Cognito: CreateUserPoolClient 32 | AWS_Cognito-->>Lambda: Client ID and Secret 33 | Lambda->>DynamoDB: Store Registration Data 34 | Lambda-->>API_Gateway: Registration Response 35 | API_Gateway-->>AutoClient: Client Credentials 36 | 37 | %% Continue with OAuth flow using new credentials 38 | AutoClient->>AWS_Cognito: Start OAuth 2.1 Flow with Dynamic Credentials 39 | AWS_Cognito-->>AutoClient: Authorization Code (via redirect) 40 | AutoClient->>AWS_Cognito: Exchange Code + Code Verifier for Token 41 | AWS_Cognito-->>AutoClient: Access Token 42 | AutoClient->>MCP_Server: Request with Bearer Token 43 | MCP_Server-->>AutoClient: Protected Resource Response 44 | -------------------------------------------------------------------------------- /src/shared/config.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | // Helper function to extract domain prefix 4 | function extractCognitoDomainPrefix(domainUrl) { 5 | // Remove protocol and .auth.region.amazoncognito.com 6 | const urlWithoutProtocol = domainUrl.replace(/^https?:\/\//, ''); 7 | const domainPrefix = urlWithoutProtocol.split('.')[0]; 8 | return domainPrefix; 9 | } 10 | 11 | module.exports = { 12 | // MCP Server Configuration 13 | mcpServer: { 14 | baseUrl: process.env.MCP_SERVER_URL || 'http://localhost:3001', 15 | }, 16 | 17 | // Base URL for the client 18 | baseUrl: process.env.CLIENT_URL || 'http://localhost:3000', 19 | 20 | // Auto Client Base URL 21 | autoClientBaseUrl: process.env.AUTO_CLIENT_URL || 'http://localhost:3002', 22 | 23 | // Dynamic Client Registration endpoint 24 | dcrEndpoint: process.env.DCR_ENDPOINT || 'https://api-gateway-url/v1/register', 25 | 26 | // AWS Cognito Configuration 27 | cognito: { 28 | region: process.env.COGNITO_REGION || 'us-east-1', 29 | userPoolId: process.env.COGNITO_USER_POOL_ID, 30 | clientId: process.env.COGNITO_CLIENT_ID, 31 | 32 | // Extract domain prefix from full domain URL 33 | domain: process.env.COGNITO_DOMAIN 34 | ? extractCognitoDomainPrefix(process.env.COGNITO_DOMAIN) 35 | : '', 36 | 37 | // Full domain URL 38 | domainUrl: `https://${process.env.COGNITO_DOMAIN}`, 39 | 40 | // Construct the issuer URL 41 | issuer: process.env.COGNITO_ISSUER || 42 | `https://cognito-idp.${process.env.COGNITO_REGION || 'us-east-1'}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`, 43 | 44 | // Construct the full authorization server metadata URL 45 | authServerUrl: (() => { 46 | // If COGNITO_AUTH_SERVER_URL is explicitly set, use it 47 | if (process.env.COGNITO_AUTH_SERVER_URL) { 48 | return process.env.COGNITO_AUTH_SERVER_URL; 49 | } 50 | 51 | // Construct the URL manually if not set 52 | const region = process.env.COGNITO_REGION || 'us-east-1'; 53 | const userPoolId = process.env.COGNITO_USER_POOL_ID; 54 | 55 | if (!region || !userPoolId) { 56 | console.warn('Missing Cognito region or user pool ID for constructing auth server URL'); 57 | return ''; 58 | } 59 | 60 | return `https://cognito-idp.${region}.amazonaws.com/${userPoolId}/.well-known/openid-configuration` 61 | })() 62 | }, 63 | 64 | // OAuth Configuration 65 | oauth: { 66 | clientId: process.env.OAUTH_CLIENT_ID || process.env.COGNITO_CLIENT_ID, 67 | clientSecret: process.env.OAUTH_CLIENT_SECRET || process.env.COGNITO_CLIENT_SECRET, 68 | redirectUri: process.env.OAUTH_REDIRECT_URI || 'http://localhost:3000/callback', 69 | scope: process.env.OAUTH_SCOPE || 'openid profile email' 70 | } 71 | }; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # Snowpack dependency directory (https://snowpack.dev/) 46 | web_modules/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional stylelint cache 58 | .stylelintcache 59 | 60 | # Microbundle cache 61 | .rpt2_cache/ 62 | .rts2_cache_cjs/ 63 | .rts2_cache_es/ 64 | .rts2_cache_umd/ 65 | 66 | # Optional REPL history 67 | .node_repl_history 68 | 69 | # Output of 'npm pack' 70 | *.tgz 71 | 72 | # Yarn Integrity file 73 | .yarn-integrity 74 | 75 | # dotenv environment variable files 76 | .env 77 | .env.development.local 78 | .env.test.local 79 | .env.production.local 80 | .env.local 81 | 82 | # parcel-bundler cache (https://parceljs.org/) 83 | .cache 84 | .parcel-cache 85 | 86 | # Next.js build output 87 | .next 88 | out 89 | 90 | # Nuxt.js build / generate output 91 | .nuxt 92 | dist 93 | 94 | # Gatsby files 95 | .cache/ 96 | # Comment in the public line in if your project uses Gatsby and not Next.js 97 | # https://nextjs.org/blog/next-9-1#public-directory-support 98 | # public 99 | 100 | # vuepress build output 101 | .vuepress/dist 102 | 103 | # vuepress v2.x temp and cache directory 104 | .temp 105 | .cache 106 | 107 | # vitepress build output 108 | **/.vitepress/dist 109 | 110 | # vitepress cache directory 111 | **/.vitepress/cache 112 | 113 | # Docusaurus cache and generated files 114 | .docusaurus 115 | 116 | # Serverless directories 117 | .serverless/ 118 | 119 | # FuseBox cache 120 | .fusebox/ 121 | 122 | # DynamoDB Local files 123 | .dynamodb/ 124 | 125 | # TernJS port file 126 | .tern-port 127 | 128 | # Stores VSCode versions used for testing VSCode extensions 129 | .vscode-test 130 | 131 | # yarn v2 132 | .yarn/cache 133 | .yarn/unplugged 134 | .yarn/build-state.yml 135 | .yarn/install-state.gz 136 | .pnp.* 137 | -------------------------------------------------------------------------------- /src/client/discovery.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const config = require('../shared/config'); 3 | 4 | async function discovery(mcpServerUrl) { 5 | try { 6 | console.log(`Making initial request to MCP server: ${mcpServerUrl}`); 7 | await axios.get(`${mcpServerUrl}/v1/contexts`); 8 | } catch (error) { 9 | // Expected 401 error with WWW-Authenticate header 10 | if (error.response && error.response.status === 401) { 11 | const wwwAuthHeader = error.response.headers['www-authenticate']; 12 | 13 | if (!wwwAuthHeader) { 14 | throw new Error('WWW-Authenticate header missing from 401 response'); 15 | } 16 | 17 | // Extract resource_metadata URL from the header 18 | const resourceMetadataMatch = wwwAuthHeader.match(/resource_metadata="([^"]+)"/); 19 | 20 | if (!resourceMetadataMatch) { 21 | throw new Error('resource_metadata not found in WWW-Authenticate header'); 22 | } 23 | 24 | const resourceMetadataUrl = resourceMetadataMatch[1]; 25 | const fullResourceMetadataUrl = resourceMetadataUrl.startsWith('http') 26 | ? resourceMetadataUrl 27 | : `${mcpServerUrl}${resourceMetadataUrl}`; 28 | 29 | console.log(`Discovered resource metadata URL: ${fullResourceMetadataUrl}`); 30 | 31 | // Fetch the resource metadata 32 | const resourceMetadataResponse = await axios.get(fullResourceMetadataUrl); 33 | const resourceMetadata = resourceMetadataResponse.data; 34 | 35 | if (!resourceMetadata.authorization_servers || resourceMetadata.authorization_servers.length === 0) { 36 | throw new Error('No authorization servers found in resource metadata'); 37 | } 38 | 39 | // Use the first authorization server 40 | const authServerUrl = resourceMetadata.authorization_servers[0]; 41 | console.log(`Discovered authorization server: ${authServerUrl}`); 42 | 43 | // Fetch the authorization server metadata dynamically 44 | console.log(`Fetching authorization server metadata from: ${authServerUrl}`); 45 | const authServerMetadataResponse = await axios.get(authServerUrl); 46 | const authServerMetadata = authServerMetadataResponse.data; 47 | 48 | // Validate required fields 49 | if (!authServerMetadata.authorization_endpoint || !authServerMetadata.token_endpoint) { 50 | throw new Error('Invalid authorization server metadata - missing required endpoints'); 51 | } 52 | 53 | console.log('Fetched Authorization Server Metadata:', authServerMetadata); 54 | 55 | return { 56 | resourceMetadata, 57 | authServerMetadata, 58 | authServerUrl 59 | }; 60 | } else { 61 | console.error('Unexpected error during discovery:', error); 62 | throw new Error(`Unexpected response from MCP server: ${error.message}`); 63 | } 64 | } 65 | 66 | throw new Error('Expected 401 response not received from MCP server'); 67 | } 68 | 69 | module.exports = { 70 | discovery 71 | }; -------------------------------------------------------------------------------- /src/auto-client/discovery.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | /** 4 | * Perform MCP Server discovery with auto-discovery of authorization server 5 | * @param {string} mcpServerUrl - The URL of the MCP server 6 | * @returns {Promise} - Information about the authorization server 7 | */ 8 | async function discovery(mcpServerUrl) { 9 | try { 10 | console.log(`Making initial request to MCP server: ${mcpServerUrl}`); 11 | 12 | // Make a request to the MCP server that will return a 401 13 | await axios.get(`${mcpServerUrl}/v1/contexts`); 14 | 15 | // If we get here, something is wrong - we expected a 401 16 | throw new Error('Expected 401 response not received from MCP server'); 17 | } catch (error) { 18 | // Expected 401 error with WWW-Authenticate header 19 | if (error.response && error.response.status === 401) { 20 | const wwwAuthHeader = error.response.headers['www-authenticate']; 21 | 22 | if (!wwwAuthHeader) { 23 | throw new Error('WWW-Authenticate header missing from 401 response'); 24 | } 25 | 26 | console.log('Received WWW-Authenticate header:', wwwAuthHeader); 27 | 28 | // Extract resource_metadata URL from the header 29 | const resourceMetadataMatch = wwwAuthHeader.match(/resource_metadata="([^"]+)"/); 30 | 31 | if (!resourceMetadataMatch) { 32 | throw new Error('resource_metadata not found in WWW-Authenticate header'); 33 | } 34 | 35 | const resourceMetadataUrl = resourceMetadataMatch[1]; 36 | const fullResourceMetadataUrl = resourceMetadataUrl.startsWith('http') 37 | ? resourceMetadataUrl 38 | : `${mcpServerUrl}${resourceMetadataUrl}`; 39 | 40 | console.log(`Discovered resource metadata URL: ${fullResourceMetadataUrl}`); 41 | 42 | // Fetch the resource metadata 43 | const resourceMetadataResponse = await axios.get(fullResourceMetadataUrl); 44 | const resourceMetadata = resourceMetadataResponse.data; 45 | 46 | if (!resourceMetadata.authorization_servers || resourceMetadata.authorization_servers.length === 0) { 47 | throw new Error('No authorization servers found in resource metadata'); 48 | } 49 | 50 | // Use the first authorization server 51 | const authServerUrl = resourceMetadata.authorization_servers[0]; 52 | console.log(`Discovered authorization server: ${authServerUrl}`); 53 | 54 | // Use the authorization server URL directly for metadata discovery 55 | const authServerMetadataUrl = authServerUrl; 56 | 57 | // Fetch the authorization server metadata 58 | try { 59 | console.log(`Fetching auth server metadata from: ${authServerMetadataUrl}`); 60 | const authServerMetadataResponse = await axios.get(authServerMetadataUrl); 61 | const authServerMetadata = authServerMetadataResponse.data; 62 | 63 | console.log('Authorization Server Metadata:', authServerMetadata); 64 | 65 | return { 66 | resourceMetadata, 67 | authServerMetadata, 68 | authServerUrl 69 | }; 70 | } catch (authServerError) { 71 | console.error('Error fetching authorization server metadata:', authServerError.message); 72 | if (authServerError.response) { 73 | console.error('Status:', authServerError.response.status); 74 | console.error('Response:', authServerError.response.data); 75 | } 76 | throw new Error(`Failed to fetch authorization server metadata: ${authServerError.message}`); 77 | } 78 | } else { 79 | console.error('Unexpected error during discovery:', error); 80 | throw new Error(`Unexpected response from MCP server: ${error.message}`); 81 | } 82 | } 83 | } 84 | 85 | module.exports = { 86 | discovery 87 | }; -------------------------------------------------------------------------------- /docs/dcr-security-recommendations.md: -------------------------------------------------------------------------------- 1 | # Dynamic Client Registration Security Recommendations 2 | 3 | For production deployments, consider implementing the following security enhancements to the Dynamic Client Registration flow: 4 | 5 | ## 1. Registration Authentication 6 | 7 | ### Initial Access Tokens 8 | - Require pre-authorized tokens for client registration 9 | - Use short-lived tokens specific to registration 10 | - Implement as API Gateway authorizer 11 | 12 | ```javascript 13 | // Example Lambda authorizer for Initial Access Tokens 14 | exports.handler = async (event) => { 15 | const token = event.headers.Authorization.split(' ')[1]; 16 | 17 | // Validate the token against a secure store 18 | const isValid = await validateInitialAccessToken(token); 19 | 20 | if (!isValid) { 21 | return { 22 | isAuthorized: false, 23 | context: { 24 | error: 'invalid_token' 25 | } 26 | }; 27 | } 28 | 29 | return { 30 | isAuthorized: true, 31 | context: { 32 | // Additional claims or permissions can be included here 33 | } 34 | }; 35 | }; 36 | ``` 37 | 38 | ### Mutual TLS (mTLS) 39 | - Require client certificates for registration 40 | - Configure API Gateway with Custom Domain and TLS 41 | - Validate client certificates during registration 42 | 43 | ## 2. Rate Limiting and Throttling 44 | 45 | ### API Gateway Usage Plans 46 | - Create API keys for registration clients 47 | - Set throttling limits at the API Gateway level 48 | - Monitor and alert on unusual patterns 49 | 50 | ```yaml 51 | # CloudFormation example 52 | UsagePlan: 53 | Type: AWS::ApiGateway::UsagePlan 54 | Properties: 55 | ApiStages: 56 | - ApiId: !Ref ApiGateway 57 | Stage: v1 58 | Throttle: 59 | BurstLimit: 5 60 | RateLimit: 10 61 | Quota: 62 | Limit: 100 63 | Period: DAY 64 | ``` 65 | 66 | ### CloudWatch Alarms 67 | - Set up alarms for high registration rates 68 | - Trigger Lambda functions for automated responses 69 | - Send notifications to security teams 70 | 71 | ## 3. Client Validation 72 | 73 | ### Redirect URI Validation 74 | - Whitelist allowed domains for redirect URIs 75 | - Implement regex pattern matching for URIs 76 | - Reject registration with suspicious redirect URIs 77 | 78 | ```javascript 79 | function validateRedirectURIs(redirectURIs) { 80 | const allowedDomains = [ 81 | 'example\\.com$', 82 | 'trusted-partner\\.org$', 83 | 'localhost' 84 | ]; 85 | 86 | const pattern = new RegExp(`https?:\\/\\/([^\\/]+\\.)*(${allowedDomains.join('|')})(\\/|$)`); 87 | 88 | return redirectURIs.every(uri => pattern.test(uri)); 89 | } 90 | ``` 91 | 92 | ### Software Statement Validation 93 | - Require software statements for registration 94 | - Validate statements against trusted authorities 95 | - Include metadata about the registering client 96 | 97 | ## 4. Scope and Access Restrictions 98 | 99 | ### Limited Default Scopes 100 | - Automatically restrict scopes for dynamically registered clients 101 | - Start with minimal permissions (principle of least privilege) 102 | - Implement a scope elevation process for trusted clients 103 | 104 | ### Registration Approval Workflow 105 | - Store new registrations with 'pending' status 106 | - Require admin approval for full activation 107 | - Implement temporary access with limited capabilities 108 | 109 | ```javascript 110 | // Example Lambda function for client approval 111 | exports.handler = async (event) => { 112 | const { clientId, approved } = JSON.parse(event.body); 113 | 114 | // Update client status in DynamoDB 115 | await updateClientStatus(clientId, approved); 116 | 117 | if (approved) { 118 | // Update Cognito app client with full permissions 119 | await updateCognitoClient(clientId, { 120 | AllowedOAuthScopes: ['openid', 'profile', 'email', 'api/full-access'] 121 | }); 122 | } 123 | 124 | return { 125 | statusCode: 200, 126 | body: JSON.stringify({ 127 | message: `Client ${clientId} ${approved ? 'approved' : 'rejected'}` 128 | }) 129 | }; 130 | }; 131 | ``` 132 | 133 | ## 5. Monitoring and Auditing 134 | 135 | ### CloudTrail Integration 136 | - Enable detailed logging for registration activities 137 | - Track admin approvals and credential generations 138 | - Maintain compliance with regulatory requirements 139 | 140 | ### Client Activity Monitoring 141 | - Track usage patterns of dynamically registered clients 142 | - Detect anomalies in client behavior 143 | - Implement automated response to suspicious activities 144 | -------------------------------------------------------------------------------- /src/mcp-server/token-validator.js: -------------------------------------------------------------------------------- 1 | const jwt = require('jsonwebtoken'); 2 | const jwkToPem = require('jwk-to-pem'); 3 | const axios = require('axios'); 4 | const config = require('../shared/config'); 5 | 6 | // Cache for JWKs and authorization server metadata 7 | let jwksCache = null; 8 | let jwksCacheTime = null; 9 | let authServerMetadataCache = null; 10 | let authServerMetadataCacheTime = null; 11 | const CACHE_DURATION = 3600000; // 1 hour in milliseconds 12 | 13 | async function getAuthServerMetadata() { 14 | // Check if we have cached, non-expired metadata 15 | const now = Date.now(); 16 | if (authServerMetadataCache && authServerMetadataCacheTime && 17 | (now - authServerMetadataCacheTime < CACHE_DURATION)) { 18 | return authServerMetadataCache; 19 | } 20 | 21 | // Fetch authorization server metadata from Cognito 22 | const authServerUrl = config.cognito.authServerUrl; 23 | 24 | try { 25 | const response = await axios.get(authServerUrl); 26 | authServerMetadataCache = response.data; 27 | authServerMetadataCacheTime = now; 28 | return authServerMetadataCache; 29 | } catch (error) { 30 | console.error('Error fetching authorization server metadata:', error); 31 | throw new Error('Unable to fetch authorization server metadata'); 32 | } 33 | } 34 | 35 | async function getJwks() { 36 | // Check if we have a cached, non-expired JWKs 37 | const now = Date.now(); 38 | if (jwksCache && jwksCacheTime && (now - jwksCacheTime < CACHE_DURATION)) { 39 | return jwksCache; 40 | } 41 | 42 | // Get JWKS URL from authorization server metadata 43 | const authServerMetadata = await getAuthServerMetadata(); 44 | const jwksUrl = authServerMetadata.jwks_uri; 45 | 46 | if (!jwksUrl) { 47 | throw new Error('jwks_uri not found in authorization server metadata'); 48 | } 49 | 50 | try { 51 | const response = await axios.get(jwksUrl); 52 | jwksCache = response.data; 53 | jwksCacheTime = now; 54 | return jwksCache; 55 | } catch (error) { 56 | console.error('Error fetching JWKs:', error); 57 | throw new Error('Unable to fetch JWKs'); 58 | } 59 | } 60 | 61 | async function validateToken(token) { 62 | try { 63 | // Decode the token without verification to get the kid (key ID) 64 | const decodedToken = jwt.decode(token, { complete: true }); 65 | 66 | if (!decodedToken) { 67 | throw new Error('Invalid token format'); 68 | } 69 | 70 | //TODO: debug log for dev testing purposes only 71 | //console.log('Decoded Token Payload:', decodedToken.payload); 72 | //console.log('Configured Issuer:', config.cognito.issuer); 73 | //console.log('Configured Client ID:', config.cognito.clientId); 74 | 75 | const { kid } = decodedToken.header; 76 | 77 | // Get the JWKs 78 | const jwks = await getJwks(); 79 | 80 | // Find the key that matches the kid 81 | const key = jwks.keys.find(k => k.kid === kid); 82 | 83 | if (!key) { 84 | throw new Error('Invalid token - key not found'); 85 | } 86 | 87 | // Convert JWK to PEM 88 | const pem = jwkToPem(key); 89 | 90 | // Get issuer from authorization server metadata 91 | const authServerMetadata = await getAuthServerMetadata(); 92 | const expectedIssuer = authServerMetadata.issuer; 93 | 94 | if (!expectedIssuer) { 95 | throw new Error('issuer not found in authorization server metadata'); 96 | } 97 | 98 | // Verify the token with appropriate validation 99 | // Note: Cognito tokens typically have client_id as audience, not the resource server URL 100 | const verifiedToken = jwt.verify(token, pem, { 101 | issuer: expectedIssuer, 102 | // For now, skip audience validation as Cognito uses client_id as audience 103 | // In a full RFC 8707 implementation, we would validate the resource parameter 104 | algorithms: ['RS256'] // Ensure only secure algorithms are accepted 105 | }); 106 | 107 | // Additional manual validation for RFC 8707 Resource Indicators 108 | // Check if token was issued for this specific resource server 109 | if (verifiedToken.aud && Array.isArray(verifiedToken.aud)) { 110 | // Token has multiple audiences - check if our resource is included 111 | console.log('Token audiences:', verifiedToken.aud); 112 | } else if (verifiedToken.aud) { 113 | // Single audience - typically the client_id in Cognito 114 | console.log('Token audience:', verifiedToken.aud); 115 | } 116 | 117 | return verifiedToken; 118 | } catch (error) { 119 | console.error('Token validation error:', error.message); 120 | throw new Error('Token validation failed: ' + error.message); 121 | } 122 | } 123 | 124 | module.exports = { 125 | validateToken 126 | }; -------------------------------------------------------------------------------- /scripts/cleanup.js: -------------------------------------------------------------------------------- 1 | // cleanup.js 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | require('dotenv').config(); 5 | 6 | // Import AWS SDK v3 modules 7 | const { 8 | CloudFormationClient, 9 | DeleteStackCommand, 10 | DescribeStacksCommand, 11 | ListStackResourcesCommand 12 | } = require('@aws-sdk/client-cloudformation'); 13 | 14 | // Configuration 15 | const STACK_NAME = process.env.STACK_NAME || 'mcp-oauth-demo'; 16 | const REGION = process.env.AWS_REGION || 'us-east-1'; 17 | 18 | // Initialize CloudFormation client 19 | const cfn = new CloudFormationClient({ region: REGION }); 20 | 21 | async function cleanupResources() { 22 | try { 23 | console.log(`Starting cleanup for stack: ${STACK_NAME}`); 24 | 25 | // Check if stack exists 26 | try { 27 | const describeCommand = new DescribeStacksCommand({ StackName: STACK_NAME }); 28 | await cfn.send(describeCommand); 29 | 30 | // List resources for information purposes 31 | console.log('Listing resources that will be deleted:'); 32 | try { 33 | const listResourcesCommand = new ListStackResourcesCommand({ StackName: STACK_NAME }); 34 | const resources = await cfn.send(listResourcesCommand); 35 | 36 | if (resources.StackResourceSummaries && resources.StackResourceSummaries.length > 0) { 37 | resources.StackResourceSummaries.forEach(resource => { 38 | console.log(`- ${resource.ResourceType}: ${resource.PhysicalResourceId}`); 39 | }); 40 | } else { 41 | console.log('No resources found in stack.'); 42 | } 43 | } catch (listError) { 44 | console.warn('Could not list stack resources:', listError.message); 45 | } 46 | 47 | // Confirm deletion 48 | if (process.env.SKIP_CONFIRMATION !== 'true') { 49 | const readline = require('readline').createInterface({ 50 | input: process.stdin, 51 | output: process.stdout 52 | }); 53 | 54 | await new Promise((resolve) => { 55 | readline.question(`Are you sure you want to delete stack "${STACK_NAME}" and all its resources? (y/N): `, (answer) => { 56 | readline.close(); 57 | if (answer.toLowerCase() !== 'y') { 58 | console.log('Cleanup cancelled.'); 59 | process.exit(0); 60 | } 61 | resolve(); 62 | }); 63 | }); 64 | } 65 | 66 | // Delete the stack 67 | console.log(`Deleting stack: ${STACK_NAME}`); 68 | const deleteCommand = new DeleteStackCommand({ StackName: STACK_NAME }); 69 | await cfn.send(deleteCommand); 70 | 71 | console.log('Stack deletion initiated. This process may take several minutes.'); 72 | console.log('You can check the status in the AWS CloudFormation console.'); 73 | 74 | console.log('Waiting for stack deletion to complete...'); 75 | 76 | // Poll for stack deletion status 77 | let deleted = false; 78 | let attempts = 0; 79 | const maxAttempts = 30; // 30 attempts, 10 seconds each = 5 minutes max wait time 80 | 81 | while (!deleted && attempts < maxAttempts) { 82 | try { 83 | await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10 seconds between checks 84 | await cfn.send(new DescribeStacksCommand({ StackName: STACK_NAME })); 85 | console.log(`Stack deletion in progress... (${attempts + 1}/${maxAttempts})`); 86 | attempts++; 87 | } catch (error) { 88 | // If we get an error saying the stack doesn't exist, it's been successfully deleted 89 | if (error.name === 'ValidationError' && error.message.includes('does not exist')) { 90 | deleted = true; 91 | console.log('Stack deletion completed successfully!'); 92 | } else { 93 | throw error; 94 | } 95 | } 96 | } 97 | 98 | if (!deleted) { 99 | console.log('Stack deletion is taking longer than expected.'); 100 | console.log('The deletion will continue in the background.'); 101 | console.log('You can check the status in the AWS CloudFormation console.'); 102 | } 103 | 104 | } catch (error) { 105 | // If stack doesn't exist 106 | if (error.name === 'ValidationError' && error.message.includes('does not exist')) { 107 | console.log(`Stack "${STACK_NAME}" does not exist. Nothing to clean up.`); 108 | } else { 109 | throw error; 110 | } 111 | } 112 | 113 | console.log('Cleanup process completed.'); 114 | 115 | } catch (error) { 116 | console.error('Error during cleanup:', error); 117 | process.exit(1); 118 | } 119 | } 120 | 121 | // Run the cleanup 122 | cleanupResources().catch(console.error); -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const session = require('express-session'); 3 | const { discovery } = require('./discovery'); 4 | const { initiateAuthFlow, handleCallback, refreshToken } = require('./auth-flow'); 5 | const { getMcpData } = require('./mcp-api'); 6 | const config = require('../shared/config'); 7 | 8 | const app = express(); 9 | const PORT = process.env.PORT || 3000; 10 | 11 | // Middleware 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: true })); 14 | app.use(session({ 15 | secret: 'mcp-oauth-demo-secret', 16 | resave: false, 17 | saveUninitialized: true, 18 | cookie: { secure: false } // Set to true in production with HTTPS 19 | })); 20 | 21 | // Home page 22 | app.get('/', (req, res) => { 23 | const isAuthenticated = req.session.tokens && req.session.tokens.access_token; 24 | 25 | res.send(` 26 |

MCP OAuth 2.1 Demo Client

27 | ${isAuthenticated 28 | ? '

Status: Authenticated

' 29 | : '

Status: Not authenticated

'} 30 |
31 | ${isAuthenticated 32 | ? '' 33 | : ''} 34 | ${isAuthenticated 35 | ? '' 36 | : ''} 37 |
38 | `); 39 | }); 40 | 41 | // Initiate login 42 | app.get('/login', async (req, res) => { 43 | try { 44 | // Start the discovery process 45 | const authServerInfo = await discovery(config.mcpServer.baseUrl); 46 | 47 | // Store discovered info in session 48 | req.session.authServerInfo = authServerInfo; 49 | 50 | // Initiate the authentication flow 51 | const authUrl = await initiateAuthFlow(authServerInfo); 52 | 53 | // Redirect to the authorization server 54 | res.redirect(authUrl); 55 | } catch (error) { 56 | console.error('Error starting auth flow:', error); 57 | res.status(500).send(`Error starting authentication: ${error.message}`); 58 | } 59 | }); 60 | 61 | // OAuth callback handler 62 | app.get('/callback', async (req, res) => { 63 | const { code } = req.query; 64 | const { authServerInfo } = req.session; 65 | 66 | if (!code || !authServerInfo) { 67 | return res.status(400).send('Missing authorization code or server info'); 68 | } 69 | 70 | try { 71 | // Exchange code for tokens 72 | const tokens = await handleCallback(code, authServerInfo); 73 | 74 | // Store tokens in session 75 | req.session.tokens = tokens; 76 | 77 | // Redirect to home page 78 | res.redirect('/'); 79 | } catch (error) { 80 | console.error('Error handling callback:', error); 81 | res.status(500).send(`Error exchanging code for token: ${error.message}`); 82 | } 83 | }); 84 | 85 | app.get('/.well-known/oauth-protected-resource', (req, res) => { 86 | res.json({ 87 | resource: config.baseUrl, 88 | authorization_servers: [ 89 | config.cognito.authServerUrl 90 | ], 91 | bearer_methods_supported: ["header"], 92 | scopes_supported: [ 93 | "openid", 94 | "profile", 95 | "email", 96 | "mcp-api/read", 97 | "mcp-api/write" 98 | ], 99 | resource_documentation: `${config.baseUrl}/docs` 100 | }); 101 | }); 102 | 103 | // Fetch MCP data 104 | app.get('/mcp-data', async (req, res) => { 105 | const { tokens } = req.session; 106 | 107 | if (!tokens || !tokens.access_token) { 108 | return res.redirect('/login'); 109 | } 110 | 111 | try { 112 | // Check if token is expired and refresh if needed 113 | if (tokens.expires_at && Date.now() > tokens.expires_at) { 114 | const newTokens = await refreshToken(tokens.refresh_token, req.session.authServerInfo); 115 | req.session.tokens = newTokens; 116 | } 117 | 118 | // Make API call to MCP server 119 | const mcpData = await getMcpData(tokens.access_token); 120 | 121 | res.send(` 122 |

MCP Data

123 |
${JSON.stringify(mcpData, null, 2)}
124 | 125 | `); 126 | } catch (error) { 127 | console.error('Error fetching MCP data:', error); 128 | 129 | if (error.response && error.response.status === 401) { 130 | // Token is invalid, redirect to login 131 | return res.redirect('/login'); 132 | } 133 | 134 | res.status(500).send(`Error fetching MCP data: ${error.message}`); 135 | } 136 | }); 137 | 138 | // Logout 139 | app.get('/logout', (req, res) => { 140 | // Clear session 141 | req.session.destroy(); 142 | res.redirect('/'); 143 | }); 144 | 145 | // Start server 146 | app.listen(PORT, () => { 147 | console.log(`MCP Client running on port ${PORT}`); 148 | console.log(`Visit http://localhost:${PORT} to start`); 149 | }); 150 | 151 | -------------------------------------------------------------------------------- /src/client/auth-flow.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const crypto = require('crypto'); 3 | const querystring = require('querystring'); 4 | const config = require('../shared/config'); 5 | 6 | // Generate a code verifier and challenge for PKCE 7 | function generatePkce() { 8 | // Generate a random code verifier 9 | const codeVerifier = crypto.randomBytes(32).toString('base64url'); 10 | 11 | // Generate code challenge using S256 method 12 | const codeChallenge = crypto 13 | .createHash('sha256') 14 | .update(codeVerifier) 15 | .digest('base64url'); 16 | 17 | return { 18 | codeVerifier, 19 | codeChallenge 20 | }; 21 | } 22 | 23 | // Initiate the OAuth authorization flow 24 | async function initiateAuthFlow(authServerInfo) { 25 | const { authServerMetadata } = authServerInfo; 26 | 27 | if (!authServerMetadata.authorization_endpoint) { 28 | throw new Error('Authorization endpoint not found in server metadata'); 29 | } 30 | 31 | // Generate PKCE values 32 | const { codeVerifier, codeChallenge } = generatePkce(); 33 | 34 | // Store code verifier in memory (in a real app, store per user/session) 35 | global.codeVerifier = codeVerifier; 36 | 37 | // Generate a random state value for CSRF protection 38 | const state = crypto.randomBytes(16).toString('hex'); 39 | global.authState = state; 40 | 41 | // Build the authorization URL 42 | const authUrl = new URL(authServerMetadata.authorization_endpoint); 43 | 44 | // Add query parameters 45 | authUrl.searchParams.append('client_id', config.oauth.clientId); 46 | authUrl.searchParams.append('redirect_uri', config.oauth.redirectUri); 47 | authUrl.searchParams.append('response_type', 'code'); 48 | authUrl.searchParams.append('scope', config.oauth.scope); 49 | authUrl.searchParams.append('state', state); 50 | authUrl.searchParams.append('code_challenge', codeChallenge); 51 | authUrl.searchParams.append('code_challenge_method', 'S256'); 52 | // RFC 8707 Resource Indicators - specify intended resource server 53 | authUrl.searchParams.append('resource', config.mcpServer.baseUrl); 54 | 55 | return authUrl.toString(); 56 | } 57 | 58 | // Handle the callback from the authorization server 59 | async function handleCallback(code, authServerInfo) { 60 | const { authServerMetadata } = authServerInfo; 61 | 62 | if (!authServerMetadata.token_endpoint) { 63 | throw new Error('Token endpoint not found in server metadata'); 64 | } 65 | 66 | // Get the code verifier from global (in a real app, retrieve from user session) 67 | const codeVerifier = global.codeVerifier; 68 | 69 | if (!codeVerifier) { 70 | throw new Error('Code verifier not found'); 71 | } 72 | 73 | // Exchange the authorization code for tokens 74 | const tokenResponse = await axios.post( 75 | authServerMetadata.token_endpoint, 76 | querystring.stringify({ 77 | grant_type: 'authorization_code', 78 | client_id: config.oauth.clientId, 79 | client_secret: config.oauth.clientSecret, 80 | redirect_uri: config.oauth.redirectUri, 81 | code, 82 | code_verifier: codeVerifier, 83 | // RFC 8707 Resource Indicators - specify intended resource server 84 | resource: config.mcpServer.baseUrl 85 | }), 86 | { 87 | headers: { 88 | 'Content-Type': 'application/x-www-form-urlencoded' 89 | } 90 | } 91 | ); 92 | 93 | const tokens = tokenResponse.data; 94 | 95 | // Add expiration time if expires_in is provided 96 | if (tokens.expires_in) { 97 | tokens.expires_at = Date.now() + (tokens.expires_in * 1000); 98 | } 99 | 100 | return tokens; 101 | } 102 | 103 | // Refresh an access token using a refresh token 104 | async function refreshToken(refreshToken, authServerInfo) { 105 | const { authServerMetadata } = authServerInfo; 106 | 107 | if (!authServerMetadata.token_endpoint) { 108 | throw new Error('Token endpoint not found in server metadata'); 109 | } 110 | 111 | // Exchange the refresh token for a new access token 112 | const tokenResponse = await axios.post( 113 | authServerMetadata.token_endpoint, 114 | querystring.stringify({ 115 | grant_type: 'refresh_token', 116 | client_id: config.oauth.clientId, 117 | refresh_token: refreshToken 118 | }), 119 | { 120 | headers: { 121 | 'Content-Type': 'application/x-www-form-urlencoded' 122 | } 123 | } 124 | ); 125 | 126 | const tokens = tokenResponse.data; 127 | 128 | // Add expiration time if expires_in is provided 129 | if (tokens.expires_in) { 130 | tokens.expires_at = Date.now() + (tokens.expires_in * 1000); 131 | } 132 | 133 | // Keep the refresh token if a new one wasn't provided 134 | if (!tokens.refresh_token && refreshToken) { 135 | tokens.refresh_token = refreshToken; 136 | } 137 | 138 | return tokens; 139 | } 140 | 141 | module.exports = { 142 | initiateAuthFlow, 143 | handleCallback, 144 | refreshToken 145 | }; 146 | -------------------------------------------------------------------------------- /src/mcp-server/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const cors = require('cors'); 3 | const { createResourceMetadata } = require('./resource-metadata'); 4 | const { validateToken } = require('./token-validator'); 5 | const config = require('../shared/config'); 6 | 7 | const app = express(); 8 | const PORT = process.env.PORT || 3001; 9 | 10 | // Middleware 11 | app.use(cors()); 12 | app.use(express.json()); 13 | 14 | // Log MCP-Protocol-Version header if present 15 | app.use((req, res, next) => { 16 | const mcpVersion = req.headers['mcp-protocol-version']; 17 | if (mcpVersion) { 18 | console.log(`MCP-Protocol-Version: ${mcpVersion}`); 19 | } 20 | next(); 21 | }); 22 | 23 | // Protected Resource Metadata endpoint 24 | app.get('/.well-known/oauth-protected-resource', (req, res) => { 25 | const metadata = createResourceMetadata(); 26 | res.json(metadata); 27 | }); 28 | 29 | // Generic OAuth authorization server metadata endpoint (proxies to Cognito) 30 | app.get('/.well-known/oauth-authorization-server', async (req, res) => { 31 | try { 32 | // Proxy request to the actual Cognito authorization server metadata 33 | const axios = require('axios'); 34 | 35 | // Cognito uses OpenID Connect configuration, not OAuth authorization server endpoint 36 | // Convert the configured auth server URL to the correct OpenID configuration endpoint 37 | let cognitoMetadataUrl = config.cognito.authServerUrl; 38 | 39 | // If the configured URL points to oauth-authorization-server, change it to openid-configuration 40 | if (cognitoMetadataUrl.includes('/.well-known/oauth-authorization-server')) { 41 | cognitoMetadataUrl = cognitoMetadataUrl.replace('/.well-known/oauth-authorization-server', '/.well-known/openid-configuration'); 42 | } 43 | // If it doesn't have any well-known endpoint, add the OpenID configuration one 44 | else if (!cognitoMetadataUrl.includes('/.well-known/')) { 45 | cognitoMetadataUrl = `${cognitoMetadataUrl}/.well-known/openid-configuration`; 46 | } 47 | 48 | console.log(`Proxying authorization server metadata request to: ${cognitoMetadataUrl}`); 49 | const response = await axios.get(cognitoMetadataUrl); 50 | 51 | // Add registration_endpoint to the metadata for RFC 8414 compliance 52 | const metadata = response.data; 53 | metadata.registration_endpoint = process.env.DCR_ENDPOINT; 54 | 55 | // Add PKCE support indication if not already present 56 | // AWS Cognito supports PKCE but doesn't advertise it in metadata 57 | if (!metadata.code_challenge_methods_supported) { 58 | metadata.code_challenge_methods_supported = ['S256']; 59 | } 60 | 61 | res.json(metadata); 62 | } catch (error) { 63 | console.error('Error proxying authorization server metadata:', error.message); 64 | res.status(500).json({ 65 | error: 'server_error', 66 | error_description: 'Unable to retrieve authorization server metadata' 67 | }); 68 | } 69 | }); 70 | 71 | 72 | // Middleware to validate access tokens 73 | const requireAuth = async (req, res, next) => { 74 | const authHeader = req.headers.authorization; 75 | 76 | if (!authHeader || !authHeader.startsWith('Bearer ')) { 77 | return res.status(401).set({ 78 | 'WWW-Authenticate': `Bearer resource_metadata="${config.mcpServer.baseUrl}/.well-known/oauth-protected-resource"` 79 | }).json({ 80 | error: 'unauthorized', 81 | error_description: 'Valid bearer token required' 82 | }); 83 | } 84 | 85 | const token = authHeader.split(' ')[1]; 86 | 87 | try { 88 | const decodedToken = await validateToken(token); 89 | req.user = decodedToken; 90 | next(); 91 | } catch (error) { 92 | console.error('Token validation failed:', error.message); 93 | return res.status(401).set({ 94 | 'WWW-Authenticate': `Bearer resource_metadata="${config.mcpServer.baseUrl}/.well-known/oauth-protected-resource", error="invalid_token", error_description="${error.message}"` 95 | }).json({ 96 | error: 'unauthorized', 97 | error_description: error.message 98 | }); 99 | } 100 | }; 101 | 102 | // MCP API endpoints 103 | app.get('/v1/contexts', requireAuth, (req, res) => { 104 | // This would typically fetch data from a database 105 | const contexts = [ 106 | { 107 | id: 'ctx_123456', 108 | name: 'Default Context', 109 | created_at: new Date().toISOString() 110 | } 111 | ]; 112 | 113 | res.json(contexts); 114 | }); 115 | 116 | app.post('/v1/contexts', requireAuth, (req, res) => { 117 | // Create a new context (in a real app, would save to database) 118 | const newContext = { 119 | id: `ctx_${Date.now()}`, 120 | name: req.body.name || 'New Context', 121 | created_at: new Date().toISOString() 122 | }; 123 | 124 | res.status(201).json(newContext); 125 | }); 126 | 127 | // Start server 128 | app.listen(PORT, () => { 129 | console.log(`MCP Server running on port ${PORT}`); 130 | console.log(`Protected Resource Metadata available at: http://localhost:${PORT}/.well-known/oauth-protected-resource`); 131 | console.log(`Authorization Server Metadata (includes registration_endpoint): http://localhost:${PORT}/.well-known/oauth-authorization-server`); 132 | }); 133 | -------------------------------------------------------------------------------- /src/auto-client/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const session = require('express-session'); 3 | const { discovery } = require('./discovery'); 4 | const { registerClient, initiateAuthFlow, handleCallback, refreshToken } = require('./auth-flow'); 5 | const { getMcpData } = require('./mcp-api'); 6 | const config = require('../shared/config'); 7 | 8 | const app = express(); 9 | const PORT = process.env.AUTO_CLIENT_PORT || 3002; 10 | 11 | // Middleware 12 | app.use(express.json()); 13 | app.use(express.urlencoded({ extended: true })); 14 | app.use(session({ 15 | secret: 'mcp-oauth-auto-client-secret', 16 | resave: false, 17 | saveUninitialized: true, 18 | cookie: { secure: false } // Set to true in production with HTTPS 19 | })); 20 | 21 | // Home page 22 | app.get('/', (req, res) => { 23 | const isAuthenticated = req.session.tokens && req.session.tokens.access_token; 24 | 25 | res.send(` 26 |

MCP OAuth 2.1 Auto-Discovery Client with DCR

27 | ${isAuthenticated 28 | ? '

Status: Authenticated

' 29 | : '

Status: Not authenticated

'} 30 |
31 | ${isAuthenticated 32 | ? '' 33 | : ''} 34 | ${isAuthenticated 35 | ? '' 36 | : ''} 37 |
38 | `); 39 | }); 40 | 41 | // Initiate login with dynamic client registration 42 | app.get('/login', async (req, res) => { 43 | try { 44 | // Start the discovery process 45 | const authServerInfo = await discovery(config.mcpServer.baseUrl); 46 | 47 | // Store discovered info in session 48 | req.session.authServerInfo = authServerInfo; 49 | 50 | // Check if we need to dynamically register a client 51 | if (!req.session.clientInfo) { 52 | console.log('No client registration found. Initiating dynamic client registration...'); 53 | 54 | // Register a client using discovered registration_endpoint 55 | const clientInfo = await registerClient(authServerInfo, authServerInfo.authServerMetadata.registration_endpoint); 56 | 57 | // Store client info in session 58 | req.session.clientInfo = clientInfo; 59 | console.log('Client registered:', clientInfo.client_id); 60 | } 61 | 62 | // Initiate the authentication flow using the registered client 63 | const authUrl = await initiateAuthFlow(authServerInfo, req.session.clientInfo); 64 | 65 | // Redirect to the authorization server 66 | res.redirect(authUrl); 67 | } catch (error) { 68 | console.error('Error starting auth flow:', error); 69 | res.status(500).send(`Error starting authentication: ${error.message}`); 70 | } 71 | }); 72 | 73 | // OAuth callback handler 74 | app.get('/callback', async (req, res) => { 75 | const { code } = req.query; 76 | const { authServerInfo, clientInfo } = req.session; 77 | 78 | if (!code || !authServerInfo || !clientInfo) { 79 | return res.status(400).send('Missing authorization code, server info, or client info'); 80 | } 81 | 82 | try { 83 | // Exchange code for tokens 84 | const tokens = await handleCallback(code, authServerInfo, clientInfo); 85 | 86 | // Store tokens in session 87 | req.session.tokens = tokens; 88 | 89 | // Redirect to home page 90 | res.redirect('/'); 91 | } catch (error) { 92 | console.error('Error handling callback:', error); 93 | res.status(500).send(`Error exchanging code for token: ${error.message}`); 94 | } 95 | }); 96 | 97 | // Fetch MCP data 98 | app.get('/mcp-data', async (req, res) => { 99 | const { tokens, clientInfo, authServerInfo } = req.session; 100 | 101 | if (!tokens || !tokens.access_token || !clientInfo || !authServerInfo) { 102 | return res.redirect('/login'); 103 | } 104 | 105 | try { 106 | // Check if token is expired and refresh if needed 107 | if (tokens.expires_at && Date.now() > tokens.expires_at) { 108 | console.log('Access token expired. Refreshing...'); 109 | const newTokens = await refreshToken(tokens.refresh_token, authServerInfo, clientInfo); 110 | req.session.tokens = newTokens; 111 | } 112 | 113 | // Make API call to MCP server 114 | const mcpData = await getMcpData(tokens.access_token); 115 | 116 | res.send(` 117 |

MCP Data from Auto-Discovery Client

118 |

Client ID: ${clientInfo.client_id}

119 |

Registered At: ${new Date(clientInfo.client_id_issued_at * 1000).toLocaleString()}

120 |
${JSON.stringify(mcpData, null, 2)}
121 | 122 | `); 123 | } catch (error) { 124 | console.error('Error fetching MCP data:', error); 125 | 126 | if (error.response && error.response.status === 401) { 127 | // Token is invalid, redirect to login 128 | return res.redirect('/login'); 129 | } 130 | 131 | res.status(500).send(`Error fetching MCP data: ${error.message}`); 132 | } 133 | }); 134 | 135 | // Logout 136 | app.get('/logout', (req, res) => { 137 | // Clear session 138 | req.session.destroy(); 139 | res.redirect('/'); 140 | }); 141 | 142 | // Display client registration details 143 | app.get('/client-info', (req, res) => { 144 | const { clientInfo } = req.session; 145 | 146 | if (!clientInfo) { 147 | return res.status(404).send('No client registration found. Register a client'); 148 | } 149 | 150 | res.send(` 151 |

Registered Client Details

152 |
${JSON.stringify(clientInfo, null, 2)}
153 | 154 | `); 155 | }); 156 | 157 | // Start server 158 | app.listen(PORT, () => { 159 | console.log(`MCP Auto-Discovery Client running on port ${PORT}`); 160 | console.log(`Visit http://localhost:${PORT} to start`); 161 | }); 162 | -------------------------------------------------------------------------------- /src/auto-client/auth-flow.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | const crypto = require('crypto'); 3 | const querystring = require('querystring'); 4 | 5 | // Generate a code verifier and challenge for PKCE 6 | function generatePkce() { 7 | // Generate a random code verifier 8 | const codeVerifier = crypto.randomBytes(32).toString('base64url'); 9 | 10 | // Generate code challenge using S256 method 11 | const codeChallenge = crypto 12 | .createHash('sha256') 13 | .update(codeVerifier) 14 | .digest('base64url'); 15 | 16 | return { 17 | codeVerifier, 18 | codeChallenge 19 | }; 20 | } 21 | 22 | // Register a client dynamically 23 | async function registerClient(authServerInfo, dcrEndpoint) { 24 | try { 25 | console.log('Registering client with DCR endpoint:', dcrEndpoint); 26 | 27 | // Create client registration request 28 | const registrationRequest = { 29 | redirect_uris: ['http://localhost:3002/callback'], 30 | client_name: 'mcp-oauth-demo-auto-discovery-client', 31 | scope: 'openid profile email mcp-api/read' 32 | }; 33 | 34 | // Send registration request 35 | const response = await axios.post(dcrEndpoint, registrationRequest, { 36 | headers: { 37 | 'Content-Type': 'application/json' 38 | } 39 | }); 40 | 41 | return response.data; 42 | } catch (error) { 43 | console.error('Error registering client:', error.message); 44 | if (error.response) { 45 | console.error('Response data:', error.response.data); 46 | } 47 | throw new Error(`Client registration failed: ${error.message}`); 48 | } 49 | } 50 | 51 | // Initiate the OAuth authorization flow 52 | async function initiateAuthFlow(authServerInfo, clientInfo) { 53 | const { authServerMetadata } = authServerInfo; 54 | 55 | if (!authServerMetadata.authorization_endpoint) { 56 | throw new Error('Authorization endpoint not found in server metadata'); 57 | } 58 | 59 | // Generate PKCE values 60 | const { codeVerifier, codeChallenge } = generatePkce(); 61 | 62 | // Store code verifier in memory (in a real app, store per user/session) 63 | global.codeVerifier = codeVerifier; 64 | 65 | // Generate a random state value for CSRF protection 66 | const state = crypto.randomBytes(16).toString('hex'); 67 | global.authState = state; 68 | 69 | // Build the authorization URL 70 | const authUrl = new URL(authServerMetadata.authorization_endpoint); 71 | 72 | // Add query parameters 73 | authUrl.searchParams.append('client_id', clientInfo.client_id); 74 | authUrl.searchParams.append('redirect_uri', clientInfo.redirect_uris[0]); 75 | authUrl.searchParams.append('response_type', 'code'); 76 | authUrl.searchParams.append('scope', clientInfo.scope || 'openid profile email'); 77 | authUrl.searchParams.append('state', state); 78 | authUrl.searchParams.append('code_challenge', codeChallenge); 79 | authUrl.searchParams.append('code_challenge_method', 'S256'); 80 | // RFC 8707 Resource Indicators - specify intended resource server 81 | const config = require('../shared/config'); 82 | authUrl.searchParams.append('resource', config.mcpServer.baseUrl); 83 | 84 | return authUrl.toString(); 85 | } 86 | 87 | // Handle the callback from the authorization server 88 | async function handleCallback(code, authServerInfo, clientInfo) { 89 | const { authServerMetadata } = authServerInfo; 90 | 91 | if (!authServerMetadata.token_endpoint) { 92 | throw new Error('Token endpoint not found in server metadata'); 93 | } 94 | 95 | // Get the code verifier from global (in a real app, retrieve from user session) 96 | const codeVerifier = global.codeVerifier; 97 | 98 | if (!codeVerifier) { 99 | throw new Error('Code verifier not found'); 100 | } 101 | 102 | // Exchange the authorization code for tokens 103 | const config = require('../shared/config'); 104 | const tokenResponse = await axios.post( 105 | authServerMetadata.token_endpoint, 106 | querystring.stringify({ 107 | grant_type: 'authorization_code', 108 | client_id: clientInfo.client_id, 109 | client_secret: clientInfo.client_secret, 110 | redirect_uri: clientInfo.redirect_uris[0], 111 | code, 112 | code_verifier: codeVerifier, 113 | // RFC 8707 Resource Indicators - specify intended resource server 114 | resource: config.mcpServer.baseUrl 115 | }), 116 | { 117 | headers: { 118 | 'Content-Type': 'application/x-www-form-urlencoded' 119 | } 120 | } 121 | ); 122 | 123 | const tokens = tokenResponse.data; 124 | 125 | // Add expiration time if expires_in is provided 126 | if (tokens.expires_in) { 127 | tokens.expires_at = Date.now() + (tokens.expires_in * 1000); 128 | } 129 | 130 | return tokens; 131 | } 132 | 133 | // Refresh an access token using a refresh token 134 | async function refreshToken(refreshToken, authServerInfo, clientInfo) { 135 | const { authServerMetadata } = authServerInfo; 136 | 137 | if (!authServerMetadata.token_endpoint) { 138 | throw new Error('Token endpoint not found in server metadata'); 139 | } 140 | 141 | // Exchange the refresh token for a new access token 142 | const tokenResponse = await axios.post( 143 | authServerMetadata.token_endpoint, 144 | querystring.stringify({ 145 | grant_type: 'refresh_token', 146 | client_id: clientInfo.client_id, 147 | client_secret: clientInfo.client_secret, 148 | refresh_token: refreshToken 149 | }), 150 | { 151 | headers: { 152 | 'Content-Type': 'application/x-www-form-urlencoded' 153 | } 154 | } 155 | ); 156 | 157 | const tokens = tokenResponse.data; 158 | 159 | // Add expiration time if expires_in is provided 160 | if (tokens.expires_in) { 161 | tokens.expires_at = Date.now() + (tokens.expires_in * 1000); 162 | } 163 | 164 | // Keep the refresh token if a new one wasn't provided 165 | if (!tokens.refresh_token && refreshToken) { 166 | tokens.refresh_token = refreshToken; 167 | } 168 | 169 | return tokens; 170 | } 171 | 172 | module.exports = { 173 | registerClient, 174 | initiateAuthFlow, 175 | handleCallback, 176 | refreshToken 177 | }; -------------------------------------------------------------------------------- /scripts/deploy.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs"); 2 | const path = require("path"); 3 | require("dotenv").config(); 4 | 5 | // Import AWS SDK v3 modules 6 | const { 7 | CloudFormationClient, 8 | CreateStackCommand, 9 | UpdateStackCommand, 10 | DescribeStacksCommand, 11 | waitUntilStackCreateComplete, 12 | waitUntilStackUpdateComplete, 13 | } = require("@aws-sdk/client-cloudformation"); 14 | 15 | // Configuration 16 | const STACK_NAME = process.env.STACK_NAME || "mcp-oauth-demo"; 17 | const REGION = process.env.AWS_REGION || "us-east-1"; 18 | const TEMPLATE_FILE = path.join( 19 | __dirname, 20 | "..", 21 | "infrastructure", 22 | "cloudformation", 23 | "mcp-oauth-stack.yml" 24 | ); 25 | 26 | // Initialize CloudFormation client 27 | const cfn = new CloudFormationClient({ region: REGION }); 28 | 29 | async function deployStack() { 30 | try { 31 | console.log("Starting MCP OAuth + DCR CloudFormation deployment..."); 32 | 33 | // Read the CloudFormation template 34 | const templateBody = fs.readFileSync(TEMPLATE_FILE, "utf8"); 35 | 36 | // Check if stack already exists 37 | let stackExists = false; 38 | try { 39 | const describeCommand = new DescribeStacksCommand({ 40 | StackName: STACK_NAME, 41 | }); 42 | await cfn.send(describeCommand); 43 | stackExists = true; 44 | console.log(`Stack "${STACK_NAME}" already exists. Will update...`); 45 | } catch (error) { 46 | // Stack doesn't exist, will create new 47 | console.log(`Stack "${STACK_NAME}" doesn't exist. Will create new...`); 48 | } 49 | 50 | // Create or update the stack 51 | const params = { 52 | StackName: STACK_NAME, 53 | TemplateBody: templateBody, 54 | Capabilities: ["CAPABILITY_IAM"], 55 | Parameters: [ 56 | { 57 | ParameterKey: "ProjectName", 58 | ParameterValue: process.env.PROJECT_NAME || "mcp-oauth-demo", 59 | }, 60 | { 61 | ParameterKey: "ClientCallbackUrl", 62 | ParameterValue: 63 | process.env.CLIENT_CALLBACK_URL || "http://localhost:3000/callback", 64 | }, 65 | { 66 | ParameterKey: "AutoClientCallbackUrl", 67 | ParameterValue: 68 | process.env.AUTO_CLIENT_CALLBACK_URL || 69 | "http://localhost:3002/callback", 70 | }, 71 | { 72 | ParameterKey: "LogoutUrl", 73 | ParameterValue: 74 | process.env.LOGOUT_URL || "http://localhost:3000/logout", 75 | }, 76 | ], 77 | }; 78 | 79 | // Create or update the stack 80 | if (stackExists) { 81 | console.log("Updating stack..."); 82 | try { 83 | const updateCommand = new UpdateStackCommand(params); 84 | await cfn.send(updateCommand); 85 | console.log("Waiting for stack update to complete..."); 86 | await waitUntilStackUpdateComplete( 87 | { client: cfn, maxWaitTime: 600 }, 88 | { StackName: STACK_NAME } 89 | ); 90 | console.log("Stack update completed successfully!"); 91 | } catch (error) { 92 | if (error.message.includes("No updates are to be performed")) { 93 | console.log("No updates are needed for the stack."); 94 | } else { 95 | throw error; 96 | } 97 | } 98 | } else { 99 | console.log("Creating stack..."); 100 | const createCommand = new CreateStackCommand(params); 101 | await cfn.send(createCommand); 102 | console.log("Waiting for stack creation to complete..."); 103 | 104 | try { 105 | await waitUntilStackCreateComplete( 106 | { client: cfn, maxWaitTime: 600 }, 107 | { StackName: STACK_NAME } 108 | ); 109 | console.log("Stack creation completed successfully!"); 110 | } catch (error) { 111 | console.error("Stack creation failed!"); 112 | // Get detailed error information 113 | const describeCommand = new DescribeStacksCommand({ 114 | StackName: STACK_NAME, 115 | }); 116 | try { 117 | const stackInfo = await cfn.send(describeCommand); 118 | const stack = stackInfo.Stacks[0]; 119 | console.error(`Stack status: ${stack.StackStatus}`); 120 | console.error(`Stack status reason: ${stack.StackStatusReason}`); 121 | } catch (describeError) { 122 | console.error( 123 | "Could not get detailed error information:", 124 | describeError 125 | ); 126 | } 127 | throw new Error( 128 | "Stack creation failed. Check AWS CloudFormation console for details." 129 | ); 130 | } 131 | } 132 | 133 | // Get stack outputs 134 | console.log("Getting stack outputs..."); 135 | const describeCommand = new DescribeStacksCommand({ 136 | StackName: STACK_NAME, 137 | }); 138 | const stackInfo = await cfn.send(describeCommand); 139 | const outputs = stackInfo.Stacks[0].Outputs; 140 | 141 | // Format and display outputs 142 | console.log("\nStack deployment successful! Here are the details:"); 143 | console.log("================================================="); 144 | 145 | const outputMap = {}; 146 | outputs.forEach((output) => { 147 | console.log(`${output.OutputKey}: ${output.OutputValue}`); 148 | outputMap[output.OutputKey] = output.OutputValue; 149 | }); 150 | 151 | // Create .env files for all components 152 | createEnvFiles(outputMap); 153 | 154 | console.log("\nDeployment complete! Environment files have been created."); 155 | } catch (error) { 156 | console.error("Error deploying stack:", error); 157 | process.exit(1); 158 | } 159 | } 160 | 161 | function createEnvFiles(outputs) { 162 | // Get values from outputs 163 | const userPoolId = outputs.UserPoolId; 164 | const clientId = outputs.UserPoolClientId; 165 | const userPoolDomain = outputs.UserPoolDomain; 166 | const dcrEndpoint = outputs.RegisterClientEndpoint; 167 | 168 | // Base path 169 | const basePath = path.dirname(__dirname); 170 | 171 | // Create client .env file 172 | const clientEnv = `# Client Configuration 173 | PORT=3000 174 | CLIENT_URL=http://localhost:3000 175 | 176 | # MCP Server URL 177 | MCP_SERVER_URL=http://localhost:3001 178 | 179 | # Cognito Configuration 180 | COGNITO_REGION=${REGION} 181 | COGNITO_USER_POOL_ID=${userPoolId} 182 | COGNITO_CLIENT_ID=${clientId} 183 | COGNITO_CLIENT_SECRET= 184 | COGNITO_DOMAIN=${userPoolDomain} 185 | 186 | # OAuth Configuration 187 | OAUTH_REDIRECT_URI=http://localhost:3000/callback 188 | OAUTH_SCOPE=openid profile email 189 | `; 190 | 191 | // Create auto-client .env file 192 | const autoClientEnv = `# Auto Client Configuration 193 | AUTO_CLIENT_PORT=3002 194 | AUTO_CLIENT_URL=http://localhost:3002 195 | 196 | # MCP Server URL 197 | MCP_SERVER_URL=http://localhost:3001 198 | 199 | # Dynamic Client Registration Endpoint 200 | DCR_ENDPOINT=${dcrEndpoint} 201 | `; 202 | 203 | // Create server .env file 204 | const serverEnv = `# Server Configuration 205 | PORT=3001 206 | BASE_URL=http://localhost:3001 207 | 208 | # Cognito Configuration 209 | COGNITO_REGION=${REGION} 210 | COGNITO_USER_POOL_ID=${userPoolId} 211 | COGNITO_CLIENT_ID=${clientId} 212 | COGNITO_CLIENT_SECRET= 213 | COGNITO_DOMAIN=${userPoolDomain} 214 | 215 | # Dynamic Client Registration Endpoint 216 | DCR_ENDPOINT=${dcrEndpoint} 217 | `; 218 | 219 | // Ensure directories exist 220 | ensureDirectoryExists(path.join(basePath, "src", "client")); 221 | ensureDirectoryExists(path.join(basePath, "src", "auto-client")); 222 | ensureDirectoryExists(path.join(basePath, "src", "mcp-server")); 223 | 224 | // Write the .env files 225 | fs.writeFileSync(path.join(basePath, "src", "client", ".env"), clientEnv); 226 | fs.writeFileSync( 227 | path.join(basePath, "src", "auto-client", ".env"), 228 | autoClientEnv 229 | ); 230 | fs.writeFileSync(path.join(basePath, "src", "mcp-server", ".env"), serverEnv); 231 | 232 | console.log("Created .env files for client, auto-client, and server"); 233 | } 234 | 235 | function ensureDirectoryExists(directory) { 236 | if (!fs.existsSync(directory)) { 237 | fs.mkdirSync(directory, { recursive: true }); 238 | } 239 | } 240 | 241 | // Run the deployment 242 | deployStack().catch(console.error); 243 | -------------------------------------------------------------------------------- /docs/architecture-guide.md: -------------------------------------------------------------------------------- 1 | # MCP OAuth 2.1 Provider-Agnostic Architecture Overview 2 | 3 | This document explains the architecture of the MCP OAuth 2.1 implementation. While this example uses AWS Cognito as the Authorization Server, the implementation is **provider-agnostic** and works with any OAuth 2.1 compliant authorization server. 4 | 5 | ## System Architecture 6 | 7 | The implementation is based on a clean separation between the MCP server (Resource Server) and the Authorization Server as defined in the latest MCP Authorization specification. The key innovation is that all discovery and validation happens dynamically without hardcoded provider-specific logic. 8 | 9 | ## Components 10 | 11 | ### 1. MCP Client (Express.js) 12 | 13 | A Node.js Express application that implements: 14 | - **Dynamic** discovery of Authorization Servers through Protected Resource Metadata 15 | - Provider-agnostic OAuth 2.1 Authorization Code flow with PKCE 16 | - Token management (storage, refresh, etc.) 17 | - Authenticated API calls to the MCP server 18 | 19 | The client follows this process: 20 | 1. Makes initial request to the MCP server 21 | 2. Receives 401 with WWW-Authenticate header pointing to resource metadata 22 | 3. Fetches resource metadata to discover authorization server URL 23 | 4. **Dynamically fetches** authorization server metadata from discovered URL 24 | 5. Initiates OAuth flow using discovered endpoints (with PKCE for security) 25 | 6. Receives and stores tokens 26 | 7. Uses access token for authenticated requests 27 | 28 | ### 2. MCP Server (Resource Server, Express.js) 29 | 30 | Implemented using Express.js: 31 | - Serves the Protected Resource Metadata document at `/.well-known/oauth-protected-resource` 32 | - **Exposes generic OAuth authorization server metadata** at `/.well-known/oauth-authorization-server` (proxies to backing provider) 33 | - **Dynamically validates access tokens** using discovered JWKS URIs and issuer information 34 | - Returns 401 with appropriate WWW-Authenticate header for unauthenticated requests 35 | - Provides MCP API endpoints as protected resources 36 | 37 | ### 3. Authorization Server (AWS Cognito in this example) 38 | 39 | Serves as the OAuth 2.1 Authorization Server (any compliant provider can be used): 40 | - Manages user authentication and authorization 41 | - Issues access tokens and refresh tokens 42 | - Provides JWT tokens with proper claims 43 | - Supports authorization code flow with PKCE 44 | - **Note**: While Cognito is used as an example, any OAuth 2.1 compliant authorization server can be substituted 45 | 46 | ### 4. Auto-Discovery Client (Express.js) 47 | 48 | A variant of the MCP Client that adds: 49 | - Support for OAuth 2.1 Dynamic Client Registration 50 | - **Complete auto-discovery flow** without any pre-configuration 51 | - Registration with DCR endpoint (API Gateway in this example) 52 | - OAuth flow using dynamically obtained credentials 53 | - **Fully provider-agnostic** - works with any OAuth server that supports DCR 54 | 55 | ### 5. Dynamic Client Registration API (API Gateway + Lambda) 56 | 57 | Provides the backend for Dynamic Client Registration: 58 | - API Gateway endpoints for registering clients 59 | - Lambda functions to create Cognito app clients 60 | - DynamoDB table for storing registration information 61 | - Serverless architecture for scalable client registration 62 | 63 | ### Cognito and Dynamic Client Registration 64 | 65 | While the MCP specification recommends supporting Dynamic Client Registration (DCR), AWS Cognito does not natively implement the OAuth 2.0 DCR protocol (RFC7591). To address this limitation, our implementation: 66 | 67 | 1. Deploys a custom DCR endpoint using API Gateway 68 | 2. Implements the registration logic using Lambda functions 69 | 3. Creates Cognito app clients programmatically via the AWS SDK 70 | 4. Stores registration data in DynamoDB for persistence 71 | 72 | This architecture allows our solution to offer the seamless client onboarding benefits of DCR while still leveraging Cognito's robust authentication capabilities. In production environments, you might consider adding security mechanisms to this DCR implementation as outlined in our [DCR Security Recommendations](./docs/dcr-security-recommendations.md). 73 | 74 | 75 | ## Authentication Flow 76 | 77 | The complete OAuth 2.1 flow in this implementation works as follows: 78 | 79 | 1. **Initial Request & Discovery** 80 | - Client makes a request to the MCP server without authentication 81 | - Server responds with 401 and WWW-Authenticate header 82 | - Client extracts the resource metadata URL from the header 83 | - Client fetches the Protected Resource Metadata document 84 | - Client discovers the authorization server URL from the metadata 85 | 86 | 2. **Authorization** 87 | - Client generates PKCE code verifier and challenge 88 | - Client redirects user to Cognito authorization endpoint 89 | - User authenticates with Cognito 90 | - Cognito redirects back to client with authorization code 91 | 92 | 3. **Token Exchange** 93 | - Client exchanges authorization code + code verifier for tokens 94 | - Cognito issues access token, ID token, and refresh token 95 | - Client stores tokens securely 96 | 97 | 4. **Authenticated Requests** 98 | - Client includes access token in Authorization header 99 | - MCP server validates the token with Cognito 100 | - If valid, server processes the request and returns protected resources 101 | - If invalid, server returns 401 with WWW-Authenticate header 102 | 103 | 5. **Token Refresh** 104 | - When access token expires, client uses refresh token 105 | - Client requests new access token from Cognito 106 | - Client updates stored tokens 107 | 108 | ## Dynamic Client Registration Flow 109 | 110 | The Dynamic Client Registration (DCR) process works as follows: 111 | 112 | 1. **Client Initialization** 113 | - Auto-client starts without any pre-configured OAuth credentials 114 | - Only the MCP server URL is known to the client 115 | 116 | 2. **MCP Server Discovery** 117 | - Client makes unauthenticated request to MCP server 118 | - Server responds with 401 and WWW-Authenticate header 119 | - Client discovers Protected Resource Metadata (PRM) 120 | 121 | 3. **Authorization Server Discovery** 122 | - Client extracts authorization server URL from PRM 123 | - Client fetches OpenID Connect configuration from Cognito 124 | 125 | 4. **Dynamic Registration** 126 | - Client sends registration request to DCR endpoint 127 | - Request includes redirect URIs and desired scopes 128 | - Lambda function creates new Cognito app client 129 | - DynamoDB stores the registration details 130 | 131 | 5. **Credential Management** 132 | - Registration response includes client_id and client_secret 133 | - Client stores these credentials in memory/session 134 | - Credentials are used for standard OAuth authorization flow 135 | 136 | 6. **Standard OAuth Flow** 137 | - Using the dynamically obtained credentials, the client 138 | proceeds with the standard OAuth 2.1 authorization flow 139 | 140 | 141 | ## AWS Resource Configuration 142 | 143 | ### Cognito User Pool 144 | - User management and authentication 145 | - OAuth app client with authorization code grant 146 | - Domain for hosted UI 147 | 148 | ### Express.js Server Configuration 149 | - Implements OAuth 2.1 discovery mechanisms 150 | - Serves Protected Resource Metadata 151 | - Validates tokens using Cognito JWT verification 152 | 153 | ### API Gateway 154 | - Provides REST API for Dynamic Client Registration 155 | - Routes registration requests to Lambda functions 156 | - Enables scalable client registration capability 157 | 158 | ### Lambda Functions 159 | - Handle client registration requests 160 | - Create Cognito app clients programmatically 161 | - Store registration information in DynamoDB 162 | 163 | ### DynamoDB 164 | - Stores client registration details 165 | - Provides persistence for DCR relationships 166 | - Enables lookup of client information 167 | 168 | ## Security Considerations 169 | 170 | This implementation follows OAuth 2.1 and MCP security best practices: 171 | 172 | 1. **PKCE (Proof Key for Code Exchange)** 173 | - Protects against authorization code interception 174 | - Required for all authorization code flows 175 | 176 | 2. **Token Validation** 177 | - JWT signature verification 178 | - Audience and issuer validation 179 | - Expiration checking 180 | 181 | 3. **HTTPS Only** 182 | - All endpoints use HTTPS 183 | - Secure token transmission 184 | 185 | 4. **Short-lived Tokens** 186 | - Access tokens have limited lifetime 187 | - Refresh tokens for obtaining new access tokens 188 | 189 | 5. **Proper Authorization** 190 | - Bearer token usage according to RFC6750 191 | - WWW-Authenticate headers following RFC9728 192 | 193 | 6. **Dynamic Client Registration Security** 194 | - This implementation uses anonymous DCR for simplicity 195 | - Production systems should implement additional security: 196 | * Initial access tokens for registration authorization 197 | * Client authentication (mTLS) for secure registration 198 | * Registration policies and approval workflows 199 | * Rate limiting to prevent abuse 200 | 201 | ## Diagrams 202 | - [Architecture Diagram](./mcp-oauth-architecture.mermaid) 203 | - [Sequence Diagram](./mcp-oauth-sequence.mermaid) 204 | -------------------------------------------------------------------------------- /docs/setup-guide.md: -------------------------------------------------------------------------------- 1 | # MCP + OAuth 2.1 + AWS Cognito Setup Guide 2 | 3 | This guide walks through setting up and running the MCP OAuth 2.1 demo with AWS Cognito. The project demonstrates a complete implementation of the Model Context Protocol (MCP) authorization flow using OAuth 2.1 with AWS Cognito as the Authorization Server. 4 | ## Detailed Configuration Steps 5 | 6 | ### Prerequisites 7 | 8 | - AWS Account with IAM permissions 9 | - Node.js 18+ 10 | - AWS CLI configured with credentials 11 | - Basic understanding of OAuth 2.1 and AWS Cognito 12 | 13 | ### Environment Setup 14 | 15 | 1. **AWS Credentials** 16 | - Configure AWS CLI: `aws configure` 17 | - Ensure you have permissions to create: 18 | * Cognito User Pools 19 | * IAM Roles 20 | * API Gateway Resources 21 | * Lambda Functions 22 | * DynamoDB Table 23 | 24 | 2. **Clone the Repository** 25 | 26 | ```bash 27 | git clone https://github.com/empires-security/mcp-oauth2-aws-cognito.git 28 | cd mcp-oauth2-aws-cognito 29 | ``` 30 | 31 | 3. **Node.js Dependencies** 32 | ```bash 33 | npm run install:all 34 | ``` 35 | 36 | 4. **Deploy Cognito Resources** 37 | ```bash 38 | npm run deploy 39 | ``` 40 | 41 | 5. **Environment Configuration** 42 | - Review generated `.env` files in: 43 | * `src/client/.env` 44 | * `src/auto-client/.env` 45 | * `src/mcp-server/.env` 46 | - Compare with `.env.example` files 47 | - Manually verify/update CLIENT_SECRET if needed 48 | 49 | ## Running the Application 50 | 51 | ### 1. Run both Server and Client 52 | ```bash 53 | npm run dev 54 | ``` 55 | 56 | ### 2. MCP Server 57 | 58 | - Run the MCP server locally. 59 | - Ensure it serves a `.well-known/oauth-protected-resource` endpoint with links to Cognito Authorization Server. 60 | 61 | ### 3. Client 62 | 63 | - Run the example client. 64 | - It will: 65 | - Discover the Authorization Server. 66 | - Redirect user for authentication via browser. 67 | - Obtain an access token. 68 | - Access protected MCP server endpoints. 69 | 70 | ## Project Structure 71 | 72 | ``` 73 | /mcp-server 74 | - Minimal protected resource server 75 | - Serves /.well-known/oauth-protected-resource 76 | 77 | /auto-client 78 | - Dynamic client registration example 79 | - Auto-discovers and registers with authorization server 80 | - Handles OAuth 2.1 flow with dynamically obtained credentials 81 | 82 | /client 83 | - Example client to demonstrate the OAuth 2.1 flow 84 | - Handles discovery, PKCE, and token usage 85 | 86 | /docs 87 | - Setup guides and explanations 88 | 89 | /infrastructure 90 | - CloudFormation templates for AWS resources 91 | - API Gateway for Dynamic Client Registration 92 | - Lambda functions for client registration management 93 | - DynamoDB for storing client registrations 94 | ``` 95 | 96 | ### 4. Test the OAuth Flow 97 | 98 | Visit `http://localhost:3000` in your browser and follow these steps: 99 | 100 | 1. Click the "Log in" button 101 | 2. You'll be redirected to the Cognito hosted UI for authentication 102 | 3. Create an account or log in with existing credentials 103 | 4. After successful authentication, you'll be redirected back to the client app 104 | 5. Click "Fetch MCP Data" to make an authenticated request to the MCP server 105 | 106 | ### 5. Auto-Discovery Client with DCR 107 | 108 | Visit `http://localhost:3002` in your browser and follow these steps: 109 | 110 | 1. Click the "Log in" button 111 | 2. The client will: 112 | - Auto-discover the MCP server and authorization endpoints 113 | - Register itself with the DCR endpoint 114 | - Redirect you to Cognito for authentication 115 | 3. After authentication, you'll be redirected back to the auto-client 116 | 4. Click "Fetch MCP Data" to make an authenticated request with the dynamically registered client 117 | 118 | 119 | ## Architecture Overview 120 | - Detailed [Architecture Overview](./architecture-guide.md) 121 | 122 | ### Components 123 | 124 | 1. **MCP Client**: A Node.js Express application that: 125 | - Discovers the authorization server using the Protected Resource Metadata 126 | - Implements OAuth 2.1 authorization code flow with PKCE 127 | - Makes authenticated requests to the MCP server 128 | 129 | 2. **MCP Server**: Implemented as AWS API Gateway + Lambda that: 130 | - Serves the Protected Resource Metadata document 131 | - Validates access tokens from Cognito 132 | - Provides protected API endpoints for MCP operations 133 | 134 | 3. **AWS Cognito**: Acts as the OAuth 2.1 Authorization Server: 135 | - Handles user authentication 136 | - Issues access and refresh tokens 137 | - Provides authorization server metadata 138 | 139 | ### Authorization Flow 140 | 141 | The flow follows the sequence described in the MCP Authorization specification: 142 | 143 | 1. Client makes a request to MCP server without a token 144 | 2. Server responds with 401 + WWW-Authenticate header pointing to resource metadata 145 | 3. Client fetches resource metadata to discover the authorization server 146 | 4. Client redirects user to authorization server (Cognito) for authentication 147 | 5. User authenticates with Cognito and is redirected back with an authorization code 148 | 6. Client exchanges code for tokens using PKCE flow 149 | 7. Client makes authenticated requests to MCP server using access token 150 | 8. Server validates token and allows access to protected resources 151 | 152 | ## Project Structure 153 | 154 | ``` 155 | mcp-oauth2-aws-cognito/ 156 | ├── infrastructure/ # AWS infrastructure code 157 | │ └── cloudformation/ # CloudFormation templates 158 | ├── scripts/ # Deployment scripts 159 | ├── src/ 160 | │ ├── mcp-server/ # MCP server implementation 161 | │ ├── auto-client/ # MCP auto discovery client implementation 162 | │ ├── client/ # MCP client implementation 163 | │ └── shared/ # Shared utilities 164 | └── docs/ # Documentation 165 | ``` 166 | 167 | ## Cleanup 168 | 169 | To delete all AWS resources created for this demo: 170 | 171 | ```bash 172 | npm run cleanup 173 | ``` 174 | 175 | This will remove: 176 | - Cognito User Pool and app client 177 | - Associated IAM roles and policies 178 | 179 | ## Troubleshooting 180 | 181 | ### Common Issues 182 | 183 | 1. MCP + OAuth 2.1. **401 Unauthorized but no WWW-Authenticate header** 184 | - Check that your MCP server implementation is correctly setting the WWW-Authenticate header 185 | - Verify the format follows RFC9728 requirements 186 | 187 | 2. **Token validation failures** 188 | - Ensure the Cognito client app is configured with the correct callback URL 189 | - Check that PKCE is properly implemented on the client side 190 | - Verify the scopes requested match what's configured in Cognito 191 | 192 | 3. **CORS issues** 193 | - If experiencing CORS errors, check the API Gateway CORS configuration 194 | - Ensure OPTIONS requests are properly handled 195 | 196 | 4. **CloudFormation deployment failures** 197 | - Check that your AWS CLI has sufficient permissions 198 | - Look at CloudFormation events for detailed error messages 199 | 200 | 5. **Dynamic Client Registration issues** 201 | - Check API Gateway logs for registration errors 202 | - Verify Lambda functions have proper permissions to create Cognito clients 203 | - Make sure DynamoDB table exists and is accessible 204 | - If using the auto-client, check console logs for detailed discovery and registration errors 205 | 206 | ## Advanced Configuration 207 | 208 | ### Custom Domain 209 | 210 | To use a custom domain instead of the default Cognito and API Gateway URLs: 211 | 212 | 1. Register a domain in AWS Route 53 or your preferred domain registrar 213 | 2. Create an ACM certificate for your domain 214 | 3. Configure a custom domain for Cognito User Pool 215 | 4. Set up a custom domain for API Gateway 216 | 217 | ### Cognito User Pool Configuration 218 | 219 | For additional security or customization: 220 | 221 | 1. Enable MFA for stronger authentication 222 | 2. Customize the Cognito hosted UI with your branding 223 | 3. Add additional identity providers (Google, Facebook, etc.) 224 | 4. Configure custom email or SMS messages 225 | 226 | ### Testing with Multiple Authorization Servers 227 | 228 | The MCP specification supports multiple authorization servers. To test this: 229 | 230 | 1. Set up another OAuth provider (Auth0, Okta, etc.) 231 | 2. Add it to the `authorization_servers` array in your Protected Resource Metadata 232 | 3. Update your client to handle selection between multiple authorization servers 233 | 234 | ### Dynamic Client Registration Security 235 | 236 | For production environments, enhance DCR security with: 237 | 238 | 1. **Initial Access Tokens** 239 | - Require pre-authorized tokens for client registration 240 | - Use API Gateway authorizers to validate tokens 241 | 242 | 2. **Client Authentication** 243 | - Implement mutual TLS (mTLS) for API Gateway endpoints 244 | - Require client certificates for registration requests 245 | 246 | 3. **Registration Policies** 247 | - Implement approval workflows for new clients 248 | - Restrict allowed redirect URIs to trusted domains 249 | - Limit scopes available to dynamically registered clients 250 | 251 | 4. **Rate Limiting** 252 | - Add API Gateway usage plans and API keys 253 | - Implement CloudWatch alarms for unusual registration patterns -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MCP + OAuth2.1 + AWS Cognito Example 2 | 3 | ## Overview 4 | 5 | This repository demonstrates how to secure a Model Context Protocol (MCP) server using OAuth 2.1 authorization flows, implemented entirely with Node.js and Express.js. While this example uses AWS Cognito as the backing authorization server, the implementation is **provider-agnostic** and can work with any OAuth 2.1 compliant authorization server. 6 | 7 | Based on the MCP Authorization Specification (version 2025-06-18), this project showcases: 8 | - MCP server acting as a **Resource Server** (RS) with generic OAuth endpoints 9 | - Provider-agnostic OAuth 2.1 implementation (example uses AWS Cognito) 10 | - OAuth 2.1 Authorization Code Flow with PKCE and RFC 8707 Resource Indicators 11 | - Protected Resource Metadata (PRM) document discovery 12 | - **Fully dynamic** authorization server metadata discovery 13 | - Dynamic Client Registration (DCR) support 14 | - Enhanced security features from MCP 2025-06-18 specification 15 | - Two client implementations: 16 | - Static client with pre-configured credentials 17 | - Auto-discovery client with dynamic registration 18 | 19 | ## Provider-Agnostic Design 20 | 21 | This implementation follows OAuth 2.1 standards to ensure compatibility with any compliant authorization server: 22 | - **MCP Server**: Exposes standard OAuth metadata endpoints and proxies to the backing authorization server 23 | - **Clients**: Discover authorization servers dynamically without hardcoded provider-specific logic 24 | - **Token Validation**: Uses discovered JWKS URIs and issuer information from authorization server metadata 25 | - **Flexible Backend**: While Cognito is used as an example, any OAuth 2.1 server can be substituted 26 | 27 | ## Understanding the New MCP Authorization Spec 28 | 29 | The new MCP Authorization Specification introduces a clean separation between Resource Servers and Authorization Servers, making it easier to integrate with existing identity providers like AWS Cognito, Okta, Auth0, and others. 30 | 31 | Key components of the specification: 32 | 33 | 1. **Protected Resource Metadata (PRM)** document 34 | - The MCP server serves this document at `/.well-known/oauth-protected-resource` 35 | - Contains information about authorization servers, supported scopes, etc. 36 | - Follows RFC9728 (OAuth 2.0 Protected Resource Metadata) 37 | 38 | 2. **Discovery Process** 39 | - When a client receives a 401 Unauthorized response, the WWW-Authenticate header contains a pointer to the PRM document 40 | - Client fetches the PRM document to discover the authorization server URL 41 | - Client fetches authorization server metadata dynamically from the discovered URL (no hardcoded endpoints) 42 | 43 | 3. **OAuth 2.1 Authorization** 44 | - Authorization Code flow with PKCE 45 | - Bearer token usage for authenticated requests 46 | - Dynamic token validation using discovered JWKS URIs and issuer information 47 | 48 | 4. **Dynamic Client Registration (DCR)** 49 | - Allows clients to automatically register with new MCP servers 50 | - Eliminates the need for manual client registration processes 51 | - Enables seamless discovery and connection to new services 52 | - MCP specification strongly recommends implementing DCR since "clients do not know the set of MCP servers in advance" 53 | - Provides a standardized way to obtain OAuth client credentials 54 | - Follows RFC7591 (OAuth 2.0 Dynamic Client Registration Protocol) 55 | 56 | This implementation showcases how to apply these concepts in a provider-agnostic way. The example uses AWS Cognito with custom Dynamic Client Registration through API Gateway endpoints and Lambda functions, but the core OAuth flow works with any compliant authorization server. 57 | 58 | ## Architecture 59 | ``` 60 | Client → MCP Server → Authorization Server (e.g., AWS Cognito) 61 | (Resource Server) (OAuth 2.1 Provider) 62 | ``` 63 | 1. Client sends a request without a token. 64 | 2. MCP server responds with 401 Unauthorized + WWW-Authenticate header pointing to PRM metadata. 65 | 3. Client retrieves PRM, discovers the Authorization Server URL dynamically. 66 | 4. Client fetches authorization server metadata and performs OAuth 2.1 Authorization Code flow (with PKCE). 67 | 5. Client obtains an access token and retries request to MCP server. 68 | 6. MCP server validates token using dynamically discovered JWKS and grants access to the protected resource. 69 | 70 | For detailed overview, see the [Architecture Overview](./docs/architecture-guide.md). 71 | 72 | Diagrams: 73 | - [Architecture Diagram](./docs/mcp-oauth-architecture.mermaid) 74 | - [Sequence Diagram](./docs/mcp-oauth-sequence.mermaid) 75 | - [DCR Sequence Diagram](./docs/mcp-oauth-sequence-dcr.mermaid) 76 | 77 | ## Dynamic Client Registration (DCR) 78 | 79 | This implementation includes support for OAuth 2.1 Dynamic Client Registration, allowing clients to: 80 | 81 | 1. Dynamically discover the MCP server and authorization endpoints 82 | 2. Register themselves with the authorization server 83 | 3. Obtain credentials for the OAuth flow 84 | 85 | The DCR flow works as follows: 86 | 87 | 1. Client discovers the MCP server's protected resource metadata 88 | 2. Client discovers the authorization server (Cognito) 89 | 3. Client registers with the DCR endpoint in API Gateway 90 | 4. Registration creates a Cognito app client and returns credentials 91 | 5. Client uses these credentials for the standard OAuth 2.1 flow 92 | 93 | **Implementation Note:** AWS Cognito does not natively support Dynamic Client Registration as specified in OAuth 2.0 DCR (RFC7591). This implementation bridges this gap by using: 94 | - API Gateway endpoints to provide the DCR API interface 95 | - Lambda functions to create Cognito app clients programmatically 96 | - DynamoDB to store the registration data 97 | 98 | This approach allows us to maintain compliance with the MCP specification's DCR recommendation while leveraging AWS Cognito for robust authentication and authorization. 99 | 100 | **Security Note**: This implementation uses anonymous DCR without additional authentication. For production environments, consider adding: 101 | - Rate limiting on registration requests 102 | - Client authentication (mTLS, initial access tokens) 103 | - Approval workflow for new clients 104 | - Limited scope access for dynamically registered clients 105 | 106 | See our [DCR Security Recommendations](./docs/dcr-security-recommendations.md) to enhance the security of the registration process. 107 | 108 | ## Quick Start 109 | 110 | ### Prerequisites 111 | - Node.js installed 112 | - AWS test account with access to: 113 | - Cognito for Authorization Server (1 user pool, 2 app clients) 114 | - API Gateway / Lambda / DynamoDB for DCR (2 resources, 2 functions, 1 table) 115 | - CloudFormation for deploy (1 stack) 116 | - Basic knowledge of OAuth 2.1 flows 117 | 118 | ### Setup 119 | 1. Clone the repository 120 | ```bash 121 | git clone https://github.com/empires-security/mcp-oauth2-aws-cognito.git 122 | cd mcp-oauth2-aws-cognito 123 | ``` 124 | 125 | 2. Install dependencies for clients and server 126 | ```bash 127 | npm run install:all 128 | ``` 129 | 130 | 3. Deploy AWS resources 131 | ```bash 132 | npm run deploy 133 | ``` 134 | 135 | 4. Review generated `.env` files in: 136 | - `src/client/.env` 137 | - `src/auto-client/.env` 138 | - `src/mcp-server/.env` 139 | - Compare with `.env.example` files 140 | - Manually verify/update CLIENT_SECRET if needed 141 | 142 | ### Running the Application 143 | 1. Start both clients and server 144 | ```bash 145 | npm run dev 146 | ``` 147 | 2. Visit http://localhost:3000 to test the OAuth flow 148 | 149 | 3. Sign Up for a New User 150 | - Click the "Log in" button 151 | - Select "Sign up" in the Cognito hosted UI 152 | - Create a new user account 153 | - Verify your account by entering the confirmation code sent to your email 154 | - After successful verification, you'll be redirected back to the application 155 | 156 | 4. Click the "Fetch MCP Data" button to make an authenticated request to the MCP server 157 | 158 | 5. Visit http://localhost:3002 to test the DCR flow, auto-discovery client with Dynamic Client Registration. 159 | 160 | ### Cleanup 161 | 1. Cleanup AWS resources 162 | ```bash 163 | npm run cleanup 164 | ``` 165 | 166 | For detailed setup instructions, see the [Setup Guide](./docs/setup-guide.md). 167 | 168 | ## Contributing 169 | 170 | Contributions are welcome! Please feel free to submit a Pull Request. 171 | 172 | 1. Fork the repository 173 | 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 174 | 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 175 | 4. Push to the branch (`git push origin feature/amazing-feature`) 176 | 5. Open a Pull Request 177 | 178 | ## References 179 | 180 | - [Model Context Protocol Official Specification](https://modelcontextprotocol.io/specification/draft/basic/authorization) 181 | - [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1-12) 182 | - [OAuth 2.0 Protected Resource Metadata](https://datatracker.ietf.org/doc/rfc9728/) 183 | - [OAuth 2.0 Dynamic Client Registration Protocol](https://datatracker.ietf.org/doc/rfc7591/) 184 | - [AWS Cognito Developer Guide](https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools.html) 185 | 186 | ## License 187 | 188 | This project is licensed under the MIT License - see the LICENSE file for details. 189 | 190 | ## Authors 191 | 192 | - **Empires Security Labs** 🚀 193 | 194 | -------------------------------------------------------------------------------- /infrastructure/cloudformation/mcp-oauth-stack.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Description: 'MCP OAuth 2.1 with Dynamic Client Registration' 3 | 4 | Parameters: 5 | ProjectName: 6 | Type: String 7 | Default: mcp-oauth-demo 8 | Description: Project name used for resource naming 9 | 10 | ClientCallbackUrl: 11 | Type: String 12 | Default: http://localhost:3000/callback 13 | Description: Callback URL for the OAuth client 14 | 15 | AutoClientCallbackUrl: 16 | Type: String 17 | Default: http://localhost:3002/callback 18 | Description: Callback URL for the auto-discovery client 19 | 20 | LogoutUrl: 21 | Type: String 22 | Default: http://localhost:3000/logout 23 | Description: Logout URL for the OAuth flow 24 | 25 | Resources: 26 | # Cognito User Pool 27 | UserPool: 28 | Type: AWS::Cognito::UserPool 29 | Properties: 30 | UserPoolName: !Sub ${ProjectName}-user-pool 31 | AdminCreateUserConfig: 32 | AllowAdminCreateUserOnly: false 33 | EmailConfiguration: 34 | EmailSendingAccount: COGNITO_DEFAULT 35 | MfaConfiguration: 'OFF' 36 | UsernameAttributes: 37 | - email 38 | AutoVerifiedAttributes: 39 | - email 40 | Policies: 41 | PasswordPolicy: 42 | MinimumLength: 8 43 | RequireLowercase: true 44 | RequireUppercase: true 45 | RequireNumbers: false 46 | RequireSymbols: true 47 | Schema: 48 | - Name: email 49 | AttributeDataType: String 50 | Mutable: true 51 | Required: true 52 | - Name: name 53 | AttributeDataType: String 54 | Mutable: true 55 | Required: true 56 | 57 | # User Pool Domain 58 | UserPoolDomain: 59 | Type: AWS::Cognito::UserPoolDomain 60 | Properties: 61 | Domain: !Sub ${ProjectName}-domain-${AWS::AccountId} 62 | UserPoolId: !Ref UserPool 63 | 64 | # Static Client (Pre-configured) 65 | UserPoolClient: 66 | Type: AWS::Cognito::UserPoolClient 67 | Properties: 68 | UserPoolId: !Ref UserPool 69 | ClientName: !Sub ${ProjectName}-static-client 70 | GenerateSecret: true 71 | RefreshTokenValidity: 30 72 | AllowedOAuthFlows: 73 | - code 74 | AllowedOAuthFlowsUserPoolClient: true 75 | AllowedOAuthScopes: 76 | - openid 77 | - profile 78 | - email 79 | CallbackURLs: 80 | - !Ref ClientCallbackUrl 81 | LogoutURLs: 82 | - !Ref LogoutUrl 83 | SupportedIdentityProviders: 84 | - COGNITO 85 | PreventUserExistenceErrors: ENABLED 86 | EnableTokenRevocation: true 87 | ExplicitAuthFlows: 88 | - ALLOW_REFRESH_TOKEN_AUTH 89 | - ALLOW_USER_SRP_AUTH 90 | TokenValidityUnits: 91 | AccessToken: hours 92 | IdToken: hours 93 | RefreshToken: days 94 | AccessTokenValidity: 1 95 | IdTokenValidity: 1 96 | 97 | # Resource Server (API) 98 | ResourceServer: 99 | Type: AWS::Cognito::UserPoolResourceServer 100 | Properties: 101 | UserPoolId: !Ref UserPool 102 | Identifier: mcp-api 103 | Name: MCP API 104 | Scopes: 105 | - ScopeName: read 106 | ScopeDescription: Read access to MCP API 107 | - ScopeName: write 108 | ScopeDescription: Write access to MCP API 109 | 110 | # DynamoDB Table for DCR Clients 111 | DcrClientsTable: 112 | Type: AWS::DynamoDB::Table 113 | Properties: 114 | TableName: !Sub ${ProjectName}-dcr-clients 115 | BillingMode: PAY_PER_REQUEST 116 | AttributeDefinitions: 117 | - AttributeName: client_id 118 | AttributeType: S 119 | KeySchema: 120 | - AttributeName: client_id 121 | KeyType: HASH 122 | 123 | # IAM Role for Lambda Functions 124 | LambdaExecutionRole: 125 | Type: AWS::IAM::Role 126 | Properties: 127 | AssumeRolePolicyDocument: 128 | Version: '2012-10-17' 129 | Statement: 130 | - Effect: Allow 131 | Principal: 132 | Service: lambda.amazonaws.com 133 | Action: sts:AssumeRole 134 | ManagedPolicyArns: 135 | - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole 136 | Policies: 137 | - PolicyName: CognitoAccess 138 | PolicyDocument: 139 | Version: '2012-10-17' 140 | Statement: 141 | - Effect: Allow 142 | Action: 143 | - cognito-idp:CreateUserPoolClient 144 | - cognito-idp:DescribeUserPoolClient 145 | Resource: !GetAtt UserPool.Arn 146 | - PolicyName: DynamoDBAccess 147 | PolicyDocument: 148 | Version: '2012-10-17' 149 | Statement: 150 | - Effect: Allow 151 | Action: 152 | - dynamodb:PutItem 153 | - dynamodb:GetItem 154 | Resource: !GetAtt DcrClientsTable.Arn 155 | 156 | # Lambda Functions 157 | RegisterClientFunction: 158 | Type: AWS::Lambda::Function 159 | Properties: 160 | FunctionName: !Sub ${ProjectName}-register-client 161 | Handler: index.handler 162 | Role: !GetAtt LambdaExecutionRole.Arn 163 | Runtime: nodejs20.x 164 | Timeout: 30 165 | MemorySize: 256 166 | Environment: 167 | Variables: 168 | USER_POOL_ID: !Ref UserPool 169 | DCR_CLIENTS_TABLE: !Ref DcrClientsTable 170 | Code: 171 | ZipFile: | 172 | const { CognitoIdentityProviderClient, CreateUserPoolClientCommand } = require('@aws-sdk/client-cognito-identity-provider'); 173 | const { DynamoDBClient, PutItemCommand } = require('@aws-sdk/client-dynamodb'); 174 | const { marshall } = require('@aws-sdk/util-dynamodb'); 175 | const crypto = require('crypto'); 176 | 177 | // Initialize AWS SDK clients 178 | const cognitoClient = new CognitoIdentityProviderClient(); 179 | const dynamoClient = new DynamoDBClient(); 180 | 181 | // Configuration 182 | const USER_POOL_ID = process.env.USER_POOL_ID; 183 | const DCR_CLIENTS_TABLE = process.env.DCR_CLIENTS_TABLE; 184 | 185 | /** 186 | * Lambda function to handle dynamic client registration 187 | */ 188 | exports.handler = async (event) => { 189 | try { 190 | console.log('Event received:', JSON.stringify(event)); 191 | 192 | // Parse request body 193 | const body = JSON.parse(event.body || '{}'); 194 | 195 | // Validate required fields 196 | if (!body.redirect_uris || !Array.isArray(body.redirect_uris) || body.redirect_uris.length === 0) { 197 | return formatResponse(400, { 198 | error: 'invalid_client_metadata', 199 | error_description: 'redirect_uris is required and must be an array' 200 | }); 201 | } 202 | 203 | // Determine client name 204 | const clientName = body.client_name || `DCR Client ${Date.now()}`; 205 | 206 | // Map requested scopes or use defaults 207 | const allowedScopes = body.scope ? body.scope.split(' ') : ['openid', 'profile', 'email', 'mcp-api/read']; 208 | 209 | // Create client in Cognito 210 | const createClientParams = { 211 | UserPoolId: USER_POOL_ID, 212 | ClientName: clientName, 213 | GenerateSecret: true, 214 | RefreshTokenValidity: 30, // 30 days 215 | AllowedOAuthFlows: ['code'], 216 | AllowedOAuthFlowsUserPoolClient: true, 217 | AllowedOAuthScopes: allowedScopes, 218 | CallbackURLs: body.redirect_uris, 219 | SupportedIdentityProviders: ['COGNITO'], 220 | PreventUserExistenceErrors: 'ENABLED', 221 | TokenValidityUnits: { 222 | AccessToken: 'hours', 223 | IdToken: 'hours', 224 | RefreshToken: 'days' 225 | }, 226 | AccessTokenValidity: 1, // 1 hour 227 | IdTokenValidity: 1 // 1 hour 228 | }; 229 | 230 | // Create the client in Cognito 231 | const createClientCommand = new CreateUserPoolClientCommand(createClientParams); 232 | const cognitoResponse = await cognitoClient.send(createClientCommand); 233 | 234 | console.log('Cognito client created:', cognitoResponse.UserPoolClient.ClientId); 235 | 236 | // Construct client registration response 237 | const registrationResponse = { 238 | client_id: cognitoResponse.UserPoolClient.ClientId, 239 | client_secret: cognitoResponse.UserPoolClient.ClientSecret, 240 | client_id_issued_at: Math.floor(Date.now() / 1000), 241 | client_secret_expires_at: 0, // Never expires 242 | redirect_uris: body.redirect_uris, 243 | grant_types: ['authorization_code', 'refresh_token'], 244 | token_endpoint_auth_method: 'client_secret_basic', 245 | response_types: ['code'], 246 | client_name: clientName, 247 | scope: allowedScopes.join(' ') 248 | }; 249 | 250 | // Store client metadata in DynamoDB 251 | const dynamoItem = { 252 | client_id: cognitoResponse.UserPoolClient.ClientId, 253 | client_metadata: JSON.stringify(registrationResponse), 254 | registration_time: Date.now(), 255 | initial_request: JSON.stringify(body) 256 | }; 257 | 258 | const putItemParams = { 259 | TableName: DCR_CLIENTS_TABLE, 260 | Item: marshall(dynamoItem) 261 | }; 262 | 263 | const putItemCommand = new PutItemCommand(putItemParams); 264 | await dynamoClient.send(putItemCommand); 265 | 266 | console.log('Client registration stored in DynamoDB'); 267 | 268 | // Return successful response 269 | return formatResponse(201, registrationResponse); 270 | } catch (error) { 271 | console.error('Error in client registration:', error); 272 | 273 | return formatResponse(500, { 274 | error: 'server_error', 275 | error_description: 'An error occurred during client registration' 276 | }); 277 | } 278 | }; 279 | 280 | /** 281 | * Format the API Gateway response 282 | */ 283 | function formatResponse(statusCode, body) { 284 | return { 285 | statusCode, 286 | headers: { 287 | 'Content-Type': 'application/json', 288 | 'Cache-Control': 'no-store' 289 | }, 290 | body: JSON.stringify(body) 291 | }; 292 | } 293 | 294 | GetClientFunction: 295 | Type: AWS::Lambda::Function 296 | Properties: 297 | FunctionName: !Sub ${ProjectName}-get-client 298 | Handler: index.handler 299 | Role: !GetAtt LambdaExecutionRole.Arn 300 | Runtime: nodejs20.x 301 | Timeout: 30 302 | MemorySize: 256 303 | Environment: 304 | Variables: 305 | DCR_CLIENTS_TABLE: !Ref DcrClientsTable 306 | Code: 307 | ZipFile: | 308 | const { DynamoDBClient, GetItemCommand } = require('@aws-sdk/client-dynamodb'); 309 | const { marshall, unmarshall } = require('@aws-sdk/util-dynamodb'); 310 | 311 | // Initialize AWS SDK client 312 | const dynamoClient = new DynamoDBClient(); 313 | 314 | // Configuration 315 | const DCR_CLIENTS_TABLE = process.env.DCR_CLIENTS_TABLE; 316 | 317 | /** 318 | * Lambda function to get client registration information 319 | */ 320 | exports.handler = async (event) => { 321 | try { 322 | console.log('Event received:', JSON.stringify(event)); 323 | 324 | // Get client_id from path parameters 325 | const clientId = event.pathParameters?.client_id; 326 | 327 | if (!clientId) { 328 | return formatResponse(400, { 329 | error: 'invalid_request', 330 | error_description: 'client_id is required' 331 | }); 332 | } 333 | 334 | // Get client data from DynamoDB 335 | const getItemParams = { 336 | TableName: DCR_CLIENTS_TABLE, 337 | Key: marshall({ client_id: clientId }) 338 | }; 339 | 340 | const getItemCommand = new GetItemCommand(getItemParams); 341 | const response = await dynamoClient.send(getItemCommand); 342 | 343 | if (!response.Item) { 344 | return formatResponse(404, { 345 | error: 'invalid_client', 346 | error_description: 'Client not found' 347 | }); 348 | } 349 | 350 | // Extract and return client metadata 351 | const item = unmarshall(response.Item); 352 | const clientMetadata = JSON.parse(item.client_metadata); 353 | 354 | // Remove sensitive information 355 | delete clientMetadata.client_secret; 356 | 357 | return formatResponse(200, clientMetadata); 358 | } catch (error) { 359 | console.error('Error retrieving client information:', error); 360 | 361 | return formatResponse(500, { 362 | error: 'server_error', 363 | error_description: 'An error occurred while retrieving client information' 364 | }); 365 | } 366 | }; 367 | 368 | /** 369 | * Format the API Gateway response 370 | */ 371 | function formatResponse(statusCode, body) { 372 | return { 373 | statusCode, 374 | headers: { 375 | 'Content-Type': 'application/json', 376 | 'Cache-Control': 'no-store' 377 | }, 378 | body: JSON.stringify(body) 379 | }; 380 | } 381 | 382 | # API Gateway 383 | ApiGateway: 384 | Type: AWS::ApiGateway::RestApi 385 | Properties: 386 | Name: !Sub ${ProjectName}-dcr-api 387 | Description: API for Dynamic Client Registration with MCP OAuth 2.1 388 | EndpointConfiguration: 389 | Types: 390 | - REGIONAL 391 | 392 | # Register Resource 393 | RegisterResource: 394 | Type: AWS::ApiGateway::Resource 395 | Properties: 396 | RestApiId: !Ref ApiGateway 397 | ParentId: !GetAtt ApiGateway.RootResourceId 398 | PathPart: register 399 | 400 | # Register Method 401 | RegisterMethod: 402 | Type: AWS::ApiGateway::Method 403 | Properties: 404 | RestApiId: !Ref ApiGateway 405 | ResourceId: !Ref RegisterResource 406 | HttpMethod: POST 407 | AuthorizationType: NONE 408 | Integration: 409 | Type: AWS_PROXY 410 | IntegrationHttpMethod: POST 411 | Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${RegisterClientFunction.Arn}/invocations 412 | 413 | # Clients Resource 414 | ClientsResource: 415 | Type: AWS::ApiGateway::Resource 416 | Properties: 417 | RestApiId: !Ref ApiGateway 418 | ParentId: !GetAtt ApiGateway.RootResourceId 419 | PathPart: clients 420 | 421 | # Client ID Resource 422 | ClientIdResource: 423 | Type: AWS::ApiGateway::Resource 424 | Properties: 425 | RestApiId: !Ref ApiGateway 426 | ParentId: !Ref ClientsResource 427 | PathPart: "{client_id}" 428 | 429 | # Get Client Method 430 | GetClientMethod: 431 | Type: AWS::ApiGateway::Method 432 | Properties: 433 | RestApiId: !Ref ApiGateway 434 | ResourceId: !Ref ClientIdResource 435 | HttpMethod: GET 436 | AuthorizationType: NONE 437 | RequestParameters: 438 | method.request.path.client_id: true 439 | Integration: 440 | Type: AWS_PROXY 441 | IntegrationHttpMethod: POST 442 | Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${GetClientFunction.Arn}/invocations 443 | 444 | # API Gateway Deployment 445 | ApiDeployment: 446 | Type: AWS::ApiGateway::Deployment 447 | DependsOn: 448 | - RegisterMethod 449 | - GetClientMethod 450 | Properties: 451 | RestApiId: !Ref ApiGateway 452 | StageName: v1 453 | 454 | # Lambda Permissions 455 | RegisterClientPermission: 456 | Type: AWS::Lambda::Permission 457 | Properties: 458 | Action: lambda:InvokeFunction 459 | FunctionName: !Ref RegisterClientFunction 460 | Principal: apigateway.amazonaws.com 461 | SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/POST/register 462 | 463 | GetClientPermission: 464 | Type: AWS::Lambda::Permission 465 | Properties: 466 | Action: lambda:InvokeFunction 467 | FunctionName: !Ref GetClientFunction 468 | Principal: apigateway.amazonaws.com 469 | SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${ApiGateway}/*/GET/clients/{client_id} 470 | 471 | # CORS Configuration for Register Resource 472 | RegisterCorsMethod: 473 | Type: AWS::ApiGateway::Method 474 | Properties: 475 | RestApiId: !Ref ApiGateway 476 | ResourceId: !Ref RegisterResource 477 | HttpMethod: OPTIONS 478 | AuthorizationType: NONE 479 | Integration: 480 | Type: MOCK 481 | IntegrationResponses: 482 | - StatusCode: 200 483 | ResponseParameters: 484 | method.response.header.Access-Control-Allow-Headers: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token'" 485 | method.response.header.Access-Control-Allow-Methods: "'OPTIONS,POST'" 486 | method.response.header.Access-Control-Allow-Origin: "'*'" 487 | ResponseTemplates: 488 | application/json: '' 489 | PassthroughBehavior: WHEN_NO_MATCH 490 | RequestTemplates: 491 | application/json: '{"statusCode": 200}' 492 | MethodResponses: 493 | - StatusCode: 200 494 | ResponseParameters: 495 | method.response.header.Access-Control-Allow-Headers: true 496 | method.response.header.Access-Control-Allow-Methods: true 497 | method.response.header.Access-Control-Allow-Origin: true 498 | ResponseModels: 499 | application/json: 'Empty' 500 | 501 | Outputs: 502 | UserPoolId: 503 | Description: "User Pool ID" 504 | Value: !Ref UserPool 505 | Export: 506 | Name: !Sub "${ProjectName}-UserPoolId" 507 | 508 | UserPoolClientId: 509 | Description: "Static User Pool Client ID" 510 | Value: !Ref UserPoolClient 511 | Export: 512 | Name: !Sub "${ProjectName}-UserPoolClientId" 513 | 514 | UserPoolDomain: 515 | Description: "Cognito Domain" 516 | Value: !Sub "${ProjectName}-domain-${AWS::AccountId}.auth.${AWS::Region}.amazoncognito.com" 517 | Export: 518 | Name: !Sub "${ProjectName}-UserPoolDomain" 519 | 520 | ApiGatewayUrl: 521 | Description: "API Gateway URL for Dynamic Client Registration" 522 | Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/v1" 523 | Export: 524 | Name: !Sub "${ProjectName}-ApiGatewayUrl" 525 | 526 | DynamoDBTable: 527 | Description: "DynamoDB Table for Client Registrations" 528 | Value: !Ref DcrClientsTable 529 | Export: 530 | Name: !Sub "${ProjectName}-DcrClientsTable" 531 | 532 | RegisterClientEndpoint: 533 | Description: "Endpoint for Dynamic Client Registration" 534 | Value: !Sub "https://${ApiGateway}.execute-api.${AWS::Region}.amazonaws.com/v1/register" 535 | Export: 536 | Name: !Sub "${ProjectName}-RegisterClientEndpoint" -------------------------------------------------------------------------------- /src/auto-client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito-auto-client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "mcp-oauth2-aws-cognito-auto-client", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.9.0", 13 | "crypto": "^1.0.1", 14 | "dotenv": "^16.5.0", 15 | "express": "^5.1.0", 16 | "express-session": "^1.18.1" 17 | } 18 | }, 19 | "node_modules/accepts": { 20 | "version": "2.0.0", 21 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 22 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 23 | "license": "MIT", 24 | "dependencies": { 25 | "mime-types": "^3.0.0", 26 | "negotiator": "^1.0.0" 27 | }, 28 | "engines": { 29 | "node": ">= 0.6" 30 | } 31 | }, 32 | "node_modules/asynckit": { 33 | "version": "0.4.0", 34 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 35 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 36 | "license": "MIT" 37 | }, 38 | "node_modules/axios": { 39 | "version": "1.9.0", 40 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", 41 | "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", 42 | "license": "MIT", 43 | "dependencies": { 44 | "follow-redirects": "^1.15.6", 45 | "form-data": "^4.0.0", 46 | "proxy-from-env": "^1.1.0" 47 | } 48 | }, 49 | "node_modules/body-parser": { 50 | "version": "2.2.0", 51 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 52 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 53 | "license": "MIT", 54 | "dependencies": { 55 | "bytes": "^3.1.2", 56 | "content-type": "^1.0.5", 57 | "debug": "^4.4.0", 58 | "http-errors": "^2.0.0", 59 | "iconv-lite": "^0.6.3", 60 | "on-finished": "^2.4.1", 61 | "qs": "^6.14.0", 62 | "raw-body": "^3.0.0", 63 | "type-is": "^2.0.0" 64 | }, 65 | "engines": { 66 | "node": ">=18" 67 | } 68 | }, 69 | "node_modules/bytes": { 70 | "version": "3.1.2", 71 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 72 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 73 | "license": "MIT", 74 | "engines": { 75 | "node": ">= 0.8" 76 | } 77 | }, 78 | "node_modules/call-bind-apply-helpers": { 79 | "version": "1.0.2", 80 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 81 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 82 | "license": "MIT", 83 | "dependencies": { 84 | "es-errors": "^1.3.0", 85 | "function-bind": "^1.1.2" 86 | }, 87 | "engines": { 88 | "node": ">= 0.4" 89 | } 90 | }, 91 | "node_modules/call-bound": { 92 | "version": "1.0.4", 93 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 94 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 95 | "license": "MIT", 96 | "dependencies": { 97 | "call-bind-apply-helpers": "^1.0.2", 98 | "get-intrinsic": "^1.3.0" 99 | }, 100 | "engines": { 101 | "node": ">= 0.4" 102 | }, 103 | "funding": { 104 | "url": "https://github.com/sponsors/ljharb" 105 | } 106 | }, 107 | "node_modules/combined-stream": { 108 | "version": "1.0.8", 109 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 110 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 111 | "license": "MIT", 112 | "dependencies": { 113 | "delayed-stream": "~1.0.0" 114 | }, 115 | "engines": { 116 | "node": ">= 0.8" 117 | } 118 | }, 119 | "node_modules/content-disposition": { 120 | "version": "1.0.0", 121 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 122 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 123 | "license": "MIT", 124 | "dependencies": { 125 | "safe-buffer": "5.2.1" 126 | }, 127 | "engines": { 128 | "node": ">= 0.6" 129 | } 130 | }, 131 | "node_modules/content-type": { 132 | "version": "1.0.5", 133 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 134 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 135 | "license": "MIT", 136 | "engines": { 137 | "node": ">= 0.6" 138 | } 139 | }, 140 | "node_modules/cookie": { 141 | "version": "0.7.2", 142 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 143 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 144 | "license": "MIT", 145 | "engines": { 146 | "node": ">= 0.6" 147 | } 148 | }, 149 | "node_modules/cookie-signature": { 150 | "version": "1.2.2", 151 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 152 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 153 | "license": "MIT", 154 | "engines": { 155 | "node": ">=6.6.0" 156 | } 157 | }, 158 | "node_modules/crypto": { 159 | "version": "1.0.1", 160 | "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", 161 | "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", 162 | "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", 163 | "license": "ISC" 164 | }, 165 | "node_modules/debug": { 166 | "version": "4.4.0", 167 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 168 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 169 | "license": "MIT", 170 | "dependencies": { 171 | "ms": "^2.1.3" 172 | }, 173 | "engines": { 174 | "node": ">=6.0" 175 | }, 176 | "peerDependenciesMeta": { 177 | "supports-color": { 178 | "optional": true 179 | } 180 | } 181 | }, 182 | "node_modules/delayed-stream": { 183 | "version": "1.0.0", 184 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 185 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 186 | "license": "MIT", 187 | "engines": { 188 | "node": ">=0.4.0" 189 | } 190 | }, 191 | "node_modules/depd": { 192 | "version": "2.0.0", 193 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 194 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 195 | "license": "MIT", 196 | "engines": { 197 | "node": ">= 0.8" 198 | } 199 | }, 200 | "node_modules/dotenv": { 201 | "version": "16.5.0", 202 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 203 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 204 | "license": "BSD-2-Clause", 205 | "engines": { 206 | "node": ">=12" 207 | }, 208 | "funding": { 209 | "url": "https://dotenvx.com" 210 | } 211 | }, 212 | "node_modules/dunder-proto": { 213 | "version": "1.0.1", 214 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 215 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 216 | "license": "MIT", 217 | "dependencies": { 218 | "call-bind-apply-helpers": "^1.0.1", 219 | "es-errors": "^1.3.0", 220 | "gopd": "^1.2.0" 221 | }, 222 | "engines": { 223 | "node": ">= 0.4" 224 | } 225 | }, 226 | "node_modules/ee-first": { 227 | "version": "1.1.1", 228 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 229 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 230 | "license": "MIT" 231 | }, 232 | "node_modules/encodeurl": { 233 | "version": "2.0.0", 234 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 235 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 236 | "license": "MIT", 237 | "engines": { 238 | "node": ">= 0.8" 239 | } 240 | }, 241 | "node_modules/es-define-property": { 242 | "version": "1.0.1", 243 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 244 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 245 | "license": "MIT", 246 | "engines": { 247 | "node": ">= 0.4" 248 | } 249 | }, 250 | "node_modules/es-errors": { 251 | "version": "1.3.0", 252 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 253 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 254 | "license": "MIT", 255 | "engines": { 256 | "node": ">= 0.4" 257 | } 258 | }, 259 | "node_modules/es-object-atoms": { 260 | "version": "1.1.1", 261 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 262 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 263 | "license": "MIT", 264 | "dependencies": { 265 | "es-errors": "^1.3.0" 266 | }, 267 | "engines": { 268 | "node": ">= 0.4" 269 | } 270 | }, 271 | "node_modules/es-set-tostringtag": { 272 | "version": "2.1.0", 273 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 274 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 275 | "license": "MIT", 276 | "dependencies": { 277 | "es-errors": "^1.3.0", 278 | "get-intrinsic": "^1.2.6", 279 | "has-tostringtag": "^1.0.2", 280 | "hasown": "^2.0.2" 281 | }, 282 | "engines": { 283 | "node": ">= 0.4" 284 | } 285 | }, 286 | "node_modules/escape-html": { 287 | "version": "1.0.3", 288 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 289 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 290 | "license": "MIT" 291 | }, 292 | "node_modules/etag": { 293 | "version": "1.8.1", 294 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 295 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 296 | "license": "MIT", 297 | "engines": { 298 | "node": ">= 0.6" 299 | } 300 | }, 301 | "node_modules/express": { 302 | "version": "5.1.0", 303 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 304 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 305 | "license": "MIT", 306 | "dependencies": { 307 | "accepts": "^2.0.0", 308 | "body-parser": "^2.2.0", 309 | "content-disposition": "^1.0.0", 310 | "content-type": "^1.0.5", 311 | "cookie": "^0.7.1", 312 | "cookie-signature": "^1.2.1", 313 | "debug": "^4.4.0", 314 | "encodeurl": "^2.0.0", 315 | "escape-html": "^1.0.3", 316 | "etag": "^1.8.1", 317 | "finalhandler": "^2.1.0", 318 | "fresh": "^2.0.0", 319 | "http-errors": "^2.0.0", 320 | "merge-descriptors": "^2.0.0", 321 | "mime-types": "^3.0.0", 322 | "on-finished": "^2.4.1", 323 | "once": "^1.4.0", 324 | "parseurl": "^1.3.3", 325 | "proxy-addr": "^2.0.7", 326 | "qs": "^6.14.0", 327 | "range-parser": "^1.2.1", 328 | "router": "^2.2.0", 329 | "send": "^1.1.0", 330 | "serve-static": "^2.2.0", 331 | "statuses": "^2.0.1", 332 | "type-is": "^2.0.1", 333 | "vary": "^1.1.2" 334 | }, 335 | "engines": { 336 | "node": ">= 18" 337 | }, 338 | "funding": { 339 | "type": "opencollective", 340 | "url": "https://opencollective.com/express" 341 | } 342 | }, 343 | "node_modules/express-session": { 344 | "version": "1.18.1", 345 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", 346 | "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", 347 | "license": "MIT", 348 | "dependencies": { 349 | "cookie": "0.7.2", 350 | "cookie-signature": "1.0.7", 351 | "debug": "2.6.9", 352 | "depd": "~2.0.0", 353 | "on-headers": "~1.0.2", 354 | "parseurl": "~1.3.3", 355 | "safe-buffer": "5.2.1", 356 | "uid-safe": "~2.1.5" 357 | }, 358 | "engines": { 359 | "node": ">= 0.8.0" 360 | } 361 | }, 362 | "node_modules/express-session/node_modules/cookie-signature": { 363 | "version": "1.0.7", 364 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", 365 | "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", 366 | "license": "MIT" 367 | }, 368 | "node_modules/express-session/node_modules/debug": { 369 | "version": "2.6.9", 370 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 371 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 372 | "license": "MIT", 373 | "dependencies": { 374 | "ms": "2.0.0" 375 | } 376 | }, 377 | "node_modules/express-session/node_modules/ms": { 378 | "version": "2.0.0", 379 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 380 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 381 | "license": "MIT" 382 | }, 383 | "node_modules/finalhandler": { 384 | "version": "2.1.0", 385 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 386 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 387 | "license": "MIT", 388 | "dependencies": { 389 | "debug": "^4.4.0", 390 | "encodeurl": "^2.0.0", 391 | "escape-html": "^1.0.3", 392 | "on-finished": "^2.4.1", 393 | "parseurl": "^1.3.3", 394 | "statuses": "^2.0.1" 395 | }, 396 | "engines": { 397 | "node": ">= 0.8" 398 | } 399 | }, 400 | "node_modules/follow-redirects": { 401 | "version": "1.15.9", 402 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 403 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 404 | "funding": [ 405 | { 406 | "type": "individual", 407 | "url": "https://github.com/sponsors/RubenVerborgh" 408 | } 409 | ], 410 | "license": "MIT", 411 | "engines": { 412 | "node": ">=4.0" 413 | }, 414 | "peerDependenciesMeta": { 415 | "debug": { 416 | "optional": true 417 | } 418 | } 419 | }, 420 | "node_modules/form-data": { 421 | "version": "4.0.2", 422 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", 423 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 424 | "license": "MIT", 425 | "dependencies": { 426 | "asynckit": "^0.4.0", 427 | "combined-stream": "^1.0.8", 428 | "es-set-tostringtag": "^2.1.0", 429 | "mime-types": "^2.1.12" 430 | }, 431 | "engines": { 432 | "node": ">= 6" 433 | } 434 | }, 435 | "node_modules/form-data/node_modules/mime-db": { 436 | "version": "1.52.0", 437 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 438 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 439 | "license": "MIT", 440 | "engines": { 441 | "node": ">= 0.6" 442 | } 443 | }, 444 | "node_modules/form-data/node_modules/mime-types": { 445 | "version": "2.1.35", 446 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 447 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 448 | "license": "MIT", 449 | "dependencies": { 450 | "mime-db": "1.52.0" 451 | }, 452 | "engines": { 453 | "node": ">= 0.6" 454 | } 455 | }, 456 | "node_modules/forwarded": { 457 | "version": "0.2.0", 458 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 459 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 460 | "license": "MIT", 461 | "engines": { 462 | "node": ">= 0.6" 463 | } 464 | }, 465 | "node_modules/fresh": { 466 | "version": "2.0.0", 467 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 468 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 469 | "license": "MIT", 470 | "engines": { 471 | "node": ">= 0.8" 472 | } 473 | }, 474 | "node_modules/function-bind": { 475 | "version": "1.1.2", 476 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 477 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 478 | "license": "MIT", 479 | "funding": { 480 | "url": "https://github.com/sponsors/ljharb" 481 | } 482 | }, 483 | "node_modules/get-intrinsic": { 484 | "version": "1.3.0", 485 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 486 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 487 | "license": "MIT", 488 | "dependencies": { 489 | "call-bind-apply-helpers": "^1.0.2", 490 | "es-define-property": "^1.0.1", 491 | "es-errors": "^1.3.0", 492 | "es-object-atoms": "^1.1.1", 493 | "function-bind": "^1.1.2", 494 | "get-proto": "^1.0.1", 495 | "gopd": "^1.2.0", 496 | "has-symbols": "^1.1.0", 497 | "hasown": "^2.0.2", 498 | "math-intrinsics": "^1.1.0" 499 | }, 500 | "engines": { 501 | "node": ">= 0.4" 502 | }, 503 | "funding": { 504 | "url": "https://github.com/sponsors/ljharb" 505 | } 506 | }, 507 | "node_modules/get-proto": { 508 | "version": "1.0.1", 509 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 510 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 511 | "license": "MIT", 512 | "dependencies": { 513 | "dunder-proto": "^1.0.1", 514 | "es-object-atoms": "^1.0.0" 515 | }, 516 | "engines": { 517 | "node": ">= 0.4" 518 | } 519 | }, 520 | "node_modules/gopd": { 521 | "version": "1.2.0", 522 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 523 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 524 | "license": "MIT", 525 | "engines": { 526 | "node": ">= 0.4" 527 | }, 528 | "funding": { 529 | "url": "https://github.com/sponsors/ljharb" 530 | } 531 | }, 532 | "node_modules/has-symbols": { 533 | "version": "1.1.0", 534 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 535 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 536 | "license": "MIT", 537 | "engines": { 538 | "node": ">= 0.4" 539 | }, 540 | "funding": { 541 | "url": "https://github.com/sponsors/ljharb" 542 | } 543 | }, 544 | "node_modules/has-tostringtag": { 545 | "version": "1.0.2", 546 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 547 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 548 | "license": "MIT", 549 | "dependencies": { 550 | "has-symbols": "^1.0.3" 551 | }, 552 | "engines": { 553 | "node": ">= 0.4" 554 | }, 555 | "funding": { 556 | "url": "https://github.com/sponsors/ljharb" 557 | } 558 | }, 559 | "node_modules/hasown": { 560 | "version": "2.0.2", 561 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 562 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 563 | "license": "MIT", 564 | "dependencies": { 565 | "function-bind": "^1.1.2" 566 | }, 567 | "engines": { 568 | "node": ">= 0.4" 569 | } 570 | }, 571 | "node_modules/http-errors": { 572 | "version": "2.0.0", 573 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 574 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 575 | "license": "MIT", 576 | "dependencies": { 577 | "depd": "2.0.0", 578 | "inherits": "2.0.4", 579 | "setprototypeof": "1.2.0", 580 | "statuses": "2.0.1", 581 | "toidentifier": "1.0.1" 582 | }, 583 | "engines": { 584 | "node": ">= 0.8" 585 | } 586 | }, 587 | "node_modules/iconv-lite": { 588 | "version": "0.6.3", 589 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 590 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 591 | "license": "MIT", 592 | "dependencies": { 593 | "safer-buffer": ">= 2.1.2 < 3.0.0" 594 | }, 595 | "engines": { 596 | "node": ">=0.10.0" 597 | } 598 | }, 599 | "node_modules/inherits": { 600 | "version": "2.0.4", 601 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 602 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 603 | "license": "ISC" 604 | }, 605 | "node_modules/ipaddr.js": { 606 | "version": "1.9.1", 607 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 608 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 609 | "license": "MIT", 610 | "engines": { 611 | "node": ">= 0.10" 612 | } 613 | }, 614 | "node_modules/is-promise": { 615 | "version": "4.0.0", 616 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 617 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 618 | "license": "MIT" 619 | }, 620 | "node_modules/math-intrinsics": { 621 | "version": "1.1.0", 622 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 623 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 624 | "license": "MIT", 625 | "engines": { 626 | "node": ">= 0.4" 627 | } 628 | }, 629 | "node_modules/media-typer": { 630 | "version": "1.1.0", 631 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 632 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 633 | "license": "MIT", 634 | "engines": { 635 | "node": ">= 0.8" 636 | } 637 | }, 638 | "node_modules/merge-descriptors": { 639 | "version": "2.0.0", 640 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 641 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 642 | "license": "MIT", 643 | "engines": { 644 | "node": ">=18" 645 | }, 646 | "funding": { 647 | "url": "https://github.com/sponsors/sindresorhus" 648 | } 649 | }, 650 | "node_modules/mime-db": { 651 | "version": "1.54.0", 652 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 653 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 654 | "license": "MIT", 655 | "engines": { 656 | "node": ">= 0.6" 657 | } 658 | }, 659 | "node_modules/mime-types": { 660 | "version": "3.0.1", 661 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 662 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 663 | "license": "MIT", 664 | "dependencies": { 665 | "mime-db": "^1.54.0" 666 | }, 667 | "engines": { 668 | "node": ">= 0.6" 669 | } 670 | }, 671 | "node_modules/ms": { 672 | "version": "2.1.3", 673 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 674 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 675 | "license": "MIT" 676 | }, 677 | "node_modules/negotiator": { 678 | "version": "1.0.0", 679 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 680 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 681 | "license": "MIT", 682 | "engines": { 683 | "node": ">= 0.6" 684 | } 685 | }, 686 | "node_modules/object-inspect": { 687 | "version": "1.13.4", 688 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 689 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 690 | "license": "MIT", 691 | "engines": { 692 | "node": ">= 0.4" 693 | }, 694 | "funding": { 695 | "url": "https://github.com/sponsors/ljharb" 696 | } 697 | }, 698 | "node_modules/on-finished": { 699 | "version": "2.4.1", 700 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 701 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 702 | "license": "MIT", 703 | "dependencies": { 704 | "ee-first": "1.1.1" 705 | }, 706 | "engines": { 707 | "node": ">= 0.8" 708 | } 709 | }, 710 | "node_modules/on-headers": { 711 | "version": "1.0.2", 712 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 713 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", 714 | "license": "MIT", 715 | "engines": { 716 | "node": ">= 0.8" 717 | } 718 | }, 719 | "node_modules/once": { 720 | "version": "1.4.0", 721 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 722 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 723 | "license": "ISC", 724 | "dependencies": { 725 | "wrappy": "1" 726 | } 727 | }, 728 | "node_modules/parseurl": { 729 | "version": "1.3.3", 730 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 731 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 732 | "license": "MIT", 733 | "engines": { 734 | "node": ">= 0.8" 735 | } 736 | }, 737 | "node_modules/path-to-regexp": { 738 | "version": "8.2.0", 739 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 740 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 741 | "license": "MIT", 742 | "engines": { 743 | "node": ">=16" 744 | } 745 | }, 746 | "node_modules/proxy-addr": { 747 | "version": "2.0.7", 748 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 749 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 750 | "license": "MIT", 751 | "dependencies": { 752 | "forwarded": "0.2.0", 753 | "ipaddr.js": "1.9.1" 754 | }, 755 | "engines": { 756 | "node": ">= 0.10" 757 | } 758 | }, 759 | "node_modules/proxy-from-env": { 760 | "version": "1.1.0", 761 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 762 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 763 | "license": "MIT" 764 | }, 765 | "node_modules/qs": { 766 | "version": "6.14.0", 767 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 768 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 769 | "license": "BSD-3-Clause", 770 | "dependencies": { 771 | "side-channel": "^1.1.0" 772 | }, 773 | "engines": { 774 | "node": ">=0.6" 775 | }, 776 | "funding": { 777 | "url": "https://github.com/sponsors/ljharb" 778 | } 779 | }, 780 | "node_modules/random-bytes": { 781 | "version": "1.0.0", 782 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 783 | "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", 784 | "license": "MIT", 785 | "engines": { 786 | "node": ">= 0.8" 787 | } 788 | }, 789 | "node_modules/range-parser": { 790 | "version": "1.2.1", 791 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 792 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 793 | "license": "MIT", 794 | "engines": { 795 | "node": ">= 0.6" 796 | } 797 | }, 798 | "node_modules/raw-body": { 799 | "version": "3.0.0", 800 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 801 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 802 | "license": "MIT", 803 | "dependencies": { 804 | "bytes": "3.1.2", 805 | "http-errors": "2.0.0", 806 | "iconv-lite": "0.6.3", 807 | "unpipe": "1.0.0" 808 | }, 809 | "engines": { 810 | "node": ">= 0.8" 811 | } 812 | }, 813 | "node_modules/router": { 814 | "version": "2.2.0", 815 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 816 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 817 | "license": "MIT", 818 | "dependencies": { 819 | "debug": "^4.4.0", 820 | "depd": "^2.0.0", 821 | "is-promise": "^4.0.0", 822 | "parseurl": "^1.3.3", 823 | "path-to-regexp": "^8.0.0" 824 | }, 825 | "engines": { 826 | "node": ">= 18" 827 | } 828 | }, 829 | "node_modules/safe-buffer": { 830 | "version": "5.2.1", 831 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 832 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 833 | "funding": [ 834 | { 835 | "type": "github", 836 | "url": "https://github.com/sponsors/feross" 837 | }, 838 | { 839 | "type": "patreon", 840 | "url": "https://www.patreon.com/feross" 841 | }, 842 | { 843 | "type": "consulting", 844 | "url": "https://feross.org/support" 845 | } 846 | ], 847 | "license": "MIT" 848 | }, 849 | "node_modules/safer-buffer": { 850 | "version": "2.1.2", 851 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 852 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 853 | "license": "MIT" 854 | }, 855 | "node_modules/send": { 856 | "version": "1.2.0", 857 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 858 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 859 | "license": "MIT", 860 | "dependencies": { 861 | "debug": "^4.3.5", 862 | "encodeurl": "^2.0.0", 863 | "escape-html": "^1.0.3", 864 | "etag": "^1.8.1", 865 | "fresh": "^2.0.0", 866 | "http-errors": "^2.0.0", 867 | "mime-types": "^3.0.1", 868 | "ms": "^2.1.3", 869 | "on-finished": "^2.4.1", 870 | "range-parser": "^1.2.1", 871 | "statuses": "^2.0.1" 872 | }, 873 | "engines": { 874 | "node": ">= 18" 875 | } 876 | }, 877 | "node_modules/serve-static": { 878 | "version": "2.2.0", 879 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 880 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 881 | "license": "MIT", 882 | "dependencies": { 883 | "encodeurl": "^2.0.0", 884 | "escape-html": "^1.0.3", 885 | "parseurl": "^1.3.3", 886 | "send": "^1.2.0" 887 | }, 888 | "engines": { 889 | "node": ">= 18" 890 | } 891 | }, 892 | "node_modules/setprototypeof": { 893 | "version": "1.2.0", 894 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 895 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 896 | "license": "ISC" 897 | }, 898 | "node_modules/side-channel": { 899 | "version": "1.1.0", 900 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 901 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 902 | "license": "MIT", 903 | "dependencies": { 904 | "es-errors": "^1.3.0", 905 | "object-inspect": "^1.13.3", 906 | "side-channel-list": "^1.0.0", 907 | "side-channel-map": "^1.0.1", 908 | "side-channel-weakmap": "^1.0.2" 909 | }, 910 | "engines": { 911 | "node": ">= 0.4" 912 | }, 913 | "funding": { 914 | "url": "https://github.com/sponsors/ljharb" 915 | } 916 | }, 917 | "node_modules/side-channel-list": { 918 | "version": "1.0.0", 919 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 920 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 921 | "license": "MIT", 922 | "dependencies": { 923 | "es-errors": "^1.3.0", 924 | "object-inspect": "^1.13.3" 925 | }, 926 | "engines": { 927 | "node": ">= 0.4" 928 | }, 929 | "funding": { 930 | "url": "https://github.com/sponsors/ljharb" 931 | } 932 | }, 933 | "node_modules/side-channel-map": { 934 | "version": "1.0.1", 935 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 936 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 937 | "license": "MIT", 938 | "dependencies": { 939 | "call-bound": "^1.0.2", 940 | "es-errors": "^1.3.0", 941 | "get-intrinsic": "^1.2.5", 942 | "object-inspect": "^1.13.3" 943 | }, 944 | "engines": { 945 | "node": ">= 0.4" 946 | }, 947 | "funding": { 948 | "url": "https://github.com/sponsors/ljharb" 949 | } 950 | }, 951 | "node_modules/side-channel-weakmap": { 952 | "version": "1.0.2", 953 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 954 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 955 | "license": "MIT", 956 | "dependencies": { 957 | "call-bound": "^1.0.2", 958 | "es-errors": "^1.3.0", 959 | "get-intrinsic": "^1.2.5", 960 | "object-inspect": "^1.13.3", 961 | "side-channel-map": "^1.0.1" 962 | }, 963 | "engines": { 964 | "node": ">= 0.4" 965 | }, 966 | "funding": { 967 | "url": "https://github.com/sponsors/ljharb" 968 | } 969 | }, 970 | "node_modules/statuses": { 971 | "version": "2.0.1", 972 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 973 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 974 | "license": "MIT", 975 | "engines": { 976 | "node": ">= 0.8" 977 | } 978 | }, 979 | "node_modules/toidentifier": { 980 | "version": "1.0.1", 981 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 982 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 983 | "license": "MIT", 984 | "engines": { 985 | "node": ">=0.6" 986 | } 987 | }, 988 | "node_modules/type-is": { 989 | "version": "2.0.1", 990 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 991 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 992 | "license": "MIT", 993 | "dependencies": { 994 | "content-type": "^1.0.5", 995 | "media-typer": "^1.1.0", 996 | "mime-types": "^3.0.0" 997 | }, 998 | "engines": { 999 | "node": ">= 0.6" 1000 | } 1001 | }, 1002 | "node_modules/uid-safe": { 1003 | "version": "2.1.5", 1004 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 1005 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 1006 | "license": "MIT", 1007 | "dependencies": { 1008 | "random-bytes": "~1.0.0" 1009 | }, 1010 | "engines": { 1011 | "node": ">= 0.8" 1012 | } 1013 | }, 1014 | "node_modules/unpipe": { 1015 | "version": "1.0.0", 1016 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1017 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1018 | "license": "MIT", 1019 | "engines": { 1020 | "node": ">= 0.8" 1021 | } 1022 | }, 1023 | "node_modules/vary": { 1024 | "version": "1.1.2", 1025 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1026 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1027 | "license": "MIT", 1028 | "engines": { 1029 | "node": ">= 0.8" 1030 | } 1031 | }, 1032 | "node_modules/wrappy": { 1033 | "version": "1.0.2", 1034 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1035 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1036 | "license": "ISC" 1037 | } 1038 | } 1039 | } 1040 | -------------------------------------------------------------------------------- /src/client/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcp-oauth2-aws-cognito-client", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "mcp-oauth2-aws-cognito-client", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "axios": "^1.9.0", 13 | "crypto": "^1.0.1", 14 | "dotenv": "^16.5.0", 15 | "express": "^5.1.0", 16 | "express-session": "^1.18.1", 17 | "open": "^10.1.1" 18 | } 19 | }, 20 | "node_modules/accepts": { 21 | "version": "2.0.0", 22 | "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", 23 | "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", 24 | "license": "MIT", 25 | "dependencies": { 26 | "mime-types": "^3.0.0", 27 | "negotiator": "^1.0.0" 28 | }, 29 | "engines": { 30 | "node": ">= 0.6" 31 | } 32 | }, 33 | "node_modules/asynckit": { 34 | "version": "0.4.0", 35 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 36 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", 37 | "license": "MIT" 38 | }, 39 | "node_modules/axios": { 40 | "version": "1.9.0", 41 | "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", 42 | "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", 43 | "license": "MIT", 44 | "dependencies": { 45 | "follow-redirects": "^1.15.6", 46 | "form-data": "^4.0.0", 47 | "proxy-from-env": "^1.1.0" 48 | } 49 | }, 50 | "node_modules/body-parser": { 51 | "version": "2.2.0", 52 | "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", 53 | "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", 54 | "license": "MIT", 55 | "dependencies": { 56 | "bytes": "^3.1.2", 57 | "content-type": "^1.0.5", 58 | "debug": "^4.4.0", 59 | "http-errors": "^2.0.0", 60 | "iconv-lite": "^0.6.3", 61 | "on-finished": "^2.4.1", 62 | "qs": "^6.14.0", 63 | "raw-body": "^3.0.0", 64 | "type-is": "^2.0.0" 65 | }, 66 | "engines": { 67 | "node": ">=18" 68 | } 69 | }, 70 | "node_modules/bundle-name": { 71 | "version": "4.1.0", 72 | "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", 73 | "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", 74 | "license": "MIT", 75 | "dependencies": { 76 | "run-applescript": "^7.0.0" 77 | }, 78 | "engines": { 79 | "node": ">=18" 80 | }, 81 | "funding": { 82 | "url": "https://github.com/sponsors/sindresorhus" 83 | } 84 | }, 85 | "node_modules/bytes": { 86 | "version": "3.1.2", 87 | "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", 88 | "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", 89 | "license": "MIT", 90 | "engines": { 91 | "node": ">= 0.8" 92 | } 93 | }, 94 | "node_modules/call-bind-apply-helpers": { 95 | "version": "1.0.2", 96 | "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", 97 | "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", 98 | "license": "MIT", 99 | "dependencies": { 100 | "es-errors": "^1.3.0", 101 | "function-bind": "^1.1.2" 102 | }, 103 | "engines": { 104 | "node": ">= 0.4" 105 | } 106 | }, 107 | "node_modules/call-bound": { 108 | "version": "1.0.4", 109 | "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", 110 | "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", 111 | "license": "MIT", 112 | "dependencies": { 113 | "call-bind-apply-helpers": "^1.0.2", 114 | "get-intrinsic": "^1.3.0" 115 | }, 116 | "engines": { 117 | "node": ">= 0.4" 118 | }, 119 | "funding": { 120 | "url": "https://github.com/sponsors/ljharb" 121 | } 122 | }, 123 | "node_modules/combined-stream": { 124 | "version": "1.0.8", 125 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 126 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 127 | "license": "MIT", 128 | "dependencies": { 129 | "delayed-stream": "~1.0.0" 130 | }, 131 | "engines": { 132 | "node": ">= 0.8" 133 | } 134 | }, 135 | "node_modules/content-disposition": { 136 | "version": "1.0.0", 137 | "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", 138 | "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", 139 | "license": "MIT", 140 | "dependencies": { 141 | "safe-buffer": "5.2.1" 142 | }, 143 | "engines": { 144 | "node": ">= 0.6" 145 | } 146 | }, 147 | "node_modules/content-type": { 148 | "version": "1.0.5", 149 | "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", 150 | "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", 151 | "license": "MIT", 152 | "engines": { 153 | "node": ">= 0.6" 154 | } 155 | }, 156 | "node_modules/cookie": { 157 | "version": "0.7.2", 158 | "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", 159 | "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", 160 | "license": "MIT", 161 | "engines": { 162 | "node": ">= 0.6" 163 | } 164 | }, 165 | "node_modules/cookie-signature": { 166 | "version": "1.2.2", 167 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", 168 | "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", 169 | "license": "MIT", 170 | "engines": { 171 | "node": ">=6.6.0" 172 | } 173 | }, 174 | "node_modules/crypto": { 175 | "version": "1.0.1", 176 | "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", 177 | "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", 178 | "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", 179 | "license": "ISC" 180 | }, 181 | "node_modules/debug": { 182 | "version": "4.4.0", 183 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 184 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 185 | "license": "MIT", 186 | "dependencies": { 187 | "ms": "^2.1.3" 188 | }, 189 | "engines": { 190 | "node": ">=6.0" 191 | }, 192 | "peerDependenciesMeta": { 193 | "supports-color": { 194 | "optional": true 195 | } 196 | } 197 | }, 198 | "node_modules/default-browser": { 199 | "version": "5.2.1", 200 | "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", 201 | "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", 202 | "license": "MIT", 203 | "dependencies": { 204 | "bundle-name": "^4.1.0", 205 | "default-browser-id": "^5.0.0" 206 | }, 207 | "engines": { 208 | "node": ">=18" 209 | }, 210 | "funding": { 211 | "url": "https://github.com/sponsors/sindresorhus" 212 | } 213 | }, 214 | "node_modules/default-browser-id": { 215 | "version": "5.0.0", 216 | "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", 217 | "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", 218 | "license": "MIT", 219 | "engines": { 220 | "node": ">=18" 221 | }, 222 | "funding": { 223 | "url": "https://github.com/sponsors/sindresorhus" 224 | } 225 | }, 226 | "node_modules/define-lazy-prop": { 227 | "version": "3.0.0", 228 | "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", 229 | "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", 230 | "license": "MIT", 231 | "engines": { 232 | "node": ">=12" 233 | }, 234 | "funding": { 235 | "url": "https://github.com/sponsors/sindresorhus" 236 | } 237 | }, 238 | "node_modules/delayed-stream": { 239 | "version": "1.0.0", 240 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 241 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 242 | "license": "MIT", 243 | "engines": { 244 | "node": ">=0.4.0" 245 | } 246 | }, 247 | "node_modules/depd": { 248 | "version": "2.0.0", 249 | "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", 250 | "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", 251 | "license": "MIT", 252 | "engines": { 253 | "node": ">= 0.8" 254 | } 255 | }, 256 | "node_modules/dotenv": { 257 | "version": "16.5.0", 258 | "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", 259 | "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", 260 | "license": "BSD-2-Clause", 261 | "engines": { 262 | "node": ">=12" 263 | }, 264 | "funding": { 265 | "url": "https://dotenvx.com" 266 | } 267 | }, 268 | "node_modules/dunder-proto": { 269 | "version": "1.0.1", 270 | "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", 271 | "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", 272 | "license": "MIT", 273 | "dependencies": { 274 | "call-bind-apply-helpers": "^1.0.1", 275 | "es-errors": "^1.3.0", 276 | "gopd": "^1.2.0" 277 | }, 278 | "engines": { 279 | "node": ">= 0.4" 280 | } 281 | }, 282 | "node_modules/ee-first": { 283 | "version": "1.1.1", 284 | "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", 285 | "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", 286 | "license": "MIT" 287 | }, 288 | "node_modules/encodeurl": { 289 | "version": "2.0.0", 290 | "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", 291 | "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", 292 | "license": "MIT", 293 | "engines": { 294 | "node": ">= 0.8" 295 | } 296 | }, 297 | "node_modules/es-define-property": { 298 | "version": "1.0.1", 299 | "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", 300 | "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", 301 | "license": "MIT", 302 | "engines": { 303 | "node": ">= 0.4" 304 | } 305 | }, 306 | "node_modules/es-errors": { 307 | "version": "1.3.0", 308 | "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", 309 | "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", 310 | "license": "MIT", 311 | "engines": { 312 | "node": ">= 0.4" 313 | } 314 | }, 315 | "node_modules/es-object-atoms": { 316 | "version": "1.1.1", 317 | "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", 318 | "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", 319 | "license": "MIT", 320 | "dependencies": { 321 | "es-errors": "^1.3.0" 322 | }, 323 | "engines": { 324 | "node": ">= 0.4" 325 | } 326 | }, 327 | "node_modules/es-set-tostringtag": { 328 | "version": "2.1.0", 329 | "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", 330 | "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", 331 | "license": "MIT", 332 | "dependencies": { 333 | "es-errors": "^1.3.0", 334 | "get-intrinsic": "^1.2.6", 335 | "has-tostringtag": "^1.0.2", 336 | "hasown": "^2.0.2" 337 | }, 338 | "engines": { 339 | "node": ">= 0.4" 340 | } 341 | }, 342 | "node_modules/escape-html": { 343 | "version": "1.0.3", 344 | "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", 345 | "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", 346 | "license": "MIT" 347 | }, 348 | "node_modules/etag": { 349 | "version": "1.8.1", 350 | "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", 351 | "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", 352 | "license": "MIT", 353 | "engines": { 354 | "node": ">= 0.6" 355 | } 356 | }, 357 | "node_modules/express": { 358 | "version": "5.1.0", 359 | "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", 360 | "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", 361 | "license": "MIT", 362 | "dependencies": { 363 | "accepts": "^2.0.0", 364 | "body-parser": "^2.2.0", 365 | "content-disposition": "^1.0.0", 366 | "content-type": "^1.0.5", 367 | "cookie": "^0.7.1", 368 | "cookie-signature": "^1.2.1", 369 | "debug": "^4.4.0", 370 | "encodeurl": "^2.0.0", 371 | "escape-html": "^1.0.3", 372 | "etag": "^1.8.1", 373 | "finalhandler": "^2.1.0", 374 | "fresh": "^2.0.0", 375 | "http-errors": "^2.0.0", 376 | "merge-descriptors": "^2.0.0", 377 | "mime-types": "^3.0.0", 378 | "on-finished": "^2.4.1", 379 | "once": "^1.4.0", 380 | "parseurl": "^1.3.3", 381 | "proxy-addr": "^2.0.7", 382 | "qs": "^6.14.0", 383 | "range-parser": "^1.2.1", 384 | "router": "^2.2.0", 385 | "send": "^1.1.0", 386 | "serve-static": "^2.2.0", 387 | "statuses": "^2.0.1", 388 | "type-is": "^2.0.1", 389 | "vary": "^1.1.2" 390 | }, 391 | "engines": { 392 | "node": ">= 18" 393 | }, 394 | "funding": { 395 | "type": "opencollective", 396 | "url": "https://opencollective.com/express" 397 | } 398 | }, 399 | "node_modules/express-session": { 400 | "version": "1.18.1", 401 | "resolved": "https://registry.npmjs.org/express-session/-/express-session-1.18.1.tgz", 402 | "integrity": "sha512-a5mtTqEaZvBCL9A9aqkrtfz+3SMDhOVUnjafjo+s7A9Txkq+SVX2DLvSp1Zrv4uCXa3lMSK3viWnh9Gg07PBUA==", 403 | "license": "MIT", 404 | "dependencies": { 405 | "cookie": "0.7.2", 406 | "cookie-signature": "1.0.7", 407 | "debug": "2.6.9", 408 | "depd": "~2.0.0", 409 | "on-headers": "~1.0.2", 410 | "parseurl": "~1.3.3", 411 | "safe-buffer": "5.2.1", 412 | "uid-safe": "~2.1.5" 413 | }, 414 | "engines": { 415 | "node": ">= 0.8.0" 416 | } 417 | }, 418 | "node_modules/express-session/node_modules/cookie-signature": { 419 | "version": "1.0.7", 420 | "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", 421 | "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", 422 | "license": "MIT" 423 | }, 424 | "node_modules/express-session/node_modules/debug": { 425 | "version": "2.6.9", 426 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 427 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 428 | "license": "MIT", 429 | "dependencies": { 430 | "ms": "2.0.0" 431 | } 432 | }, 433 | "node_modules/express-session/node_modules/ms": { 434 | "version": "2.0.0", 435 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 436 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", 437 | "license": "MIT" 438 | }, 439 | "node_modules/finalhandler": { 440 | "version": "2.1.0", 441 | "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", 442 | "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", 443 | "license": "MIT", 444 | "dependencies": { 445 | "debug": "^4.4.0", 446 | "encodeurl": "^2.0.0", 447 | "escape-html": "^1.0.3", 448 | "on-finished": "^2.4.1", 449 | "parseurl": "^1.3.3", 450 | "statuses": "^2.0.1" 451 | }, 452 | "engines": { 453 | "node": ">= 0.8" 454 | } 455 | }, 456 | "node_modules/follow-redirects": { 457 | "version": "1.15.9", 458 | "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", 459 | "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", 460 | "funding": [ 461 | { 462 | "type": "individual", 463 | "url": "https://github.com/sponsors/RubenVerborgh" 464 | } 465 | ], 466 | "license": "MIT", 467 | "engines": { 468 | "node": ">=4.0" 469 | }, 470 | "peerDependenciesMeta": { 471 | "debug": { 472 | "optional": true 473 | } 474 | } 475 | }, 476 | "node_modules/form-data": { 477 | "version": "4.0.2", 478 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz", 479 | "integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==", 480 | "license": "MIT", 481 | "dependencies": { 482 | "asynckit": "^0.4.0", 483 | "combined-stream": "^1.0.8", 484 | "es-set-tostringtag": "^2.1.0", 485 | "mime-types": "^2.1.12" 486 | }, 487 | "engines": { 488 | "node": ">= 6" 489 | } 490 | }, 491 | "node_modules/form-data/node_modules/mime-db": { 492 | "version": "1.52.0", 493 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 494 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 495 | "license": "MIT", 496 | "engines": { 497 | "node": ">= 0.6" 498 | } 499 | }, 500 | "node_modules/form-data/node_modules/mime-types": { 501 | "version": "2.1.35", 502 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 503 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 504 | "license": "MIT", 505 | "dependencies": { 506 | "mime-db": "1.52.0" 507 | }, 508 | "engines": { 509 | "node": ">= 0.6" 510 | } 511 | }, 512 | "node_modules/forwarded": { 513 | "version": "0.2.0", 514 | "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", 515 | "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", 516 | "license": "MIT", 517 | "engines": { 518 | "node": ">= 0.6" 519 | } 520 | }, 521 | "node_modules/fresh": { 522 | "version": "2.0.0", 523 | "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", 524 | "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", 525 | "license": "MIT", 526 | "engines": { 527 | "node": ">= 0.8" 528 | } 529 | }, 530 | "node_modules/function-bind": { 531 | "version": "1.1.2", 532 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 533 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 534 | "license": "MIT", 535 | "funding": { 536 | "url": "https://github.com/sponsors/ljharb" 537 | } 538 | }, 539 | "node_modules/get-intrinsic": { 540 | "version": "1.3.0", 541 | "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", 542 | "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", 543 | "license": "MIT", 544 | "dependencies": { 545 | "call-bind-apply-helpers": "^1.0.2", 546 | "es-define-property": "^1.0.1", 547 | "es-errors": "^1.3.0", 548 | "es-object-atoms": "^1.1.1", 549 | "function-bind": "^1.1.2", 550 | "get-proto": "^1.0.1", 551 | "gopd": "^1.2.0", 552 | "has-symbols": "^1.1.0", 553 | "hasown": "^2.0.2", 554 | "math-intrinsics": "^1.1.0" 555 | }, 556 | "engines": { 557 | "node": ">= 0.4" 558 | }, 559 | "funding": { 560 | "url": "https://github.com/sponsors/ljharb" 561 | } 562 | }, 563 | "node_modules/get-proto": { 564 | "version": "1.0.1", 565 | "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", 566 | "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", 567 | "license": "MIT", 568 | "dependencies": { 569 | "dunder-proto": "^1.0.1", 570 | "es-object-atoms": "^1.0.0" 571 | }, 572 | "engines": { 573 | "node": ">= 0.4" 574 | } 575 | }, 576 | "node_modules/gopd": { 577 | "version": "1.2.0", 578 | "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", 579 | "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", 580 | "license": "MIT", 581 | "engines": { 582 | "node": ">= 0.4" 583 | }, 584 | "funding": { 585 | "url": "https://github.com/sponsors/ljharb" 586 | } 587 | }, 588 | "node_modules/has-symbols": { 589 | "version": "1.1.0", 590 | "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", 591 | "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", 592 | "license": "MIT", 593 | "engines": { 594 | "node": ">= 0.4" 595 | }, 596 | "funding": { 597 | "url": "https://github.com/sponsors/ljharb" 598 | } 599 | }, 600 | "node_modules/has-tostringtag": { 601 | "version": "1.0.2", 602 | "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", 603 | "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", 604 | "license": "MIT", 605 | "dependencies": { 606 | "has-symbols": "^1.0.3" 607 | }, 608 | "engines": { 609 | "node": ">= 0.4" 610 | }, 611 | "funding": { 612 | "url": "https://github.com/sponsors/ljharb" 613 | } 614 | }, 615 | "node_modules/hasown": { 616 | "version": "2.0.2", 617 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 618 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 619 | "license": "MIT", 620 | "dependencies": { 621 | "function-bind": "^1.1.2" 622 | }, 623 | "engines": { 624 | "node": ">= 0.4" 625 | } 626 | }, 627 | "node_modules/http-errors": { 628 | "version": "2.0.0", 629 | "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", 630 | "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", 631 | "license": "MIT", 632 | "dependencies": { 633 | "depd": "2.0.0", 634 | "inherits": "2.0.4", 635 | "setprototypeof": "1.2.0", 636 | "statuses": "2.0.1", 637 | "toidentifier": "1.0.1" 638 | }, 639 | "engines": { 640 | "node": ">= 0.8" 641 | } 642 | }, 643 | "node_modules/iconv-lite": { 644 | "version": "0.6.3", 645 | "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", 646 | "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", 647 | "license": "MIT", 648 | "dependencies": { 649 | "safer-buffer": ">= 2.1.2 < 3.0.0" 650 | }, 651 | "engines": { 652 | "node": ">=0.10.0" 653 | } 654 | }, 655 | "node_modules/inherits": { 656 | "version": "2.0.4", 657 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 658 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", 659 | "license": "ISC" 660 | }, 661 | "node_modules/ipaddr.js": { 662 | "version": "1.9.1", 663 | "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", 664 | "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", 665 | "license": "MIT", 666 | "engines": { 667 | "node": ">= 0.10" 668 | } 669 | }, 670 | "node_modules/is-docker": { 671 | "version": "3.0.0", 672 | "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", 673 | "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", 674 | "license": "MIT", 675 | "bin": { 676 | "is-docker": "cli.js" 677 | }, 678 | "engines": { 679 | "node": "^12.20.0 || ^14.13.1 || >=16.0.0" 680 | }, 681 | "funding": { 682 | "url": "https://github.com/sponsors/sindresorhus" 683 | } 684 | }, 685 | "node_modules/is-inside-container": { 686 | "version": "1.0.0", 687 | "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", 688 | "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", 689 | "license": "MIT", 690 | "dependencies": { 691 | "is-docker": "^3.0.0" 692 | }, 693 | "bin": { 694 | "is-inside-container": "cli.js" 695 | }, 696 | "engines": { 697 | "node": ">=14.16" 698 | }, 699 | "funding": { 700 | "url": "https://github.com/sponsors/sindresorhus" 701 | } 702 | }, 703 | "node_modules/is-promise": { 704 | "version": "4.0.0", 705 | "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", 706 | "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", 707 | "license": "MIT" 708 | }, 709 | "node_modules/is-wsl": { 710 | "version": "3.1.0", 711 | "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", 712 | "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", 713 | "license": "MIT", 714 | "dependencies": { 715 | "is-inside-container": "^1.0.0" 716 | }, 717 | "engines": { 718 | "node": ">=16" 719 | }, 720 | "funding": { 721 | "url": "https://github.com/sponsors/sindresorhus" 722 | } 723 | }, 724 | "node_modules/math-intrinsics": { 725 | "version": "1.1.0", 726 | "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", 727 | "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", 728 | "license": "MIT", 729 | "engines": { 730 | "node": ">= 0.4" 731 | } 732 | }, 733 | "node_modules/media-typer": { 734 | "version": "1.1.0", 735 | "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", 736 | "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", 737 | "license": "MIT", 738 | "engines": { 739 | "node": ">= 0.8" 740 | } 741 | }, 742 | "node_modules/merge-descriptors": { 743 | "version": "2.0.0", 744 | "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", 745 | "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", 746 | "license": "MIT", 747 | "engines": { 748 | "node": ">=18" 749 | }, 750 | "funding": { 751 | "url": "https://github.com/sponsors/sindresorhus" 752 | } 753 | }, 754 | "node_modules/mime-db": { 755 | "version": "1.54.0", 756 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", 757 | "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", 758 | "license": "MIT", 759 | "engines": { 760 | "node": ">= 0.6" 761 | } 762 | }, 763 | "node_modules/mime-types": { 764 | "version": "3.0.1", 765 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", 766 | "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", 767 | "license": "MIT", 768 | "dependencies": { 769 | "mime-db": "^1.54.0" 770 | }, 771 | "engines": { 772 | "node": ">= 0.6" 773 | } 774 | }, 775 | "node_modules/ms": { 776 | "version": "2.1.3", 777 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 778 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 779 | "license": "MIT" 780 | }, 781 | "node_modules/negotiator": { 782 | "version": "1.0.0", 783 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 784 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 785 | "license": "MIT", 786 | "engines": { 787 | "node": ">= 0.6" 788 | } 789 | }, 790 | "node_modules/object-inspect": { 791 | "version": "1.13.4", 792 | "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", 793 | "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", 794 | "license": "MIT", 795 | "engines": { 796 | "node": ">= 0.4" 797 | }, 798 | "funding": { 799 | "url": "https://github.com/sponsors/ljharb" 800 | } 801 | }, 802 | "node_modules/on-finished": { 803 | "version": "2.4.1", 804 | "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", 805 | "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", 806 | "license": "MIT", 807 | "dependencies": { 808 | "ee-first": "1.1.1" 809 | }, 810 | "engines": { 811 | "node": ">= 0.8" 812 | } 813 | }, 814 | "node_modules/on-headers": { 815 | "version": "1.0.2", 816 | "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", 817 | "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", 818 | "license": "MIT", 819 | "engines": { 820 | "node": ">= 0.8" 821 | } 822 | }, 823 | "node_modules/once": { 824 | "version": "1.4.0", 825 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 826 | "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", 827 | "license": "ISC", 828 | "dependencies": { 829 | "wrappy": "1" 830 | } 831 | }, 832 | "node_modules/open": { 833 | "version": "10.1.1", 834 | "resolved": "https://registry.npmjs.org/open/-/open-10.1.1.tgz", 835 | "integrity": "sha512-zy1wx4+P3PfhXSEPJNtZmJXfhkkIaxU1VauWIrDZw1O7uJRDRJtKr9n3Ic4NgbA16KyOxOXO2ng9gYwCdXuSXA==", 836 | "license": "MIT", 837 | "dependencies": { 838 | "default-browser": "^5.2.1", 839 | "define-lazy-prop": "^3.0.0", 840 | "is-inside-container": "^1.0.0", 841 | "is-wsl": "^3.1.0" 842 | }, 843 | "engines": { 844 | "node": ">=18" 845 | }, 846 | "funding": { 847 | "url": "https://github.com/sponsors/sindresorhus" 848 | } 849 | }, 850 | "node_modules/parseurl": { 851 | "version": "1.3.3", 852 | "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", 853 | "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", 854 | "license": "MIT", 855 | "engines": { 856 | "node": ">= 0.8" 857 | } 858 | }, 859 | "node_modules/path-to-regexp": { 860 | "version": "8.2.0", 861 | "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", 862 | "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", 863 | "license": "MIT", 864 | "engines": { 865 | "node": ">=16" 866 | } 867 | }, 868 | "node_modules/proxy-addr": { 869 | "version": "2.0.7", 870 | "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", 871 | "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", 872 | "license": "MIT", 873 | "dependencies": { 874 | "forwarded": "0.2.0", 875 | "ipaddr.js": "1.9.1" 876 | }, 877 | "engines": { 878 | "node": ">= 0.10" 879 | } 880 | }, 881 | "node_modules/proxy-from-env": { 882 | "version": "1.1.0", 883 | "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", 884 | "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", 885 | "license": "MIT" 886 | }, 887 | "node_modules/qs": { 888 | "version": "6.14.0", 889 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", 890 | "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", 891 | "license": "BSD-3-Clause", 892 | "dependencies": { 893 | "side-channel": "^1.1.0" 894 | }, 895 | "engines": { 896 | "node": ">=0.6" 897 | }, 898 | "funding": { 899 | "url": "https://github.com/sponsors/ljharb" 900 | } 901 | }, 902 | "node_modules/random-bytes": { 903 | "version": "1.0.0", 904 | "resolved": "https://registry.npmjs.org/random-bytes/-/random-bytes-1.0.0.tgz", 905 | "integrity": "sha512-iv7LhNVO047HzYR3InF6pUcUsPQiHTM1Qal51DcGSuZFBil1aBBWG5eHPNek7bvILMaYJ/8RU1e8w1AMdHmLQQ==", 906 | "license": "MIT", 907 | "engines": { 908 | "node": ">= 0.8" 909 | } 910 | }, 911 | "node_modules/range-parser": { 912 | "version": "1.2.1", 913 | "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", 914 | "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", 915 | "license": "MIT", 916 | "engines": { 917 | "node": ">= 0.6" 918 | } 919 | }, 920 | "node_modules/raw-body": { 921 | "version": "3.0.0", 922 | "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", 923 | "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", 924 | "license": "MIT", 925 | "dependencies": { 926 | "bytes": "3.1.2", 927 | "http-errors": "2.0.0", 928 | "iconv-lite": "0.6.3", 929 | "unpipe": "1.0.0" 930 | }, 931 | "engines": { 932 | "node": ">= 0.8" 933 | } 934 | }, 935 | "node_modules/router": { 936 | "version": "2.2.0", 937 | "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", 938 | "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", 939 | "license": "MIT", 940 | "dependencies": { 941 | "debug": "^4.4.0", 942 | "depd": "^2.0.0", 943 | "is-promise": "^4.0.0", 944 | "parseurl": "^1.3.3", 945 | "path-to-regexp": "^8.0.0" 946 | }, 947 | "engines": { 948 | "node": ">= 18" 949 | } 950 | }, 951 | "node_modules/run-applescript": { 952 | "version": "7.0.0", 953 | "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", 954 | "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", 955 | "license": "MIT", 956 | "engines": { 957 | "node": ">=18" 958 | }, 959 | "funding": { 960 | "url": "https://github.com/sponsors/sindresorhus" 961 | } 962 | }, 963 | "node_modules/safe-buffer": { 964 | "version": "5.2.1", 965 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 966 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 967 | "funding": [ 968 | { 969 | "type": "github", 970 | "url": "https://github.com/sponsors/feross" 971 | }, 972 | { 973 | "type": "patreon", 974 | "url": "https://www.patreon.com/feross" 975 | }, 976 | { 977 | "type": "consulting", 978 | "url": "https://feross.org/support" 979 | } 980 | ], 981 | "license": "MIT" 982 | }, 983 | "node_modules/safer-buffer": { 984 | "version": "2.1.2", 985 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 986 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", 987 | "license": "MIT" 988 | }, 989 | "node_modules/send": { 990 | "version": "1.2.0", 991 | "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", 992 | "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", 993 | "license": "MIT", 994 | "dependencies": { 995 | "debug": "^4.3.5", 996 | "encodeurl": "^2.0.0", 997 | "escape-html": "^1.0.3", 998 | "etag": "^1.8.1", 999 | "fresh": "^2.0.0", 1000 | "http-errors": "^2.0.0", 1001 | "mime-types": "^3.0.1", 1002 | "ms": "^2.1.3", 1003 | "on-finished": "^2.4.1", 1004 | "range-parser": "^1.2.1", 1005 | "statuses": "^2.0.1" 1006 | }, 1007 | "engines": { 1008 | "node": ">= 18" 1009 | } 1010 | }, 1011 | "node_modules/serve-static": { 1012 | "version": "2.2.0", 1013 | "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", 1014 | "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", 1015 | "license": "MIT", 1016 | "dependencies": { 1017 | "encodeurl": "^2.0.0", 1018 | "escape-html": "^1.0.3", 1019 | "parseurl": "^1.3.3", 1020 | "send": "^1.2.0" 1021 | }, 1022 | "engines": { 1023 | "node": ">= 18" 1024 | } 1025 | }, 1026 | "node_modules/setprototypeof": { 1027 | "version": "1.2.0", 1028 | "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", 1029 | "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", 1030 | "license": "ISC" 1031 | }, 1032 | "node_modules/side-channel": { 1033 | "version": "1.1.0", 1034 | "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", 1035 | "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", 1036 | "license": "MIT", 1037 | "dependencies": { 1038 | "es-errors": "^1.3.0", 1039 | "object-inspect": "^1.13.3", 1040 | "side-channel-list": "^1.0.0", 1041 | "side-channel-map": "^1.0.1", 1042 | "side-channel-weakmap": "^1.0.2" 1043 | }, 1044 | "engines": { 1045 | "node": ">= 0.4" 1046 | }, 1047 | "funding": { 1048 | "url": "https://github.com/sponsors/ljharb" 1049 | } 1050 | }, 1051 | "node_modules/side-channel-list": { 1052 | "version": "1.0.0", 1053 | "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", 1054 | "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", 1055 | "license": "MIT", 1056 | "dependencies": { 1057 | "es-errors": "^1.3.0", 1058 | "object-inspect": "^1.13.3" 1059 | }, 1060 | "engines": { 1061 | "node": ">= 0.4" 1062 | }, 1063 | "funding": { 1064 | "url": "https://github.com/sponsors/ljharb" 1065 | } 1066 | }, 1067 | "node_modules/side-channel-map": { 1068 | "version": "1.0.1", 1069 | "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", 1070 | "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", 1071 | "license": "MIT", 1072 | "dependencies": { 1073 | "call-bound": "^1.0.2", 1074 | "es-errors": "^1.3.0", 1075 | "get-intrinsic": "^1.2.5", 1076 | "object-inspect": "^1.13.3" 1077 | }, 1078 | "engines": { 1079 | "node": ">= 0.4" 1080 | }, 1081 | "funding": { 1082 | "url": "https://github.com/sponsors/ljharb" 1083 | } 1084 | }, 1085 | "node_modules/side-channel-weakmap": { 1086 | "version": "1.0.2", 1087 | "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", 1088 | "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", 1089 | "license": "MIT", 1090 | "dependencies": { 1091 | "call-bound": "^1.0.2", 1092 | "es-errors": "^1.3.0", 1093 | "get-intrinsic": "^1.2.5", 1094 | "object-inspect": "^1.13.3", 1095 | "side-channel-map": "^1.0.1" 1096 | }, 1097 | "engines": { 1098 | "node": ">= 0.4" 1099 | }, 1100 | "funding": { 1101 | "url": "https://github.com/sponsors/ljharb" 1102 | } 1103 | }, 1104 | "node_modules/statuses": { 1105 | "version": "2.0.1", 1106 | "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", 1107 | "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", 1108 | "license": "MIT", 1109 | "engines": { 1110 | "node": ">= 0.8" 1111 | } 1112 | }, 1113 | "node_modules/toidentifier": { 1114 | "version": "1.0.1", 1115 | "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", 1116 | "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", 1117 | "license": "MIT", 1118 | "engines": { 1119 | "node": ">=0.6" 1120 | } 1121 | }, 1122 | "node_modules/type-is": { 1123 | "version": "2.0.1", 1124 | "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", 1125 | "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", 1126 | "license": "MIT", 1127 | "dependencies": { 1128 | "content-type": "^1.0.5", 1129 | "media-typer": "^1.1.0", 1130 | "mime-types": "^3.0.0" 1131 | }, 1132 | "engines": { 1133 | "node": ">= 0.6" 1134 | } 1135 | }, 1136 | "node_modules/uid-safe": { 1137 | "version": "2.1.5", 1138 | "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", 1139 | "integrity": "sha512-KPHm4VL5dDXKz01UuEd88Df+KzynaohSL9fBh096KWAxSKZQDI2uBrVqtvRM4rwrIrRRKsdLNML/lnaaVSRioA==", 1140 | "license": "MIT", 1141 | "dependencies": { 1142 | "random-bytes": "~1.0.0" 1143 | }, 1144 | "engines": { 1145 | "node": ">= 0.8" 1146 | } 1147 | }, 1148 | "node_modules/unpipe": { 1149 | "version": "1.0.0", 1150 | "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", 1151 | "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", 1152 | "license": "MIT", 1153 | "engines": { 1154 | "node": ">= 0.8" 1155 | } 1156 | }, 1157 | "node_modules/vary": { 1158 | "version": "1.1.2", 1159 | "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", 1160 | "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", 1161 | "license": "MIT", 1162 | "engines": { 1163 | "node": ">= 0.8" 1164 | } 1165 | }, 1166 | "node_modules/wrappy": { 1167 | "version": "1.0.2", 1168 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1169 | "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1170 | "license": "ISC" 1171 | } 1172 | } 1173 | } 1174 | --------------------------------------------------------------------------------