mirror of
https://github.com/documenso/documenso.git
synced 2025-11-13 00:03:33 +10:00
feat: support windows for 2fa tokens (#1478)
## Description When using 2fa enabled authentication on direct templates we run into an issue where a 2fa token has been attached to a field but it's submitted at a later point. To better facilitate this we have introduced the ability to have a window of valid tokens. This won't affect other signing methods since tokens are verified immediately after they're entered. ## Related Issue N/A ## Changes Made - Updated our validate2FAToken method to use a window based approach rather than the default verify method. ## Testing Performed - Created a series of tokens and tested upon different intervals and windows to confirm functionality works as expected.
This commit is contained in:
@ -1,21 +1,25 @@
|
|||||||
import { base32 } from '@scure/base';
|
import { base32 } from '@scure/base';
|
||||||
import { TOTPController } from 'oslo/otp';
|
import { generateHOTP } from 'oslo/otp';
|
||||||
|
|
||||||
import type { User } from '@documenso/prisma/client';
|
import type { User } from '@documenso/prisma/client';
|
||||||
|
|
||||||
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
|
||||||
import { symmetricDecrypt } from '../../universal/crypto';
|
import { symmetricDecrypt } from '../../universal/crypto';
|
||||||
|
|
||||||
const totp = new TOTPController();
|
|
||||||
|
|
||||||
type VerifyTwoFactorAuthenticationTokenOptions = {
|
type VerifyTwoFactorAuthenticationTokenOptions = {
|
||||||
user: User;
|
user: User;
|
||||||
totpCode: string;
|
totpCode: string;
|
||||||
|
// The number of windows to look back
|
||||||
|
window?: number;
|
||||||
|
// The duration that the token is valid for in seconds
|
||||||
|
period?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const verifyTwoFactorAuthenticationToken = async ({
|
export const verifyTwoFactorAuthenticationToken = async ({
|
||||||
user,
|
user,
|
||||||
totpCode,
|
totpCode,
|
||||||
|
window = 1,
|
||||||
|
period = 30_000,
|
||||||
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
}: VerifyTwoFactorAuthenticationTokenOptions) => {
|
||||||
const key = DOCUMENSO_ENCRYPTION_KEY;
|
const key = DOCUMENSO_ENCRYPTION_KEY;
|
||||||
|
|
||||||
@ -27,7 +31,21 @@ export const verifyTwoFactorAuthenticationToken = async ({
|
|||||||
'utf-8',
|
'utf-8',
|
||||||
);
|
);
|
||||||
|
|
||||||
const isValidToken = await totp.verify(totpCode, base32.decode(secret));
|
const decodedSecret = base32.decode(secret);
|
||||||
|
|
||||||
return isValidToken;
|
let now = Date.now();
|
||||||
|
|
||||||
|
for (let i = 0; i < window; i++) {
|
||||||
|
const counter = Math.floor(now / period);
|
||||||
|
|
||||||
|
const hotp = await generateHOTP(decodedSecret, counter);
|
||||||
|
|
||||||
|
if (totpCode === hotp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
now -= period;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -112,6 +112,7 @@ export const isRecipientAuthorized = async ({
|
|||||||
return await verifyTwoFactorAuthenticationToken({
|
return await verifyTwoFactorAuthenticationToken({
|
||||||
user,
|
user,
|
||||||
totpCode: token,
|
totpCode: token,
|
||||||
|
window: 10, // 5 minutes worth of tokens
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.exhaustive();
|
.exhaustive();
|
||||||
|
|||||||
Reference in New Issue
Block a user