@@ -8,7 +8,7 @@ import { db } from '@acme/db'
88
99import * as mailService from '../../mail/mail.service'
1010import { emailLogin , emailVerifyOtp } from '../auth.service'
11- import { createAuthToken } from '../auth.utils'
11+ import { createAuthToken , createVfnIdentifier } from '../auth.utils'
1212
1313const mockedMailService = mock ( mailService )
1414
@@ -20,18 +20,20 @@ describe('auth.service', () => {
2020 describe ( 'emailLogin' , ( ) => {
2121 it ( 'should create a verification token and send OTP email' , async ( ) => {
222223+ const nonce = 'test-nonce-123'
2324
24- const result = await emailLogin ( email )
25+ const result = await emailLogin ( { email, nonce } )
2526
2627 expect ( result ) . toEqual ( {
2728 email,
2829 token : expect . any ( String ) ,
2930 otpPrefix : expect . any ( String ) ,
3031 } )
3132
32- // Verify token was created in database
33+ // Verify token was created in database with vfnIdentifier
34+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
3335 const token = await db . verificationToken . findUnique ( {
34- where : { identifier : email } ,
36+ where : { identifier : vfnIdentifier } ,
3537 } )
3638 expect ( token ) . toBeDefined ( )
3739 expect ( mockedMailService . sendMail ) . toHaveBeenCalledWith ( {
@@ -41,165 +43,242 @@ describe('auth.service', () => {
4143 } )
4244 } )
4345
44- it ( 'should reset attempts when sending new OTP for existing user ' , async ( ) => {
46+ it ( 'should reset attempts when sending new OTP for existing nonce ' , async ( ) => {
454748+ const nonce = 'test-nonce-123'
4649
4750 // First login
48- await emailLogin ( email )
51+ await emailLogin ( { email, nonce } )
4952
53+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
5054 // Simulate failed attempts
5155 await db . verificationToken . update ( {
52- where : { identifier : email } ,
56+ where : { identifier : vfnIdentifier } ,
5357 data : { attempts : 3 } ,
5458 } )
5559
5660 // Second login should reset attempts
57- await emailLogin ( email )
61+ await emailLogin ( { email, nonce } )
5862
5963 const token = await db . verificationToken . findUnique ( {
60- where : { identifier : email } ,
64+ where : { identifier : vfnIdentifier } ,
6165 } )
6266 expect ( token ?. attempts ) . toBe ( 0 )
6367 } )
6468
65- it ( 'should update existing token instead of creating duplicate' , async ( ) => {
69+ it ( 'should update existing token instead of creating duplicate for same nonce ' , async ( ) => {
667071+ const nonce = 'test-nonce-123'
6772
68- await emailLogin ( email )
69- await emailLogin ( email )
73+ await emailLogin ( { email, nonce } )
74+ await emailLogin ( { email, nonce } )
7075
76+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
7177 // Should only have one record
7278 const tokens = await db . verificationToken . findMany ( {
73- where : { identifier : email } ,
79+ where : { identifier : vfnIdentifier } ,
7480 } )
7581 expect ( tokens ) . toHaveLength ( 1 )
7682 } )
83+
84+ it ( 'should allow different nonces for same email' , async ( ) => {
85+ 86+ const nonce1 = 'test-nonce-1'
87+ const nonce2 = 'test-nonce-2'
88+
89+ await emailLogin ( { email, nonce : nonce1 } )
90+ await emailLogin ( { email, nonce : nonce2 } )
91+
92+ // Should have two records with different nonces
93+ const vfnIdentifier1 = createVfnIdentifier ( { email, nonce : nonce1 } )
94+ const vfnIdentifier2 = createVfnIdentifier ( { email, nonce : nonce2 } )
95+ const token1 = await db . verificationToken . findUnique ( {
96+ where : { identifier : vfnIdentifier1 } ,
97+ } )
98+ const token2 = await db . verificationToken . findUnique ( {
99+ where : { identifier : vfnIdentifier2 } ,
100+ } )
101+
102+ expect ( token1 ) . toBeDefined ( )
103+ expect ( token2 ) . toBeDefined ( )
104+ expect ( token1 ?. token ) . not . toBe ( token2 ?. token )
105+ } )
77106 } )
78107
79108 describe ( 'emailVerifyOtp' , ( ) => {
80109 it ( 'should successfully verify a valid OTP' , async ( ) => {
81110111+ const nonce = 'test-nonce-123'
82112
83113 // Create a verification token
84- const { token } = await emailLogin ( email )
114+ const { token } = await emailLogin ( { email, nonce } )
85115
86116 // Should not throw
87- await expect ( emailVerifyOtp ( { email, token } ) ) . resolves . not . toThrow ( )
117+ await expect (
118+ emailVerifyOtp ( { email, token, nonce } ) ,
119+ ) . resolves . not . toThrow ( )
88120
89121 // Token should be deleted after successful verification
122+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
90123 const verificationToken = await db . verificationToken . findUnique ( {
91- where : { identifier : email } ,
124+ where : { identifier : vfnIdentifier } ,
92125 } )
93126 expect ( verificationToken ) . toBeNull ( )
94127 } )
95128
96129 it ( 'should reject an invalid OTP' , async ( ) => {
97130131+ const nonce = 'test-nonce-123'
98132 const token = 'WRONG6'
99133
100- await emailLogin ( email )
101- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
134+ await emailLogin ( { email, nonce } )
135+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
102136 'Token is invalid or has expired' ,
103137 )
104138 } )
105139
106140 it ( 'should reject an expired OTP' , async ( ) => {
107141142+ const nonce = 'test-nonce-123'
108143
109- const { token, hashedToken } = createAuthToken ( email )
144+ const { token, hashedToken } = createAuthToken ( { email, nonce } )
110145
146+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
111147 // Create a verification token with an old issuedAt date
112148 const oldDate = add ( new Date ( ) , { seconds : - 700 } ) // 700 seconds ago (beyond 600s expiry)
113149 await db . verificationToken . create ( {
114150 data : {
115- identifier : email ,
151+ identifier : vfnIdentifier ,
116152 token : hashedToken ,
117153 issuedAt : oldDate ,
118154 } ,
119155 } )
120156
121- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
157+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
122158 'Token is invalid or has expired' ,
123159 )
124160 } )
125161
126162 it ( 'should increment attempts on each verification try' , async ( ) => {
127163164+ const nonce = 'test-nonce-123'
128165 const token = 'WRONG6'
129166
130- await emailLogin ( email )
167+ await emailLogin ( { email, nonce } )
131168
169+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
132170 // First attempt
133- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
171+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
134172 let verificationToken = await db . verificationToken . findUnique ( {
135- where : { identifier : email } ,
173+ where : { identifier : vfnIdentifier } ,
136174 } )
137175 expect ( verificationToken ?. attempts ) . toBe ( 1 )
138176
139177 // Second attempt
140- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
178+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
141179 verificationToken = await db . verificationToken . findUnique ( {
142- where : { identifier : email } ,
180+ where : { identifier : vfnIdentifier } ,
143181 } )
144182 expect ( verificationToken ?. attempts ) . toBe ( 2 )
145183 } )
146184
147185 it ( 'should reject after too many failed attempts (>5)' , async ( ) => {
148186187+ const nonce = 'test-nonce-123'
149188 const token = 'WRONG6'
150189
151- await emailLogin ( email )
190+ await emailLogin ( { email, nonce } )
152191
153192 // Make 5 failed attempts
154193 for ( let i = 0 ; i < 5 ; i ++ ) {
155- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
194+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
156195 }
157196
158197 // 6th attempt should give TOO_MANY_REQUESTS
159- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
198+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
160199 'Wrong OTP was entered too many times' ,
161200 )
162201 } )
163202
164- it ( 'should throw error for non-existent email' , async ( ) => {
165- 203+ it ( 'should throw error for non-existent nonce' , async ( ) => {
204+ 205+ const nonce = 'nonexistent-nonce'
166206 const token = '123456'
167207
168- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
169- 'Invalid login email' ,
208+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
209+ 'Invalid login email or missing nonce ' ,
170210 )
171211 } )
172212
173213 it ( 'should delete verification token after successful verification' , async ( ) => {
174- // Arrange
175214176- const { token } = await emailLogin ( email )
215+ const nonce = 'test-nonce-123'
216+ const { token } = await emailLogin ( { email, nonce } )
177217
178- // Act
179- await emailVerifyOtp ( { email, token } )
218+ await emailVerifyOtp ( { email, token, nonce } )
180219
181- // Assert
182220 // Token should be deleted
221+ const vfnIdentifier = createVfnIdentifier ( { email, nonce } )
183222 const verificationToken = await db . verificationToken . findUnique ( {
184- where : { identifier : email } ,
223+ where : { identifier : vfnIdentifier } ,
185224 } )
186225 expect ( verificationToken ) . toBeNull ( )
187226 } )
188227
189228 it ( 'should prevent token reuse after successful verification' , async ( ) => {
190- // Arrange
191229192- const { token } = await emailLogin ( email )
230+ const nonce = 'test-nonce-123'
231+ const { token } = await emailLogin ( { email, nonce } )
193232
194- // Act
195233 // First verification succeeds
196- await expect ( emailVerifyOtp ( { email, token } ) ) . resolves . toBeDefined ( )
234+ await expect (
235+ emailVerifyOtp ( { email, token, nonce } ) ,
236+ ) . resolves . toBeDefined ( )
197237
198- // Assert
199238 // Second verification with same token should fail
200- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
201- 'Invalid login email' ,
239+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
240+ 'Invalid login email or missing nonce ' ,
202241 )
203242 } )
243+
244+ it ( 'should not allow using token with wrong nonce' , async ( ) => {
245+ 246+ const nonce1 = 'test-nonce-1'
247+ const nonce2 = 'test-nonce-2'
248+
249+ const { token } = await emailLogin ( { email, nonce : nonce1 } )
250+
251+ // Try to verify with wrong nonce
252+ await expect (
253+ emailVerifyOtp ( { email, token, nonce : nonce2 } ) ,
254+ ) . rejects . toThrow ( 'Invalid login email or missing nonce' )
255+
256+ // Original token should still exist
257+ const vfnIdentifier1 = createVfnIdentifier ( { email, nonce : nonce1 } )
258+ const verificationToken = await db . verificationToken . findUnique ( {
259+ where : { identifier : vfnIdentifier1 } ,
260+ } )
261+ expect ( verificationToken ) . toBeDefined ( )
262+ } )
263+
264+ it ( 'should ensure OTP is tied to specific session via nonce' , async ( ) => {
265+ 266+ const nonce1 = 'session-1-nonce'
267+ const nonce2 = 'session-2-nonce'
268+
269+ // Two different sessions for same email
270+ const { token : token1 } = await emailLogin ( { email, nonce : nonce1 } )
271+ const { token : token2 } = await emailLogin ( { email, nonce : nonce2 } )
272+
273+ // Each token should only work with its own nonce
274+ await expect (
275+ emailVerifyOtp ( { email, token : token1 , nonce : nonce1 } ) ,
276+ ) . resolves . toBeDefined ( )
277+
278+ // token2 with nonce2 should still work (not affected by token1 verification)
279+ await expect (
280+ emailVerifyOtp ( { email, token : token2 , nonce : nonce2 } ) ,
281+ ) . resolves . toBeDefined ( )
282+ } )
204283 } )
205284} )
0 commit comments