UNPKG

@auth0/auth0-mcp-server

Version:

Auth0 Model Context Protocol (MCP) Server (Beta) — A secure and extendable implementation of an MCP server that provides AI assistants with controlled access to the Auth0 Management API through natural language. This project is in beta and not intended fo

293 lines • 10.9 kB
import chalk from 'chalk'; import open from 'open'; import { startSpinner, stopSpinner, getTenantFromToken, cliOutput, promptForBrowserPermission, } from '../utils/terminal.js'; import { log, logError } from '../utils/logger.js'; import { keychain } from '../utils/keychain.js'; import { DEFAULT_SCOPES } from '../utils/scopes.js'; function getConfig(selectedScopes) { // If selectedScopes is provided, use those scopes // If not provided or empty, use DEFAULT_SCOPES (which is now empty by default) const scopes = selectedScopes && selectedScopes.length > 0 ? selectedScopes.join(' ') : DEFAULT_SCOPES.join(' '); return { tenant: 'auth0.auth0.com', clientId: '2lhnuYMRQ8IpR5hNsOhDFQqrGQUQMRm5', audience: 'https://*.auth0.com/api/v2/', scopes, }; } async function requestAuthorization(selectedScopes, interaction = true) { const config = getConfig(selectedScopes); const body = { client_id: config.clientId, }; if (config.audience) { body.audience = config.audience; } if (config.scopes.length) { body.scope = config.scopes; } try { const response = await fetch(`https://${config.tenant}/oauth/device/code`, { method: 'POST', body: new URLSearchParams(body), headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, }); const jsonRes = await response.json(); if (!jsonRes.error) { cliOutput(`\nPress ${chalk.bold('Enter')} to log in via the browser, your device code is: ${chalk.bold.green(jsonRes.user_code)}\n`); // Wait for user to press Enter to open browser if (interaction) { await promptForBrowserPermission(); } openBrowser(jsonRes.verification_uri_complete); await exchangeDeviceCodeForToken(jsonRes, selectedScopes); } else { logError('Error', jsonRes); process.exit(1); } } catch (err) { logError('Error', err); process.exit(1); } } function wait(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isValidUrl(url) { try { const parsedUrl = new URL(url); // Check for safe protocols return ['http:', 'https:'].includes(parsedUrl.protocol); } catch (error) { logError('Error', error); return false; } } function openBrowser(url) { if (!url || !isValidUrl(url)) { logError('Invalid URL provided:', url); return; } open(url) .then(() => { log('Browser opened successfully'); }) .catch((err) => { logError('Failed to open browser:', err); }); } async function exchangeDeviceCodeForToken(deviceCode, selectedScopes) { const config = getConfig(selectedScopes); startSpinner('Waiting for authorization...'); while (true) { try { const response = await fetch(`https://${config.tenant}/oauth/token`, { method: 'POST', body: new URLSearchParams({ client_id: config.clientId, device_code: deviceCode.device_code, grant_type: 'urn:ietf:params:oauth:grant-type:device_code', }), headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', }, }); const jsonRes = await response.json(); if (!jsonRes.error) { fetchUserInfo(jsonRes); // Success case break; // Exit loop once successful } else if (['authorization_pending', 'slow_down'].includes(jsonRes.error)) { await wait(5000); // Wait before polling again } else { stopSpinner(); logError('Unexpected error:', jsonRes.error); break; // Exit loop on unknown error } } catch (err) { stopSpinner(); logError('Error in token exchange:', err); break; // Exit loop on fetch failure } } stopSpinner(); } async function fetchUserInfo(tokenSet) { const tenantName = getTenantFromToken(tokenSet.access_token); // Store tokens in keychain await keychain.setToken(tokenSet.access_token); await keychain.setDomain(tenantName); if (tokenSet.refresh_token) { await keychain.setRefreshToken(tokenSet.refresh_token); log('Refresh token stored in keychain'); } if (tokenSet.expires_in) { const expiresAt = Date.now() + tokenSet.expires_in * 1000; await keychain.setTokenExpiresAt(expiresAt); log(`Token expires at: ${new Date(expiresAt).toISOString()}`); } } export async function refreshAccessToken(selectedScopes) { try { log('Attempting to refresh access token'); const refreshToken = await keychain.getRefreshToken(); if (!refreshToken) { log('No refresh token found in keychain'); return null; } const config = getConfig(selectedScopes); const response = await fetch(`https://${config.tenant}/oauth/token`, { method: 'POST', body: new URLSearchParams({ grant_type: 'refresh_token', client_id: config.clientId, refresh_token: refreshToken, }), headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); const tokenSet = await response.json(); if (tokenSet.error) { log(`Error refreshing token: ${tokenSet.error}`); return null; } // Store new tokens const tenantName = getTenantFromToken(tokenSet.access_token); await keychain.setToken(tokenSet.access_token); await keychain.setDomain(tenantName); if (tokenSet.refresh_token) { await keychain.setRefreshToken(tokenSet.refresh_token); } if (tokenSet.expires_in) { const expiresAt = Date.now() + tokenSet.expires_in * 1000; await keychain.setTokenExpiresAt(expiresAt); } log('Successfully refreshed access token'); return tokenSet.access_token; } catch (error) { log('Error refreshing access token:', error); return null; } } /** * Revokes the refresh token that is previously set within keychain when offline_access is requested. * Returns true if the call is successful or if the refresh token does not exist. * @returns {Promise<boolean>} */ export async function revokeRefreshToken() { try { log('Attempting to revoke refresh token'); const refreshToken = await keychain.getRefreshToken(); if (!refreshToken) { log('No refresh token found in keychain'); return true; } const config = getConfig(); const response = await fetch(`https://${config.tenant}/oauth/revoke`, { method: 'POST', body: new URLSearchParams({ client_id: config.clientId, token: refreshToken, }), headers: { 'Content-Type': 'application/x-www-form-urlencoded', }, }); if (response.status === 200) { log('Refresh token successfully revoked'); return true; } else { log('Error calling revoke API: ', response.statusText); return false; } } catch (error) { log('Error revoking refresh token:', error); return false; } } /** * Determines if the current access token is expired or will expire soon. * * This security check is crucial for maintaining continuous authenticated access * to Auth0 APIs. It includes a configurable buffer time to proactively detect * tokens that will expire soon, preventing potential disruptions during operations * that might span multiple API calls. This proactive approach allows the system to * initiate refresh flows before actual expiration occurs. * * The function considers a token expired in the following cases: * - No expiration time is found in the keychain via `keychain.getTokenExpiresAt()` * - Current time + buffer exceeds the token's expiration time * - Error occurs during expiration check (fails secure) * * This function is used both by `validateAuthorization()` in `run.ts` for user-friendly * startup validation and by `validateConfig()` for continuous runtime validation. * * @param {number} bufferSeconds - Seconds before actual expiration to consider token expired (default: 300s/5min) * @returns {Promise<boolean>} True if token is expired or will expire within the buffer period */ export async function isTokenExpired(bufferSeconds = 300) { try { const expiresAt = await keychain.getTokenExpiresAt(); if (!expiresAt) { log('No token expiration time found'); return true; } const now = Date.now(); const isExpired = now + bufferSeconds * 1000 >= expiresAt; if (isExpired) { log(`Token is expired or will expire soon. Expires at: ${new Date(expiresAt).toISOString()}`); } return isExpired; } catch (error) { log('Error checking token expiration:', error); return true; } } /** * Retrieves a valid access token for Auth0 API operations. * * This function serves as the main entry point for credential retrieval, * ensuring that only valid, non-expired tokens are provided to API operations. * It implements a critical security checkpoint that prevents operations from * proceeding with invalid authentication, which could lead to API failures * or unpredictable behavior. * * The function performs these key security checks: * 1. Verifies token expiration status using isTokenExpired * 2. Provides clear guidance to users when re-authentication is needed * 3. Handles errors gracefully with a fail-secure approach * * @returns {Promise<string|null>} A valid access token, or null if no valid token is available */ export async function getValidAccessToken() { try { const expired = await isTokenExpired(); if (expired) { log('Token is expired. Please authenticate again using `npx @auth0/auth0-mcp-server init`'); return null; } return await keychain.getToken(); } catch (error) { log('Error getting valid access token:', error); return null; } } export { requestAuthorization }; //# sourceMappingURL=device-auth-flow.js.map