@devmoods/express-extras 0.52.3 → 0.53.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/dist/auth/passwordless.d.ts +1 -1
- package/dist/auth/passwordless.js +20 -14
- package/dist/auth/types.d.ts +3 -1
- package/dist/auth.d.ts +3 -1
- package/dist/auth.js +32 -4
- package/dist/config.d.ts +2 -0
- package/dist/config.js +1 -0
- package/dist/errors.js +4 -0
- package/package.json +14 -14
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type AnyUser, type AuthProvider } from './types.js';
|
|
2
2
|
export interface PasswordlessAuthProviderOptions<UserModel = AnyUser> {
|
|
3
3
|
getUserByEmail(email: string): Promise<UserModel | null>;
|
|
4
|
-
onLoginRequest(user: UserModel, temporaryToken: string): Promise<void>;
|
|
4
|
+
onLoginRequest(user: UserModel, temporaryToken: string, otp: string): Promise<void>;
|
|
5
5
|
}
|
|
6
6
|
export declare function PasswordlessAuthProvider<UserModel extends AnyUser>(options: PasswordlessAuthProviderOptions<UserModel>): AuthProvider<UserModel>;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { createTokenResponse } from './token.js';
|
|
2
2
|
import {} from './types.js';
|
|
3
|
-
import { Unauthorized } from '../errors.js';
|
|
4
3
|
import { getLogger } from '../logger.js';
|
|
5
4
|
import { route } from '../middleware.js';
|
|
6
5
|
import { DoesNotExistError } from '../postgres.js';
|
|
@@ -20,19 +19,25 @@ export function PasswordlessAuthProvider(options) {
|
|
|
20
19
|
};
|
|
21
20
|
return {
|
|
22
21
|
setupRoutes(router, context) {
|
|
23
|
-
router.post('/passwordless/login
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
22
|
+
router.post('/passwordless/login', validateBody({
|
|
23
|
+
type: 'object',
|
|
24
|
+
properties: {
|
|
25
|
+
type: { type: 'string', enum: ['otp', 'token'] },
|
|
26
|
+
otp: { type: 'string' },
|
|
27
|
+
token: { type: 'string' },
|
|
28
|
+
},
|
|
29
|
+
required: ['type'],
|
|
30
|
+
}), route(async (req, res) => {
|
|
31
|
+
const { token: temporaryToken, otp, type: tokenType } = req.body;
|
|
32
|
+
const verify = tokenType === 'otp'
|
|
33
|
+
? context.verifyOtp
|
|
34
|
+
: context.verifyTemporaryToken;
|
|
35
|
+
const tokenValue = tokenType === 'otp' ? otp : temporaryToken;
|
|
36
|
+
const uid = await verify(tokenValue, null, false);
|
|
37
|
+
const user = await context.getUserById(uid);
|
|
38
|
+
await verify(tokenValue, user);
|
|
34
39
|
const accessToken = await context.login(req, res, user);
|
|
35
|
-
logger.info('passwordless login', { uid: user.id });
|
|
40
|
+
logger.info('passwordless login', { tokenType, uid: user.id });
|
|
36
41
|
if (req.accepts('html')) {
|
|
37
42
|
return res.redirect('/');
|
|
38
43
|
}
|
|
@@ -55,7 +60,8 @@ export function PasswordlessAuthProvider(options) {
|
|
|
55
60
|
return 202;
|
|
56
61
|
}
|
|
57
62
|
const token = await context.createTemporaryToken(user);
|
|
58
|
-
await
|
|
63
|
+
const otp = await context.createOtp(user);
|
|
64
|
+
await options.onLoginRequest(user, token, otp);
|
|
59
65
|
logger.info('passwordless login requested', { email });
|
|
60
66
|
return 202;
|
|
61
67
|
}));
|
package/dist/auth/types.d.ts
CHANGED
|
@@ -35,8 +35,10 @@ export interface AuthProviderContext<UserModel extends AnyUser> {
|
|
|
35
35
|
tokenStorage: AuthOptions<UserModel>['tokenStorage'];
|
|
36
36
|
setCurrentUser: (user: UserModel) => void;
|
|
37
37
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
38
|
-
verifyTemporaryToken(token: string, user: UserModel): Promise<
|
|
38
|
+
verifyTemporaryToken(token: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
39
39
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
40
|
+
createOtp(user: UserModel): Promise<string>;
|
|
41
|
+
verifyOtp(otp: string, user: UserModel | null, consume?: boolean): Promise<string>;
|
|
40
42
|
middleware: (options?: AuthMiddlewareOptions<UserModel>) => RequestHandler;
|
|
41
43
|
login: (req: Request, res: Response, user: UserModel, isRefresh?: boolean) => Promise<AccessToken>;
|
|
42
44
|
getUserById(id: string): Promise<UserModel>;
|
package/dist/auth.d.ts
CHANGED
|
@@ -12,8 +12,10 @@ export interface CreateAuth<UserModel extends AnyUser> {
|
|
|
12
12
|
before: () => RequestHandler;
|
|
13
13
|
isAuthorized: (fn?: (user: UserModel) => boolean) => RequestHandler;
|
|
14
14
|
createTemporaryToken(user: UserModel): Promise<string>;
|
|
15
|
-
verifyTemporaryToken(token: string, user: UserModel): Promise<
|
|
15
|
+
verifyTemporaryToken(token: string, user: UserModel | null): Promise<string>;
|
|
16
16
|
decodeTemporaryToken(token: string): DecodedTemporaryToken;
|
|
17
|
+
createOtp(user: UserModel): Promise<string>;
|
|
18
|
+
verifyOtp(otp: string, user: UserModel | null): Promise<string>;
|
|
17
19
|
logout: (req: Request, res: Response) => Promise<void>;
|
|
18
20
|
}
|
|
19
21
|
export declare class RedisTokenStorage extends TokenStorage {
|
package/dist/auth.js
CHANGED
|
@@ -71,6 +71,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
71
71
|
createTemporaryToken,
|
|
72
72
|
verifyTemporaryToken,
|
|
73
73
|
decodeTemporaryToken,
|
|
74
|
+
createOtp,
|
|
75
|
+
verifyOtp,
|
|
74
76
|
middleware,
|
|
75
77
|
login,
|
|
76
78
|
getUserById,
|
|
@@ -185,6 +187,24 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
185
187
|
}
|
|
186
188
|
};
|
|
187
189
|
}
|
|
190
|
+
async function createOtp(user) {
|
|
191
|
+
const otp = crypto.randomInt(100000, 999999).toString();
|
|
192
|
+
await tokenStorage.set(otp, String(user.id), config.value.AUTH_OTP_EXPIRES_IN);
|
|
193
|
+
return otp;
|
|
194
|
+
}
|
|
195
|
+
async function verifyOtp(otp, user, consume = true) {
|
|
196
|
+
const storedUid = await tokenStorage.get(otp);
|
|
197
|
+
if (!storedUid) {
|
|
198
|
+
throw new InvalidToken('Invalid OTP');
|
|
199
|
+
}
|
|
200
|
+
if (user && storedUid !== user.id) {
|
|
201
|
+
throw new InvalidToken('Invalid OTP');
|
|
202
|
+
}
|
|
203
|
+
if (consume) {
|
|
204
|
+
await tokenStorage.invalidate(otp);
|
|
205
|
+
}
|
|
206
|
+
return storedUid;
|
|
207
|
+
}
|
|
188
208
|
/**
|
|
189
209
|
* Create a short-lived token tied to a user that can be
|
|
190
210
|
* used for different kinds of verification.
|
|
@@ -213,7 +233,7 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
213
233
|
function decodeTemporaryToken(token) {
|
|
214
234
|
return JSON.parse(Buffer.from(token, 'base64url').toString('utf-8'));
|
|
215
235
|
}
|
|
216
|
-
async function verifyTemporaryToken(token, user) {
|
|
236
|
+
async function verifyTemporaryToken(token, user, consume = true) {
|
|
217
237
|
const decoded = decodeTemporaryToken(token);
|
|
218
238
|
const expectedSignature = crypto
|
|
219
239
|
.createHmac('sha256', config.value.AUTH_SECRET)
|
|
@@ -223,14 +243,20 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
223
243
|
if (expectedSignature !== decoded.signature) {
|
|
224
244
|
throw new InvalidToken('Invalid signature');
|
|
225
245
|
}
|
|
226
|
-
if (decoded.payload.uid !== user.id) {
|
|
246
|
+
if (user && decoded.payload.uid !== user.id) {
|
|
227
247
|
throw new InvalidToken(`Invalid user ${user.id}`);
|
|
228
248
|
}
|
|
229
249
|
const storedUid = await tokenStorage.get(token);
|
|
230
|
-
if (storedUid
|
|
250
|
+
if (!storedUid) {
|
|
251
|
+
throw new InvalidToken('Invalid token not found');
|
|
252
|
+
}
|
|
253
|
+
if (user && storedUid !== user.id) {
|
|
231
254
|
throw new InvalidToken(`Invalid user ${user.id}`);
|
|
232
255
|
}
|
|
233
|
-
|
|
256
|
+
if (consume) {
|
|
257
|
+
await tokenStorage.invalidate(token);
|
|
258
|
+
}
|
|
259
|
+
return storedUid;
|
|
234
260
|
}
|
|
235
261
|
return {
|
|
236
262
|
before: () => middleware(false),
|
|
@@ -241,6 +267,8 @@ export function createAuth({ getUserById, filterUser, tokenStorage, providers, l
|
|
|
241
267
|
createTemporaryToken,
|
|
242
268
|
verifyTemporaryToken,
|
|
243
269
|
decodeTemporaryToken,
|
|
270
|
+
createOtp,
|
|
271
|
+
verifyOtp,
|
|
244
272
|
logout,
|
|
245
273
|
};
|
|
246
274
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -5,6 +5,7 @@ export declare const config: {
|
|
|
5
5
|
PUBLIC_URL: string;
|
|
6
6
|
AUTH_TOKEN_EXPIRES_IN: number;
|
|
7
7
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
8
|
+
AUTH_OTP_EXPIRES_IN: number;
|
|
8
9
|
AUTH_SECRET: string;
|
|
9
10
|
AUTH_COOKIE_SAME_SITE: string;
|
|
10
11
|
REDIS_URL: string;
|
|
@@ -37,6 +38,7 @@ export declare function extendConfig<T extends Record<string, unknown>>(appConfi
|
|
|
37
38
|
PUBLIC_URL: string;
|
|
38
39
|
AUTH_TOKEN_EXPIRES_IN: number;
|
|
39
40
|
AUTH_TEMP_TOKEN_EXPIRES_IN: number;
|
|
41
|
+
AUTH_OTP_EXPIRES_IN: number;
|
|
40
42
|
AUTH_SECRET: string;
|
|
41
43
|
AUTH_COOKIE_SAME_SITE: string;
|
|
42
44
|
REDIS_URL: string;
|
package/dist/config.js
CHANGED
|
@@ -8,6 +8,7 @@ const baseConfig = {
|
|
|
8
8
|
PUBLIC_URL: readEnv('PUBLIC_URL', ''),
|
|
9
9
|
AUTH_TOKEN_EXPIRES_IN: readEnv.number('AUTH_TOKEN_EXPIRES', 3600 * 24 * 30), // 30 days
|
|
10
10
|
AUTH_TEMP_TOKEN_EXPIRES_IN: readEnv.number('AUTH_TEMP_TOKEN_EXPIRES', 3600),
|
|
11
|
+
AUTH_OTP_EXPIRES_IN: readEnv.number('AUTH_OTP_EXPIRES', 5 * 60),
|
|
11
12
|
AUTH_SECRET: readEnv('AUTH_JWT_SECRET', ''),
|
|
12
13
|
AUTH_COOKIE_SAME_SITE: readEnv('AUTH_COOKIE_SAME_SITE', 'strict'),
|
|
13
14
|
REDIS_URL: readEnv('REDIS_URL', ''),
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,7 @@ import { STATUS_CODES } from 'node:http';
|
|
|
2
2
|
import * as Sentry from '@sentry/node';
|
|
3
3
|
import { errors } from 'compose-middleware';
|
|
4
4
|
import {} from 'express';
|
|
5
|
+
import { InvalidToken } from './auth/errors.js';
|
|
5
6
|
import { getLogger } from './logger.js';
|
|
6
7
|
import { DoesNotExistError, IntegrityError } from './postgres.js';
|
|
7
8
|
export class ApiError extends Error {
|
|
@@ -48,6 +49,9 @@ function convertToAPIError(error) {
|
|
|
48
49
|
if (error instanceof DoesNotExistError) {
|
|
49
50
|
return NotFound(error.message);
|
|
50
51
|
}
|
|
52
|
+
if (error instanceof InvalidToken) {
|
|
53
|
+
return Unauthorized(error.message);
|
|
54
|
+
}
|
|
51
55
|
return error;
|
|
52
56
|
}
|
|
53
57
|
function _errorHandler({ onError }) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devmoods/express-extras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.0",
|
|
4
4
|
"description": "Kubernetes-ready utilities for rapid Node.js application development",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -46,31 +46,31 @@
|
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@devmoods/eslint-config": "^2.6.0",
|
|
48
48
|
"@types/koa-compose": "^3.2.8",
|
|
49
|
-
"@types/node": "22.13.
|
|
49
|
+
"@types/node": "22.13.5",
|
|
50
50
|
"@types/supertest": "^6.0.2",
|
|
51
51
|
"@vitejs/plugin-react": "^4.3.4",
|
|
52
|
-
"@vitest/coverage-v8": "^3.0.
|
|
53
|
-
"arktype": "^2.0
|
|
52
|
+
"@vitest/coverage-v8": "^3.0.7",
|
|
53
|
+
"arktype": "^2.1.0",
|
|
54
54
|
"cookiejar": "^2.1.4",
|
|
55
|
-
"eslint": "^9.
|
|
55
|
+
"eslint": "^9.21.0",
|
|
56
56
|
"faktory-worker": "^4.6.0",
|
|
57
|
-
"prettier": "3.5.
|
|
57
|
+
"prettier": "3.5.2",
|
|
58
58
|
"supertest": "7.0.0",
|
|
59
59
|
"ts-node": "10.9.2",
|
|
60
|
-
"tsx": "^4.19.
|
|
60
|
+
"tsx": "^4.19.3",
|
|
61
61
|
"typescript": "5.7.3",
|
|
62
|
-
"vite": "^6.1.
|
|
63
|
-
"vitest": "^3.0.
|
|
62
|
+
"vite": "^6.1.1",
|
|
63
|
+
"vitest": "^3.0.7",
|
|
64
64
|
"vitest-fail-on-console": "^0.7.1"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@devmoods/fetch": "^4.0.1",
|
|
68
|
-
"@sentry/node": "^9.
|
|
69
|
-
"@sentry/profiling-node": "^9.
|
|
68
|
+
"@sentry/node": "^9.2.0",
|
|
69
|
+
"@sentry/profiling-node": "^9.2.0",
|
|
70
70
|
"@types/bcrypt": "^5.0.2",
|
|
71
71
|
"@types/cookie-parser": "^1.4.8",
|
|
72
72
|
"@types/express": "^5.0.0",
|
|
73
|
-
"@types/jsonwebtoken": "^9.0.
|
|
73
|
+
"@types/jsonwebtoken": "^9.0.9",
|
|
74
74
|
"@types/nodemailer": "^6.4.17",
|
|
75
75
|
"@types/pg": "^8.11.11",
|
|
76
76
|
"@types/superagent": "^8.1.9",
|
|
@@ -90,8 +90,8 @@
|
|
|
90
90
|
"pg": "^8.13.3",
|
|
91
91
|
"redis": "^4.7.0",
|
|
92
92
|
"sonda": "^0.7.1",
|
|
93
|
-
"sql-formatter": "^15.4.
|
|
94
|
-
"uuid": "^11.0
|
|
93
|
+
"sql-formatter": "^15.4.11",
|
|
94
|
+
"uuid": "^11.1.0"
|
|
95
95
|
},
|
|
96
96
|
"prettier": {
|
|
97
97
|
"singleQuote": true
|