@dotpress/auth 1.1.1 → 1.2.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.
- package/README.md +211 -35
- package/dist/__tests__/functional/cookies.test.d.ts +1 -0
- package/dist/__tests__/functional/cookies.test.js +386 -0
- package/dist/__tests__/functional/handlers.test.js +4 -0
- package/dist/__tests__/unit/cookies.test.d.ts +1 -0
- package/dist/__tests__/unit/cookies.test.js +466 -0
- package/dist/index.js +4 -0
- package/dist/lib/cookies.d.ts +42 -0
- package/dist/lib/cookies.js +136 -0
- package/dist/lib/handlers.js +40 -9
- package/dist/lib/middleware.js +2 -3
- package/dist/lib/tokens.js +1 -0
- package/dist/types/types.d.ts +20 -0
- package/package.json +3 -1
|
@@ -110,6 +110,8 @@ describe('Handlers', () => {
|
|
|
110
110
|
userId: '101',
|
|
111
111
|
accessToken: 'acc-111222333',
|
|
112
112
|
refreshToken: 'ref-999888777',
|
|
113
|
+
expiresIn: expect.any(Number),
|
|
114
|
+
expiresAt: expect.any(String),
|
|
113
115
|
});
|
|
114
116
|
});
|
|
115
117
|
});
|
|
@@ -159,6 +161,8 @@ describe('Handlers', () => {
|
|
|
159
161
|
userId: '102',
|
|
160
162
|
accessToken: '111222333',
|
|
161
163
|
refreshToken: '111111111',
|
|
164
|
+
expiresIn: expect.any(Number),
|
|
165
|
+
expiresAt: expect.any(String),
|
|
162
166
|
});
|
|
163
167
|
expect(generateTokenFn).toHaveBeenCalledTimes(1);
|
|
164
168
|
expect(generateTokenFn).toHaveBeenCalledWith({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { getCookieSettings, setAuthCookies, clearAuthCookies, detectAuthMode, extractTokensFromRequest, extractAccessToken, calculateExpiration, } from '../../lib/cookies.js';
|
|
3
|
+
import { setSettings } from '../../lib/settings.js';
|
|
4
|
+
describe('Cookie Utilities', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
// Reset settings before each test
|
|
7
|
+
setSettings({
|
|
8
|
+
keys: {
|
|
9
|
+
secretKey: 'test-secret',
|
|
10
|
+
publicKey: 'test-public',
|
|
11
|
+
},
|
|
12
|
+
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
|
+
});
|
|
21
|
+
});
|
|
22
|
+
describe('getCookieSettings', () => {
|
|
23
|
+
it('should return default settings when no cookies config provided', () => {
|
|
24
|
+
const settings = getCookieSettings();
|
|
25
|
+
expect(settings).toMatchObject({
|
|
26
|
+
enabled: false,
|
|
27
|
+
httpOnly: true,
|
|
28
|
+
secure: expect.any(Boolean),
|
|
29
|
+
sameSite: 'lax',
|
|
30
|
+
path: '/',
|
|
31
|
+
accessTokenName: 'dotpress_access_token',
|
|
32
|
+
refreshTokenName: 'dotpress_refresh_token',
|
|
33
|
+
accessTokenInCookie: false,
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
it('should merge custom settings with defaults', () => {
|
|
37
|
+
setSettings({
|
|
38
|
+
keys: {
|
|
39
|
+
secretKey: 'test-secret',
|
|
40
|
+
publicKey: 'test-public',
|
|
41
|
+
},
|
|
42
|
+
dataProvider: {},
|
|
43
|
+
cookies: {
|
|
44
|
+
enabled: true,
|
|
45
|
+
domain: '.example.com',
|
|
46
|
+
accessTokenName: 'custom_access',
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
const settings = getCookieSettings();
|
|
50
|
+
expect(settings).toMatchObject({
|
|
51
|
+
enabled: true,
|
|
52
|
+
domain: '.example.com',
|
|
53
|
+
accessTokenName: 'custom_access',
|
|
54
|
+
refreshTokenName: 'dotpress_refresh_token', // Default
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
describe('setAuthCookies', () => {
|
|
59
|
+
it('should not set cookies when cookies are disabled', () => {
|
|
60
|
+
// Ensure cookies are disabled
|
|
61
|
+
setSettings({
|
|
62
|
+
keys: {
|
|
63
|
+
secretKey: 'test-secret',
|
|
64
|
+
publicKey: 'test-public',
|
|
65
|
+
},
|
|
66
|
+
dataProvider: {},
|
|
67
|
+
cookies: {
|
|
68
|
+
enabled: false,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const mockRes = {
|
|
72
|
+
cookie: vi.fn(),
|
|
73
|
+
};
|
|
74
|
+
setAuthCookies(mockRes, { accessToken: 'acc-token', refreshToken: 'ref-token' }, 3600);
|
|
75
|
+
expect(mockRes.cookie).not.toHaveBeenCalled();
|
|
76
|
+
});
|
|
77
|
+
it('should set refresh token cookie when enabled', () => {
|
|
78
|
+
setSettings({
|
|
79
|
+
keys: {
|
|
80
|
+
secretKey: 'test-secret',
|
|
81
|
+
publicKey: 'test-public',
|
|
82
|
+
},
|
|
83
|
+
dataProvider: {},
|
|
84
|
+
cookies: {
|
|
85
|
+
enabled: true,
|
|
86
|
+
secure: false,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
const mockRes = {
|
|
90
|
+
cookie: vi.fn(),
|
|
91
|
+
};
|
|
92
|
+
setAuthCookies(mockRes, { accessToken: 'acc-token', refreshToken: 'ref-token' }, 3600);
|
|
93
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('dotpress_refresh_token', 'ref-token', expect.objectContaining({
|
|
94
|
+
httpOnly: true,
|
|
95
|
+
secure: false,
|
|
96
|
+
sameSite: 'lax',
|
|
97
|
+
path: '/',
|
|
98
|
+
maxAge: 3600000,
|
|
99
|
+
}));
|
|
100
|
+
// Access token should NOT be set by default
|
|
101
|
+
expect(mockRes.cookie).toHaveBeenCalledTimes(1);
|
|
102
|
+
});
|
|
103
|
+
it('should set both cookies when accessTokenInCookie is true', () => {
|
|
104
|
+
setSettings({
|
|
105
|
+
keys: {
|
|
106
|
+
secretKey: 'test-secret',
|
|
107
|
+
publicKey: 'test-public',
|
|
108
|
+
},
|
|
109
|
+
dataProvider: {},
|
|
110
|
+
cookies: {
|
|
111
|
+
enabled: true,
|
|
112
|
+
secure: false,
|
|
113
|
+
accessTokenInCookie: true,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const mockRes = {
|
|
117
|
+
cookie: vi.fn(),
|
|
118
|
+
};
|
|
119
|
+
setAuthCookies(mockRes, { accessToken: 'acc-token', refreshToken: 'ref-token' }, 3600);
|
|
120
|
+
expect(mockRes.cookie).toHaveBeenCalledTimes(2);
|
|
121
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('dotpress_access_token', 'acc-token', expect.any(Object));
|
|
122
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('dotpress_refresh_token', 'ref-token', expect.any(Object));
|
|
123
|
+
});
|
|
124
|
+
it('should use custom cookie names', () => {
|
|
125
|
+
setSettings({
|
|
126
|
+
keys: {
|
|
127
|
+
secretKey: 'test-secret',
|
|
128
|
+
publicKey: 'test-public',
|
|
129
|
+
},
|
|
130
|
+
dataProvider: {},
|
|
131
|
+
cookies: {
|
|
132
|
+
enabled: true,
|
|
133
|
+
secure: false,
|
|
134
|
+
accessTokenName: 'my_access',
|
|
135
|
+
refreshTokenName: 'my_refresh',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
const mockRes = {
|
|
139
|
+
cookie: vi.fn(),
|
|
140
|
+
};
|
|
141
|
+
setAuthCookies(mockRes, { accessToken: 'acc-token', refreshToken: 'ref-token' }, 3600);
|
|
142
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('my_refresh', 'ref-token', expect.any(Object));
|
|
143
|
+
});
|
|
144
|
+
it('should set domain when configured', () => {
|
|
145
|
+
setSettings({
|
|
146
|
+
keys: {
|
|
147
|
+
secretKey: 'test-secret',
|
|
148
|
+
publicKey: 'test-public',
|
|
149
|
+
},
|
|
150
|
+
dataProvider: {},
|
|
151
|
+
cookies: {
|
|
152
|
+
enabled: true,
|
|
153
|
+
secure: false,
|
|
154
|
+
domain: '.example.com',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
const mockRes = {
|
|
158
|
+
cookie: vi.fn(),
|
|
159
|
+
};
|
|
160
|
+
setAuthCookies(mockRes, { accessToken: 'acc-token', refreshToken: 'ref-token' }, 3600);
|
|
161
|
+
expect(mockRes.cookie).toHaveBeenCalledWith('dotpress_refresh_token', 'ref-token', expect.objectContaining({
|
|
162
|
+
domain: '.example.com',
|
|
163
|
+
}));
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
describe('clearAuthCookies', () => {
|
|
167
|
+
it('should not clear cookies when disabled', () => {
|
|
168
|
+
// Ensure cookies are disabled
|
|
169
|
+
setSettings({
|
|
170
|
+
keys: {
|
|
171
|
+
secretKey: 'test-secret',
|
|
172
|
+
publicKey: 'test-public',
|
|
173
|
+
},
|
|
174
|
+
dataProvider: {},
|
|
175
|
+
cookies: {
|
|
176
|
+
enabled: false,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
const mockRes = {
|
|
180
|
+
clearCookie: vi.fn(),
|
|
181
|
+
};
|
|
182
|
+
clearAuthCookies(mockRes);
|
|
183
|
+
expect(mockRes.clearCookie).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
it('should clear refresh token cookie when enabled', () => {
|
|
186
|
+
setSettings({
|
|
187
|
+
keys: {
|
|
188
|
+
secretKey: 'test-secret',
|
|
189
|
+
publicKey: 'test-public',
|
|
190
|
+
},
|
|
191
|
+
dataProvider: {},
|
|
192
|
+
cookies: {
|
|
193
|
+
enabled: true,
|
|
194
|
+
secure: false,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const mockRes = {
|
|
198
|
+
clearCookie: vi.fn(),
|
|
199
|
+
};
|
|
200
|
+
clearAuthCookies(mockRes);
|
|
201
|
+
expect(mockRes.clearCookie).toHaveBeenCalledWith('dotpress_refresh_token', expect.objectContaining({
|
|
202
|
+
httpOnly: true,
|
|
203
|
+
secure: false,
|
|
204
|
+
sameSite: 'lax',
|
|
205
|
+
path: '/',
|
|
206
|
+
}));
|
|
207
|
+
});
|
|
208
|
+
it('should clear both cookies when accessTokenInCookie is true', () => {
|
|
209
|
+
setSettings({
|
|
210
|
+
keys: {
|
|
211
|
+
secretKey: 'test-secret',
|
|
212
|
+
publicKey: 'test-public',
|
|
213
|
+
},
|
|
214
|
+
dataProvider: {},
|
|
215
|
+
cookies: {
|
|
216
|
+
enabled: true,
|
|
217
|
+
secure: false,
|
|
218
|
+
accessTokenInCookie: true,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
const mockRes = {
|
|
222
|
+
clearCookie: vi.fn(),
|
|
223
|
+
};
|
|
224
|
+
clearAuthCookies(mockRes);
|
|
225
|
+
expect(mockRes.clearCookie).toHaveBeenCalledTimes(2);
|
|
226
|
+
expect(mockRes.clearCookie).toHaveBeenCalledWith('dotpress_access_token', expect.any(Object));
|
|
227
|
+
expect(mockRes.clearCookie).toHaveBeenCalledWith('dotpress_refresh_token', expect.any(Object));
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
describe('detectAuthMode', () => {
|
|
231
|
+
it('should return "cookie" when refresh token cookie is present', () => {
|
|
232
|
+
const mockReq = {
|
|
233
|
+
cookies: {
|
|
234
|
+
dotpress_refresh_token: 'ref-token',
|
|
235
|
+
},
|
|
236
|
+
};
|
|
237
|
+
const mode = detectAuthMode(mockReq);
|
|
238
|
+
expect(mode).toBe('cookie');
|
|
239
|
+
});
|
|
240
|
+
it('should return "body" when refresh token is in body', () => {
|
|
241
|
+
const mockReq = {
|
|
242
|
+
cookies: {},
|
|
243
|
+
body: {
|
|
244
|
+
refreshToken: 'ref-token',
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
const mode = detectAuthMode(mockReq);
|
|
248
|
+
expect(mode).toBe('body');
|
|
249
|
+
});
|
|
250
|
+
it('should prioritize cookies over body', () => {
|
|
251
|
+
const mockReq = {
|
|
252
|
+
cookies: {
|
|
253
|
+
dotpress_refresh_token: 'cookie-ref-token',
|
|
254
|
+
},
|
|
255
|
+
body: {
|
|
256
|
+
refreshToken: 'body-ref-token',
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
const mode = detectAuthMode(mockReq);
|
|
260
|
+
expect(mode).toBe('cookie');
|
|
261
|
+
});
|
|
262
|
+
it('should return null when no tokens found', () => {
|
|
263
|
+
const mockReq = {
|
|
264
|
+
cookies: {},
|
|
265
|
+
body: {},
|
|
266
|
+
};
|
|
267
|
+
const mode = detectAuthMode(mockReq);
|
|
268
|
+
expect(mode).toBeNull();
|
|
269
|
+
});
|
|
270
|
+
it('should work with custom cookie names', () => {
|
|
271
|
+
setSettings({
|
|
272
|
+
keys: {
|
|
273
|
+
secretKey: 'test-secret',
|
|
274
|
+
publicKey: 'test-public',
|
|
275
|
+
},
|
|
276
|
+
dataProvider: {},
|
|
277
|
+
cookies: {
|
|
278
|
+
enabled: true,
|
|
279
|
+
refreshTokenName: 'custom_refresh',
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const mockReq = {
|
|
283
|
+
cookies: {
|
|
284
|
+
custom_refresh: 'ref-token',
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
const mode = detectAuthMode(mockReq);
|
|
288
|
+
expect(mode).toBe('cookie');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe('extractTokensFromRequest', () => {
|
|
292
|
+
it('should extract tokens from cookies', () => {
|
|
293
|
+
// Ensure cookies are enabled with default settings
|
|
294
|
+
setSettings({
|
|
295
|
+
keys: {
|
|
296
|
+
secretKey: 'test-secret',
|
|
297
|
+
publicKey: 'test-public',
|
|
298
|
+
},
|
|
299
|
+
dataProvider: {},
|
|
300
|
+
cookies: {
|
|
301
|
+
enabled: true,
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
const mockReq = {
|
|
305
|
+
cookies: {
|
|
306
|
+
dotpress_refresh_token: 'ref-token',
|
|
307
|
+
},
|
|
308
|
+
body: {
|
|
309
|
+
accessToken: 'acc-token-from-body',
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
const tokens = extractTokensFromRequest(mockReq);
|
|
313
|
+
expect(tokens).toEqual({
|
|
314
|
+
accessToken: 'acc-token-from-body',
|
|
315
|
+
refreshToken: 'ref-token',
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
it('should extract tokens from body when no cookies', () => {
|
|
319
|
+
const mockReq = {
|
|
320
|
+
cookies: {},
|
|
321
|
+
body: {
|
|
322
|
+
accessToken: 'acc-token',
|
|
323
|
+
refreshToken: 'ref-token',
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
const tokens = extractTokensFromRequest(mockReq);
|
|
327
|
+
expect(tokens).toEqual({
|
|
328
|
+
accessToken: 'acc-token',
|
|
329
|
+
refreshToken: 'ref-token',
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
it('should extract both tokens from cookies when accessTokenInCookie is true', () => {
|
|
333
|
+
setSettings({
|
|
334
|
+
keys: {
|
|
335
|
+
secretKey: 'test-secret',
|
|
336
|
+
publicKey: 'test-public',
|
|
337
|
+
},
|
|
338
|
+
dataProvider: {},
|
|
339
|
+
cookies: {
|
|
340
|
+
enabled: true,
|
|
341
|
+
accessTokenInCookie: true,
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
const mockReq = {
|
|
345
|
+
cookies: {
|
|
346
|
+
dotpress_access_token: 'acc-token-cookie',
|
|
347
|
+
dotpress_refresh_token: 'ref-token-cookie',
|
|
348
|
+
},
|
|
349
|
+
body: {},
|
|
350
|
+
};
|
|
351
|
+
const tokens = extractTokensFromRequest(mockReq);
|
|
352
|
+
expect(tokens).toEqual({
|
|
353
|
+
accessToken: 'acc-token-cookie',
|
|
354
|
+
refreshToken: 'ref-token-cookie',
|
|
355
|
+
});
|
|
356
|
+
});
|
|
357
|
+
it('should return null when tokens are missing', () => {
|
|
358
|
+
const mockReq = {
|
|
359
|
+
cookies: {
|
|
360
|
+
dotpress_refresh_token: 'ref-token',
|
|
361
|
+
},
|
|
362
|
+
body: {},
|
|
363
|
+
};
|
|
364
|
+
const tokens = extractTokensFromRequest(mockReq);
|
|
365
|
+
expect(tokens).toBeNull();
|
|
366
|
+
});
|
|
367
|
+
});
|
|
368
|
+
describe('extractAccessToken', () => {
|
|
369
|
+
it('should prioritize Authorization header', () => {
|
|
370
|
+
const mockReq = {
|
|
371
|
+
headers: {
|
|
372
|
+
authorization: 'Bearer header-token',
|
|
373
|
+
},
|
|
374
|
+
cookies: {
|
|
375
|
+
dotpress_access_token: 'cookie-token',
|
|
376
|
+
},
|
|
377
|
+
body: {
|
|
378
|
+
accessToken: 'body-token',
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
const token = extractAccessToken(mockReq);
|
|
382
|
+
expect(token).toBe('header-token');
|
|
383
|
+
});
|
|
384
|
+
it('should extract from cookie when no header and accessTokenInCookie is true', () => {
|
|
385
|
+
setSettings({
|
|
386
|
+
keys: {
|
|
387
|
+
secretKey: 'test-secret',
|
|
388
|
+
publicKey: 'test-public',
|
|
389
|
+
},
|
|
390
|
+
dataProvider: {},
|
|
391
|
+
cookies: {
|
|
392
|
+
enabled: true,
|
|
393
|
+
accessTokenInCookie: true,
|
|
394
|
+
},
|
|
395
|
+
});
|
|
396
|
+
const mockReq = {
|
|
397
|
+
headers: {},
|
|
398
|
+
cookies: {
|
|
399
|
+
dotpress_access_token: 'cookie-token',
|
|
400
|
+
},
|
|
401
|
+
body: {
|
|
402
|
+
accessToken: 'body-token',
|
|
403
|
+
},
|
|
404
|
+
};
|
|
405
|
+
const token = extractAccessToken(mockReq);
|
|
406
|
+
expect(token).toBe('cookie-token');
|
|
407
|
+
});
|
|
408
|
+
it('should extract from body as fallback', () => {
|
|
409
|
+
const mockReq = {
|
|
410
|
+
headers: {},
|
|
411
|
+
cookies: {},
|
|
412
|
+
body: {
|
|
413
|
+
accessToken: 'body-token',
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
const token = extractAccessToken(mockReq);
|
|
417
|
+
expect(token).toBe('body-token');
|
|
418
|
+
});
|
|
419
|
+
it('should return null when no token found', () => {
|
|
420
|
+
const mockReq = {
|
|
421
|
+
headers: {},
|
|
422
|
+
cookies: {},
|
|
423
|
+
body: {},
|
|
424
|
+
};
|
|
425
|
+
const token = extractAccessToken(mockReq);
|
|
426
|
+
expect(token).toBeNull();
|
|
427
|
+
});
|
|
428
|
+
it('should handle Authorization header without Bearer prefix', () => {
|
|
429
|
+
const mockReq = {
|
|
430
|
+
headers: {
|
|
431
|
+
authorization: 'invalid-format',
|
|
432
|
+
},
|
|
433
|
+
cookies: {},
|
|
434
|
+
body: {},
|
|
435
|
+
};
|
|
436
|
+
const token = extractAccessToken(mockReq);
|
|
437
|
+
expect(token).toBeNull();
|
|
438
|
+
});
|
|
439
|
+
});
|
|
440
|
+
describe('calculateExpiration', () => {
|
|
441
|
+
it('should calculate correct expiration timestamp', () => {
|
|
442
|
+
const expiresInSeconds = 3600;
|
|
443
|
+
const before = Date.now();
|
|
444
|
+
const result = calculateExpiration(expiresInSeconds);
|
|
445
|
+
const after = Date.now();
|
|
446
|
+
expect(result.expiresIn).toBe(3600);
|
|
447
|
+
expect(result.expiresAt).toBeDefined();
|
|
448
|
+
const expiresAt = new Date(result.expiresAt).getTime();
|
|
449
|
+
expect(expiresAt).toBeGreaterThanOrEqual(before + 3600 * 1000);
|
|
450
|
+
expect(expiresAt).toBeLessThanOrEqual(after + 3600 * 1000);
|
|
451
|
+
});
|
|
452
|
+
it('should return ISO string format for expiresAt', () => {
|
|
453
|
+
const result = calculateExpiration(1800);
|
|
454
|
+
expect(result.expiresAt).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
|
|
455
|
+
});
|
|
456
|
+
it('should handle different TTL values', () => {
|
|
457
|
+
const result1 = calculateExpiration(60); // 1 minute
|
|
458
|
+
const result2 = calculateExpiration(86400); // 1 day
|
|
459
|
+
expect(result1.expiresIn).toBe(60);
|
|
460
|
+
expect(result2.expiresIn).toBe(86400);
|
|
461
|
+
const date1 = new Date(result1.expiresAt);
|
|
462
|
+
const date2 = new Date(result2.expiresAt);
|
|
463
|
+
expect(date2.getTime() - date1.getTime()).toBeGreaterThan(86000000); // Roughly 1 day
|
|
464
|
+
});
|
|
465
|
+
});
|
|
466
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import bcrypt from 'bcryptjs';
|
|
2
|
+
import cookieParser from 'cookie-parser';
|
|
2
3
|
import { setSettings } from './lib/settings.js';
|
|
3
4
|
import { registerHandlers } from './lib/handlers.js';
|
|
4
5
|
import { attachAuthenticatedUser } from './lib/middleware.js';
|
|
@@ -7,6 +8,9 @@ export const configurePlugin = (settings) => {
|
|
|
7
8
|
setSettings(settings);
|
|
8
9
|
return (plugin) => {
|
|
9
10
|
plugin.useBeforeRoutes((app) => {
|
|
11
|
+
if (settings.cookies?.enabled) {
|
|
12
|
+
app.use(cookieParser());
|
|
13
|
+
}
|
|
10
14
|
app.use(attachAuthenticatedUser);
|
|
11
15
|
});
|
|
12
16
|
registerHandlers(plugin);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Response, Request } from 'express';
|
|
2
|
+
import type { CookieSettings, AuthMode } from '../types/types.js';
|
|
3
|
+
type ResolvedCookieSettings = Required<Omit<CookieSettings, 'domain'>> & {
|
|
4
|
+
domain?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare const getCookieSettings: () => ResolvedCookieSettings;
|
|
7
|
+
/**
|
|
8
|
+
* Set authentication cookies on the response
|
|
9
|
+
*/
|
|
10
|
+
export declare const setAuthCookies: (res: Response, tokens: {
|
|
11
|
+
accessToken: string;
|
|
12
|
+
refreshToken: string;
|
|
13
|
+
}, expiresInSeconds: number) => void;
|
|
14
|
+
/**
|
|
15
|
+
* Clear authentication cookies
|
|
16
|
+
*/
|
|
17
|
+
export declare const clearAuthCookies: (res: Response) => void;
|
|
18
|
+
/**
|
|
19
|
+
* Detect authentication mode from request
|
|
20
|
+
* Priority: cookies first, then body
|
|
21
|
+
*/
|
|
22
|
+
export declare const detectAuthMode: (req: Request) => AuthMode | null;
|
|
23
|
+
/**
|
|
24
|
+
* Extract tokens from request based on detected mode
|
|
25
|
+
*/
|
|
26
|
+
export declare const extractTokensFromRequest: (req: Request) => {
|
|
27
|
+
accessToken: string;
|
|
28
|
+
refreshToken: string;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* Extract access token from request
|
|
32
|
+
* Priority: Authorization header > cookie > body
|
|
33
|
+
*/
|
|
34
|
+
export declare const extractAccessToken: (req: Request) => string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Calculate expiration timestamp
|
|
37
|
+
*/
|
|
38
|
+
export declare const calculateExpiration: (expiresInSeconds: number) => {
|
|
39
|
+
expiresIn: number;
|
|
40
|
+
expiresAt: string;
|
|
41
|
+
};
|
|
42
|
+
export {};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { getSettings } from './settings.js';
|
|
2
|
+
const DEFAULT_COOKIE_SETTINGS = {
|
|
3
|
+
enabled: false,
|
|
4
|
+
httpOnly: true,
|
|
5
|
+
secure: process.env.NODE_ENV === 'production',
|
|
6
|
+
sameSite: 'lax',
|
|
7
|
+
path: '/',
|
|
8
|
+
accessTokenName: 'dotpress_access_token',
|
|
9
|
+
refreshTokenName: 'dotpress_refresh_token',
|
|
10
|
+
accessTokenInCookie: false,
|
|
11
|
+
domain: undefined,
|
|
12
|
+
};
|
|
13
|
+
export const getCookieSettings = () => {
|
|
14
|
+
const settings = getSettings();
|
|
15
|
+
return {
|
|
16
|
+
...DEFAULT_COOKIE_SETTINGS,
|
|
17
|
+
...settings.cookies,
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
/**
|
|
21
|
+
* Set authentication cookies on the response
|
|
22
|
+
*/
|
|
23
|
+
export const setAuthCookies = (res, tokens, expiresInSeconds) => {
|
|
24
|
+
const cookieSettings = getCookieSettings();
|
|
25
|
+
if (!cookieSettings.enabled) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const cookieOptions = {
|
|
29
|
+
httpOnly: cookieSettings.httpOnly,
|
|
30
|
+
secure: cookieSettings.secure,
|
|
31
|
+
sameSite: cookieSettings.sameSite,
|
|
32
|
+
path: cookieSettings.path,
|
|
33
|
+
domain: cookieSettings.domain,
|
|
34
|
+
maxAge: expiresInSeconds * 1000,
|
|
35
|
+
};
|
|
36
|
+
// Always set refresh token in cookie for 'cookie' mode
|
|
37
|
+
res.cookie(cookieSettings.refreshTokenName, tokens.refreshToken, cookieOptions);
|
|
38
|
+
// Optionally set access token in cookie
|
|
39
|
+
if (cookieSettings.accessTokenInCookie) {
|
|
40
|
+
res.cookie(cookieSettings.accessTokenName, tokens.accessToken, cookieOptions);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Clear authentication cookies
|
|
45
|
+
*/
|
|
46
|
+
export const clearAuthCookies = (res) => {
|
|
47
|
+
const cookieSettings = getCookieSettings();
|
|
48
|
+
if (!cookieSettings.enabled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const cookieOptions = {
|
|
52
|
+
httpOnly: cookieSettings.httpOnly,
|
|
53
|
+
secure: cookieSettings.secure,
|
|
54
|
+
sameSite: cookieSettings.sameSite,
|
|
55
|
+
path: cookieSettings.path,
|
|
56
|
+
domain: cookieSettings.domain,
|
|
57
|
+
};
|
|
58
|
+
res.clearCookie(cookieSettings.refreshTokenName, cookieOptions);
|
|
59
|
+
if (cookieSettings.accessTokenInCookie) {
|
|
60
|
+
res.clearCookie(cookieSettings.accessTokenName, cookieOptions);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Detect authentication mode from request
|
|
65
|
+
* Priority: cookies first, then body
|
|
66
|
+
*/
|
|
67
|
+
export const detectAuthMode = (req) => {
|
|
68
|
+
const cookieSettings = getCookieSettings();
|
|
69
|
+
// Check if refresh token exists in cookies
|
|
70
|
+
if (req.cookies?.[cookieSettings.refreshTokenName]) {
|
|
71
|
+
return 'cookie';
|
|
72
|
+
}
|
|
73
|
+
// Check if refresh token exists in body
|
|
74
|
+
if (req.body?.refreshToken) {
|
|
75
|
+
return 'body';
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Extract tokens from request based on detected mode
|
|
81
|
+
*/
|
|
82
|
+
export const extractTokensFromRequest = (req) => {
|
|
83
|
+
const mode = detectAuthMode(req);
|
|
84
|
+
const cookieSettings = getCookieSettings();
|
|
85
|
+
if (mode === 'cookie') {
|
|
86
|
+
const refreshToken = req.cookies?.[cookieSettings.refreshTokenName];
|
|
87
|
+
const accessToken = cookieSettings.accessTokenInCookie
|
|
88
|
+
? req.cookies?.[cookieSettings.accessTokenName]
|
|
89
|
+
: req.body?.accessToken;
|
|
90
|
+
if (!refreshToken || !accessToken) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
return { accessToken, refreshToken };
|
|
94
|
+
}
|
|
95
|
+
if (mode === 'body') {
|
|
96
|
+
const { accessToken, refreshToken } = req.body || {};
|
|
97
|
+
if (!accessToken || !refreshToken) {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
return { accessToken, refreshToken };
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Extract access token from request
|
|
106
|
+
* Priority: Authorization header > cookie > body
|
|
107
|
+
*/
|
|
108
|
+
export const extractAccessToken = (req) => {
|
|
109
|
+
// Priority 1: Authorization header
|
|
110
|
+
const authHeader = req.headers.authorization;
|
|
111
|
+
if (authHeader?.startsWith('Bearer ')) {
|
|
112
|
+
return authHeader.replace('Bearer ', '');
|
|
113
|
+
}
|
|
114
|
+
const cookieSettings = getCookieSettings();
|
|
115
|
+
// Priority 2: Cookie (if enabled)
|
|
116
|
+
if (cookieSettings.enabled &&
|
|
117
|
+
cookieSettings.accessTokenInCookie &&
|
|
118
|
+
req.cookies?.[cookieSettings.accessTokenName]) {
|
|
119
|
+
return req.cookies[cookieSettings.accessTokenName];
|
|
120
|
+
}
|
|
121
|
+
// Priority 3: Body
|
|
122
|
+
if (req.body?.accessToken) {
|
|
123
|
+
return req.body.accessToken;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Calculate expiration timestamp
|
|
129
|
+
*/
|
|
130
|
+
export const calculateExpiration = (expiresInSeconds) => {
|
|
131
|
+
const expiresAt = new Date(Date.now() + expiresInSeconds * 1000);
|
|
132
|
+
return {
|
|
133
|
+
expiresIn: expiresInSeconds,
|
|
134
|
+
expiresAt: expiresAt.toISOString(),
|
|
135
|
+
};
|
|
136
|
+
};
|