Skip to main content
Glama
device-auth-flow.ts9.92 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?: string[]) { // 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?: string[]) { const config = getConfig(selectedScopes); const body: any = { 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(`\nVerify this code on screen: ${chalk.bold.green(jsonRes.user_code)}\n`); // Wait for user to press Enter to open browser 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: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } function isValidUrl(url: string): boolean { 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: string) { 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: any, selectedScopes?: string[]) { 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: any) { 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?: string[]): Promise<string | null> { 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(): Promise<boolean> { 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): Promise<boolean> { 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(): Promise<string | null> { 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 };

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/auth0/auth0-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server