From 0d46d56adf9c554789d9382093588a446772b7b1 Mon Sep 17 00:00:00 2001 From: denihs Date: Mon, 28 Mar 2022 15:40:42 -0400 Subject: [PATCH 1/7] - updating history.md - Throwing error when user is not found on `Accounts._is2faEnabledForUser` --- docs/history.md | 5 ++++ packages/accounts-2fa/2fa-server.js | 30 +++++++++++-------- packages/accounts-password/package.js | 2 +- packages/accounts-passwordless/package.js | 2 +- .../passwordless_server.js | 5 +--- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/docs/history.md b/docs/history.md index e11704c5be..d5dc7877bf 100644 --- a/docs/history.md +++ b/docs/history.md @@ -12,6 +12,11 @@ N/A * `accounts-2fa@1.0.1` - Reduce one DB call on 2FA login. [PR](https://github.com/meteor/meteor/pull/11985) + - Throw error when user is not found on `Accounts._is2faEnabledForUser` +* `accounts-password@2.3.1` + - Use method `Accounts._check2faEnabled` when validating 2FA +* `accounts-passwordless@2.1.1` + - Use method `Accounts._check2faEnabled` when validating 2FA * `oauth@2.1.2` - Check effectively if popup was blocked by browser. [PR](https://github.com/meteor/meteor/pull/11984) * `standard-minifier-css@1.8.1` diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 2969909b0a..e1c8043499 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -29,7 +29,13 @@ Accounts._is2faEnabledForUser = selector => { } } - const user = Meteor.users.findOne(selector) || {}; + const user = Meteor.users.findOne(selector); + if (!user) { + throw new Meteor.Error( + 500, + `User not found with the selector ${JSON.stringify(selector)}` + ); + } return Accounts._check2faEnabled(user); }; @@ -60,7 +66,7 @@ Meteor.methods({ const { secret, uri } = twofactor.generateSecret({ name: appName.trim(), - account: user.username || user._id + account: user.username || user._id, }); const svg = new QRCode(uri).svg(); @@ -69,9 +75,9 @@ Meteor.methods({ { $set: { 'services.twoFactorAuthentication': { - secret - } - } + secret, + }, + }, } ); @@ -86,7 +92,7 @@ Meteor.methods({ } const { - services: { twoFactorAuthentication } + services: { twoFactorAuthentication }, } = user; if (!twoFactorAuthentication || !twoFactorAuthentication.secret) { @@ -105,9 +111,9 @@ Meteor.methods({ $set: { 'services.twoFactorAuthentication': { ...twoFactorAuthentication, - type: 'otp' - } - } + type: 'otp', + }, + }, } ); }, @@ -122,8 +128,8 @@ Meteor.methods({ { _id: userId }, { $unset: { - 'services.twoFactorAuthentication': 1 - } + 'services.twoFactorAuthentication': 1, + }, } ); }, @@ -139,5 +145,5 @@ Meteor.methods({ } return Accounts._is2faEnabledForUser(selector); - } + }, }); diff --git a/packages/accounts-password/package.js b/packages/accounts-password/package.js index 8ed4267272..c4f9cadbd3 100644 --- a/packages/accounts-password/package.js +++ b/packages/accounts-password/package.js @@ -5,7 +5,7 @@ Package.describe({ // 2.2.x in the future. The version was also bumped to 2.0.0 temporarily // during the Meteor 1.5.1 release process, so versions 2.0.0-beta.2 // through -beta.5 and -rc.0 have already been published. - version: '2.3.0', + version: '2.3.1', }); Npm.depends({ diff --git a/packages/accounts-passwordless/package.js b/packages/accounts-passwordless/package.js index d9c87f7c97..1939095089 100644 --- a/packages/accounts-passwordless/package.js +++ b/packages/accounts-passwordless/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: 'No-password login/sign-up support for accounts', - version: '2.1.0', + version: '2.1.1', }); Package.onUse(api => { diff --git a/packages/accounts-passwordless/passwordless_server.js b/packages/accounts-passwordless/passwordless_server.js index 42bbf43bf2..6a0b57b3b7 100644 --- a/packages/accounts-passwordless/passwordless_server.js +++ b/packages/accounts-passwordless/passwordless_server.js @@ -89,10 +89,7 @@ Accounts.registerLoginHandler('passwordless', options => { if (!error && verifiedEmail) { // This method is added by the package accounts-2fa - if ( - Accounts._is2faEnabledForUser && - Accounts._is2faEnabledForUser({ _id: user._id }) - ) { + if (Accounts._check2faEnabled?.(user)) { if (!options.code) { Accounts._handleError('2FA code must be informed', true, 'no-2fa-code'); return; From ec913e4ff37e7e7140c041e51f2d30adca87a337 Mon Sep 17 00:00:00 2001 From: denihs Date: Tue, 29 Mar 2022 09:58:19 -0400 Subject: [PATCH 2/7] - updating history.md - Remove vulnerability from the method `has2faEnabled` - updating accounts-2fa.md - updating accounts-2fa tests --- docs/history.md | 1 + docs/source/packages/accounts-2fa.md | 5 ++- packages/accounts-2fa/2fa-client.js | 7 ++--- packages/accounts-2fa/2fa-server.js | 31 +++---------------- packages/accounts-2fa/client_tests.js | 9 +++--- packages/accounts-2fa/server_tests.js | 25 +++++++++++---- .../accounts-base/accounts_client_tests.js | 4 +-- 7 files changed, 37 insertions(+), 45 deletions(-) diff --git a/docs/history.md b/docs/history.md index d5dc7877bf..6f7ab42cfa 100644 --- a/docs/history.md +++ b/docs/history.md @@ -13,6 +13,7 @@ N/A * `accounts-2fa@1.0.1` - Reduce one DB call on 2FA login. [PR](https://github.com/meteor/meteor/pull/11985) - Throw error when user is not found on `Accounts._is2faEnabledForUser` + - Remove vulnerability from the method `has2faEnabled` * `accounts-password@2.3.1` - Use method `Accounts._check2faEnabled` when validating 2FA * `accounts-passwordless@2.1.1` diff --git a/docs/source/packages/accounts-2fa.md b/docs/source/packages/accounts-2fa.md index 26e01801a7..b4569d459b 100644 --- a/docs/source/packages/accounts-2fa.md +++ b/docs/source/packages/accounts-2fa.md @@ -186,7 +186,7 @@ To integrate this package with any other existing Login method, it's necessary f 1 - For the client, create a new method from your current login method. So for example, from the method `Meteor.loginWithPassword` we created a new one called `Meteor.loginWithPasswordAnd2faCode`, and the only difference between them is that the latest one receives one additional parameter, the 2FA code, but we call the same function on the server side. -2 - For the server, inside the function that will log the user in, you verify if the function `Accounts._is2faEnabledForUser` exists, and if yes, you call it providing the user you want to check if the 2FA is enabled, and if either of these statements are false, you proceed with the login flow. This function exists only when the package `accounts-2fa` is added to the project. +2 - For the server, inside the function that will log the user in, you verify if the function `Accounts._check2faEnabled` exists, and if yes, you call it providing the user you want to check if the 2FA is enabled, and if either of these statements are false, you proceed with the login flow. This function exists only when the package `accounts-2fa` is added to the project. If both statements are true, and the login validation succeeds, you verify if a code was provided: if not, throw an error; if it was provided, verify if the code is valid by calling the function `Accounts._isTokenValid`. if `Accounts._isTokenValid` returns false, throw an error. @@ -196,8 +196,7 @@ Here it's an example: const result = validateLogin(); if ( !result.error && - Accounts._is2faEnabledForUser && - Accounts._is2faEnabledForUser(user) + Accounts._check2faEnabled?.(user) ) { if (!code) { Accounts._handleError('2FA code must be informed.'); diff --git a/packages/accounts-2fa/2fa-client.js b/packages/accounts-2fa/2fa-client.js index 0ae7a3ee87..7ca592151f 100644 --- a/packages/accounts-2fa/2fa-client.js +++ b/packages/accounts-2fa/2fa-client.js @@ -10,14 +10,13 @@ const reportError = (error, callback) => { }; /** - * @summary Verify if the user has 2FA enabled + * @summary Verify if the logged user has 2FA enabled * @locus Client - * @param {Object|String} selector Username, email or custom selector to identify the user. * @param {Function} [callback] Called with a boolean on success that indicates whether the user has * or not 2FA enabled, or with a single `Error` argument on failure. */ -Accounts.has2faEnabled = (selector, callback) => { - Accounts.connection.call('has2faEnabled', selector, callback); +Accounts.has2faEnabled = callback => { + Accounts.connection.call('has2faEnabled', callback); }; /** diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index e1c8043499..73d205c6fe 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -13,7 +13,7 @@ Accounts._check2faEnabled = user => { ); }; -Accounts._is2faEnabledForUser = selector => { +Accounts._is2faEnabledForUser = () => { if (!Meteor.isServer) { throw new Meteor.Error( 400, @@ -21,20 +21,9 @@ Accounts._is2faEnabledForUser = selector => { ); } - if (typeof selector === 'string') { - if (!selector.includes('@')) { - selector = { $or: [{ _id: selector }, { username: selector }] }; - } else { - selector = { email: selector }; - } - } - - const user = Meteor.users.findOne(selector); + const user = Meteor.user(); if (!user) { - throw new Meteor.Error( - 500, - `User not found with the selector ${JSON.stringify(selector)}` - ); + throw new Meteor.Error('no-logged-user', 'No user logged in.'); } return Accounts._check2faEnabled(user); }; @@ -133,17 +122,7 @@ Meteor.methods({ } ); }, - has2faEnabled(selector) { - check(selector, Match.Maybe(Match.OneOf(String, Object))); - const userId = Meteor.userId(); - if (!userId) { - throw new Meteor.Error(400, 'No user logged in.'); - } - - if (!selector) { - selector = { _id: userId }; - } - - return Accounts._is2faEnabledForUser(selector); + has2faEnabled() { + return Accounts._is2faEnabledForUser(); }, }); diff --git a/packages/accounts-2fa/client_tests.js b/packages/accounts-2fa/client_tests.js index 11fe9022fb..59f396b78f 100644 --- a/packages/accounts-2fa/client_tests.js +++ b/packages/accounts-2fa/client_tests.js @@ -1,13 +1,14 @@ import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; -Tinytest.add('account - 2fa - has2faEnabled', test => { - const userId = Accounts.createUser({ +Tinytest.addAsync('account - 2fa - has2faEnabled - client', (test, done) => { + Accounts.createUser({ username: Random.id(), password: Random.id(), }); - Accounts.has2faEnabled(userId, (error, result) => { - test.equal(result, false); + Accounts.has2faEnabled((error, result) => { + test.isFalse(result); + done(); }); }); diff --git a/packages/accounts-2fa/server_tests.js b/packages/accounts-2fa/server_tests.js index 9f4e089026..8a95fc5602 100644 --- a/packages/accounts-2fa/server_tests.js +++ b/packages/accounts-2fa/server_tests.js @@ -1,13 +1,26 @@ import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; -Tinytest.add('account - 2fa - has2faEnabled', test => { - // Create users - const userWithout2FA = Accounts.insertUserDoc({}, { emails: [{address: `${Random.id()}@meteorapp.com`, verified: true}] }); - const userWith2FA = Accounts.insertUserDoc({}, { emails: [{address: `${Random.id()}@meteorapp.com`, verified: true}], services: { twoFactorAuthentication: { type: 'otp', secret: 'superSecret' } } }); +const findUserById = id => Meteor.users.findOne(id); - test.equal(Accounts._is2faEnabledForUser(userWithout2FA), false); - test.equal(Accounts._is2faEnabledForUser(userWith2FA), true); +Tinytest.add('account - 2fa - has2faEnabled - server', test => { + // Create users + const userWithout2FA = Accounts.insertUserDoc( + {}, + { emails: [{ address: `${Random.id()}@meteorapp.com`, verified: true }] } + ); + const userWith2FA = Accounts.insertUserDoc( + {}, + { + emails: [{ address: `${Random.id()}@meteorapp.com`, verified: true }], + services: { + twoFactorAuthentication: { type: 'otp', secret: 'superSecret' }, + }, + } + ); + + test.equal(Accounts._check2faEnabled(findUserById(userWithout2FA)), false); + test.equal(Accounts._check2faEnabled(findUserById(userWith2FA)), true); // cleanup Accounts.users.remove(userWithout2FA); diff --git a/packages/accounts-base/accounts_client_tests.js b/packages/accounts-base/accounts_client_tests.js index 3e179eea7e..9ebb7d4b9f 100644 --- a/packages/accounts-base/accounts_client_tests.js +++ b/packages/accounts-base/accounts_client_tests.js @@ -240,12 +240,12 @@ Tinytest.addAsync( // enable 2fa Accounts.enableUser2fa(token, () => { // verifies if 2fa is enabled - Accounts.has2faEnabled(username, (err, isEnabled) => { + Accounts.has2faEnabled((err, isEnabled) => { test.isTrue(isEnabled); // disable 2fa Accounts.disableUser2fa(() => { // verifies if 2fa is disabled - Accounts.has2faEnabled(username, (err, isEnabled) => { + Accounts.has2faEnabled((err, isEnabled) => { test.isFalse(!!isEnabled); removeTestUser(done); }); From 6c0cc8d6f885daad2929922520b7902f8bc294db Mon Sep 17 00:00:00 2001 From: denihs Date: Tue, 29 Mar 2022 10:31:15 -0400 Subject: [PATCH 3/7] - updating history.md with the accounts-2fa breaking change - removing the unnecessary Meteor.isServer test in the function _is2faEnabledForUser --- docs/history.md | 3 ++- packages/accounts-2fa/2fa-server.js | 7 ------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/docs/history.md b/docs/history.md index 6f7ab42cfa..f74bfae130 100644 --- a/docs/history.md +++ b/docs/history.md @@ -4,7 +4,8 @@ #### Breaking Changes -N/A +* `accounts-2fa@1.0.1` + - The method `has2faEnabled` no longer takes a selector as an argument, just the callback. #### Migration Steps diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 73d205c6fe..338334636b 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -14,13 +14,6 @@ Accounts._check2faEnabled = user => { }; Accounts._is2faEnabledForUser = () => { - if (!Meteor.isServer) { - throw new Meteor.Error( - 400, - 'The function _is2faEnabledForUser can only be called on the server' - ); - } - const user = Meteor.user(); if (!user) { throw new Meteor.Error('no-logged-user', 'No user logged in.'); From 4f9c4763b0144f8bf1cbc93a7421aa52444acd1c Mon Sep 17 00:00:00 2001 From: denihs Date: Tue, 29 Mar 2022 11:20:05 -0400 Subject: [PATCH 4/7] - updating history.md with the accounts-2fa breaking change - auto-publishing the field `services.twoFactorAuthentication.type` for logged in users. - `generate2faActivationQrCode` now throws an error if it's being called when the user already has 2FA enabled. --- docs/history.md | 2 ++ packages/accounts-2fa/2fa-server.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/history.md b/docs/history.md index f74bfae130..eb57d26456 100644 --- a/docs/history.md +++ b/docs/history.md @@ -6,6 +6,7 @@ * `accounts-2fa@1.0.1` - The method `has2faEnabled` no longer takes a selector as an argument, just the callback. + - `generate2faActivationQrCode` now throws an error if it's being called when the user already has 2FA enabled. #### Migration Steps @@ -15,6 +16,7 @@ - Reduce one DB call on 2FA login. [PR](https://github.com/meteor/meteor/pull/11985) - Throw error when user is not found on `Accounts._is2faEnabledForUser` - Remove vulnerability from the method `has2faEnabled` + - Now the package auto-publish the field `services.twoFactorAuthentication.type` for logged in users. * `accounts-password@2.3.1` - Use method `Accounts._check2faEnabled` when validating 2FA * `accounts-passwordless@2.1.1` diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 338334636b..fcefe57ff1 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -46,9 +46,16 @@ Meteor.methods({ ); } + if (Accounts._check2faEnabled(user)) { + throw new Meteor.Error( + '2fa-activated', + 'The 2FA is activated. You need to disable the 2FA first before trying to generate a new activation code.' + ); + } + const { secret, uri } = twofactor.generateSecret({ name: appName.trim(), - account: user.username || user._id, + account: user.username || user.email || user._id, }); const svg = new QRCode(uri).svg(); @@ -119,3 +126,7 @@ Meteor.methods({ return Accounts._is2faEnabledForUser(); }, }); + +Accounts.addAutopublishFields({ + forLoggedInUser: ['services.twoFactorAuthentication.type'], +}); From 41d297bcc8485445530374b59b686251fa6d72e1 Mon Sep 17 00:00:00 2001 From: denihs Date: Tue, 29 Mar 2022 11:23:08 -0400 Subject: [PATCH 5/7] - bumping the major version instead of the patch, as there's breaking changes --- docs/history.md | 4 ++-- packages/accounts-2fa/package.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/history.md b/docs/history.md index eb57d26456..f34c64c782 100644 --- a/docs/history.md +++ b/docs/history.md @@ -4,7 +4,7 @@ #### Breaking Changes -* `accounts-2fa@1.0.1` +* `accounts-2fa@2.0.0` - The method `has2faEnabled` no longer takes a selector as an argument, just the callback. - `generate2faActivationQrCode` now throws an error if it's being called when the user already has 2FA enabled. @@ -12,7 +12,7 @@ #### Meteor Version Release -* `accounts-2fa@1.0.1` +* `accounts-2fa@2.0.0` - Reduce one DB call on 2FA login. [PR](https://github.com/meteor/meteor/pull/11985) - Throw error when user is not found on `Accounts._is2faEnabledForUser` - Remove vulnerability from the method `has2faEnabled` diff --git a/packages/accounts-2fa/package.js b/packages/accounts-2fa/package.js index 552e0f4126..40316e0769 100644 --- a/packages/accounts-2fa/package.js +++ b/packages/accounts-2fa/package.js @@ -1,5 +1,5 @@ Package.describe({ - version: '1.0.1', + version: '2.0.0', summary: 'Package used to enable two factor authentication through OTP protocol', }); From d89c57aff3a297c64292b509f72c279ddd195f92 Mon Sep 17 00:00:00 2001 From: denihs Date: Tue, 29 Mar 2022 12:48:54 -0400 Subject: [PATCH 6/7] - getting user email from `emails` --- packages/accounts-2fa/2fa-server.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index fcefe57ff1..08ec434afe 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -53,9 +53,10 @@ Meteor.methods({ ); } + const emails = user.emails || []; const { secret, uri } = twofactor.generateSecret({ name: appName.trim(), - account: user.username || user.email || user._id, + account: user.username || emails[0]?.address || user._id, }); const svg = new QRCode(uri).svg(); From 4561d8314531a13acde4219c4c90b809f81b1141 Mon Sep 17 00:00:00 2001 From: denihs Date: Wed, 30 Mar 2022 17:13:24 -0400 Subject: [PATCH 7/7] - updating accounts-2fa docs - adjust details in the accounts-2fa server side --- docs/source/packages/accounts-2fa.md | 20 ++++++++++++++++---- packages/accounts-2fa/2fa-client.js | 5 +++-- packages/accounts-2fa/2fa-server.js | 8 +++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/docs/source/packages/accounts-2fa.md b/docs/source/packages/accounts-2fa.md index b4569d459b..af304baa97 100644 --- a/docs/source/packages/accounts-2fa.md +++ b/docs/source/packages/accounts-2fa.md @@ -5,7 +5,7 @@ description: Documentation of Meteor's `accounts-2fa` package. This package allows you to provide a way for your users to enable 2FA on their accounts, using an authenticator app such as Google Authenticator, or 1Password. When the user is logged in on your app, they will be able to generate a new QR code and read this code on the app they prefer. After that, they'll start receiving their codes. Then, they can finish enabling 2FA on your app, and every time they try to log in to your app, you can redirect them to a place where they can provide a code they received from the authenticator. -This package uses [node-2fa](https://www.npmjs.com/package/node-2fa) which works on top of [notp](https://github.com/guyht/notp), **that** implements TOTP ([RFC 6238](https://www.ietf.org/rfc/rfc6238.txt)) (the Authenticator standard), which is based on HOTP ([RFC 4226](https://www.ietf.org/rfc/rfc4226.txt)) to provide codes that are exactly compatible with all other Authenticator apps and services that use them. +To provide codes that are exactly compatible with all other Authenticator apps and services that implements TOTP, this package uses [node-2fa](https://www.npmjs.com/package/node-2fa) which works on top of [notp](https://github.com/guyht/notp), **that** implements TOTP ([RFC 6238](https://www.ietf.org/rfc/rfc6238.txt)) (the Authenticator standard), which is based on HOTP ([RFC 4226](https://www.ietf.org/rfc/rfc4226.txt)). > This package is meant to be used with [`accounts-password`](https://docs.meteor.com/api/passwords.html) or [`accounts-passwordless`](https://docs.meteor.com/packages/accounts-passwordless.html), so if you don't have either of those in your project, you'll need to add one of them. In the future, we want to enable the use of this package with other login methods, our oauth methods (Google, GitHub, etc...). @@ -15,7 +15,7 @@ The first step, in order to enable 2FA, is to generate a QR code so that the use {% apibox "Accounts.generate2faActivationQrCode" "module":"accounts-base" %} -Receives an `appName` which is the name of your app that will show up when the user scans the QR code. Also, a callback called with a QR code in SVG format and QR secret on success +Receives an `appName` which is the name of your app that will show up when the user scans the QR code. Also, a callback called, on success, with a QR code in SVG format, a QR secret, and the URI that can be used to activate the 2FA in an authenticator app, or a single `Error` argument on failure. On success, this function will also add an object to the logged user's services object containing the QR secret: @@ -43,8 +43,9 @@ const [qrCode, setQrCode] = useState(null); ``` +This method can fail throwing one of the following errors: +* "2FA code must be informed [no-2fa-code]" if a 2FA code was not provided. +* "Invalid 2FA code [invalid-2fa-code]" if the provided 2FA code is invalid. +

Working with accounts-passwordless

Following the same logic from the previous package, if the 2FA is enabled, an error will be returned to the callback of the function [`Meteor.passwordlessLoginWithToken`](https://docs.meteor.com/packages/accounts-passwordless.html#Meteor-passwordlessLoginWithToken), then you can redirect the user to a place where they can provide a code. @@ -180,13 +188,17 @@ Now you can call the function `Meteor.passwordlessLoginWithTokenAnd2faCode` that {% apibox "Meteor.passwordlessLoginWithTokenAnd2faCode" %} +This method can fail throwing one of the following errors: +* "2FA code must be informed [no-2fa-code]" if a 2FA code was not provided. +* "Invalid 2FA code [invalid-2fa-code]" if the provided 2FA code is invalid. +

How to integrate an Authentication Package with accounts-2fa

To integrate this package with any other existing Login method, it's necessary following two steps: 1 - For the client, create a new method from your current login method. So for example, from the method `Meteor.loginWithPassword` we created a new one called `Meteor.loginWithPasswordAnd2faCode`, and the only difference between them is that the latest one receives one additional parameter, the 2FA code, but we call the same function on the server side. -2 - For the server, inside the function that will log the user in, you verify if the function `Accounts._check2faEnabled` exists, and if yes, you call it providing the user you want to check if the 2FA is enabled, and if either of these statements are false, you proceed with the login flow. This function exists only when the package `accounts-2fa` is added to the project. +2 - For the server, inside the function that will log the user in, you verify if the function `Accounts._check2faEnabled` exists, and if yes, you call it providing the user object you want to check if the 2FA is enabled, and if either of these statements are false, you proceed with the login flow. This function exists only when the package `accounts-2fa` is added to the project. If both statements are true, and the login validation succeeds, you verify if a code was provided: if not, throw an error; if it was provided, verify if the code is valid by calling the function `Accounts._isTokenValid`. if `Accounts._isTokenValid` returns false, throw an error. diff --git a/packages/accounts-2fa/2fa-client.js b/packages/accounts-2fa/2fa-client.js index 7ca592151f..4a87dbda1a 100644 --- a/packages/accounts-2fa/2fa-client.js +++ b/packages/accounts-2fa/2fa-client.js @@ -24,8 +24,9 @@ Accounts.has2faEnabled = callback => { * @locus Client * @param {String} appName It's the name of your app that will show up when the user scans the QR code. * @param {Function} callback - * Called with a QR code in SVG format on success, or with a single `Error` argument - * on failure. + * Called with a single `Error` argument on failure. + * Or, on success, called with an object containing the QR code in SVG format (svg), + * the QR secret (secret), and the URI so the user can manually activate the 2FA without reading the QR code (uri). */ Accounts.generate2faActivationQrCode = (appName, callback) => { if (!appName) { diff --git a/packages/accounts-2fa/2fa-server.js b/packages/accounts-2fa/2fa-server.js index 08ec434afe..0155d53f00 100644 --- a/packages/accounts-2fa/2fa-server.js +++ b/packages/accounts-2fa/2fa-server.js @@ -31,7 +31,9 @@ Accounts._isTokenValid = (secret, code) => { ); } const { delta } = twofactor.verifyToken(secret, code, 10) || {}; - return delta != null && delta >= 0; + // we are using != instead of !==, which means "undefined != null" and "null != null" are both false, + // so we don't need to check delta !== undefined + return delta != null && delta === 0; }; Meteor.methods({ @@ -71,7 +73,7 @@ Meteor.methods({ } ); - return { svg, secret }; + return { svg, secret, uri }; }, enableUser2fa(code) { check(code, String); @@ -92,7 +94,7 @@ Meteor.methods({ ); } if (!Accounts._isTokenValid(twoFactorAuthentication.secret, code)) { - throw new Meteor.Error(400, 'Invalid code.'); + Accounts._handleError('Invalid 2FA code', true, 'invalid-2fa-code'); } Meteor.users.update(