@adminforth/two-factors-auth 1.0.28 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Changelog.md +6 -0
- package/custom/TwoFactorsConfirmation.vue +2 -2
- package/custom/TwoFactorsSetup.vue +34 -17
- package/dist/custom/TwoFactorsConfirmation.vue +2 -2
- package/dist/custom/TwoFactorsSetup.vue +34 -17
- package/dist/index.js +30 -5
- package/index.ts +36 -7
- package/package.json +1 -1
- package/types.ts +9 -1
package/Changelog.md
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
/>
|
|
29
29
|
</div>
|
|
30
30
|
<!-- <Vue2FACodeInput v-model="code"/> -->
|
|
31
|
-
<
|
|
31
|
+
<LinkButton to="/login" class="w-full">Back to login</LinkButton>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
50
50
|
import { useRouter } from 'vue-router';
|
|
51
51
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
52
|
-
import {
|
|
52
|
+
import { LinkButton } from '@/afcl';
|
|
53
53
|
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
54
54
|
import VOtpInput from "vue3-otp-input";
|
|
55
55
|
|
|
@@ -22,19 +22,12 @@
|
|
|
22
22
|
<div class="w-full max-w-[46rem]">
|
|
23
23
|
<div class="relative">
|
|
24
24
|
<label for="npm-install-copy-text" class="sr-only">Label</label>
|
|
25
|
-
<input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-
|
|
25
|
+
<input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-12" :value="totp.newSecret" readonly>
|
|
26
26
|
<button @click="onCopyClick" data-copy-to-clipboard-target="npm-install-copy-text" class="absolute end-2.5 top-1/2 -translate-y-1/2 text-gray-900 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 rounded-lg py-2 px-2.5 inline-flex items-center justify-center bg-white border-gray-200 border">
|
|
27
|
-
<span
|
|
28
|
-
<svg class="w-3 h-
|
|
27
|
+
<span id="default-message" class="inline-flex items-center">
|
|
28
|
+
<svg class="w-3 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
29
29
|
<path d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z"/>
|
|
30
30
|
</svg>
|
|
31
|
-
<span class="text-xs font-semibold">Copy</span>
|
|
32
|
-
</span>
|
|
33
|
-
<span v-if="isCopied === true" id="success-message" class="inline-flex items-center">
|
|
34
|
-
<svg class="w-3 h-3 text-lightPrimary dark:text-darkPrimary me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
|
|
35
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
|
|
36
|
-
</svg>
|
|
37
|
-
<span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
|
|
38
31
|
</span>
|
|
39
32
|
</button>
|
|
40
33
|
</div>
|
|
@@ -53,9 +46,14 @@
|
|
|
53
46
|
/>
|
|
54
47
|
</div>
|
|
55
48
|
<!-- <Vue2FACodeInput v-model="code" autofocus /> -->
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
<div class="flex flex-row gap-2.5">
|
|
50
|
+
<LinkButton to="/login" class="w-full">
|
|
51
|
+
Back to login
|
|
52
|
+
</LinkButton>
|
|
53
|
+
<Button v-if="skipAllowed" @click="handleSkip" class="w-full">
|
|
54
|
+
Skip for now
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
59
57
|
</div>
|
|
60
58
|
</div>
|
|
61
59
|
</div>
|
|
@@ -75,12 +73,11 @@ import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbi
|
|
|
75
73
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
76
74
|
import { useRouter } from 'vue-router';
|
|
77
75
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
78
|
-
import { Button } from '@/afcl';
|
|
76
|
+
import { Button, LinkButton } from '@/afcl';
|
|
79
77
|
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
80
78
|
import VOtpInput from "vue3-otp-input";
|
|
81
79
|
|
|
82
80
|
const code = ref(null);
|
|
83
|
-
const isCopied = ref<boolean>(false);
|
|
84
81
|
const handleOnComplete = (value) => {
|
|
85
82
|
sendCode(value);
|
|
86
83
|
};
|
|
@@ -91,6 +88,8 @@ const inProgress = ref(false);
|
|
|
91
88
|
const coreStore = useCoreStore();
|
|
92
89
|
const user = useUserStore();
|
|
93
90
|
|
|
91
|
+
const skipAllowed = ref(false);
|
|
92
|
+
|
|
94
93
|
// use this simple function to automatically focus on the next input
|
|
95
94
|
|
|
96
95
|
const showPw = ref(false);
|
|
@@ -122,7 +121,6 @@ function parseJwt(token) {
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
function onCopyClick(){
|
|
125
|
-
isCopied.value = true;
|
|
126
124
|
navigator.clipboard.writeText(totp.value.newSecret);
|
|
127
125
|
window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
|
|
128
126
|
}
|
|
@@ -136,9 +134,16 @@ onMounted(async () => {
|
|
|
136
134
|
totpJWT.value = resp.totpJWT;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
|
|
140
137
|
totp.value=parseJwt(totpJWT.value);
|
|
138
|
+
|
|
139
|
+
const allowSkipResp = await callAdminForthApi({
|
|
140
|
+
path: '/plugin/twofa/skip-allow',
|
|
141
|
+
method: 'GET',
|
|
142
|
+
});
|
|
141
143
|
|
|
144
|
+
if (allowSkipResp?.skipAllowed){
|
|
145
|
+
skipAllowed.value = true;
|
|
146
|
+
}
|
|
142
147
|
|
|
143
148
|
window.addEventListener('paste', handlePaste);
|
|
144
149
|
});
|
|
@@ -177,6 +182,18 @@ function handlePaste(event) {
|
|
|
177
182
|
code.value?.fillInput(pastedText);
|
|
178
183
|
}
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
const handleSkip = async () => {
|
|
187
|
+
const resp = await callAdminForthApi({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
path: '/plugin/twofa/confirmSetup',
|
|
190
|
+
});
|
|
191
|
+
if (resp.allowedLogin){
|
|
192
|
+
await user.finishLogin()
|
|
193
|
+
} else {
|
|
194
|
+
showErrorTost('Something went wrong');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
180
197
|
</script>
|
|
181
198
|
<style>
|
|
182
199
|
.otp-input {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
/>
|
|
29
29
|
</div>
|
|
30
30
|
<!-- <Vue2FACodeInput v-model="code"/> -->
|
|
31
|
-
<
|
|
31
|
+
<LinkButton to="/login" class="w-full">Back to login</LinkButton>
|
|
32
32
|
</div>
|
|
33
33
|
</div>
|
|
34
34
|
</div>
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
50
50
|
import { useRouter } from 'vue-router';
|
|
51
51
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
52
|
-
import {
|
|
52
|
+
import { LinkButton } from '@/afcl';
|
|
53
53
|
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
54
54
|
import VOtpInput from "vue3-otp-input";
|
|
55
55
|
|
|
@@ -22,19 +22,12 @@
|
|
|
22
22
|
<div class="w-full max-w-[46rem]">
|
|
23
23
|
<div class="relative">
|
|
24
24
|
<label for="npm-install-copy-text" class="sr-only">Label</label>
|
|
25
|
-
<input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-
|
|
25
|
+
<input id="npm-install-copy-text" type="text" class="col-span-10 bg-gray-50 border border-gray-300 text-gray-500 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full px-2.5 py-4 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-gray-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 pr-12" :value="totp.newSecret" readonly>
|
|
26
26
|
<button @click="onCopyClick" data-copy-to-clipboard-target="npm-install-copy-text" class="absolute end-2.5 top-1/2 -translate-y-1/2 text-gray-900 dark:text-gray-400 hover:bg-gray-100 dark:bg-gray-800 dark:border-gray-600 dark:hover:bg-gray-700 rounded-lg py-2 px-2.5 inline-flex items-center justify-center bg-white border-gray-200 border">
|
|
27
|
-
<span
|
|
28
|
-
<svg class="w-3 h-
|
|
27
|
+
<span id="default-message" class="inline-flex items-center">
|
|
28
|
+
<svg class="w-3 h-4" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
29
29
|
<path d="M16 1h-3.278A1.992 1.992 0 0 0 11 0H7a1.993 1.993 0 0 0-1.722 1H2a2 2 0 0 0-2 2v15a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V3a2 2 0 0 0-2-2Zm-3 14H5a1 1 0 0 1 0-2h8a1 1 0 0 1 0 2Zm0-4H5a1 1 0 0 1 0-2h8a1 1 0 1 1 0 2Zm0-5H5a1 1 0 0 1 0-2h2V2h4v2h2a1 1 0 1 1 0 2Z"/>
|
|
30
30
|
</svg>
|
|
31
|
-
<span class="text-xs font-semibold">Copy</span>
|
|
32
|
-
</span>
|
|
33
|
-
<span v-if="isCopied === true" id="success-message" class="inline-flex items-center">
|
|
34
|
-
<svg class="w-3 h-3 text-lightPrimary dark:text-darkPrimary me-1.5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 16 12">
|
|
35
|
-
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 5.917 5.724 10.5 15 1.5"/>
|
|
36
|
-
</svg>
|
|
37
|
-
<span class="text-xs font-semibold text-lightPrimary dark:text-darkPrimary">Copied</span>
|
|
38
31
|
</span>
|
|
39
32
|
</button>
|
|
40
33
|
</div>
|
|
@@ -53,9 +46,14 @@
|
|
|
53
46
|
/>
|
|
54
47
|
</div>
|
|
55
48
|
<!-- <Vue2FACodeInput v-model="code" autofocus /> -->
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
<div class="flex flex-row gap-2.5">
|
|
50
|
+
<LinkButton to="/login" class="w-full">
|
|
51
|
+
Back to login
|
|
52
|
+
</LinkButton>
|
|
53
|
+
<Button v-if="skipAllowed" @click="handleSkip" class="w-full">
|
|
54
|
+
Skip for now
|
|
55
|
+
</Button>
|
|
56
|
+
</div>
|
|
59
57
|
</div>
|
|
60
58
|
</div>
|
|
61
59
|
</div>
|
|
@@ -75,12 +73,11 @@ import { IconEyeSolid, IconEyeSlashSolid } from '@iconify-prerendered/vue-flowbi
|
|
|
75
73
|
import { callAdminForthApi, loadFile } from '@/utils';
|
|
76
74
|
import { useRouter } from 'vue-router';
|
|
77
75
|
import { showErrorTost } from '@/composables/useFrontendApi';
|
|
78
|
-
import { Button } from '@/afcl';
|
|
76
|
+
import { Button, LinkButton } from '@/afcl';
|
|
79
77
|
import Vue2FACodeInput from '@loltech/vue3-2fa-code-input';
|
|
80
78
|
import VOtpInput from "vue3-otp-input";
|
|
81
79
|
|
|
82
80
|
const code = ref(null);
|
|
83
|
-
const isCopied = ref<boolean>(false);
|
|
84
81
|
const handleOnComplete = (value) => {
|
|
85
82
|
sendCode(value);
|
|
86
83
|
};
|
|
@@ -91,6 +88,8 @@ const inProgress = ref(false);
|
|
|
91
88
|
const coreStore = useCoreStore();
|
|
92
89
|
const user = useUserStore();
|
|
93
90
|
|
|
91
|
+
const skipAllowed = ref(false);
|
|
92
|
+
|
|
94
93
|
// use this simple function to automatically focus on the next input
|
|
95
94
|
|
|
96
95
|
const showPw = ref(false);
|
|
@@ -122,7 +121,6 @@ function parseJwt(token) {
|
|
|
122
121
|
}
|
|
123
122
|
|
|
124
123
|
function onCopyClick(){
|
|
125
|
-
isCopied.value = true;
|
|
126
124
|
navigator.clipboard.writeText(totp.value.newSecret);
|
|
127
125
|
window.adminforth.alert({message: 'Copied to clipboard', variant: 'success'})
|
|
128
126
|
}
|
|
@@ -136,9 +134,16 @@ onMounted(async () => {
|
|
|
136
134
|
totpJWT.value = resp.totpJWT;
|
|
137
135
|
}
|
|
138
136
|
|
|
139
|
-
|
|
140
137
|
totp.value=parseJwt(totpJWT.value);
|
|
138
|
+
|
|
139
|
+
const allowSkipResp = await callAdminForthApi({
|
|
140
|
+
path: '/plugin/twofa/skip-allow',
|
|
141
|
+
method: 'GET',
|
|
142
|
+
});
|
|
141
143
|
|
|
144
|
+
if (allowSkipResp?.skipAllowed){
|
|
145
|
+
skipAllowed.value = true;
|
|
146
|
+
}
|
|
142
147
|
|
|
143
148
|
window.addEventListener('paste', handlePaste);
|
|
144
149
|
});
|
|
@@ -177,6 +182,18 @@ function handlePaste(event) {
|
|
|
177
182
|
code.value?.fillInput(pastedText);
|
|
178
183
|
}
|
|
179
184
|
}
|
|
185
|
+
|
|
186
|
+
const handleSkip = async () => {
|
|
187
|
+
const resp = await callAdminForthApi({
|
|
188
|
+
method: 'POST',
|
|
189
|
+
path: '/plugin/twofa/confirmSetup',
|
|
190
|
+
});
|
|
191
|
+
if (resp.allowedLogin){
|
|
192
|
+
await user.finishLogin()
|
|
193
|
+
} else {
|
|
194
|
+
showErrorTost('Something went wrong');
|
|
195
|
+
}
|
|
196
|
+
}
|
|
180
197
|
</script>
|
|
181
198
|
<style>
|
|
182
199
|
.otp-input {
|
package/dist/index.js
CHANGED
|
@@ -55,12 +55,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
55
55
|
if (!userNeeds2FA) {
|
|
56
56
|
return { body: { loginAllowed: true }, ok: true };
|
|
57
57
|
}
|
|
58
|
+
const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
|
|
58
59
|
if (!secret) {
|
|
59
60
|
const tempSecret = twofactor.generateSecret({ name: brandName, account: userName });
|
|
60
61
|
newSecret = tempSecret.secret;
|
|
61
62
|
}
|
|
62
63
|
else {
|
|
63
|
-
const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk }, 'tempTotp', '2h');
|
|
64
|
+
const value = this.adminforth.auth.issueJWT({ userName, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
64
65
|
response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
|
|
65
66
|
return {
|
|
66
67
|
body: {
|
|
@@ -70,7 +71,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
70
71
|
ok: true
|
|
71
72
|
};
|
|
72
73
|
}
|
|
73
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk }, 'tempTotp', '2h');
|
|
74
|
+
const totpTemporaryJWT = this.adminforth.auth.issueJWT({ userName, newSecret, issuer: brandName, pk: userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
74
75
|
response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString()} `);
|
|
75
76
|
return {
|
|
76
77
|
body: {
|
|
@@ -108,11 +109,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
108
109
|
return { status: 'error', message: 'Invalid token' };
|
|
109
110
|
}
|
|
110
111
|
if (decoded.newSecret) {
|
|
111
|
-
const verified = twofactor.verifyToken(decoded.newSecret, body.code);
|
|
112
|
+
const verified = decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code);
|
|
112
113
|
if (verified) {
|
|
113
114
|
this.connectors = this.adminforth.connectors;
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
if (!decoded.userCanSkipSetup) {
|
|
116
|
+
const connector = this.connectors[this.authResource.dataSource];
|
|
117
|
+
yield connector.updateRecord({ resource: this.authResource, recordId: decoded.pk, newValues: { [this.options.twoFaSecretFieldName]: decoded.newSecret } });
|
|
118
|
+
}
|
|
116
119
|
this.adminforth.auth.removeCustomCookie({ response, name: 'totpTemporaryJWT' });
|
|
117
120
|
this.adminforth.auth.setAuthCookie({ response, username: decoded.userName, pk: decoded.pk });
|
|
118
121
|
return { status: 'ok', allowedLogin: true };
|
|
@@ -138,5 +141,27 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
138
141
|
}
|
|
139
142
|
})
|
|
140
143
|
});
|
|
144
|
+
server.endpoint({
|
|
145
|
+
method: "GET",
|
|
146
|
+
path: "/plugin/twofa/skip-allow",
|
|
147
|
+
noAuth: true,
|
|
148
|
+
handler: (_d) => __awaiter(this, [_d], void 0, function* ({ cookies }) {
|
|
149
|
+
var _e;
|
|
150
|
+
const totpTemporaryJWT = (_e = cookies.find((cookie) => cookie.key === "adminforth_totpTemporaryJWT")) === null || _e === void 0 ? void 0 : _e.value;
|
|
151
|
+
const decoded = yield this.adminforth.auth.verify(totpTemporaryJWT, "tempTotp");
|
|
152
|
+
if (!decoded) {
|
|
153
|
+
return { status: "error", message: "Invalid token" };
|
|
154
|
+
}
|
|
155
|
+
if (!decoded.newSecret) {
|
|
156
|
+
return { status: "ok", skipAllowed: false };
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
return {
|
|
160
|
+
status: "ok",
|
|
161
|
+
skipAllowed: decoded.userCanSkipSetup,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
}),
|
|
165
|
+
});
|
|
141
166
|
}
|
|
142
167
|
}
|
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AdminForthPlugin } from "adminforth";
|
|
1
|
+
import { AdminForthPlugin, Filters } from "adminforth";
|
|
2
2
|
import type { AdminForthResource, AdminUser, IAdminForth, IHttpServer, IAdminForthAuth, BeforeLoginConfirmationFunction, IAdminForthHttpResponse } from "adminforth";
|
|
3
3
|
import twofactor from 'node-2fa';
|
|
4
4
|
import { PluginOptions } from "./types.js"
|
|
@@ -61,11 +61,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
61
61
|
return { body:{loginAllowed: true}, ok: true}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const userCanSkipSetup = this.options.usersFilterToAllowSkipSetup ? this.options.usersFilterToAllowSkipSetup(adminUser) : false;
|
|
65
|
+
|
|
64
66
|
if (!secret){
|
|
65
67
|
const tempSecret = twofactor.generateSecret({name: brandName,account: userName})
|
|
66
68
|
newSecret = tempSecret.secret
|
|
67
69
|
} else {
|
|
68
|
-
const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk }, 'tempTotp', '2h');
|
|
70
|
+
const value = this.adminforth.auth.issueJWT({userName, issuer:brandName, pk:userPk, userCanSkipSetup }, 'tempTotp', '2h');
|
|
69
71
|
response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${value}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; max-age=3600; `);
|
|
70
72
|
|
|
71
73
|
return {
|
|
@@ -76,7 +78,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
76
78
|
ok: true
|
|
77
79
|
}
|
|
78
80
|
}
|
|
79
|
-
const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk },'tempTotp', '2h');
|
|
81
|
+
const totpTemporaryJWT = this.adminforth.auth.issueJWT({userName, newSecret, issuer:brandName, pk:userPk, userCanSkipSetup },'tempTotp', '2h');
|
|
80
82
|
response.setHeader('Set-Cookie', `adminforth_totpTemporaryJWT=${totpTemporaryJWT}; Path=${this.adminforth.config.baseUrl || '/'}; HttpOnly; SameSite=Strict; Expires=${new Date(Date.now() + '1h').toUTCString() } `);
|
|
81
83
|
|
|
82
84
|
return {
|
|
@@ -115,11 +117,13 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
115
117
|
return {status:'error',message:'Invalid token'}
|
|
116
118
|
}
|
|
117
119
|
if (decoded.newSecret) {
|
|
118
|
-
const verified = twofactor.verifyToken(decoded.newSecret, body.code);
|
|
119
|
-
if (verified) {
|
|
120
|
+
const verified = decoded.userCanSkipSetup ? true : twofactor.verifyToken(decoded.newSecret, body.code);
|
|
121
|
+
if (verified) {
|
|
120
122
|
this.connectors = this.adminforth.connectors
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
if (!decoded.userCanSkipSetup) {
|
|
124
|
+
const connector = this.connectors[this.authResource.dataSource];
|
|
125
|
+
await connector.updateRecord({resource:this.authResource, recordId:decoded.pk, newValues:{[this.options.twoFaSecretFieldName]: decoded.newSecret}})
|
|
126
|
+
}
|
|
123
127
|
this.adminforth.auth.removeCustomCookie({response, name:'totpTemporaryJWT'})
|
|
124
128
|
this.adminforth.auth.setAuthCookie({response, username:decoded.userName, pk:decoded.pk})
|
|
125
129
|
return { status: 'ok', allowedLogin: true }
|
|
@@ -142,5 +146,30 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin {
|
|
|
142
146
|
}
|
|
143
147
|
}
|
|
144
148
|
})
|
|
149
|
+
server.endpoint({
|
|
150
|
+
method: "GET",
|
|
151
|
+
path: "/plugin/twofa/skip-allow",
|
|
152
|
+
noAuth: true,
|
|
153
|
+
handler: async ({ cookies }) => {
|
|
154
|
+
const totpTemporaryJWT = cookies.find(
|
|
155
|
+
(cookie) => cookie.key === "adminforth_totpTemporaryJWT"
|
|
156
|
+
)?.value;
|
|
157
|
+
const decoded = await this.adminforth.auth.verify(
|
|
158
|
+
totpTemporaryJWT,
|
|
159
|
+
"tempTotp"
|
|
160
|
+
);
|
|
161
|
+
if (!decoded) {
|
|
162
|
+
return { status: "error", message: "Invalid token" };
|
|
163
|
+
}
|
|
164
|
+
if (!decoded.newSecret) {
|
|
165
|
+
return { status: "ok", skipAllowed: false };
|
|
166
|
+
} else {
|
|
167
|
+
return {
|
|
168
|
+
status: "ok",
|
|
169
|
+
skipAllowed: decoded.userCanSkipSetup,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
});
|
|
145
174
|
}
|
|
146
175
|
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -16,5 +16,13 @@ export type PluginOptions = {
|
|
|
16
16
|
* @param adminUser
|
|
17
17
|
* @returns true if 2FA should be applied to the user and false if AdminForth should not challenge the user with 2FA.
|
|
18
18
|
*/
|
|
19
|
-
usersFilterToApply
|
|
19
|
+
usersFilterToApply?: (adminUser: AdminUser) => boolean;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Optional function to allow users to skip 2FA setup.
|
|
23
|
+
* Should return true if the user should be allowed to skip the 2FA setup and false if AdminForth should require the user to set up 2FA.
|
|
24
|
+
* @param adminUser
|
|
25
|
+
* @returns true if the user should be allowed to skip the 2FA setup and false if AdminForth should require the user to set up 2FA.
|
|
26
|
+
*/
|
|
27
|
+
usersFilterToAllowSkipSetup?: (adminUser: AdminUser) => boolean;
|
|
20
28
|
}
|