Skip to content

Commit d5f5a0d

Browse files
feat(webauthn): add remaining WebAuthn tools (Phase 2)
Added tools: - webauthn_remove_authenticator: Remove a virtual authenticator - webauthn_get_credentials: List credentials on an authenticator - webauthn_add_credential: Add a pre-seeded credential - webauthn_clear_credentials: Clear all credentials - webauthn_set_user_verified: Toggle user verification state All tools follow the established pattern using CDP WebAuthn domain. Tests verify each tool works correctly (except add_credential which requires complex key generation - verified schema only). Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 248f408 commit d5f5a0d

2 files changed

Lines changed: 328 additions & 0 deletions

File tree

src/tools/webauthn.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,3 +85,181 @@ export const addVirtualAuthenticator = defineTool({
8585
);
8686
},
8787
});
88+
89+
export const removeVirtualAuthenticator = defineTool({
90+
name: 'webauthn_remove_authenticator',
91+
description: 'Remove a virtual WebAuthn authenticator.',
92+
annotations: {
93+
category: ToolCategory.EMULATION,
94+
readOnlyHint: false,
95+
},
96+
schema: {
97+
authenticatorId: zod
98+
.string()
99+
.describe('The ID of the authenticator to remove.'),
100+
},
101+
handler: async (request, response, context) => {
102+
const page = context.getSelectedPage();
103+
// @ts-expect-error _client is internal Puppeteer API
104+
const session = page._client() as CDPSession;
105+
106+
await session.send('WebAuthn.removeVirtualAuthenticator', {
107+
authenticatorId: request.params.authenticatorId,
108+
});
109+
110+
response.appendResponseLine(
111+
`Removed virtual authenticator (authenticatorId: ${request.params.authenticatorId})`,
112+
);
113+
},
114+
});
115+
116+
export const getCredentials = defineTool({
117+
name: 'webauthn_get_credentials',
118+
description: 'Get all credentials registered with a virtual authenticator.',
119+
annotations: {
120+
category: ToolCategory.EMULATION,
121+
readOnlyHint: true,
122+
},
123+
schema: {
124+
authenticatorId: zod
125+
.string()
126+
.describe('The ID of the authenticator to get credentials from.'),
127+
},
128+
handler: async (request, response, context) => {
129+
const page = context.getSelectedPage();
130+
// @ts-expect-error _client is internal Puppeteer API
131+
const session = page._client() as CDPSession;
132+
133+
const result = await session.send('WebAuthn.getCredentials', {
134+
authenticatorId: request.params.authenticatorId,
135+
});
136+
137+
if (result.credentials.length === 0) {
138+
response.appendResponseLine('No credentials registered.');
139+
} else {
140+
response.appendResponseLine(
141+
`Found ${result.credentials.length} credential(s):`,
142+
);
143+
for (const cred of result.credentials) {
144+
response.appendResponseLine(
145+
`- credentialId: ${cred.credentialId}, rpId: ${cred.rpId}, signCount: ${cred.signCount}`,
146+
);
147+
}
148+
}
149+
},
150+
});
151+
152+
export const addCredential = defineTool({
153+
name: 'webauthn_add_credential',
154+
description: 'Add a credential to a virtual authenticator.',
155+
annotations: {
156+
category: ToolCategory.EMULATION,
157+
readOnlyHint: false,
158+
},
159+
schema: {
160+
authenticatorId: zod
161+
.string()
162+
.describe('The ID of the authenticator to add the credential to.'),
163+
credentialId: zod.string().describe('The credential ID (base64 encoded).'),
164+
isResidentCredential: zod
165+
.boolean()
166+
.describe('Whether this is a resident (discoverable) credential.'),
167+
rpId: zod.string().describe('The relying party ID.'),
168+
privateKey: zod
169+
.string()
170+
.describe('The private key in PKCS#8 format (base64 encoded).'),
171+
userHandle: zod
172+
.string()
173+
.optional()
174+
.describe('The user handle (base64 encoded).'),
175+
signCount: zod.number().int().optional().describe('The signature counter.'),
176+
},
177+
handler: async (request, response, context) => {
178+
const page = context.getSelectedPage();
179+
// @ts-expect-error _client is internal Puppeteer API
180+
const session = page._client() as CDPSession;
181+
182+
const {
183+
authenticatorId,
184+
credentialId,
185+
isResidentCredential,
186+
rpId,
187+
privateKey,
188+
userHandle,
189+
signCount,
190+
} = request.params;
191+
192+
await session.send('WebAuthn.addCredential', {
193+
authenticatorId,
194+
credential: {
195+
credentialId,
196+
isResidentCredential,
197+
rpId,
198+
privateKey,
199+
userHandle,
200+
signCount: signCount ?? 0,
201+
},
202+
});
203+
204+
response.appendResponseLine(
205+
`Added credential (credentialId: ${credentialId}) to authenticator ${authenticatorId}`,
206+
);
207+
},
208+
});
209+
210+
export const clearCredentials = defineTool({
211+
name: 'webauthn_clear_credentials',
212+
description: 'Clear all credentials from a virtual authenticator.',
213+
annotations: {
214+
category: ToolCategory.EMULATION,
215+
readOnlyHint: false,
216+
},
217+
schema: {
218+
authenticatorId: zod
219+
.string()
220+
.describe('The ID of the authenticator to clear credentials from.'),
221+
},
222+
handler: async (request, response, context) => {
223+
const page = context.getSelectedPage();
224+
// @ts-expect-error _client is internal Puppeteer API
225+
const session = page._client() as CDPSession;
226+
227+
await session.send('WebAuthn.clearCredentials', {
228+
authenticatorId: request.params.authenticatorId,
229+
});
230+
231+
response.appendResponseLine(
232+
`Cleared all credentials from authenticator ${request.params.authenticatorId}`,
233+
);
234+
},
235+
});
236+
237+
export const setUserVerified = defineTool({
238+
name: 'webauthn_set_user_verified',
239+
description:
240+
'Set whether user verification succeeds or fails for a virtual authenticator.',
241+
annotations: {
242+
category: ToolCategory.EMULATION,
243+
readOnlyHint: false,
244+
},
245+
schema: {
246+
authenticatorId: zod.string().describe('The ID of the authenticator.'),
247+
isUserVerified: zod
248+
.boolean()
249+
.describe('Whether user verification should succeed.'),
250+
},
251+
handler: async (request, response, context) => {
252+
const page = context.getSelectedPage();
253+
// @ts-expect-error _client is internal Puppeteer API
254+
const session = page._client() as CDPSession;
255+
256+
await session.send('WebAuthn.setUserVerified', {
257+
authenticatorId: request.params.authenticatorId,
258+
isUserVerified: request.params.isUserVerified,
259+
});
260+
261+
response.appendResponseLine(
262+
`Set user verification to ${request.params.isUserVerified} for authenticator ${request.params.authenticatorId}`,
263+
);
264+
},
265+
});

