Skip to content

Commit aff3b9e

Browse files
authored
Merge pull request #393 from devforth/AdminForth/906
docs: add docs for the 2fa modal
2 parents 44f12d6 + 3b39e75 commit aff3b9e

File tree

1 file changed

+232
-0
lines changed

1 file changed

+232
-0
lines changed

adminforth/documentation/docs/tutorial/07-Plugins/02-TwoFactorsAuth.md

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,238 @@ plugins: [
207207
...
208208
```
209209
210+
## Request 2FA on custom Actions
211+
212+
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.
213+
214+
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 `window.adminforthTwoFaModal.getCode(cb?)` 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)
215+
216+
```ts title='/custom/RequireTwoFaGate.vue'
217+
<template>
218+
<div class="contents" @click.stop.prevent="onClick">
219+
<slot /> <!-- render action default content - button/icon -->
220+
</div>
221+
</template>
222+
223+
<script setup lang="ts">
224+
const emit = defineEmits<{ (e: 'callAction', payload?: any): void }>();
225+
const props = defineProps<{ disabled?: boolean; meta?: Record<string, any> }>();
226+
227+
async function onClick() {
228+
if (props.disabled) return;
229+
230+
const verificationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult(); // this will ask user to enter code
231+
emit('callAction', { verificationResult }); // then we pass this verification result to action (from fronted to backend)
232+
}
233+
</script>
234+
```
235+
236+
Now we need to use verification result which we got from user on frontend, inside of backend action handler and verify that it is valid (and not expired):
237+
238+
```ts title='/adminuser.ts'
239+
options: {
240+
actions: [
241+
{
242+
name: 'Auto submit',
243+
icon: 'flowbite:play-solid',
244+
allowed: () => true,
245+
action: async ({ recordId, adminUser, adminforth, extra, cookies }) => {
246+
//diff-add
247+
const verificationResult = extra?.verificationResult
248+
//diff-add
249+
if (!verificationResult) {
250+
//diff-add
251+
return { ok: false, error: 'No verification result provided' };
252+
//diff-add
253+
}
254+
//diff-add
255+
const t2fa = adminforth.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
256+
//diff-add
257+
const result = await t2fa.verify(verificationResult, {
258+
//diff-add
259+
adminUser: adminUser,
260+
//diff-add
261+
userPk: adminUser.pk,
262+
//diff-add
263+
cookies: cookies
264+
//diff-add
265+
});
266+
267+
//diff-add
268+
if (!result?.ok) {
269+
//diff-add
270+
return { ok: false, error: result?.error ?? 'Provided 2fa verification data is invalid' };
271+
//diff-add
272+
}
273+
//diff-add
274+
await adminforth
275+
//diff-add
276+
.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin')
277+
//diff-add
278+
.logCustomAction({
279+
//diff-add
280+
resourceId: 'aparts',
281+
//diff-add
282+
recordId: null,
283+
//diff-add
284+
actionId: 'visitedDashboard',
285+
//diff-add
286+
oldData: null,
287+
//diff-add
288+
data: { dashboard: 'main' },
289+
//diff-add
290+
user: adminUser,
291+
//diff-add
292+
});
293+
294+
//your critical action logic
295+
296+
return { ok: true, successMessage: 'Auto submitted' };
297+
},
298+
showIn: { showButton: true, showThreeDotsMenu: true, list: true },
299+
//diff-add
300+
customComponent: '@@/RequireTwoFaGate.vue',
301+
},
302+
],
303+
}
304+
```
305+
306+
## Request 2FA from custom components
307+
308+
Imagine you have some button which does some API call
309+
310+
```ts
311+
<template>
312+
<Button @click="callAdminAPI">Call critical API</Button>
313+
</template>
314+
315+
316+
<script setup lang="ts">
317+
import { callApi } from '@/utils';
318+
import adminforth from '@/adminforth';
319+
320+
async function callAdminAPI() {
321+
const verificationResult = await window.adminforthTwoFaModal.get2FaConfirmationResult();
322+
323+
const res = await callApi({
324+
path: '/myCriticalAction',
325+
method: 'POST',
326+
body: {
327+
param: 1
328+
},
329+
});
330+
}
331+
</script>
332+
```
333+
334+
On backend you have simple express api
335+
336+
```ts
337+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
338+
admin.express.authorize(
339+
async (req: any, res: any) => {
340+
341+
// ... your critical logic ...
342+
343+
return res.json({ ok: true, successMessage: 'Action executed' });
344+
}
345+
)
346+
);
347+
```
348+
349+
You might want to protect this call with a second factor also. To do it, we need to make this change
350+
351+
```ts
352+
<template>
353+
<Button @click="callAdminAPI">Call critical API</Button>
354+
</template>
355+
356+
357+
<script setup lang="ts">
358+
import { callApi } from '@/utils';
359+
import adminforth from '@/adminforth';
360+
361+
async function callAdminAPI() {
362+
// diff-add
363+
const verificationResult = await window.adminforthTwoFaModal.getCode();
364+
365+
const res = await callApi({
366+
path: '/myCriticalAction',
367+
method: 'POST',
368+
body: {
369+
param: 1,
370+
// diff-add
371+
verificationResult: String(verificationResult)
372+
},
373+
});
374+
375+
// diff-add
376+
if (!res?.ok) {
377+
// diff-add
378+
adminforth.alert({ message: res.error, variant: 'danger' });
379+
// diff-add
380+
}
381+
}
382+
</script>
383+
384+
```
385+
386+
And oin API call we need to verify it:
387+
388+
389+
```ts
390+
app.post(`${ADMIN_BASE_URL}/myCriticalAction`,
391+
admin.express.authorize(
392+
async (req: any, res: any) => {
393+
394+
// diff-add
395+
const { adminUser } = req;
396+
// diff-add
397+
const { param, verificationResult } = req.body ?? {};
398+
// diff-add
399+
const t2fa = admin.getPluginByClassName<TwoFactorsAuthPlugin>('TwoFactorsAuthPlugin');
400+
// diff-add
401+
const verifyRes = await t2fa.verify(verificationResult, {
402+
// diff-add
403+
adminUser: adminUser,
404+
// diff-add
405+
userPk: adminUser.pk,
406+
// diff-add
407+
cookies: cookies
408+
// diff-add
409+
});
410+
// diff-add
411+
if (!('ok' in verifyRes)) {
412+
// diff-add
413+
return res.status(400).json({ ok: false, error: verifyRes.error || 'Verification failed' });
414+
// diff-add
415+
}
416+
// diff-add
417+
await admin.getPluginByClassName<AuditLogPlugin>('AuditLogPlugin').logCustomAction({
418+
// diff-add
419+
resourceId: 'aparts',
420+
// diff-add
421+
recordId: null,
422+
// diff-add
423+
actionId: 'myCriticalAction',
424+
// diff-add
425+
oldData: null,
426+
// diff-add
427+
data: { param },
428+
// diff-add
429+
user: adminUser,
430+
// diff-add
431+
});
432+
433+
// ... your critical logic ...
434+
435+
return res.json({ ok: true, successMessage: 'Action executed' });
436+
}
437+
)
438+
);
439+
```
440+
441+
210442
## Custom label prefix in authenticator app
211443
212444
By default label prefix in Authenticator app is formed from Adminforth [brandName setting](/docs/tutorial/Customization/branding/) which is best behaviour for most admin apps (always remember to configure brandName correctly e.g. "RoyalFinTech Admin")

0 commit comments

Comments
 (0)