@access-mcp/shared
Version:
Shared utilities for ACCESS-CI MCP servers
189 lines (188 loc) • 6.45 kB
JavaScript
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}`);
}
}