Skip to content

Commit 114885e

Browse files
committed
docs: document 2FA-protected actions in MCP and verification error codes
1 parent 3e9a175 commit 114885e

2 files changed

Lines changed: 81 additions & 13 deletions

File tree

‎adminforth/documentation/docs/tutorial/09-Plugins/02-TwoFactorsAuth.md‎

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,24 @@ Here is an example:
276276
277277
Under the hood, this metod uses websocket.
278278
279+
To tell the user what they confirm, pass a short description of the action as the second argument:
280+
281+
```ts
282+
const verifyResult = await t2fa.verifyAuto(adminUser, `Approve a $${amount} bonus for ${employee.name}`);
283+
```
284+
285+
The 2FA modal shows the description above the authenticator code field. When an AI agent runs the action through the [MCP server](/docs/tutorial/Plugins/mcp/), the confirmation page in the admin panel shows the description instead of the raw arguments of the agent's call.
286+
287+
`verifyAuto` also works in a tool call of the MCP server. There is no browser tab to show the modal in, so a missing verification stops the whole call, and the agent gets a link to [confirm the action in the admin panel](/docs/tutorial/Plugins/mcp/#actions-that-require-2fa), where the action runs again. Call `verifyAuto` before the action changes anything, otherwise the changes made before it happen twice.
288+
279289
## Manual Step-Up MFA (Two-Factor re-authentication on critical operations)
280290
But if you websocket doesn't work in you application, or you wan't to perform verification manually, here are manual verification examples
281291
282292
### Request 2FA on custom Actions
283293
284294
You might want to to allow to call some custom critical/money related actions with additional 2FA approval. This eliminates risks caused by user cookies theft by some virous/doorway software after login.
285295
286-
To do it, first, create frontend custom component which wraps and intercepts click event to menu item, and in click handler do a call to `get2FaConfirmationResult` frontend API exposed by this plugin. This is awaitable call wich shows 2FA popup and asks user to authenticate with 2nd factor (if passkey is enabled it will be suggested first, with ability to fallback to TOTP)
296+
To do it, first, create frontend custom component which wraps and intercepts click event to menu item, and in click handler do a call to `get2FaConfirmationResult` frontend API exposed by this plugin. This is awaitable call wich shows 2FA popup and asks user to authenticate with 2nd factor (if passkey is enabled it will be suggested first, with ability to fallback to TOTP). The popup checks the TOTP code and stays open until the user enters a correct code or closes it. Instead of the code, it returns a token that confirms the checked code for 1 minute, so `verify` does not check the code a second time.
287297
288298
```ts title='/custom/RequireTwoFaGate.vue'
289299
<template>
@@ -320,18 +330,10 @@ options: {
320330
icon: 'flowbite:play-solid',
321331
allowed: () => true,
322332
action: async ({ recordId, adminUser, adminforth, response, extra }) => {
323-
//diff-add
324-
const verificationResult = extra?.verificationResult
325-
//diff-add
326-
if (!verificationResult) {
327-
//diff-add
328-
return { ok: false, error: 'No verification result provided' };
329-
//diff-add
330-
}
331333
//diff-add
332334
const t2fa = adminforth.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
333335
//diff-add
334-
const result = await t2fa.verify(verificationResult, {
336+
const result = await t2fa.verify(extra?.verificationResult, {
335337
//diff-add
336338
adminUser: adminUser,
337339
//diff-add
@@ -342,9 +344,9 @@ options: {
342344
});
343345

344346
//diff-add
345-
if (!result?.ok) {
347+
if ('error' in result) {
346348
//diff-add
347-
return { ok: false, error: result?.error ?? 'Provided 2fa verification data is invalid' };
349+
return { ok: false, error: result.error, code: result.code };
348350
//diff-add
349351
}
350352
//diff-add
@@ -380,6 +382,26 @@ options: {
380382
}
381383
```
382384
385+
Pass the verification result to `t2fa.verify` as is, even when the action was called without it, and return the `code` of a failed verification together with the error. `verify` lets users excluded by `usersFilterToApply` and users in the [grace period](#step-up-authentication-grace-period) through without a verification result, and returns the `TWO_FACTOR_REQUIRED` code for everyone else, so callers such as AI agents can tell that the action needs a second factor. The [MCP server](/docs/tutorial/Plugins/mcp/#actions-that-require-2fa) then lets the user confirm the action in the admin panel.
386+
387+
### Verification error codes
388+
389+
A failed `verify` returns `{ error, code }`, and a failed `verifyAuto` returns `{ ok: false, error, code }`. In a tool call of the MCP server a failed `verifyAuto` does not return: it stops the call, and the agent gets the error. The `code` is one of `TwoFactorErrorCode`, exported by the plugin:
390+
391+
| Code | Meaning |
392+
|------|---------|
393+
| `TWO_FACTOR_REQUIRED` | No verification result was passed, and the user is not excluded from 2FA or in the grace period. |
394+
| `TWO_FACTOR_INVALID` | The TOTP code is wrong or expired, the code confirmation returned by the popup expired, or the passkey is invalid. |
395+
| `TWO_FACTOR_RATE_LIMITED` | The user entered TOTP codes too often. Every TOTP check, at login and for step-up verification, counts as an attempt, and a user gets 30 attempts per minute, 300 per hour and 1000 per day. |
396+
| `TWO_FACTOR_NOT_SET_UP` | The user has not set up 2FA yet. |
397+
| `USER_NOT_FOUND` | The user no longer exists. |
398+
| `UNSUPPORTED_CONFIRMATION_MODE` | The verification result has an unknown `mode`. |
399+
| `BROWSER_CONTEXT_REQUIRED` | `verifyAuto` was called outside a browser request and outside a tool call of the MCP server, so there is no browser tab to show the modal in. |
400+
| `VERIFICATION_CANCELLED` | The user closed the modal opened by `verifyAuto`. |
401+
| `VERIFICATION_TIMEOUT` | The user did not answer the modal opened by `verifyAuto` within 5 minutes. |
402+
| `INVALID_VERIFICATION_SESSION` | The modal answered a `verifyAuto` session that is invalid or belongs to another user. |
403+
| `INTERNAL_ERROR` | The verification failed with an unexpected error. |
404+
383405
### Request 2FA for create/edit (secure save gating)
384406
385407
To protect create and edit operations, use a Save Interceptor injected on the page to gate the save with 2FA, and forward the result to the backend for verification. This avoids wrapping the Save button and works with the default UI.

‎adminforth/documentation/docs/tutorial/09-Plugins/30-mcp.md‎

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,7 @@ The same numbers are quoted in the instructions and skills the MCP server gives
240240

241241
A tool call fails with a timeout error when its handler runs longer than `toolTimeoutMs`, 15 seconds by default. The handler gets an aborted signal, and the error tells the agent that a change it started may still complete, so it checks the result before calling again.
242242

243-
The instructions also ask the agent to make at most `toolCallsPerRequest` calls, 10 by default, for one user request, and to ask the user before it continues with more. MCP clients do not tell the server where a user request starts, so this is guidance for the agent, not a limit the server enforces.
243+
The instructions also ask the agent to make at most `toolCallsPerRequest` calls, 10 by default, for one user request, and to ask the user before it continues with more. Calls of `wait_for_confirmation`, which waits for an [action confirmed in the admin panel](#actions-that-require-2fa), do not count. MCP clients do not tell the server where a user request starts, so this is guidance for the agent, not a limit the server enforces.
244244

245245
```ts title="./index.ts"
246246
new AdminForthMcpPlugin({
@@ -260,6 +260,41 @@ The MCP server sends agents short rules in its instructions: to treat record val
260260

261261
Tools that change data, such as `delete_record`, are marked as destructive for MCP clients, and their descriptions repeat that the agent must get a confirmation in chat, even when the client itself does not ask for approval.
262262

263+
## Actions that require 2FA
264+
265+
With the [Two-Factor Authentication plugin](/docs/tutorial/Plugins/two-factors-auth/) installed, agents can run actions protected with a second factor: the user confirms such an action in the admin panel. The action asks for the second factor with [`verifyAuto`](/docs/tutorial/Plugins/two-factors-auth/#calling-2fa-modal-from-backend) or [`verify`](/docs/tutorial/Plugins/two-factors-auth/#request-2fa-on-custom-actions) and returns the `code` of a failed verification:
266+
267+
```ts title="./resources/cars.ts"
268+
action: async ({ recordId, adminUser, adminforth }) => {
269+
const t2fa = adminforth.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
270+
const result = await t2fa.verifyAuto(adminUser, `Approve listing of car ${recordId}`);
271+
if ('error' in result) {
272+
return { ok: false, error: result.error, code: result.code };
273+
}
274+
await adminforth.resource('cars').update(recordId, { listed: true });
275+
return { ok: true, successMessage: 'Listing approved' };
276+
},
277+
```
278+
279+
An agent's call has no second factor, so the action fails with the `TWO_FACTOR_REQUIRED` code. Then:
280+
281+
1. Instead of the error, the agent gets a `confirmationUrl` to a page of the admin panel and a `confirmationId`.
282+
2. The agent shows the link to the user and calls the `wait_for_confirmation` tool with the `confirmationId`. The tool returns a waiting status after about 50 seconds, and the agent calls it again to keep waiting.
283+
3. The user opens the link, signed in to the admin panel, and clicks **Confirm** or **Decline**. The page shows the description passed to `verifyAuto`, or, without one, the tool and the arguments of the agent's call. **Confirm** asks for the second factor.
284+
4. The admin panel runs the action again with the user's verification, and `wait_for_confirmation` returns its result. If the user declines, the agent gets an error.
285+
286+
A confirmation link:
287+
288+
- opens only for the user who owns the MCP connection, and only this connection can wait for its result;
289+
- expires after 10 minutes and works once: a wrong code or a closed 2FA modal ends it, and the agent has to call the action again;
290+
- runs the action with the same [tool timeout](#tool-timeout-and-call-limit).
291+
292+
The admin panel runs the whole call again, so ask for the second factor before the action changes anything. Otherwise the changes made before the check happen twice.
293+
294+
Pending confirmations are kept in the memory of the server process. If the admin panel runs on several instances, a link works only when the page request reaches the instance that served the agent's call.
295+
296+
Without the Two-Factor Authentication plugin, the server has no `wait_for_confirmation` tool and no confirmation page.
297+
263298
## Tool responses
264299

265300
Tool responses are trimmed to save the agent's context:
@@ -281,6 +316,17 @@ new AdminForthMcpPlugin({
281316
}),
282317
```
283318

319+
To make only some users' connections read-only, pass a function of the user instead, as in [`allowedActions`](/docs/tutorial/Customization/limitingAccess/). It may be async. The function is called on every MCP request, so a user it starts returning `true` for loses write access from the next request. It also locks the **Read only** checkbox for that user in **MCP Settings** and on the consent page:
320+
321+
```ts
322+
new AdminForthMcpPlugin({
323+
readOnly: ({ adminUser }) => adminUser.dbUser.role !== 'superadmin',
324+
// ...
325+
}),
326+
```
327+
328+
A connection is read-only when either the option or its stored `read_only` value says so.
329+
284330
In read-only mode the server exposes only endpoints marked with `agent: { onlyReadsData: true }`, such as `get_resource`, `get_resource_data`, and `aggregate`. Creating, updating, and deleting records and running actions are not available. To make your own endpoint available in read-only mode, mark it with `onlyReadsData: true`.
285331

286332
## Audit attribution

0 commit comments

Comments
 (0)