11import crypto from "node:crypto" ;
22import bcrypt from "bcrypt" ;
3- import { authenticator } from "otplib" ;
3+ import { createGuardrails , generateSecret , generateURI , verify } from "otplib" ;
44import errs from "../lib/error.js" ;
55import authModel from "../models/auth.js" ;
66import internalUser from "./user.js" ;
@@ -27,7 +27,6 @@ const generateBackupCodes = async () => {
2727} ;
2828
2929const internal2fa = {
30-
3130 /**
3231 * Check if user has 2FA enabled
3332 * @param {number } userId
@@ -72,8 +71,12 @@ const internal2fa = {
7271 startSetup : async ( access , userId ) => {
7372 await access . can ( "users:password" , userId ) ;
7473 const user = await internalUser . get ( access , { id : userId } ) ;
75- const secret = authenticator . generateSecret ( ) ;
76- const otpauth_url = authenticator . keyuri ( user . email , APP_NAME , secret ) ;
74+ const secret = generateSecret ( ) ;
75+ const otpauth_url = generateURI ( {
76+ issuer : APP_NAME ,
77+ label : user . email ,
78+ secret : secret ,
79+ } ) ;
7780 const auth = await internal2fa . getUserPasswordAuth ( userId ) ;
7881
7982 // ensure user isn't already setup for 2fa
@@ -85,7 +88,8 @@ const internal2fa = {
8588 const meta = auth . meta || { } ;
8689 meta . totp_pending_secret = secret ;
8790
88- await authModel . query ( )
91+ await authModel
92+ . query ( )
8993 . where ( "id" , auth . id )
9094 . andWhere ( "user_id" , userId )
9195 . andWhere ( "type" , "password" )
@@ -112,8 +116,8 @@ const internal2fa = {
112116 throw new errs . ValidationError ( "No pending 2FA setup found" ) ;
113117 }
114118
115- const valid = authenticator . verify ( { token : code , secret } ) ;
116- if ( ! valid ) {
119+ const result = await verify ( { token : code , secret } ) ;
120+ if ( ! result . valid ) {
117121 throw new errs . ValidationError ( "Invalid verification code" ) ;
118122 }
119123
@@ -156,12 +160,12 @@ const internal2fa = {
156160 throw new errs . ValidationError ( "2FA is not enabled" ) ;
157161 }
158162
159- const valid = authenticator . verify ( {
163+ const result = await verify ( {
160164 token : code ,
161165 secret : auth . meta . totp_secret ,
162166 } ) ;
163167
164- if ( ! valid ) {
168+ if ( ! result . valid ) {
165169 throw new errs . AuthError ( "Invalid verification code" ) ;
166170 }
167171
@@ -194,20 +198,30 @@ const internal2fa = {
194198 return false ;
195199 }
196200
197- // Try TOTP code first
198- const valid = authenticator . verify ( {
199- token,
200- secret,
201- } ) ;
202-
203- if ( valid ) {
204- return true ;
201+ // Try TOTP code first, if it's 6 chars. it will throw errors if it's not 6 chars
202+ // and the backup codes are 8 chars.
203+ if ( token . length === 6 ) {
204+ const result = await verify ( {
205+ token,
206+ secret,
207+ // These guardrails lower the minimum length requirement for secrets.
208+ // In v12 of otplib the default minimum length is 10 and in v13 it is 16.
209+ // Since there are 2fa secrets in the wild generated with v12 we need to allow shorter secrets
210+ // so people won't be locked out when upgrading.
211+ guardrails : createGuardrails ( {
212+ MIN_SECRET_BYTES : 10 ,
213+ } ) ,
214+ } ) ;
215+
216+ if ( result . valid ) {
217+ return true ;
218+ }
205219 }
206220
207221 // Try backup codes
208222 const backupCodes = auth ?. meta ?. backup_codes || [ ] ;
209223 for ( let i = 0 ; i < backupCodes . length ; i ++ ) {
210- const match = await bcrypt . compare ( code . toUpperCase ( ) , backupCodes [ i ] ) ;
224+ const match = await bcrypt . compare ( token . toUpperCase ( ) , backupCodes [ i ] ) ;
211225 if ( match ) {
212226 // Remove used backup code
213227 const updatedCodes = [ ...backupCodes ] ;
@@ -248,12 +262,12 @@ const internal2fa = {
248262 throw new errs . ValidationError ( "No 2FA secret found" ) ;
249263 }
250264
251- const valid = authenticator . verify ( {
265+ const result = await verify ( {
252266 token,
253267 secret,
254268 } ) ;
255269
256- if ( ! valid ) {
270+ if ( ! result . valid ) {
257271 throw new errs . ValidationError ( "Invalid verification code" ) ;
258272 }
259273
0 commit comments