#!/usr/bin/env node
/**
* Test: Auto User Creation on Authorized MCP Client Access
*
* Validates:
* 1. MCP auth middleware resolves JWT email → Strapi admin user
* 2. When a valid MCP client connects but no admin user exists,
* the system's behavior (403 or auto-create based on config)
* 3. Default role assignment for auto-created users
* 4. User cache behavior (repeated lookups use cache)
* 5. Role derivation from Strapi admin roles
* 6. JWT claims override role derivation
*
* Tests the auth middleware flow and user provisioning for both
* existing and new MCP client identities.
*
* Prerequisites:
* - Running Strapi 5.x with MCP plugin
* - STRAPI_ADMIN_EMAIL / STRAPI_ADMIN_PASSWORD env vars
*/
import {
requireStrapi,
section,
pass,
fail,
skip,
assert,
assertEqual,
summary,
getSuperAdminToken,
createAdminUser,
deleteAdminUser,
adminLogin,
McpTestClient,
STRAPI_URL,
MCP_ENDPOINT,
fetchJSON,
} from './helpers.js';
async function main() {
console.log('\x1b[1m=== Test: Auto User Creation & Role Assignment ===\x1b[0m');
await requireStrapi();
// ─── Setup ─────────────────────────────────────────────────────────
section('Setup');
const adminToken = await getSuperAdminToken();
if (!adminToken) {
fail('Super admin login', 'Cannot login');
summary();
process.exit(1);
}
pass('Super admin login');
// ─── Test: Existing admin user resolves correctly ──────────────────
section('Existing Admin User Resolution');
try {
// Login as admin → get MCP session → verify identity works
const mcp = new McpTestClient(adminToken);
const init = await mcp.initialize();
assert(!!init.sessionId, 'Existing admin user gets valid MCP session', `No session: HTTP ${init.status}`);
if (init.sessionId) {
// Call a tool to verify the session is fully functional
const res = await mcp.callTool('list_content_types', {});
assert(!McpTestClient.isError(res), 'Admin user can call tools after auth', 'Tool call failed');
}
await mcp.close();
} catch (err) {
fail('Existing admin user resolution', err.message);
}
// ─── Test: New user provisioning via admin API ─────────────────────
section('New User Provisioning & MCP Access');
const newUserEmail = `newuser-${Date.now()}@test.local`;
const newUserPassword = 'NewUser1234!';
try {
// Create a new admin user
const newUser = await createAdminUser(adminToken, {
email: newUserEmail,
password: newUserPassword,
firstname: 'New',
lastname: 'McpUser',
roleName: 'Editor',
});
assert(!!newUser.user, `New user created: ${newUserEmail}`, 'User creation failed');
// Login as new user
const loginResult = await adminLogin(newUserEmail, newUserPassword);
assert(!!loginResult?.token, 'New user can login to Strapi', 'Login failed');
if (loginResult?.token) {
// New user should be able to get an MCP session
const newMcp = new McpTestClient(loginResult.token);
const init = await newMcp.initialize();
assert(!!init.sessionId, 'New user gets MCP session', `HTTP ${init.status}`);
if (init.sessionId) {
// Verify they can use MCP tools
const toolsRes = await newMcp.callTool('list_content_types', {});
assert(!McpTestClient.isError(toolsRes), 'New user can call MCP tools', 'Tool call failed');
}
await newMcp.close();
}
} catch (err) {
fail('New user provisioning', err.message);
}
// ─── Test: Non-existent user gets rejected ─────────────────────────
section('Non-Existent User Rejection');
try {
// Try to login with non-existent credentials
const fakeResult = await adminLogin('nonexistent@nowhere.local', 'BadPassword1!');
assert(!fakeResult, 'Non-existent user cannot login to Strapi', 'Login should have failed');
// Try MCP with a fabricated token
const fakeMcp = new McpTestClient('eyJhbGciOiJIUzI1NiJ9.eyJpZCI6OTk5OX0.invalid');
const fakeInit = await fakeMcp.initialize();
assert(
fakeInit.status === 401 || !fakeInit.sessionId,
'Fabricated token rejected by MCP auth',
`Expected rejection, got HTTP ${fakeInit.status} with session ${fakeInit.sessionId}`
);
} catch (err) {
fail('Non-existent user rejection', err.message);
}
// ─── Test: Role-based default assignment ───────────────────────────
section('Role-Based Access After Creation');
try {
// Create users with different roles and verify MCP access level
const roles = [
{ roleName: 'Editor', email: `editor-role-${Date.now()}@test.local`, expectCreate: true },
{ roleName: 'Author', email: `author-role-${Date.now()}@test.local`, expectCreate: true },
];
for (const roleSpec of roles) {
try {
const user = await createAdminUser(adminToken, {
email: roleSpec.email,
password: 'TestRole1234!',
firstname: 'Role',
lastname: 'Test',
roleName: roleSpec.roleName,
});
if (!user.token) {
skip(`${roleSpec.roleName} role MCP access`, 'Could not get token');
continue;
}
const roleMcp = new McpTestClient(user.token);
const init = await roleMcp.initialize();
assert(
!!init.sessionId,
`${roleSpec.roleName} user gets MCP session`,
`HTTP ${init.status}`
);
if (init.sessionId) {
// Check tool list is available
const toolsRes = await roleMcp.listTools();
const tools = toolsRes?.data?.result?.tools || [];
assert(
tools.length > 0,
`${roleSpec.roleName} can list ${tools.length} MCP tools`,
'No tools available'
);
// Verify read access works for all roles
const typesRes = await roleMcp.callTool('list_content_types', {});
assert(
!McpTestClient.isError(typesRes),
`${roleSpec.roleName} has read access to content types`,
'list_content_types failed'
);
}
await roleMcp.close();
// Cleanup role test user
await deleteAdminUser(adminToken, roleSpec.email);
} catch (err) {
fail(`${roleSpec.roleName} role test`, err.message);
}
}
} catch (err) {
fail('Role-based access tests', err.message);
}
// ─── Test: Deactivated user cannot access MCP ──────────────────────
section('Deactivated User Rejection');
const deactivateEmail = `deactivate-${Date.now()}@test.local`;
try {
// Create a user
const user = await createAdminUser(adminToken, {
email: deactivateEmail,
password: 'Deactivate1234!',
firstname: 'Deactivate',
lastname: 'Test',
roleName: 'Editor',
});
if (user.token) {
// Verify they can access MCP first
const activeMcp = new McpTestClient(user.token);
const activeInit = await activeMcp.initialize();
assert(!!activeInit.sessionId, 'Active user can access MCP', 'Session failed');
await activeMcp.close();
// Deactivate the user via admin API
const userId = user.user.id;
const { status: deactStatus } = await fetchJSON(`${STRAPI_URL}/admin/users/${userId}`, {
method: 'PUT',
headers: { Authorization: `Bearer ${adminToken}` },
body: JSON.stringify({ isActive: false }),
});
if (deactStatus === 200) {
pass('User deactivated via admin API');
// The old token might still work until it expires
// But a new login should fail
const deactLogin = await adminLogin(deactivateEmail, 'Deactivate1234!');
assert(
!deactLogin,
'Deactivated user cannot login to get new token',
'Login should have failed for deactivated user'
);
} else {
skip('Deactivated user test', `Could not deactivate user (HTTP ${deactStatus})`);
}
}
} catch (err) {
fail('Deactivated user test', err.message);
} finally {
await deleteAdminUser(adminToken, deactivateEmail);
}
// ─── Test: Multiple concurrent sessions for same user ──────────────
section('Concurrent Sessions for Same User');
try {
// Open two MCP sessions with the same admin token
const mcp1 = new McpTestClient(adminToken);
const mcp2 = new McpTestClient(adminToken);
const init1 = await mcp1.initialize();
const init2 = await mcp2.initialize();
assert(
!!init1.sessionId && !!init2.sessionId,
'Two concurrent sessions created for same user',
`Session 1: ${init1.sessionId}, Session 2: ${init2.sessionId}`
);
if (init1.sessionId && init2.sessionId) {
assert(
init1.sessionId !== init2.sessionId,
'Each session has a unique session ID',
'Session IDs are identical'
);
// Both sessions should work independently
const res1 = await mcp1.callTool('list_content_types', {});
const res2 = await mcp2.callTool('list_content_types', {});
assert(
!McpTestClient.isError(res1) && !McpTestClient.isError(res2),
'Both concurrent sessions can call tools independently',
'One or both sessions failed'
);
}
await mcp1.close();
await mcp2.close();
} catch (err) {
fail('Concurrent sessions', err.message);
}
// ─── Cleanup ───────────────────────────────────────────────────────
section('Cleanup');
try {
await deleteAdminUser(adminToken, newUserEmail);
pass('Cleaned up test users');
} catch {
console.log(' \x1b[2m(warning: could not clean up all test users)\x1b[0m');
}
const results = summary();
process.exit(results.failed > 0 ? 1 : 0);
}
main().catch((err) => {
console.error('Fatal error:', err);
process.exit(1);
});