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:
Lucas Smith
2024-11-16 09:17:45 +11:00
committed by GitHub
parent f15f9ecdd1
commit 08a446fefd
2 changed files with 24 additions and 5 deletions

View File

@ -1,21 +1,25 @@
import { base32 } from '@scure/base';
import { TOTPController } from 'oslo/otp';
import { generateHOTP } from 'oslo/otp';
import type { User } from '@documenso/prisma/client';
import { DOCUMENSO_ENCRYPTION_KEY } from '../../constants/crypto';
import { symmetricDecrypt } from '../../universal/crypto';
const totp = new TOTPController();
type VerifyTwoFactorAuthenticationTokenOptions = {
user: User;
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 ({
user,
totpCode,
window = 1,
period = 30_000,
}: VerifyTwoFactorAuthenticationTokenOptions) => {
const key = DOCUMENSO_ENCRYPTION_KEY;
@ -27,7 +31,21 @@ export const verifyTwoFactorAuthenticationToken = async ({
'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;
};

View File

@ -112,6 +112,7 @@ export const isRecipientAuthorized = async ({
return await verifyTwoFactorAuthenticationToken({
user,
totpCode: token,
window: 10, // 5 minutes worth of tokens
});
})
.exhaustive();