@dotpress/auth 1.3.1 → 2.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,621 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from 'vitest';
2
- import request from 'supertest';
3
- import bcrypt from 'bcryptjs';
4
- import { $clearRoutes, createApp } from 'dotpress';
5
- import * as tokenFns from '../../lib/tokens.js';
6
- import { configurePlugin } from '../../index.js';
7
- describe('Cookie Mode Authentication', () => {
8
- beforeEach(() => {
9
- vi.resetAllMocks();
10
- $clearRoutes();
11
- });
12
- const dataProvider = {
13
- storeRefreshToken: vi.fn(),
14
- findRefreshToken: vi.fn(),
15
- markRefreshTokenAsUsed: vi.fn(),
16
- revokeTokens: vi.fn(),
17
- findUserById: vi.fn(),
18
- findUserIdentifiers: vi.fn(),
19
- };
20
- const authPlugin = configurePlugin({
21
- keys: {
22
- secretKey: '',
23
- publicKey: '',
24
- },
25
- dataProvider,
26
- cookies: {
27
- enabled: true,
28
- secure: false, // For testing
29
- httpOnly: true,
30
- sameSite: 'lax',
31
- },
32
- });
33
- describe('POST /auth/login with cookie mode', () => {
34
- it('should set cookies when mode is "cookie"', async () => {
35
- const pwdHash = await bcrypt.hash('123456', 10);
36
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
37
- userId: '101',
38
- passwordHash: pwdHash,
39
- });
40
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
41
- accessToken: 'acc-111222333',
42
- refreshToken: 'ref-999888777',
43
- });
44
- const app = await createApp({
45
- plugins: [authPlugin],
46
- });
47
- const res = await request(app).post('/auth/login').send({
48
- username: 'JaneDoe',
49
- password: '123456',
50
- mode: 'cookie',
51
- });
52
- expect(res.status).toBe(200);
53
- // Check response body
54
- expect(res.body).toMatchObject({
55
- userId: '101',
56
- accessToken: 'acc-111222333',
57
- refreshToken: 'ref-999888777',
58
- expiresIn: expect.any(Number),
59
- expiresAt: expect.any(String),
60
- });
61
- // Check cookies are set
62
- expect(res.headers['set-cookie']).toBeDefined();
63
- const cookies = Array.isArray(res.headers['set-cookie'])
64
- ? res.headers['set-cookie']
65
- : [res.headers['set-cookie']];
66
- // Check refresh token cookie
67
- const refreshCookie = cookies.find((c) => c.startsWith('dotpress_refresh_token='));
68
- expect(refreshCookie).toBeDefined();
69
- expect(refreshCookie).toContain('HttpOnly');
70
- expect(refreshCookie).toContain('Path=/');
71
- expect(refreshCookie).toContain('SameSite=Lax');
72
- // Access token should NOT be in cookie by default
73
- const accessCookie = cookies.find((c) => c.startsWith('dotpress_access_token='));
74
- expect(accessCookie).toBeUndefined();
75
- });
76
- it('should set access token cookie when accessTokenInCookie is true', async () => {
77
- const pwdHash = await bcrypt.hash('123456', 10);
78
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
79
- userId: '101',
80
- passwordHash: pwdHash,
81
- });
82
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
83
- accessToken: 'acc-111222333',
84
- refreshToken: 'ref-999888777',
85
- });
86
- const authPluginWithAccessCookie = configurePlugin({
87
- keys: {
88
- secretKey: '',
89
- publicKey: '',
90
- },
91
- dataProvider,
92
- cookies: {
93
- enabled: true,
94
- secure: false,
95
- accessTokenInCookie: true, // Enable access token in cookie
96
- },
97
- });
98
- const app = await createApp({
99
- plugins: [authPluginWithAccessCookie],
100
- });
101
- const res = await request(app).post('/auth/login').send({
102
- username: 'JaneDoe',
103
- password: '123456',
104
- mode: 'cookie',
105
- });
106
- expect(res.status).toBe(200);
107
- const cookies = Array.isArray(res.headers['set-cookie'])
108
- ? res.headers['set-cookie']
109
- : [res.headers['set-cookie']];
110
- // Both tokens should be in cookies
111
- const refreshCookie = cookies.find((c) => c.startsWith('dotpress_refresh_token='));
112
- const accessCookie = cookies.find((c) => c.startsWith('dotpress_access_token='));
113
- expect(refreshCookie).toBeDefined();
114
- expect(accessCookie).toBeDefined();
115
- });
116
- it('should NOT set cookies when mode is "body"', async () => {
117
- const pwdHash = await bcrypt.hash('123456', 10);
118
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
119
- userId: '101',
120
- passwordHash: pwdHash,
121
- });
122
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
123
- accessToken: 'acc-111222333',
124
- refreshToken: 'ref-999888777',
125
- });
126
- const app = await createApp({
127
- plugins: [authPlugin],
128
- });
129
- const res = await request(app).post('/auth/login').send({
130
- username: 'JaneDoe',
131
- password: '123456',
132
- mode: 'body',
133
- });
134
- expect(res.status).toBe(200);
135
- expect(res.body.accessToken).toBe('acc-111222333');
136
- // No cookies should be set
137
- expect(res.headers['set-cookie']).toBeUndefined();
138
- });
139
- it('should NOT set cookies when mode is not specified (default)', async () => {
140
- const pwdHash = await bcrypt.hash('123456', 10);
141
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
142
- userId: '101',
143
- passwordHash: pwdHash,
144
- });
145
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
146
- accessToken: 'acc-111222333',
147
- refreshToken: 'ref-999888777',
148
- });
149
- const app = await createApp({
150
- plugins: [authPlugin],
151
- });
152
- const res = await request(app).post('/auth/login').send({
153
- username: 'JaneDoe',
154
- password: '123456',
155
- });
156
- expect(res.status).toBe(200);
157
- // No cookies should be set (backward compatibility)
158
- expect(res.headers['set-cookie']).toBeUndefined();
159
- });
160
- });
161
- describe('POST /auth/refresh with cookies', () => {
162
- it('should refresh tokens using cookies', async () => {
163
- vi.spyOn(tokenFns, 'generateTokensFromRefreshToken').mockResolvedValue({
164
- userId: '102',
165
- accessToken: 'new-acc-token',
166
- refreshToken: 'new-ref-token',
167
- });
168
- const app = await createApp({
169
- plugins: [authPlugin],
170
- });
171
- // Simulate request with cookies
172
- const res = await request(app)
173
- .post('/auth/refresh')
174
- .set('Cookie', [
175
- 'dotpress_refresh_token=ref-999888777',
176
- 'dotpress_access_token=acc-111222333',
177
- ])
178
- .send({});
179
- expect(res.status).toBe(200);
180
- expect(res.body).toMatchObject({
181
- userId: '102',
182
- accessToken: 'new-acc-token',
183
- refreshToken: 'new-ref-token',
184
- expiresIn: expect.any(Number),
185
- expiresAt: expect.any(String),
186
- });
187
- // New cookies should be set
188
- expect(res.headers['set-cookie']).toBeDefined();
189
- const cookies = Array.isArray(res.headers['set-cookie'])
190
- ? res.headers['set-cookie']
191
- : [res.headers['set-cookie']];
192
- expect(cookies.some((c) => c.includes('new-ref-token'))).toBe(true);
193
- });
194
- it('should fallback to body mode when no cookies present', async () => {
195
- vi.spyOn(tokenFns, 'generateTokensFromRefreshToken').mockResolvedValue({
196
- userId: '102',
197
- accessToken: 'new-acc-token',
198
- refreshToken: 'new-ref-token',
199
- });
200
- const app = await createApp({
201
- plugins: [authPlugin],
202
- });
203
- const res = await request(app).post('/auth/refresh').send({
204
- accessToken: 'acc-111222333',
205
- refreshToken: 'ref-999888777',
206
- });
207
- expect(res.status).toBe(200);
208
- expect(res.body.accessToken).toBe('new-acc-token');
209
- // No cookies should be set in body mode
210
- expect(res.headers['set-cookie']).toBeUndefined();
211
- });
212
- it('should return 401 when no tokens provided', async () => {
213
- const app = await createApp({
214
- plugins: [authPlugin],
215
- });
216
- const res = await request(app).post('/auth/refresh').send({});
217
- expect(res.status).toBe(401);
218
- expect(res.body.error).toBe('UNAUTHORIZED');
219
- expect(res.body.message).toBe('NO_TOKENS_PROVIDED');
220
- });
221
- });
222
- describe('POST /auth/logout with cookies', () => {
223
- it('should clear cookies on logout', async () => {
224
- vi.mocked(dataProvider.findUserById).mockResolvedValue({
225
- id: 'usr-123',
226
- username: 'JohnDoe',
227
- email: 'john@example.com',
228
- });
229
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockReturnValue({
230
- userId: 'usr-123',
231
- });
232
- const app = await createApp({
233
- plugins: [authPlugin],
234
- });
235
- const res = await request(app)
236
- .post('/auth/logout')
237
- .set('Authorization', 'Bearer valid-token')
238
- .set('Cookie', ['dotpress_refresh_token=ref-token']);
239
- expect(res.status).toBe(200);
240
- expect(res.body).toEqual({ success: true });
241
- // Cookies should be cleared
242
- expect(res.headers['set-cookie']).toBeDefined();
243
- const cookies = Array.isArray(res.headers['set-cookie'])
244
- ? res.headers['set-cookie']
245
- : [res.headers['set-cookie']];
246
- // Check that cookies are being cleared (Max-Age=0 or Expires in the past)
247
- const refreshCookie = cookies.find((c) => c.startsWith('dotpress_refresh_token='));
248
- expect(refreshCookie).toBeDefined();
249
- // Cookie should have an expiry in the past or Max-Age=0
250
- expect(refreshCookie.includes('Max-Age=0') ||
251
- refreshCookie.includes('Expires=Thu, 01 Jan 1970')).toBe(true);
252
- });
253
- });
254
- describe('GET /auth/me with cookies', () => {
255
- it('should authenticate user with access token from Authorization header', async () => {
256
- vi.mocked(dataProvider.findUserById).mockResolvedValue({
257
- id: 'usr-123',
258
- username: 'JohnDoe',
259
- email: 'john@example.com',
260
- });
261
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockReturnValue({
262
- userId: 'usr-123',
263
- });
264
- const app = await createApp({
265
- plugins: [authPlugin],
266
- });
267
- const res = await request(app)
268
- .get('/auth/me')
269
- .set('Authorization', 'Bearer valid-token');
270
- expect(res.status).toBe(200);
271
- expect(res.body).toEqual({
272
- id: 'usr-123',
273
- username: 'JohnDoe',
274
- email: 'john@example.com',
275
- });
276
- });
277
- it('should authenticate user with access token from cookie when accessTokenInCookie is true', async () => {
278
- vi.mocked(dataProvider.findUserById).mockResolvedValue({
279
- id: 'usr-456',
280
- username: 'JaneDoe',
281
- email: 'jane@example.com',
282
- });
283
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockReturnValue({
284
- userId: 'usr-456',
285
- });
286
- const authPluginWithAccessCookie = configurePlugin({
287
- keys: {
288
- secretKey: '',
289
- publicKey: '',
290
- },
291
- dataProvider,
292
- cookies: {
293
- enabled: true,
294
- secure: false,
295
- accessTokenInCookie: true,
296
- },
297
- });
298
- const app = await createApp({
299
- plugins: [authPluginWithAccessCookie],
300
- });
301
- const res = await request(app)
302
- .get('/auth/me')
303
- .set('Cookie', ['dotpress_access_token=cookie-token']);
304
- expect(res.status).toBe(200);
305
- expect(res.body.username).toBe('JaneDoe');
306
- });
307
- });
308
- describe('Custom cookie names', () => {
309
- it('should use custom cookie names when configured', async () => {
310
- const pwdHash = await bcrypt.hash('123456', 10);
311
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
312
- userId: '101',
313
- passwordHash: pwdHash,
314
- });
315
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
316
- accessToken: 'acc-token',
317
- refreshToken: 'ref-token',
318
- });
319
- const customAuthPlugin = configurePlugin({
320
- keys: {
321
- secretKey: '',
322
- publicKey: '',
323
- },
324
- dataProvider,
325
- cookies: {
326
- enabled: true,
327
- secure: false,
328
- accessTokenName: 'custom_access',
329
- refreshTokenName: 'custom_refresh',
330
- },
331
- });
332
- const app = await createApp({
333
- plugins: [customAuthPlugin],
334
- });
335
- const res = await request(app).post('/auth/login').send({
336
- username: 'JaneDoe',
337
- password: '123456',
338
- mode: 'cookie',
339
- });
340
- expect(res.status).toBe(200);
341
- const cookies = Array.isArray(res.headers['set-cookie'])
342
- ? res.headers['set-cookie']
343
- : [res.headers['set-cookie']];
344
- expect(cookies.some((c) => c.startsWith('custom_refresh='))).toBe(true);
345
- });
346
- });
347
- describe('Cross-domain cookie configuration', () => {
348
- it('should set domain attribute when configured', async () => {
349
- const pwdHash = await bcrypt.hash('123456', 10);
350
- vi.mocked(dataProvider.findUserIdentifiers).mockResolvedValue({
351
- userId: '101',
352
- passwordHash: pwdHash,
353
- });
354
- vi.spyOn(tokenFns, 'generateTokensForUser').mockResolvedValue({
355
- accessToken: 'acc-token',
356
- refreshToken: 'ref-token',
357
- });
358
- const domainAuthPlugin = configurePlugin({
359
- keys: {
360
- secretKey: '',
361
- publicKey: '',
362
- },
363
- dataProvider,
364
- cookies: {
365
- enabled: true,
366
- secure: false,
367
- domain: '.example.com',
368
- },
369
- });
370
- const app = await createApp({
371
- plugins: [domainAuthPlugin],
372
- });
373
- const res = await request(app).post('/auth/login').send({
374
- username: 'JaneDoe',
375
- password: '123456',
376
- mode: 'cookie',
377
- });
378
- expect(res.status).toBe(200);
379
- const cookies = Array.isArray(res.headers['set-cookie'])
380
- ? res.headers['set-cookie']
381
- : [res.headers['set-cookie']];
382
- const refreshCookie = cookies.find((c) => c.startsWith('dotpress_refresh_token='));
383
- expect(refreshCookie).toContain('Domain=.example.com');
384
- });
385
- });
386
- describe('POST /auth/restore', () => {
387
- it('should return 401 when cookies are not enabled', async () => {
388
- const authPluginNoCookies = configurePlugin({
389
- keys: {
390
- secretKey: '',
391
- publicKey: '',
392
- },
393
- dataProvider,
394
- cookies: {
395
- enabled: false,
396
- },
397
- });
398
- const app = await createApp({
399
- plugins: [authPluginNoCookies],
400
- });
401
- const res = await request(app).post('/auth/restore');
402
- expect(res.status).toBe(401);
403
- });
404
- it('should return 401 when accessTokenInCookie is not enabled', async () => {
405
- const authPluginNoAccessCookie = configurePlugin({
406
- keys: {
407
- secretKey: '',
408
- publicKey: '',
409
- },
410
- dataProvider,
411
- cookies: {
412
- enabled: true,
413
- accessTokenInCookie: false, // Required for restore
414
- },
415
- });
416
- const app = await createApp({
417
- plugins: [authPluginNoAccessCookie],
418
- });
419
- const res = await request(app)
420
- .post('/auth/restore')
421
- .set('Cookie', ['dotpress_refresh_token=ref-token']);
422
- expect(res.status).toBe(401);
423
- });
424
- it('should return 401 when refresh token is missing', async () => {
425
- const authPluginWithAccessCookie = configurePlugin({
426
- keys: {
427
- secretKey: '',
428
- publicKey: '',
429
- },
430
- dataProvider,
431
- cookies: {
432
- enabled: true,
433
- accessTokenInCookie: true,
434
- },
435
- });
436
- const app = await createApp({
437
- plugins: [authPluginWithAccessCookie],
438
- });
439
- const res = await request(app)
440
- .post('/auth/restore')
441
- .set('Cookie', ['dotpress_access_token=acc-token']);
442
- expect(res.status).toBe(401);
443
- });
444
- it('should return 401 when access token is missing', async () => {
445
- const authPluginWithAccessCookie = configurePlugin({
446
- keys: {
447
- secretKey: '',
448
- publicKey: '',
449
- },
450
- dataProvider,
451
- cookies: {
452
- enabled: true,
453
- accessTokenInCookie: true,
454
- },
455
- });
456
- const app = await createApp({
457
- plugins: [authPluginWithAccessCookie],
458
- });
459
- const res = await request(app)
460
- .post('/auth/restore')
461
- .set('Cookie', ['dotpress_refresh_token=ref-token']);
462
- expect(res.status).toBe(401);
463
- });
464
- it('should return user and tokens when access token is still valid', async () => {
465
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockReturnValue({
466
- userId: 'usr-123',
467
- });
468
- vi.mocked(dataProvider.findUserById).mockResolvedValue({
469
- id: 'usr-123',
470
- username: 'JohnDoe',
471
- email: 'john@example.com',
472
- });
473
- const authPluginWithAccessCookie = configurePlugin({
474
- keys: {
475
- secretKey: '',
476
- publicKey: '',
477
- },
478
- dataProvider,
479
- cookies: {
480
- enabled: true,
481
- accessTokenInCookie: true,
482
- },
483
- });
484
- const app = await createApp({
485
- plugins: [authPluginWithAccessCookie],
486
- });
487
- const res = await request(app)
488
- .post('/auth/restore')
489
- .set('Cookie', [
490
- 'dotpress_access_token=valid-access-token',
491
- 'dotpress_refresh_token=valid-refresh-token',
492
- ]);
493
- expect(res.status).toBe(200);
494
- expect(res.body).toMatchObject({
495
- userId: 'usr-123',
496
- accessToken: 'valid-access-token',
497
- refreshToken: 'valid-refresh-token',
498
- expiresIn: expect.any(Number),
499
- expiresAt: expect.any(String),
500
- user: {
501
- id: 'usr-123',
502
- username: 'JohnDoe',
503
- email: 'john@example.com',
504
- },
505
- });
506
- });
507
- it('should refresh tokens and return new tokens when access token is expired', async () => {
508
- // First call: validateAndDecodeAccessToken throws (expired)
509
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockImplementation(() => {
510
- throw new Error('Token expired');
511
- });
512
- // Second call: generateTokensFromRefreshToken returns new tokens
513
- vi.spyOn(tokenFns, 'generateTokensFromRefreshToken').mockResolvedValue({
514
- userId: 'usr-456',
515
- accessToken: 'new-access-token',
516
- refreshToken: 'new-refresh-token',
517
- });
518
- vi.mocked(dataProvider.findUserById).mockResolvedValue({
519
- id: 'usr-456',
520
- username: 'JaneDoe',
521
- email: 'jane@example.com',
522
- });
523
- const authPluginWithAccessCookie = configurePlugin({
524
- keys: {
525
- secretKey: '',
526
- publicKey: '',
527
- },
528
- dataProvider,
529
- cookies: {
530
- enabled: true,
531
- accessTokenInCookie: true,
532
- },
533
- });
534
- const app = await createApp({
535
- plugins: [authPluginWithAccessCookie],
536
- });
537
- const res = await request(app)
538
- .post('/auth/restore')
539
- .set('Cookie', [
540
- 'dotpress_access_token=expired-access-token',
541
- 'dotpress_refresh_token=valid-refresh-token',
542
- ]);
543
- expect(res.status).toBe(200);
544
- expect(res.body).toMatchObject({
545
- userId: 'usr-456',
546
- accessToken: 'new-access-token',
547
- refreshToken: 'new-refresh-token',
548
- expiresIn: expect.any(Number),
549
- expiresAt: expect.any(String),
550
- user: {
551
- id: 'usr-456',
552
- username: 'JaneDoe',
553
- email: 'jane@example.com',
554
- },
555
- });
556
- // Verify new cookies were set
557
- expect(res.headers['set-cookie']).toBeDefined();
558
- const cookies = Array.isArray(res.headers['set-cookie'])
559
- ? res.headers['set-cookie']
560
- : [res.headers['set-cookie']];
561
- const accessCookie = cookies.find((c) => c.startsWith('dotpress_access_token='));
562
- const refreshCookie = cookies.find((c) => c.startsWith('dotpress_refresh_token='));
563
- expect(accessCookie).toBeDefined();
564
- expect(refreshCookie).toBeDefined();
565
- });
566
- it('should return 401 when refresh token is invalid', async () => {
567
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockImplementation(() => {
568
- throw new Error('Token expired');
569
- });
570
- vi.spyOn(tokenFns, 'generateTokensFromRefreshToken').mockRejectedValue(new Error('Invalid_Refresh_Token'));
571
- const authPluginWithAccessCookie = configurePlugin({
572
- keys: {
573
- secretKey: '',
574
- publicKey: '',
575
- },
576
- dataProvider,
577
- cookies: {
578
- enabled: true,
579
- accessTokenInCookie: true,
580
- },
581
- });
582
- const app = await createApp({
583
- plugins: [authPluginWithAccessCookie],
584
- });
585
- const res = await request(app)
586
- .post('/auth/restore')
587
- .set('Cookie', [
588
- 'dotpress_access_token=expired-access-token',
589
- 'dotpress_refresh_token=invalid-refresh-token',
590
- ]);
591
- expect(res.status).toBe(401);
592
- });
593
- it('should return 401 when refresh token is expired', async () => {
594
- vi.spyOn(tokenFns, 'validateAndDecodeAccessToken').mockImplementation(() => {
595
- throw new Error('Token expired');
596
- });
597
- vi.spyOn(tokenFns, 'generateTokensFromRefreshToken').mockRejectedValue(new Error('Expired_Refresh_Token'));
598
- const authPluginWithAccessCookie = configurePlugin({
599
- keys: {
600
- secretKey: '',
601
- publicKey: '',
602
- },
603
- dataProvider,
604
- cookies: {
605
- enabled: true,
606
- accessTokenInCookie: true,
607
- },
608
- });
609
- const app = await createApp({
610
- plugins: [authPluginWithAccessCookie],
611
- });
612
- const res = await request(app)
613
- .post('/auth/restore')
614
- .set('Cookie', [
615
- 'dotpress_access_token=expired-access-token',
616
- 'dotpress_refresh_token=expired-refresh-token',
617
- ]);
618
- expect(res.status).toBe(401);
619
- });
620
- });
621
- });
@@ -1 +0,0 @@
1
- export {};