|
1 | | -import { AdminForthPlugin, Filters, suggestIfTypo, HttpExtra } from "adminforth"; |
| 1 | +import { AdminForthPlugin, Filters, suggestIfTypo, HttpExtra, convertPeriodToSeconds } 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" |
@@ -29,6 +29,36 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
29 | 29 | return `single`; |
30 | 30 | } |
31 | 31 |
|
| 32 | + private useChellenge(challenge: string, expiresIn?: string): void { |
| 33 | + const expiresInSeconds = expiresIn ? convertPeriodToSeconds(expiresIn) : undefined; |
| 34 | + this.options.passkeys.keyValueAdapter.set(challenge, 'stub_value', expiresInSeconds); |
| 35 | + } |
| 36 | + |
| 37 | + private async checkIfChellengeNotUsed(challenge: string): Promise<boolean> { |
| 38 | + const res = await this.options.passkeys.keyValueAdapter.get(challenge); |
| 39 | + if (!res) { |
| 40 | + return true; |
| 41 | + } |
| 42 | + return false; |
| 43 | + } |
| 44 | + |
| 45 | + private async validateCookiesForPasskeyLogin(cookies: any): Promise<{ok: boolean, decodedPasskeysCookies?: any, error?: string}> { |
| 46 | + const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`}); |
| 47 | + if (!passkeysCookies) { |
| 48 | + return { ok: false, error: 'Passkey token is required' }; |
| 49 | + } |
| 50 | + |
| 51 | + const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false); |
| 52 | + const isChallangeValid = await this.checkIfChellengeNotUsed(decodedPasskeysCookies.challenge); |
| 53 | + if (isChallangeValid) { |
| 54 | + this.useChellenge(decodedPasskeysCookies.challenge, this.options.passkeys?.challengeValidityPeriod || '2m'); |
| 55 | + } |
| 56 | + |
| 57 | + if (!decodedPasskeysCookies || !isChallangeValid) { |
| 58 | + return { ok: false, error: 'Invalid passkey' }; |
| 59 | + } |
| 60 | + return { ok: true, decodedPasskeysCookies }; |
| 61 | + } |
32 | 62 |
|
33 | 63 | public async checkIfSkipSetupAllowSkipVerify(adminUser: AdminUser): Promise<{ skipAllowed: boolean }> { |
34 | 64 | if (this.options.usersFilterToAllowSkipSetup) { |
@@ -171,18 +201,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
171 | 201 | return { error: "Wrong or expired OTP code" } |
172 | 202 | }; |
173 | 203 | } else if (confirmationResult.mode === "passkey") { |
174 | | - //TODO: fix ts-ignore after releasing new version of adminforth with updated types |
175 | | - //@ts-ignore |
176 | | - const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`}); |
177 | | - if (!passkeysCookies) { |
178 | | - return { error: 'Passkey token is required' }; |
179 | | - } |
180 | | - |
181 | | - const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false); |
182 | | - if (!decodedPasskeysCookies) { |
183 | | - return { error: 'Invalid passkey' }; |
| 204 | + const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies); |
| 205 | + if (!cookiesValidationResult.ok) { |
| 206 | + return { error: cookiesValidationResult.error }; |
184 | 207 | } |
185 | | - const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, decodedPasskeysCookies ); |
| 208 | + const verificationResult = await this.verifyPasskeyResponse(confirmationResult.result, opts.userPk, cookiesValidationResult.decodedPasskeysCookies ); |
186 | 209 |
|
187 | 210 | if (verificationResult.ok && verificationResult.passkeyConfirmed) { |
188 | 211 |
|
@@ -340,6 +363,10 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
340 | 363 | throw new Error('Passkeys credentialIdFieldName is required'); |
341 | 364 | } |
342 | 365 |
|
| 366 | + if (!this.options.passkeys.keyValueAdapter) { |
| 367 | + throw new Error('Passkeys keyValueAdapter is required'); |
| 368 | + } |
| 369 | + |
343 | 370 | const credentialResource = adminforth.config.resources.find(r => r.resourceId === this.options.passkeys.credentialResourceID); |
344 | 371 | const credentialIDField = credentialResource.columns.find(c => c.name === this.options.passkeys.credentialIdFieldName); |
345 | 372 | if ( !credentialIDField ) { |
@@ -529,15 +556,11 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
529 | 556 | let verified = null; |
530 | 557 | if (body.usePasskey && this.options.passkeys) { |
531 | 558 | // passkeys are enabled and user wants to use them |
532 | | - const passkeysCookies = this.adminforth.auth.getCustomCookie({cookies: cookies, name: `passkeyLoginTemporaryJWT`}); |
533 | | - if (!passkeysCookies) { |
534 | | - return { error: 'Passkey token is required' }; |
| 559 | + const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies); |
| 560 | + if (!cookiesValidationResult.ok) { |
| 561 | + return { error: cookiesValidationResult.error }; |
535 | 562 | } |
536 | | - const decodedPasskeysCookies = await this.adminforth.auth.verify(passkeysCookies, 'tempLoginPasskeyChallenge', false); |
537 | | - if (!decodedPasskeysCookies) { |
538 | | - return { error: 'Invalid passkey' }; |
539 | | - } |
540 | | - const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, decodedPasskeysCookies); |
| 563 | + const res = await this.verifyPasskeyResponse(body.passkeyOptions, decoded.pk, cookiesValidationResult.decodedPasskeysCookies); |
541 | 564 | if (res.ok && res.passkeyConfirmed) { |
542 | 565 | verified = true; |
543 | 566 | } |
@@ -572,14 +595,9 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
572 | 595 | return { error: 'Passkey response is required' }; |
573 | 596 | } |
574 | 597 |
|
575 | | - const totpTemporaryJWT = this.adminforth.auth.getCustomCookie({cookies: cookies, name: "passkeyLoginTemporaryJWT"}); |
576 | | - if (!totpTemporaryJWT) { |
577 | | - return { error: 'Authentication session is expired. Please, try again' } |
578 | | - } |
579 | | - |
580 | | - const decoded = await this.adminforth.auth.verify(totpTemporaryJWT, 'tempLoginPasskeyChallenge', false); |
581 | | - if (!decoded) { |
582 | | - return { error: 'Authentication session is expired. Please, try again' } |
| 598 | + const cookiesValidationResult = await this.validateCookiesForPasskeyLogin(cookies); |
| 599 | + if (!cookiesValidationResult.ok) { |
| 600 | + return { error: cookiesValidationResult.error }; |
583 | 601 | } |
584 | 602 |
|
585 | 603 | let parsedPasskeyResponse; |
@@ -612,7 +630,7 @@ export default class TwoFactorsAuthPlugin extends AdminForthPlugin { |
612 | 630 | return { error: 'User not found' }; |
613 | 631 | } |
614 | 632 |
|
615 | | - const verificationResult = await this.verifyPasskeyResponse(passkeyResponse, userPk, decoded); |
| 633 | + const verificationResult = await this.verifyPasskeyResponse(passkeyResponse, userPk, cookiesValidationResult.decodedPasskeysCookies); |
616 | 634 | if (!verificationResult.ok || !verificationResult.passkeyConfirmed) { |
617 | 635 | return { error: 'Passkey verification failed' }; |
618 | 636 | } |
|
0 commit comments