Skip to content

Commit 408dbc8

Browse files
committed
feat(mfa): mfa challenge expiration check
1 parent 7f4da2d commit 408dbc8

File tree

5 files changed

+51
-4
lines changed

5 files changed

+51
-4
lines changed

packages/mfa/__tests__/accounts-mfa.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,21 @@ describe('AccountsMfa', () => {
2727
).rejects.toThrowError('Invalid mfa token');
2828
});
2929
});
30+
31+
describe('isMfaChallengeValid', () => {
32+
it('should return false if challenge is deactivated', () => {
33+
const accountsMfa = new AccountsMfa({ factors: {} });
34+
expect(accountsMfa.isMfaChallengeValid({ deactivated: true } as any)).toBe(false);
35+
});
36+
37+
it('should return false if challenge is expired', () => {
38+
const accountsMfa = new AccountsMfa({ factors: {} });
39+
expect(accountsMfa.isMfaChallengeValid({ createdAt: 100 } as any)).toBe(false);
40+
});
41+
42+
it('should return true if challenge is valid', () => {
43+
const accountsMfa = new AccountsMfa({ factors: {} });
44+
expect(accountsMfa.isMfaChallengeValid({ createdAt: Date.now() } as any)).toBe(true);
45+
});
46+
});
3047
});

packages/mfa/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@
2323
"preset": "ts-jest"
2424
},
2525
"dependencies": {
26+
"ms": "2.1.2",
2627
"tslib": "2.0.1"
2728
},
2829
"devDependencies": {
2930
"@accounts/server": "^0.29.0",
3031
"@accounts/types": "^0.29.0",
3132
"@types/jest": "26.0.9",
33+
"@types/ms": "0.7.31",
3234
"@types/node": "14.0.27",
3335
"jest": "26.3.0",
3436
"rimraf": "3.0.2"

packages/mfa/src/accounts-mfa.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
MfaChallenge,
99
} from '@accounts/types';
1010
import { AccountsServer, AccountsJsError } from '@accounts/server';
11+
import ms from 'ms';
1112
import { ErrorMessages } from './types';
1213
import {
1314
errors,
@@ -27,6 +28,11 @@ export interface AccountsMfaOptions {
2728
* Factors used for mfa
2829
*/
2930
factors: { [key: string]: AuthenticatorService | undefined };
31+
/**
32+
* Expressed in milliseconds or a string describing a time span zeit/ms.
33+
* Default to '5m'.
34+
*/
35+
expiresIn?: string | number;
3036
}
3137

3238
interface AccountsMfaAuthenticateParams {
@@ -35,6 +41,7 @@ interface AccountsMfaAuthenticateParams {
3541

3642
const defaultOptions = {
3743
errors,
44+
expiresIn: '5m' as string | number,
3845
};
3946

4047
export class AccountsMfa<CustomUser extends User = User> implements AuthenticationService {
@@ -46,7 +53,11 @@ export class AccountsMfa<CustomUser extends User = User> implements Authenticati
4653

4754
constructor(options: AccountsMfaOptions) {
4855
this.options = { ...defaultOptions, ...options };
49-
this.factors = options.factors;
56+
if (typeof this.options.expiresIn === 'string') {
57+
const milliseconds = ms(this.options.expiresIn);
58+
this.options.expiresIn = milliseconds;
59+
}
60+
this.factors = this.options.factors;
5061
}
5162

5263
public setStore(store: DatabaseInterface<CustomUser>) {
@@ -66,7 +77,7 @@ export class AccountsMfa<CustomUser extends User = User> implements Authenticati
6677
): Promise<CustomUser | null> {
6778
const mfaToken = params.mfaToken;
6879
const mfaChallenge = mfaToken ? await this.db.findMfaChallengeByToken(mfaToken) : null;
69-
if (!mfaChallenge || !mfaChallenge.authenticatorId) {
80+
if (!mfaChallenge || !mfaChallenge.authenticatorId || !this.isMfaChallengeValid(mfaChallenge)) {
7081
throw new AccountsJsError(
7182
this.options.errors.invalidMfaToken,
7283
AuthenticateErrors.InvalidMfaToken
@@ -86,7 +97,6 @@ export class AccountsMfa<CustomUser extends User = User> implements Authenticati
8697
AuthenticateErrors.FactorNotFound
8798
);
8899
}
89-
// TODO we need to implement some time checking for the mfaToken (eg: expire after X minutes, probably based on the authenticator configuration)
90100
if (!(await factor.authenticate(mfaChallenge, authenticator, params, infos))) {
91101
throw new AccountsJsError(
92102
this.options.errors.authenticationFailed(authenticator.type),
@@ -263,10 +273,15 @@ export class AccountsMfa<CustomUser extends User = User> implements Authenticati
263273
}
264274

265275
public isMfaChallengeValid(mfaChallenge: MfaChallenge): boolean {
266-
// TODO need to check that the challenge is not expired
267276
if (mfaChallenge.deactivated) {
268277
return false;
269278
}
279+
const now = Date.now();
280+
const expirationDate =
281+
new Date(mfaChallenge.createdAt).getTime() + (this.options.expiresIn as number);
282+
if (now > expirationDate) {
283+
return false;
284+
}
270285
return true;
271286
}
272287
}

packages/types/src/types/mfa/mfa-challenge.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ export interface MfaChallenge {
2727
* If deactivated is true, contain the date when the challenge was used
2828
*/
2929
deactivatedAt?: string;
30+
/**
31+
* Contain the date when the challenge was created, is used to check that the challenge is not expired
32+
*/
33+
createdAt: string;
3034
/**
3135
* Custom properties
3236
*/

yarn.lock

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,10 @@ __metadata:
255255
"@accounts/server": ^0.29.0
256256
"@accounts/types": ^0.29.0
257257
"@types/jest": 26.0.9
258+
"@types/ms": 0.7.31
258259
"@types/node": 14.0.27
259260
jest: 26.3.0
261+
ms: 2.1.2
260262
rimraf: 3.0.2
261263
tslib: 2.0.1
262264
peerDependencies:
@@ -6097,6 +6099,13 @@ __metadata:
60976099
languageName: node
60986100
linkType: hard
60996101

6102+
"@types/ms@npm:0.7.31":
6103+
version: 0.7.31
6104+
resolution: "@types/ms@npm:0.7.31"
6105+
checksum: 7ff9798a408aaa371f3d0059f013d09b6eb471cd62019f35924301608f57279943b5d322f497f2ea9ae65477e2a47c26dfd8dd833a4352f883a8a86c051f7125
6106+
languageName: node
6107+
linkType: hard
6108+
61006109
"@types/node-fetch@npm:2.5.7":
61016110
version: 2.5.7
61026111
resolution: "@types/node-fetch@npm:2.5.7"

0 commit comments

Comments
 (0)