UNPKG

@access-mcp/shared

Version:

Shared utilities for ACCESS-CI MCP servers

189 lines (188 loc) 6.45 kB
import axios from "axios"; /** * Authentication provider for Drupal JSON:API using cookie-based auth. * * This is a temporary implementation for development/testing. * Production should use Key Auth with the access_mcp_author module. * * @see ../../../access-qa-planning/06-mcp-authentication.md */ export class DrupalAuthProvider { baseUrl; username; password; sessionCookie; csrfToken; logoutToken; userUuid; httpClient; isAuthenticated = false; constructor(baseUrl, username, password) { this.baseUrl = baseUrl; this.username = username; this.password = password; this.httpClient = axios.create({ baseURL: this.baseUrl, timeout: 30000, validateStatus: () => true, }); } /** * Ensure we have a valid session, logging in if necessary */ async ensureAuthenticated() { if (!this.isAuthenticated) { await this.login(); } } /** * Login to Drupal and store session cookie + CSRF token */ async login() { const response = await this.httpClient.post("/user/login?_format=json", { name: this.username, pass: this.password, }, { headers: { "Content-Type": "application/json", }, }); if (response.status !== 200) { throw new Error(`Drupal login failed: ${response.status} ${response.statusText}`); } // Extract session cookie from Set-Cookie header const setCookie = response.headers["set-cookie"]; if (setCookie && setCookie.length > 0) { // Parse the session cookie (format: SESS...=value; path=/; ...) const cookieParts = setCookie[0].split(";")[0]; this.sessionCookie = cookieParts; } // Store CSRF token and logout token from response this.csrfToken = response.data.csrf_token; this.logoutToken = response.data.logout_token; this.userUuid = response.data.current_user?.uuid; if (!this.sessionCookie || !this.csrfToken) { throw new Error("Login succeeded but missing session cookie or CSRF token"); } this.isAuthenticated = true; } /** * Get headers required for authenticated JSON:API requests */ getAuthHeaders() { if (!this.isAuthenticated || !this.sessionCookie || !this.csrfToken) { throw new Error("Not authenticated. Call ensureAuthenticated() first."); } return { Cookie: this.sessionCookie, "X-CSRF-Token": this.csrfToken, "Content-Type": "application/vnd.api+json", Accept: "application/vnd.api+json", }; } /** * Get the authenticated user's UUID */ getUserUuid() { return this.userUuid; } /** * Invalidate the current session */ invalidate() { this.sessionCookie = undefined; this.csrfToken = undefined; this.logoutToken = undefined; this.userUuid = undefined; this.isAuthenticated = false; } /** * Make an authenticated GET request to JSON:API */ async get(path) { await this.ensureAuthenticated(); const response = await this.httpClient.get(path, { headers: this.getAuthHeaders(), }); if (response.status === 401 || response.status === 403) { // Session may have expired, try re-authenticating this.invalidate(); await this.ensureAuthenticated(); const retryResponse = await this.httpClient.get(path, { headers: this.getAuthHeaders(), }); return this.handleResponse(retryResponse); } return this.handleResponse(response); } /** * Make an authenticated POST request to JSON:API */ async post(path, data) { await this.ensureAuthenticated(); const response = await this.httpClient.post(path, data, { headers: this.getAuthHeaders(), }); if (response.status === 401 || response.status === 403) { this.invalidate(); await this.ensureAuthenticated(); const retryResponse = await this.httpClient.post(path, data, { headers: this.getAuthHeaders(), }); return this.handleResponse(retryResponse); } return this.handleResponse(response); } /** * Make an authenticated PATCH request to JSON:API */ async patch(path, data) { await this.ensureAuthenticated(); const response = await this.httpClient.patch(path, data, { headers: this.getAuthHeaders(), }); if (response.status === 401 || response.status === 403) { this.invalidate(); await this.ensureAuthenticated(); const retryResponse = await this.httpClient.patch(path, data, { headers: this.getAuthHeaders(), }); return this.handleResponse(retryResponse); } return this.handleResponse(response); } /** * Make an authenticated DELETE request to JSON:API */ async delete(path) { await this.ensureAuthenticated(); const response = await this.httpClient.delete(path, { headers: this.getAuthHeaders(), }); if (response.status === 401 || response.status === 403) { this.invalidate(); await this.ensureAuthenticated(); const retryResponse = await this.httpClient.delete(path, { headers: this.getAuthHeaders(), }); return this.handleResponse(retryResponse); } return this.handleResponse(response); } /** * Handle JSON:API response, throwing on errors */ handleResponse(response) { if (response.status >= 200 && response.status < 300) { return response.data; } // JSON:API error format if (response.data?.errors) { const errors = response.data.errors .map((e) => e.detail || e.title || "Unknown error") .join("; "); throw new Error(`Drupal API error (${response.status}): ${errors}`); } throw new Error(`Drupal API error: ${response.status} ${response.statusText}`); } }