Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,29 @@ export async function startServer(options?: ServerOptions) {

try {
// Check available (filtered) tools, not all handlers
const isToolAvailable = availableTools.some((t) => t.name === toolName);
if (!isToolAvailable || !HANDLERS[toolName]) {
const tool = availableTools.find((t) => t.name === toolName);
if (!tool || !HANDLERS[toolName]) {
throw new Error(`Unknown or restricted tool: ${toolName}`);
}

// Reject any argument not declared in the tool's inputSchema.
// MCP clients only surface schema-declared parameters for human approval,
// so forwarding undeclared parameters to the Auth0 Management API would let
// a prompt-injection attacker smuggle security-critical fields (e.g.
// custom_login_page, addons, client_authentication_methods) past the
// human-in-the-loop confirmation. Enforcing the schema as an allowlist
// closes that LLM-to-tool trust boundary gap.
const declaredParameters = tool.inputSchema?.properties;
if (declaredParameters) {
const allowedKeys = new Set(Object.keys(declaredParameters));
const undeclaredKeys = Object.keys(request.params.arguments || {}).filter(
(key) => !allowedKeys.has(key)
);
if (undeclaredKeys.length > 0) {
throw new Error(`Rejected undeclared parameters: ${undeclaredKeys.join(', ')}`);
}
}

// Check if config is still valid, reload if needed
if (!config || !(await validateConfig(config))) {
log('Config is invalid, attempting to reload');
Expand Down
136 changes: 69 additions & 67 deletions src/tools/applications.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import type {
ClientCreateAppTypeEnum,
ClientCreateOrganizationUsageEnum,
ClientCreateOrganizationRequireBehaviorEnum,
ClientCreateComplianceLevelEnum,
ClientCreate,
ClientUpdate,
} from 'auth0';
Expand Down Expand Up @@ -147,6 +146,49 @@ export const APPLICATION_TOOLS: Tool[] = [
'Token endpoint authentication method. When creating, defaults based on app_type: "none" for SPA/Native (public clients), "client_secret_post" for Regular Web/M2M (confidential clients).',
enum: ['none', 'client_secret_post', 'client_secret_basic'],
},
grant_types: {
type: 'array',
items: { type: 'string' },
description: 'List of grant types for this client',
},
jwt_configuration: {
type: 'object',
description: 'JWT configuration settings',
},
refresh_token: {
type: 'object',
description: 'Refresh token configuration',
},
mobile: {
type: 'object',
description: 'Mobile app configuration settings',
},
web_origins: {
type: 'array',
items: { type: 'string' },
description: 'URLs allowed to make cross-origin (CORS) requests to Auth0 from JavaScript.',
},
client_aliases: {
type: 'array',
items: { type: 'string' },
description: 'List of audiences or client identifiers used for SAML or delegation.',
},
cross_origin_loc: {
type: 'string',
description: 'URL of the location in your site where the cross-origin verification takes place for cross-origin authentication.',
},
oidc_logout: {
type: 'object',
description: 'Configuration for OIDC back-channel logout.',
},
sso: {
type: 'boolean',
description: 'Whether Single Sign On is enabled for this client.',
},
native_social_login: {
type: 'object',
description: 'Configuration for native social login (e.g. Apple, Facebook).',
},
},
required: ['name'],
},
Expand Down Expand Up @@ -258,6 +300,32 @@ export const APPLICATION_TOOLS: Tool[] = [
type: 'object',
description: 'Mobile app configuration settings',
},
web_origins: {
type: 'array',
items: { type: 'string' },
description: 'URLs allowed to make cross-origin (CORS) requests to Auth0 from JavaScript.',
},
client_aliases: {
type: 'array',
items: { type: 'string' },
description: 'List of audiences or client identifiers used for SAML or delegation.',
},
cross_origin_loc: {
type: 'string',
description: 'URL of the location in your site where the cross-origin verification takes place for cross-origin authentication.',
},
oidc_logout: {
type: 'object',
description: 'Configuration for OIDC back-channel logout.',
},
sso: {
type: 'boolean',
description: 'Whether Single Sign On is enabled for this client.',
},
native_social_login: {
type: 'object',
description: 'Configuration for native social login (e.g. Apple, Facebook).',
},
skip_non_verifiable_callback_uri_confirmation_prompt: {
type: 'boolean',
description:
Expand Down Expand Up @@ -555,28 +623,15 @@ export const APPLICATION_HANDLERS: Record<
is_first_party,
oidc_conformant,
jwt_configuration,
encryption_key,
sso,
cross_origin_authentication,
cross_origin_loc,
sso_disabled,
custom_login_page_on,
custom_login_page,
custom_login_page_preview,
form_template,
addons,
client_metadata,
mobile,
initiate_login_uri,
native_social_login,
refresh_token,
organization_usage,
organization_require_behavior,
client_authentication_methods,
require_pushed_authorization_requests,
signed_request_object,
require_proof_of_possession,
compliance_level,
} = request.parameters;

if (!name) {
Expand Down Expand Up @@ -625,39 +680,19 @@ export const APPLICATION_HANDLERS: Record<
if (is_first_party !== undefined) clientData.is_first_party = is_first_party;
if (oidc_conformant !== undefined) clientData.oidc_conformant = oidc_conformant;
if (jwt_configuration !== undefined) clientData.jwt_configuration = jwt_configuration;
if (encryption_key !== undefined) clientData.encryption_key = encryption_key;
if (sso !== undefined) clientData.sso = sso;
if (cross_origin_authentication !== undefined)
clientData.cross_origin_authentication = cross_origin_authentication;
if (cross_origin_loc !== undefined) clientData.cross_origin_loc = cross_origin_loc;
if (sso_disabled !== undefined) clientData.sso_disabled = sso_disabled;
if (custom_login_page_on !== undefined)
clientData.custom_login_page_on = custom_login_page_on;
if (custom_login_page !== undefined) clientData.custom_login_page = custom_login_page;
if (custom_login_page_preview !== undefined)
clientData.custom_login_page_preview = custom_login_page_preview;
if (form_template !== undefined) clientData.form_template = form_template;
if (addons !== undefined) clientData.addons = addons;
if (client_metadata !== undefined) clientData.client_metadata = client_metadata;
if (mobile !== undefined) clientData.mobile = mobile;
if (initiate_login_uri !== undefined) clientData.initiate_login_uri = initiate_login_uri;
if (native_social_login !== undefined) clientData.native_social_login = native_social_login;
if (refresh_token !== undefined) clientData.refresh_token = refresh_token;
if (organization_usage !== undefined)
clientData.organization_usage = organization_usage as ClientCreateOrganizationUsageEnum;
if (organization_require_behavior !== undefined)
clientData.organization_require_behavior =
organization_require_behavior as ClientCreateOrganizationRequireBehaviorEnum;
if (client_authentication_methods !== undefined)
clientData.client_authentication_methods = client_authentication_methods;
if (require_pushed_authorization_requests !== undefined)
clientData.require_pushed_authorization_requests = require_pushed_authorization_requests;
if (signed_request_object !== undefined)
clientData.signed_request_object = signed_request_object;
if (require_proof_of_possession !== undefined)
clientData.require_proof_of_possession = require_proof_of_possession;
if (compliance_level !== undefined)
clientData.compliance_level = compliance_level as ClientCreateComplianceLevelEnum;
if (callbacks && hasNonVerifiableCallbacks(callbacks)) {
clientData.skip_non_verifiable_callback_uri_confirmation_prompt = true;
}
Expand Down Expand Up @@ -764,28 +799,15 @@ export const APPLICATION_HANDLERS: Record<
is_first_party,
oidc_conformant,
jwt_configuration,
encryption_key,
sso,
cross_origin_authentication,
cross_origin_loc,
sso_disabled,
custom_login_page_on,
custom_login_page,
custom_login_page_preview,
form_template,
addons,
client_metadata,
mobile,
initiate_login_uri,
native_social_login,
refresh_token,
organization_usage,
organization_require_behavior,
client_authentication_methods,
require_pushed_authorization_requests,
signed_request_object,
require_proof_of_possession,
compliance_level,
skip_non_verifiable_callback_uri_confirmation_prompt,
} = request.parameters;

Expand All @@ -809,39 +831,19 @@ export const APPLICATION_HANDLERS: Record<
if (is_first_party !== undefined) updateData.is_first_party = is_first_party;
if (oidc_conformant !== undefined) updateData.oidc_conformant = oidc_conformant;
if (jwt_configuration !== undefined) updateData.jwt_configuration = jwt_configuration;
if (encryption_key !== undefined) updateData.encryption_key = encryption_key;
if (sso !== undefined) updateData.sso = sso;
if (cross_origin_authentication !== undefined)
updateData.cross_origin_authentication = cross_origin_authentication;
if (cross_origin_loc !== undefined) updateData.cross_origin_loc = cross_origin_loc;
if (sso_disabled !== undefined) updateData.sso_disabled = sso_disabled;
if (custom_login_page_on !== undefined)
updateData.custom_login_page_on = custom_login_page_on;
if (custom_login_page !== undefined) updateData.custom_login_page = custom_login_page;
if (custom_login_page_preview !== undefined)
updateData.custom_login_page_preview = custom_login_page_preview;
if (form_template !== undefined) updateData.form_template = form_template;
if (addons !== undefined) updateData.addons = addons;
if (client_metadata !== undefined) updateData.client_metadata = client_metadata;
if (mobile !== undefined) updateData.mobile = mobile;
if (initiate_login_uri !== undefined) updateData.initiate_login_uri = initiate_login_uri;
if (native_social_login !== undefined) updateData.native_social_login = native_social_login;
if (refresh_token !== undefined) updateData.refresh_token = refresh_token;
if (organization_usage !== undefined)
updateData.organization_usage = organization_usage as ClientCreateOrganizationUsageEnum;
if (organization_require_behavior !== undefined)
updateData.organization_require_behavior =
organization_require_behavior as ClientCreateOrganizationRequireBehaviorEnum;
if (client_authentication_methods !== undefined)
updateData.client_authentication_methods = client_authentication_methods;
if (require_pushed_authorization_requests !== undefined)
updateData.require_pushed_authorization_requests = require_pushed_authorization_requests;
if (signed_request_object !== undefined)
updateData.signed_request_object = signed_request_object;
if (require_proof_of_possession !== undefined)
updateData.require_proof_of_possession = require_proof_of_possession;
if (compliance_level !== undefined)
updateData.compliance_level = compliance_level as ClientCreateComplianceLevelEnum;
if (skip_non_verifiable_callback_uri_confirmation_prompt !== undefined)
updateData.skip_non_verifiable_callback_uri_confirmation_prompt =
skip_non_verifiable_callback_uri_confirmation_prompt;
Expand Down
8 changes: 8 additions & 0 deletions src/tools/resource-servers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,10 @@ export const RESOURCE_SERVER_TOOLS: Tool[] = [
type: 'boolean',
description: 'Whether to enforce authorization policies.',
},
client: {
type: 'object',
description: 'Client-related configuration for the resource server.',
},
token_encryption: {
type: 'object',
description: 'Token encryption configuration.',
Expand Down Expand Up @@ -218,6 +222,10 @@ export const RESOURCE_SERVER_TOOLS: Tool[] = [
type: 'boolean',
description: 'Whether to enforce authorization policies.',
},
client: {
type: 'object',
description: 'Client-related configuration for the resource server.',
},
token_encryption: {
type: 'object',
description: 'Token encryption configuration.',
Expand Down
93 changes: 92 additions & 1 deletion test/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,27 @@ vi.mock('../src/tools/index.js', () => {
});

return {
TOOLS: [{ name: 'test_tool', description: 'Test tool', inputSchema: {} }],
TOOLS: [
{ name: 'test_tool', description: 'Test tool', inputSchema: {} },
{
name: 'schema_tool',
description: 'Tool with a declared input schema',
inputSchema: {
type: 'object',
properties: {
name: { type: 'string' },
client_id: { type: 'string' },
},
required: ['name'],
},
},
],
HANDLERS: {
test_tool: mockHandler,
schema_tool: vi.fn().mockResolvedValue({
content: [{ type: 'text', text: 'Success' }],
isError: false,
}),
},
};
});
Expand Down Expand Up @@ -335,5 +353,78 @@ describe('Server', () => {
expect(result).toHaveProperty('isError', true);
expect(result.content[0].text).toContain('Error: Tool execution failed');
});

it('should reject arguments not declared in the tool inputSchema', async () => {
await startServer();

const handlerFn = getCallToolHandler();

// custom_login_page is a security-critical Auth0 param that is NOT declared
// in schema_tool's inputSchema, so it must be rejected before reaching the handler.
const request = {
params: {
name: 'schema_tool',
arguments: {
name: 'My App',
custom_login_page_on: true,
custom_login_page: '<script>steal()</script>',
},
},
};

const result = await handlerFn(request);

expect(result).toHaveProperty('isError', true);
expect(result.content[0].text).toContain('Rejected undeclared parameters');
expect(result.content[0].text).toContain('custom_login_page_on');
expect(result.content[0].text).toContain('custom_login_page');
expect(HANDLERS.schema_tool).not.toHaveBeenCalled();
});

it('should allow arguments that are all declared in the tool inputSchema', async () => {
await startServer();

const handlerFn = getCallToolHandler();

const request = {
params: {
name: 'schema_tool',
arguments: {
name: 'My App',
client_id: 'abc123',
},
},
};

const result = await handlerFn(request);

expect(result).toHaveProperty('isError', false);
expect(HANDLERS.schema_tool).toHaveBeenCalledWith(
{
token: mockConfig.token,
parameters: { name: 'My App', client_id: 'abc123' },
},
{ domain: mockConfig.domain }
);
});

it('should not enforce an allowlist when the tool declares no schema properties', async () => {
await startServer();

const handlerFn = getCallToolHandler();

// test_tool has an empty inputSchema (no properties), so validation is skipped.
const request = {
params: {
name: 'test_tool',
arguments: { anything: 'goes' },
},
};

const result = await handlerFn(request);

expect(result).toHaveProperty('isError', false);
expect(HANDLERS.test_tool).toHaveBeenCalled();
});
});
});
Loading
Loading