@@ -52,7 +52,8 @@ export interface TOTPGenerationOptions {
5252 * The secret used to generate the TOTP.
5353 * It should be Base32 encoded (Feel free to use: https://npm.im/thirty-two).
5454 *
55- * @default random Base32 secret.
55+ * Defaults to a random Base32 secret.
56+ * @default random
5657 */
5758 secret ?: string
5859
@@ -119,15 +120,15 @@ export interface SendTOTPOptions {
119120
120121/**
121122 * The sender email method.
122- * @param options The send TOTP options.
123+ * @param options The SendTOTPOptions options.
123124 */
124125export interface SendTOTP {
125126 ( options : SendTOTPOptions ) : Promise < void >
126127}
127128
128129/**
129130 * The validate email method.
130- * This can be useful to ensure it's not a disposable email address.
131+ * Useful to ensure it's not a disposable email address.
131132 *
132133 * @param email The email address to validate.
133134 */
@@ -247,7 +248,7 @@ export interface TOTPStrategyOptions {
247248
248249/**
249250 * The verify method callback.
250- * Returns the user for the email to be stored in the session.
251+ * Returns the email user to be stored in the session.
251252 */
252253export interface TOTPVerifyParams {
253254 /**
@@ -266,6 +267,21 @@ export interface TOTPVerifyParams {
266267 request : Request
267268}
268269
270+ /**
271+ * The magic link parameters.
272+ */
273+ interface MagicLinkParams {
274+ /**
275+ * The TOTP code.
276+ */
277+ code : string
278+
279+ /**
280+ * The TOTP expiry date.
281+ */
282+ expires : number
283+ }
284+
269285/**
270286 * A store class that manages TOTP-related state in a cookie.
271287 * Handles email, TOTP session data, and error messages.
@@ -367,8 +383,9 @@ class TOTPStore {
367383
368384 /**
369385 * Commits the current store state to a cookie string.
370- * @param maxAge - Optional maximum age of the cookie in seconds
371- * @returns A string representation of the cookie with all current values
386+ *
387+ * @param maxAge - Optional maximum age of the cookie in seconds.
388+ * @returns A string representation of the cookie with its current values.
372389 */
373390 commit ( maxAge ?: number ) : string {
374391 const params = new URLSearchParams ( )
@@ -400,13 +417,8 @@ class TOTPStore {
400417}
401418
402419/**
403- * Interface for encrypted magic link parameters
420+ * The TOTP Strategy.
404421 */
405- interface MagicLinkParams {
406- code : string
407- expires : number
408- }
409-
410422export class TOTPStrategy < User > extends Strategy < User , TOTPVerifyParams > {
411423 public name = STRATEGY_NAME
412424
@@ -424,8 +436,8 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
424436 private _successRedirect : string
425437 private _failureRedirect : string
426438 private readonly _totpGenerationDefaults = {
427- algorithm : 'SHA-256' , // More secure than SHA1.
428- charSet : 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789' , // No O or 0
439+ algorithm : 'SHA-256' ,
440+ charSet : 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789' , // Does not include O or 0.
429441 digits : 6 ,
430442 period : 60 ,
431443 maxAttempts : 3 ,
@@ -434,8 +446,8 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
434446 requiredEmail : ERRORS . REQUIRED_EMAIL ,
435447 invalidEmail : ERRORS . INVALID_EMAIL ,
436448 invalidTotp : ERRORS . INVALID_TOTP ,
437- rateLimitExceeded : ERRORS . RATE_LIMIT_EXCEEDED ,
438449 expiredTotp : ERRORS . EXPIRED_TOTP ,
450+ rateLimitExceeded : ERRORS . RATE_LIMIT_EXCEEDED ,
439451 missingSessionEmail : ERRORS . MISSING_SESSION_EMAIL ,
440452 missingSessionTotp : ERRORS . MISSING_SESSION_TOTP ,
441453 }
@@ -465,38 +477,38 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
465477 }
466478 }
467479
468- // Getter for emailSentRedirect
480+ /** Gets the email sent redirect URL. */
469481 get emailSentRedirect ( ) : string {
470482 return this . _emailSentRedirect
471483 }
472484
473- // Setter for emailSentRedirect
485+ /** Sets the email sent redirect URL. */
474486 set emailSentRedirect ( url : string ) {
475487 if ( ! url ) {
476488 throw new Error ( ERRORS . REQUIRED_EMAIL_SENT_REDIRECT_URL )
477489 }
478490 this . _emailSentRedirect = url
479491 }
480492
481- // Getter for successRedirect
493+ /** Gets the success redirect URL. */
482494 get successRedirect ( ) : string {
483495 return this . _successRedirect
484496 }
485497
486- // Setter for successRedirect
498+ /** Sets the success redirect URL. */
487499 set successRedirect ( url : string ) {
488500 if ( ! url ) {
489501 throw new Error ( ERRORS . REQUIRED_SUCCESS_REDIRECT_URL )
490502 }
491503 this . _successRedirect = url
492504 }
493505
494- // Getter for failureRedirect
506+ /** Gets the failure redirect URL. */
495507 get failureRedirect ( ) : string {
496508 return this . _failureRedirect
497509 }
498510
499- // Setter for failureRedirect
511+ /** Sets the failure redirect URL. */
500512 set failureRedirect ( url : string ) {
501513 if ( ! url ) {
502514 throw new Error ( ERRORS . REQUIRED_FAILURE_REDIRECT_URL )
@@ -506,19 +518,16 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
506518
507519 /**
508520 * Authenticates a user using TOTP.
509- *
510521 * If the user is already authenticated, simply returns the user.
511522 *
512523 * | Method | Email | Code | Sess. Email | Sess. TOTP | Action/Logic |
513524 * |--------|-------|------|-------------|------------|------------------------------------------|
514- * | POST | ✓ | - | - | - | Generate/send TOTP using form email. |
515- * | POST | ✗ | ✗ | ✓ | - | Generate/send TOTP using session email. |
525+ * | POST | ✓ | - | - | - | Generate/Send TOTP using form email. |
526+ * | POST | ✗ | ✗ | ✓ | - | Generate/Send TOTP using session email. |
516527 * | POST | ✗ | ✓ | ✓ | ✓ | Validate form TOTP code. |
517- * | GET | - | - | ✓ | ✓ | Validate magic link TOTP. |
528+ * | GET | - | - | ✓ | ✓ | Validate magic- link TOTP. |
518529 *
519530 * @param {Request } request - The request object.
520- * @param {SessionStorage } sessionStorage - The session storage instance.
521- * @param {AuthenticateOptions } options - The authentication options. successRedirect is required.
522531 * @returns {Promise<User> } The authenticated user.
523532 */
524533 async authenticate ( request : Request ) : Promise < User > {
@@ -535,10 +544,16 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
535544 const formDataCode = coerceToOptionalNonEmptyString ( formData . get ( this . codeFieldKey ) )
536545 const sessionEmail = coerceToOptionalString ( store . getEmail ( ) )
537546 const sessionTotp = coerceToOptionalTotpSessionData ( store . getTOTP ( ) )
538- const email =
539- request . method === 'POST'
540- ? formDataEmail ?? ( ! formDataCode ? sessionEmail : null )
541- : null
547+
548+ let email = null
549+
550+ if ( request . method === 'POST' ) {
551+ if ( formDataEmail ) {
552+ email = formDataEmail
553+ } else if ( sessionEmail && ! formDataCode ) {
554+ email = sessionEmail
555+ }
556+ }
542557
543558 try {
544559 if ( email ) {
@@ -554,24 +569,25 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
554569 request,
555570 } )
556571
572+ // Set the TOTP data in the store.
557573 const totpData : TOTPCookieData = { jwe, attempts : 0 }
558574 store . setEmail ( email )
559575 store . setTOTP ( totpData )
560576 store . setError ( undefined )
561577
578+ // Redirect to the email sent URL.
562579 throw redirect ( this . _emailSentRedirect , {
563580 headers : {
564581 'Set-Cookie' : store . commit ( this . maxAge ) ,
565582 } ,
566583 } )
567584 }
568585
569- // Get the TOTP code and expiry, either from form data or magic link.
586+ // Try to get the TOTP code either from the form data or the magic link.
570587 const { code : linkCode , expires : linkExpires } = await this . _getMagicLinkCode (
571588 request ,
572589 sessionTotp ,
573590 )
574-
575591 const code = formDataCode ?? linkCode
576592
577593 if ( code ) {
@@ -586,14 +602,18 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
586602 store . setTOTP ( undefined )
587603 store . setError ( undefined )
588604
605+ // Call the verify method, allowing developers to handle the user.
589606 await this . verify ( { email : sessionEmail , formData, request } )
590607
608+ // Redirect to the success URL.
591609 throw redirect ( this . _successRedirect , {
592610 headers : {
593611 'Set-Cookie' : store . commit ( this . maxAge ) ,
594612 } ,
595613 } )
596614 }
615+
616+ // If no email was provided, throw an error.
597617 throw new Error ( this . customErrors . requiredEmail )
598618 } catch ( err : unknown ) {
599619 if ( err instanceof Response ) {
@@ -654,14 +674,15 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
654674 urlExpires ?: number
655675 } ) {
656676 try {
657- // Check if the TOTP is expired from the URL
677+ // Check if the TOTP is expired from the URL.
658678 if ( urlExpires ) {
659679 const dateNow = Date . now ( )
660680 if ( dateNow > urlExpires ) {
661681 throw new Error ( this . customErrors . expiredTotp )
662682 }
663683 }
664684
685+ // Decrypt the TOTP data from the Cookie.
665686 // https://github.com/panva/jose/blob/main/docs/jwe/compact/decrypt/functions/compactDecrypt.md
666687 const { plaintext } = await jose . compactDecrypt (
667688 sessionTotp . jwe ,
@@ -673,6 +694,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
673694 // Check if the TOTP is expired from the Cookie.
674695 const dateNow = Date . now ( )
675696 const isExpired = dateNow - totpData . createdAt > this . totpGeneration . period * 1000
697+
676698 if ( isExpired ) {
677699 throw new Error ( this . customErrors . expiredTotp )
678700 }
@@ -683,6 +705,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
683705 secret : totpData . secret ,
684706 otp : code ,
685707 } )
708+
686709 if ( ! isValid ) {
687710 throw new Error ( this . customErrors . invalidTotp )
688711 }
@@ -737,9 +760,9 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
737760 }
738761
739762 /**
740- * Encrypts magic link parameters
741- * @param params - The parameters to encrypt
742- * @returns The encrypted JWE token
763+ * Encrypts magic link parameters.
764+ * @param params - The parameters to encrypt.
765+ * @returns The encrypted JWE token.
743766 */
744767 private async _encryptUrlParams ( params : MagicLinkParams ) : Promise < string > {
745768 const payload = new TextEncoder ( ) . encode ( JSON . stringify ( params ) )
@@ -749,9 +772,9 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
749772 }
750773
751774 /**
752- * Decrypts and validates magic link parameters
753- * @param encrypted - The encrypted JWE token
754- * @returns The decrypted and validated parameters
775+ * Decrypts and validates magic link parameters.
776+ * @param encrypted - The encrypted JWE token.
777+ * @returns The decrypted and validated parameters.
755778 */
756779 private async _decryptUrlParams (
757780 encrypted : string ,
@@ -762,7 +785,7 @@ export class TOTPStrategy<User> extends Strategy<User, TOTPVerifyParams> {
762785 const params = JSON . parse ( new TextDecoder ( ) . decode ( plaintext ) )
763786
764787 if ( ! params ?. code || ! params ?. expires || typeof params . expires !== 'number' ) {
765- throw new Error ( 'Invalid magic link format' )
788+ throw new Error ( 'Invalid magic- link format. ' )
766789 }
767790
768791 return params
0 commit comments