[nodebb-plugin-2factor] Two-Factor Authentication

NodeBB Plugins
  • In addition to regular authentication via username/password or SSO, a second layer of security can be configured, permitting access only if a time-based one-time password is supplied, typically generated/stored on a mobile device.

    The Two-Factor Authentication plugin will expose this feature to end-users, allowing them to configure their
    devices and enabling this enhanced security on their account.


    • Requires NodeBB v0.7.2 or newer.


    Install the plugin via the ACP/Plugins page.


    Token Generation Step

    Token Generation Step

    Challenge Step

    Challenge Step



    • Added the ability to disassociate user tokens via the ACP page (in case users get locked out)


    • Bug: Fixed the browser title on the TFA settings page
    • Bug: Fixed issue where hitting enter while keying in the validation code would abort the process
  • Very nice! Works fine.

  • Good to hear it @revunix ๐Ÿ˜„

    I hope to add support for reset keys and ACP deactivation, as currently, if you lose your device, you won't be able to bypass ๐Ÿ˜ฆ

  • @julian I recently had my phone smashed by a drunk friend (I could just make out the numbers on a flickering screen) and discovered how terrible the "reset code" or "add code to another device" situation is with a new phone on sooo many websites where I had 2FA, even if you can get in with your current one.

  • @drew Indeed, I don't know how many support tickets I will need to make for all the sites where I use 2 factor auth, 3 Google accounts, Steam, Cloudflare and much more...

    Anyways 2FA looks nice.

  • Published an update. (Changelog in OP)

    @drew @kowlin At least now the administrator can reset TFA keys, although getting in touch with the admin is another matter altogether ๐Ÿ˜†

  • Are backup codes supported now btw? I see a closed GitHub issue for them, which suggests they are.

  • @LB Yep, they are, although they are generated only when you start the 2FA setup process, so you will want to disable 2FA, trash your record, and re-generate one. The backup codes will be displayed a single time for you to record.

  • When I scan this with Authy or Google Authenticator it says "QR code is invalid" no matter how many times I create a new one. Is there a fix?

  • @cookieman768 The same situation(((

  • Plug-ins no longer work with the version ยซ1.7.5ยป.

  • @ilya Can you elaborate on what doesn't work? Saying "no longer work" doesn't help narrow down any problems.

  • @julian The problem was solved with FreeOTP Authenticator (Android).

  • @ilya This plugin no longer works with 1.17. Error below

    2021-04-23T13:18:12.371Z [4567/469428] - error: uncaughtException: Failed to lookup view "admin/dashboard" in views directory "/home/phenomlab/nodebb/build/public/templates"
    Error: Failed to lookup view "admin/dashboard" in views directory "/home/phenomlab/nodebb/build/public/templates"
        at Function.render (/home/phenomlab/nodebb/node_modules/express/lib/application.js:580:17)
        at ServerResponse.render (/home/phenomlab/nodebb/node_modules/express/lib/response.js:1012:7)
        at /home/phenomlab/nodebb/src/middleware/render.js:89:11
        at new Promise (<anonymous>)
        at renderContent (/home/phenomlab/nodebb/src/middleware/render.js:88:10)
        at ServerResponse.renderOverride [as render] (/home/phenomlab/nodebb/src/middleware/render.js:64:14)
        at processTicksAndRejections (node:internal/process/task_queues:96:5) {"error":{"view":{"defaultEngine":"tpl","ext":".tpl","name":"admin/dashboard","root":"/home/phenomlab/nodebb/build/public/templates"}},"stack":"Error: Failed to lookup view \"admin/dashboard\" in views directory \"/home/phenomlab/nodebb/build/public/templates\"\n    at Function.render (/home/phenomlab/nodebb/node_modules/express/lib/application.js:580:17)\n    at ServerResponse.render (/home/phenomlab/nodebb/node_modules/express/lib/response.js:1012:7)\n    at /home/phenomlab/nodebb/src/middleware/render.js:89:11\n    at new Promise (<anonymous>)\n    at renderContent (/home/phenomlab/nodebb/src/middleware/render.js:88:10)\n    at ServerResponse.renderOverride [as render] (/home/phenomlab/nodebb/src/middleware/render.js:64:14)\n    at processTicksAndRejections (node:internal/process/task_queues:96:5)","exception":true,"date":"Fri Apr 23 2021 14:18:12 GMT+0100 (British Summer Time)","process":{"pid":469428,"uid":1000,"gid":1000,"cwd":"/home/phenomlab/nodebb","execPath":"/usr/bin/node","version":"v16.0.0","argv":["/usr/bin/node","/home/phenomlab/nodebb/app.js"],"memoryUsage":{"rss":294481920,"heapTotal":195198976,"heapUsed":164432120,"external":74292726,"arrayBuffers":70953438}},"os":{"loadavg":[1.23,1.17,0.8],"uptime":340350.31},"trace":[{"column":17,"file":"/home/phenomlab/nodebb/node_modules/express/lib/application.js","function":"Function.render","line":580,"method":"render","native":false},{"column":7,"file":"/home/phenomlab/nodebb/node_modules/express/lib/response.js","function":"ServerResponse.render","line":1012,"method":"render","native":false},{"column":11,"file":"/home/phenomlab/nodebb/src/middleware/render.js","function":null,"line":89,"method":null,"native":false},{"column":null,"file":null,"function":"new Promise","line":null,"method":null,"native":false},{"column":10,"file":"/home/phenomlab/nodebb/src/middleware/render.js","function":"renderContent","line":88,"method":null,"native":false},{"column":14,"file":"/home/phenomlab/nodebb/src/middleware/render.js","function":"ServerResponse.renderOverride [as render]","line":64,"method":"renderOverride [as render]","native":false},{"column":5,"file":"node:internal/process/task_queues","function":"processTicksAndRejections","line":96,"method":null,"native":false}]}
    2021-04-23T13:18:12.371Z [4567/469428] - error: Error: Failed to lookup view "admin/dashboard" in views directory "/home/phenomlab/nodebb/build/public/templates"
        at Function.render (/home/phenomlab/nodebb/node_modules/express/lib/application.js:580:17)
        at ServerResponse.render (/home/phenomlab/nodebb/node_modules/express/lib/response.js:1012:7)
        at /home/phenomlab/nodebb/src/middleware/render.js:89:11
        at new Promise (<anonymous>)
        at renderContent (/home/phenomlab/nodebb/src/middleware/render.js:88:10)
        at ServerResponse.renderOverride [as render] (/home/phenomlab/nodebb/src/middleware/render.js:64:14)
        at processTicksAndRejections (node:internal/process/task_queues:96:5)
  • I've just seen an update for this plugin. Is it compatible now ? ๐Ÿค”

  • v5.0.0 of the 2factor authentication plugin has been published. It now allows for concurrent second factors, so you can have both a hardware key and an authenticator app in use at the same time.

    When challenged, you can use either option to verify your identity.

Suggested Topics

  • 1 Votes
    4 Posts

    @julian Definately, we will contribute some changes to upstream.

    We are a group of student developers who are deliberately not affiliated to the university to fight against censorship. The university have a history of tooking over administration from the student developers and grant censorship privilege to the "youth studies center", and we refuse to become yet another website that does so. We already lost https://bbs.pku.edu.cn and https://treehole.pku.edu.cn, and there's not yet a libre and civilzed forum for the Peking University students, faculties and almuni.

    FYI, forum-based approach isn't pretty attractive these days. Although we specially desired and wished a forum-based approach to build an online community, we failed to attract a larger userbase, at least in the short term. Currently, most students are more interested in a "tree hollow"-based pesudo anonymous approach. We have a modest user base of around 150~200 users in 2 months, but compared to the new "tree hollow" https://top2.life (which attracted >2400 users in just 3 days after creation) it's really a tiny fraction. top2.life administrators may are better than us in advocacy and have a larger social circle to attract users, but we think the primary reason is that students get accustomed to "tree hollow" fashion and don't really like traditional forums anymore.

  • 0 Votes
    5 Posts

    @aisar-g I know what you're referring to, though. The ideal scenario would be saving stuff to res.locals and then referring to that data in getReplies.

    Unfortunately, neither of those hooks pass req or res, and while we were looking into something like that before (see: continuation-local-storage), it did not ultimately pan out.

  • 0 Votes
    9 Posts

    Can't really debug as I don't know your app, but if there's a stack trace, it'll show the problem, likely.

    Also you probably want to set the cookie after passport does its local authentication, otherwise a malicious user can attempt to log into an account using a wrong password, but still get a valid jwt, and then log into the nodebb forum under that account.

  • 0 Votes
    9 Posts

    Thank you for your quick reply! It was extremely helpful.

    I have added the 1-line fix to map your from_name field to SendGrid's API. I submitted a PR to the nodebb-plugin-emailer-sendgrid GitHub repo, so that hopefully others can benefit too. This is the first time I have submitted a PR to an open source project, so I hope I did the procedure correctly. ๐Ÿ™‚

    The 1-line fix works on our installation, and I am now receiving emails which have a proper From name!

  • 0 Votes
    7 Posts


    You could do that, it's true. That said I'd like to kinda shunt people away from NginX. To hell with it and its antiquated ways. (okay okay, that is harsh-- it's very good at what it does and most likely it will continue doing that even a decade from now.)

    Anyway, try out Caddy. I use Caddy in front of my web applications, which I run in the wonderful isolation of Docker containers, and caddy slaps an automatically generated SSL/TLS cert on them, yay! If you're running without encryption, you no longer have any excuse. Both Caddy and Let's Encrypt have made trivial work out of the previously quite harsh process of getting your site secured.