tests/tools/webauthn.test.ts

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ import assert from 'node:assert';
88
import {describe, it} from 'node:test';
99

1010
import {
11+
addCredential,
1112
addVirtualAuthenticator,
13+
clearCredentials,
1214
enableWebAuthn,
15+
getCredentials,
16+
removeVirtualAuthenticator,
17+
setUserVerified,
1318
} from '../../src/tools/webauthn.js';
1419
import {withMcpContext} from '../utils.js';
1520

@@ -70,4 +75,149 @@ describe('webauthn', () => {
7075
});
7176
});
7277
});
78+
79+
describe('webauthn_remove_authenticator', () => {
80+
it('removes a virtual authenticator', async () => {
81+
await withMcpContext(async (response, context) => {
82+
// Enable and add authenticator
83+
await enableWebAuthn.handler({params: {}}, response, context);
84+
85+
const page = context.getSelectedPage();
86+
// @ts-expect-error _client is internal Puppeteer API
87+
const session = page._client();
88+
const {authenticatorId} = await session.send(
89+
'WebAuthn.addVirtualAuthenticator',
90+
{
91+
options: {
92+
protocol: 'ctap2',
93+
transport: 'internal',
94+
},
95+
},
96+
);
97+
98+
// Remove via tool
99+
await removeVirtualAuthenticator.handler(
100+
{params: {authenticatorId}},
101+
response,
102+
context,
103+
);
104+
105+
// Verify it was removed by trying to use it (should fail)
106+
await assert.rejects(async () => {
107+
await session.send('WebAuthn.getCredentials', {authenticatorId});
108+
}, /authenticator/i);
109+
});
110+
});
111+
});
112+
113+
describe('webauthn_get_credentials', () => {
114+
it('returns credentials from an authenticator', async () => {
115+
await withMcpContext(async (response, context) => {
116+
await enableWebAuthn.handler({params: {}}, response, context);
117+
118+
const page = context.getSelectedPage();
119+
// @ts-expect-error _client is internal Puppeteer API
120+
const session = page._client();
121+
const {authenticatorId} = await session.send(
122+
'WebAuthn.addVirtualAuthenticator',
123+
{
124+
options: {
125+
protocol: 'ctap2',
126+
transport: 'internal',
127+
hasResidentKey: true,
128+
},
129+
},
130+
);
131+
132+
await getCredentials.handler(
133+
{params: {authenticatorId}},
134+
response,
135+
context,
136+
);
137+
138+
const hasNoCredentials = response.responseLines.some(line =>
139+
line.includes('No credentials'),
140+
);
141+
assert.ok(hasNoCredentials, 'Should indicate no credentials initially');
142+
});
143+
});
144+
});
145+
146+
describe('webauthn_clear_credentials', () => {
147+
it('clears credentials from an authenticator', async () => {
148+
await withMcpContext(async (response, context) => {
149+
await enableWebAuthn.handler({params: {}}, response, context);
150+
151+
const page = context.getSelectedPage();
152+
// @ts-expect-error _client is internal Puppeteer API
153+
const session = page._client();
154+
const {authenticatorId} = await session.send(
155+
'WebAuthn.addVirtualAuthenticator',
156+
{
157+
options: {
158+
protocol: 'ctap2',
159+
transport: 'internal',
160+
},
161+
},
162+
);
163+
164+
await clearCredentials.handler(
165+
{params: {authenticatorId}},
166+
response,
167+
context,
168+
);
169+
170+
const hasCleared = response.responseLines.some(line =>
171+
line.includes('Cleared all credentials'),
172+
);
173+
assert.ok(hasCleared, 'Should confirm credentials cleared');
174+
});
175+
});
176+
});
177+
178+
describe('webauthn_set_user_verified', () => {
179+
it('sets user verification state', async () => {
180+
await withMcpContext(async (response, context) => {
181+
await enableWebAuthn.handler({params: {}}, response, context);
182+
183+
const page = context.getSelectedPage();
184+
// @ts-expect-error _client is internal Puppeteer API
185+
const session = page._client();
186+
const {authenticatorId} = await session.send(
187+
'WebAuthn.addVirtualAuthenticator',
188+
{
189+
options: {
190+
protocol: 'ctap2',
191+
transport: 'internal',
192+
hasUserVerification: true,
193+
isUserVerified: true,
194+
},
195+
},
196+
);
197+
198+
await setUserVerified.handler(
199+
{params: {authenticatorId, isUserVerified: false}},
200+
response,
201+
context,
202+
);
203+
204+
const hasSet = response.responseLines.some(line =>
205+
line.includes('Set user verification to false'),
206+
);
207+
assert.ok(hasSet, 'Should confirm user verification set');
208+
});
209+
});
210+
});
211+
212+
describe('webauthn_add_credential', () => {
213+
it('is defined with correct schema', async () => {
214+
// Verify the tool exists and has the expected schema
215+
assert.strictEqual(addCredential.name, 'webauthn_add_credential');
216+
assert.ok(addCredential.schema.authenticatorId);
217+
assert.ok(addCredential.schema.credentialId);
218+
assert.ok(addCredential.schema.isResidentCredential);
219+
assert.ok(addCredential.schema.rpId);
220+
assert.ok(addCredential.schema.privateKey);
221+
});
222+
});
73223
});

0 commit comments

Comments
 (0)