@@ -20,18 +20,19 @@ 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 nonce as identifier
3334 const token = await db . verificationToken . findUnique ( {
34- where : { identifier : email } ,
35+ where : { identifier : nonce } ,
3536 } )
3637 expect ( token ) . toBeDefined ( )
3738 expect ( mockedMailService . sendMail ) . toHaveBeenCalledWith ( {
@@ -41,165 +42,233 @@ describe('auth.service', () => {
4142 } )
4243 } )
4344
44- it ( 'should reset attempts when sending new OTP for existing user ' , async ( ) => {
45+ it ( 'should reset attempts when sending new OTP for existing nonce ' , async ( ) => {
454647+ const nonce = 'test-nonce-123'
4648
4749 // First login
48- await emailLogin ( email )
50+ await emailLogin ( { email, nonce } )
4951
5052 // Simulate failed attempts
5153 await db . verificationToken . update ( {
52- where : { identifier : email } ,
54+ where : { identifier : nonce } ,
5355 data : { attempts : 3 } ,
5456 } )
5557
5658 // Second login should reset attempts
57- await emailLogin ( email )
59+ await emailLogin ( { email, nonce } )
5860
5961 const token = await db . verificationToken . findUnique ( {
60- where : { identifier : email } ,
62+ where : { identifier : nonce } ,
6163 } )
6264 expect ( token ?. attempts ) . toBe ( 0 )
6365 } )
6466
65- it ( 'should update existing token instead of creating duplicate' , async ( ) => {
67+ it ( 'should update existing token instead of creating duplicate for same nonce ' , async ( ) => {
666869+ const nonce = 'test-nonce-123'
6770
68- await emailLogin ( email )
69- await emailLogin ( email )
71+ await emailLogin ( { email, nonce } )
72+ await emailLogin ( { email, nonce } )
7073
7174 // Should only have one record
7275 const tokens = await db . verificationToken . findMany ( {
73- where : { identifier : email } ,
76+ where : { identifier : nonce } ,
7477 } )
7578 expect ( tokens ) . toHaveLength ( 1 )
7679 } )
80+
81+ it ( 'should allow different nonces for same email' , async ( ) => {
82+ 83+ const nonce1 = 'test-nonce-1'
84+ const nonce2 = 'test-nonce-2'
85+
86+ await emailLogin ( { email, nonce : nonce1 } )
87+ await emailLogin ( { email, nonce : nonce2 } )
88+
89+ // Should have two records with different nonces
90+ const token1 = await db . verificationToken . findUnique ( {
91+ where : { identifier : nonce1 } ,
92+ } )
93+ const token2 = await db . verificationToken . findUnique ( {
94+ where : { identifier : nonce2 } ,
95+ } )
96+
97+ expect ( token1 ) . toBeDefined ( )
98+ expect ( token2 ) . toBeDefined ( )
99+ expect ( token1 ?. token ) . not . toBe ( token2 ?. token )
100+ } )
77101 } )
78102
79103 describe ( 'emailVerifyOtp' , ( ) => {
80104 it ( 'should successfully verify a valid OTP' , async ( ) => {
81105106+ const nonce = 'test-nonce-123'
82107
83108 // Create a verification token
84- const { token } = await emailLogin ( email )
109+ const { token } = await emailLogin ( { email, nonce } )
85110
86111 // Should not throw
87- await expect ( emailVerifyOtp ( { email, token } ) ) . resolves . not . toThrow ( )
112+ await expect (
113+ emailVerifyOtp ( { email, token, nonce } ) ,
114+ ) . resolves . not . toThrow ( )
88115
89116 // Token should be deleted after successful verification
90117 const verificationToken = await db . verificationToken . findUnique ( {
91- where : { identifier : email } ,
118+ where : { identifier : nonce } ,
92119 } )
93120 expect ( verificationToken ) . toBeNull ( )
94121 } )
95122
96123 it ( 'should reject an invalid OTP' , async ( ) => {
97124125+ const nonce = 'test-nonce-123'
98126 const token = 'WRONG6'
99127
100- await emailLogin ( email )
101- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
128+ await emailLogin ( { email, nonce } )
129+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
102130 'Token is invalid or has expired' ,
103131 )
104132 } )
105133
106134 it ( 'should reject an expired OTP' , async ( ) => {
107135136+ const nonce = 'test-nonce-123'
108137
109- const { token, hashedToken } = createAuthToken ( email )
138+ const { token, hashedToken } = createAuthToken ( { email, nonce } )
110139
111140 // Create a verification token with an old issuedAt date
112141 const oldDate = add ( new Date ( ) , { seconds : - 700 } ) // 700 seconds ago (beyond 600s expiry)
113142 await db . verificationToken . create ( {
114143 data : {
115- identifier : email ,
144+ identifier : nonce ,
116145 token : hashedToken ,
117146 issuedAt : oldDate ,
118147 } ,
119148 } )
120149
121- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
150+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
122151 'Token is invalid or has expired' ,
123152 )
124153 } )
125154
126155 it ( 'should increment attempts on each verification try' , async ( ) => {
127156157+ const nonce = 'test-nonce-123'
128158 const token = 'WRONG6'
129159
130- await emailLogin ( email )
160+ await emailLogin ( { email, nonce } )
131161
132162 // First attempt
133- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
163+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
134164 let verificationToken = await db . verificationToken . findUnique ( {
135- where : { identifier : email } ,
165+ where : { identifier : nonce } ,
136166 } )
137167 expect ( verificationToken ?. attempts ) . toBe ( 1 )
138168
139169 // Second attempt
140- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
170+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
141171 verificationToken = await db . verificationToken . findUnique ( {
142- where : { identifier : email } ,
172+ where : { identifier : nonce } ,
143173 } )
144174 expect ( verificationToken ?. attempts ) . toBe ( 2 )
145175 } )
146176
147177 it ( 'should reject after too many failed attempts (>5)' , async ( ) => {
148178179+ const nonce = 'test-nonce-123'
149180 const token = 'WRONG6'
150181
151- await emailLogin ( email )
182+ await emailLogin ( { email, nonce } )
152183
153184 // Make 5 failed attempts
154185 for ( let i = 0 ; i < 5 ; i ++ ) {
155- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow ( )
186+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow ( )
156187 }
157188
158189 // 6th attempt should give TOO_MANY_REQUESTS
159- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
190+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
160191 'Wrong OTP was entered too many times' ,
161192 )
162193 } )
163194
164- it ( 'should throw error for non-existent email' , async ( ) => {
165- 195+ it ( 'should throw error for non-existent nonce' , async ( ) => {
196+ 197+ const nonce = 'nonexistent-nonce'
166198 const token = '123456'
167199
168- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
169- 'Invalid login email' ,
200+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
201+ 'Invalid login email or missing nonce ' ,
170202 )
171203 } )
172204
173205 it ( 'should delete verification token after successful verification' , async ( ) => {
174- // Arrange
175206176- const { token } = await emailLogin ( email )
207+ const nonce = 'test-nonce-123'
208+ const { token } = await emailLogin ( { email, nonce } )
177209
178- // Act
179- await emailVerifyOtp ( { email, token } )
210+ await emailVerifyOtp ( { email, token, nonce } )
180211
181- // Assert
182212 // Token should be deleted
183213 const verificationToken = await db . verificationToken . findUnique ( {
184- where : { identifier : email } ,
214+ where : { identifier : nonce } ,
185215 } )
186216 expect ( verificationToken ) . toBeNull ( )
187217 } )
188218
189219 it ( 'should prevent token reuse after successful verification' , async ( ) => {
190- // Arrange
191220192- const { token } = await emailLogin ( email )
221+ const nonce = 'test-nonce-123'
222+ const { token } = await emailLogin ( { email, nonce } )
193223
194- // Act
195224 // First verification succeeds
196- await expect ( emailVerifyOtp ( { email, token } ) ) . resolves . toBeDefined ( )
225+ await expect (
226+ emailVerifyOtp ( { email, token, nonce } ) ,
227+ ) . resolves . toBeDefined ( )
197228
198- // Assert
199229 // Second verification with same token should fail
200- await expect ( emailVerifyOtp ( { email, token } ) ) . rejects . toThrow (
201- 'Invalid login email' ,
230+ await expect ( emailVerifyOtp ( { email, token, nonce } ) ) . rejects . toThrow (
231+ 'Invalid login email or missing nonce ' ,
202232 )
203233 } )
234+
235+ it ( 'should not allow using token with wrong nonce' , async ( ) => {
236+ 237+ const nonce1 = 'test-nonce-1'
238+ const nonce2 = 'test-nonce-2'
239+
240+ const { token } = await emailLogin ( { email, nonce : nonce1 } )
241+
242+ // Try to verify with wrong nonce
243+ await expect (
244+ emailVerifyOtp ( { email, token, nonce : nonce2 } ) ,
245+ ) . rejects . toThrow ( 'Invalid login email or missing nonce' )
246+
247+ // Original token should still exist
248+ const verificationToken = await db . verificationToken . findUnique ( {
249+ where : { identifier : nonce1 } ,
250+ } )
251+ expect ( verificationToken ) . toBeDefined ( )
252+ } )
253+
254+ it ( 'should ensure OTP is tied to specific session via nonce' , async ( ) => {
255+ 256+ const nonce1 = 'session-1-nonce'
257+ const nonce2 = 'session-2-nonce'
258+
259+ // Two different sessions for same email
260+ const { token : token1 } = await emailLogin ( { email, nonce : nonce1 } )
261+ const { token : token2 } = await emailLogin ( { email, nonce : nonce2 } )
262+
263+ // Each token should only work with its own nonce
264+ await expect (
265+ emailVerifyOtp ( { email, token : token1 , nonce : nonce1 } ) ,
266+ ) . resolves . toBeDefined ( )
267+
268+ // token2 with nonce2 should still work (not affected by token1 verification)
269+ await expect (
270+ emailVerifyOtp ( { email, token : token2 , nonce : nonce2 } ) ,
271+ ) . resolves . toBeDefined ( )
272+ } )
204273 } )
205274} )
0 commit comments