prostgles-server 4.2.134 → 4.2.136

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.
Files changed (93) hide show
  1. package/dist/Auth/AuthHandler.d.ts +66 -0
  2. package/dist/Auth/AuthHandler.d.ts.map +1 -0
  3. package/dist/Auth/AuthHandler.js +425 -0
  4. package/dist/Auth/AuthHandler.js.map +1 -0
  5. package/dist/Auth/AuthTypes.d.ts +218 -0
  6. package/dist/Auth/AuthTypes.d.ts.map +1 -0
  7. package/dist/Auth/AuthTypes.js +3 -0
  8. package/dist/Auth/AuthTypes.js.map +1 -0
  9. package/dist/Auth/authInit.d.ts +3 -0
  10. package/dist/Auth/authInit.d.ts.map +1 -0
  11. package/dist/Auth/authInit.js +155 -0
  12. package/dist/Auth/authInit.js.map +1 -0
  13. package/dist/Auth/getSafeReturnURL.d.ts +2 -0
  14. package/dist/Auth/getSafeReturnURL.d.ts.map +1 -0
  15. package/dist/Auth/getSafeReturnURL.js +35 -0
  16. package/dist/Auth/getSafeReturnURL.js.map +1 -0
  17. package/dist/Auth/setAuthSignup.d.ts +5 -0
  18. package/dist/Auth/setAuthSignup.d.ts.map +1 -0
  19. package/dist/Auth/setAuthSignup.js +85 -0
  20. package/dist/Auth/setAuthSignup.js.map +1 -0
  21. package/dist/DboBuilder/DboBuilderTypes.d.ts +3 -3
  22. package/dist/DboBuilder/DboBuilderTypes.d.ts.map +1 -1
  23. package/dist/DboBuilder/DboBuilderTypes.js.map +1 -1
  24. package/dist/DboBuilder/QueryBuilder/getNewQuery.d.ts +1 -1
  25. package/dist/DboBuilder/ViewHandler/ViewHandler.d.ts +1 -1
  26. package/dist/DboBuilder/runSQL.d.ts +1 -1
  27. package/dist/FileManager/initFileManager.js +1 -1
  28. package/dist/FileManager/initFileManager.js.map +1 -1
  29. package/dist/Filtering.d.ts +1 -1
  30. package/dist/Prostgles.d.ts +1 -1
  31. package/dist/Prostgles.d.ts.map +1 -1
  32. package/dist/Prostgles.js +1 -1
  33. package/dist/Prostgles.js.map +1 -1
  34. package/dist/ProstglesTypes.d.ts +1 -1
  35. package/dist/ProstglesTypes.d.ts.map +1 -1
  36. package/dist/PublishParser/PublishParser.d.ts +2 -2
  37. package/dist/PublishParser/PublishParser.d.ts.map +1 -1
  38. package/dist/PublishParser/PublishParser.js.map +1 -1
  39. package/dist/PublishParser/getFileTableRules.d.ts +1 -1
  40. package/dist/PublishParser/getFileTableRules.d.ts.map +1 -1
  41. package/dist/PublishParser/getSchemaFromPublish.d.ts +1 -1
  42. package/dist/PublishParser/getSchemaFromPublish.d.ts.map +1 -1
  43. package/dist/PublishParser/getTableRulesWithoutFileTable.d.ts +1 -1
  44. package/dist/PublishParser/getTableRulesWithoutFileTable.d.ts.map +1 -1
  45. package/dist/PublishParser/publishTypesAndUtils.d.ts +1 -1
  46. package/dist/PublishParser/publishTypesAndUtils.d.ts.map +1 -1
  47. package/dist/PublishParser/publishTypesAndUtils.js.map +1 -1
  48. package/dist/RestApi.d.ts +1 -1
  49. package/dist/RestApi.d.ts.map +1 -1
  50. package/dist/RestApi.js +1 -1
  51. package/dist/RestApi.js.map +1 -1
  52. package/dist/index.d.ts +1 -1
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/initProstgles.js +1 -1
  55. package/dist/initProstgles.js.map +1 -1
  56. package/dist/runClientRequest.d.ts +1 -1
  57. package/dist/runClientRequest.d.ts.map +1 -1
  58. package/dist/runClientRequest.js +1 -1
  59. package/dist/runClientRequest.js.map +1 -1
  60. package/lib/Auth/AuthHandler.ts +474 -0
  61. package/lib/Auth/AuthTypes.ts +247 -0
  62. package/lib/Auth/authInit.ts +166 -0
  63. package/lib/Auth/getSafeReturnURL.ts +35 -0
  64. package/lib/Auth/setAuthSignup.ts +100 -0
  65. package/lib/DBSchemaBuilder.ts +1 -1
  66. package/lib/DboBuilder/DboBuilderTypes.ts +4 -3
  67. package/lib/FileManager/initFileManager.ts +1 -1
  68. package/lib/Prostgles.ts +2 -2
  69. package/lib/ProstglesTypes.ts +1 -1
  70. package/lib/PublishParser/PublishParser.ts +3 -3
  71. package/lib/PublishParser/getFileTableRules.ts +1 -1
  72. package/lib/PublishParser/getSchemaFromPublish.ts +1 -1
  73. package/lib/PublishParser/getTableRulesWithoutFileTable.ts +1 -1
  74. package/lib/PublishParser/publishTypesAndUtils.ts +1 -1
  75. package/lib/RestApi.ts +2 -1
  76. package/lib/index.ts +1 -1
  77. package/lib/initProstgles.ts +1 -1
  78. package/lib/runClientRequest.ts +3 -3
  79. package/package.json +14 -4
  80. package/tests/client/index.ts +9 -7
  81. package/tests/client/package-lock.json +2827 -143
  82. package/tests/client/package.json +1 -1
  83. package/tests/client/tsconfig.json +0 -6
  84. package/tests/clientFileTests.spec.ts +2 -3
  85. package/tests/clientOnlyQueries.spec.ts +2 -2
  86. package/tests/clientRestApi.spec.ts +2 -2
  87. package/tests/server/index.ts +1 -1
  88. package/tests/server/package-lock.json +62 -77
  89. package/tests/server/package.json +2 -2
  90. package/tests/server/tsconfig.json +1 -2
  91. package/tests/serverOnlyQueries.spec.ts +2 -2
  92. package/tsconfig.json +0 -1
  93. package/lib/AuthHandler.ts +0 -816
@@ -1,816 +0,0 @@
1
- import { Express, NextFunction, Request, Response } from "express";
2
- import { AnyObject, AuthGuardLocation, AuthGuardLocationResponse, CHANNELS, FieldFilter } from "prostgles-types";
3
- import { LocalParams, PRGLIOSocket } from "./DboBuilder/DboBuilder";
4
- import { DBOFullyTyped } from "./DBSchemaBuilder";
5
- import { removeExpressRoute } from "./FileManager/FileManager";
6
- import { DB, DBHandlerServer, Prostgles } from "./Prostgles";
7
- type Awaitable<T> = T | Promise<T>;
8
- type AuthSocketSchema = {
9
- user?: AnyObject;
10
- register?: boolean;
11
- login?: boolean;
12
- logout?: boolean;
13
- pathGuard?: boolean;
14
- };
15
-
16
- export type ExpressReq = Request;
17
- export type ExpressRes = Response;
18
-
19
- export type LoginClientInfo = {
20
- ip_address: string;
21
- ip_address_remote: string | undefined;
22
- x_real_ip: string | undefined;
23
- user_agent: string | undefined;
24
- };
25
-
26
- export type BasicSession = {
27
-
28
- /** Must be hard to bruteforce */
29
- sid: string;
30
-
31
- /** UNIX millisecond timestamp */
32
- expires: number;
33
-
34
- /** On expired */
35
- onExpiration: "redirect" | "show_error";
36
- };
37
- export type AuthClientRequest = { socket: PRGLIOSocket } | { httpReq: ExpressReq };
38
- export type UserLike = {
39
- id: string;
40
- type: string;
41
- [key: string]: any;
42
- }
43
- export type SessionUser<ServerUser extends UserLike = UserLike, ClientUser extends AnyObject = AnyObject> = {
44
- /**
45
- * This user will be available in all serverside prostgles options
46
- * id and type values will be available in the prostgles.user session variable in postgres
47
- * */
48
- user: ServerUser;
49
- /**
50
- * Controls which fields from user are available in postgres session variable
51
- */
52
- sessionFields?: FieldFilter<ServerUser>;
53
- /**
54
- * User data sent to the authenticated client
55
- */
56
- clientUser: ClientUser;
57
- }
58
-
59
- export type AuthResult<SU = SessionUser> = SU & { sid: string; } | {
60
- user?: undefined;
61
- clientUser?: undefined;
62
- sid?: string;
63
- } | undefined;
64
-
65
- export const HTTPCODES = {
66
- AUTH_ERROR: 401,
67
- NOT_FOUND: 404,
68
- BAD_REQUEST: 400,
69
- INTERNAL_SERVER_ERROR: 500,
70
- };
71
-
72
- export const getLoginClientInfo = (req: AuthClientRequest): AuthClientRequest & LoginClientInfo => {
73
- if("httpReq" in req){
74
- const ip_address = req.httpReq.ip;
75
- if(!ip_address) throw new Error("ip_address missing from req.httpReq");
76
- const user_agent = req.httpReq.headers["user-agent"];
77
- return {
78
- ...req,
79
- ip_address,
80
- ip_address_remote: req.httpReq.connection.remoteAddress,
81
- x_real_ip: req.httpReq.headers['x-real-ip'] as any,
82
- user_agent,
83
- };
84
- } else {
85
- return {
86
- ...req,
87
- ip_address: req.socket.handshake.address,
88
- ip_address_remote: req.socket.request.connection.remoteAddress,
89
- x_real_ip: req.socket.handshake.headers?.["x-real-ip"],
90
- user_agent: req.socket.handshake.headers?.['user-agent'],
91
- }
92
- }
93
- }
94
-
95
- export type AuthRequestParams<S, SUser extends SessionUser> = { db: DB, dbo: DBOFullyTyped<S>; getUser: () => Promise<AuthResult<SUser>> }
96
-
97
- export type Auth<S = void, SUser extends SessionUser = SessionUser> = {
98
- /**
99
- * Name of the cookie or socket hadnshake query param that represents the session id.
100
- * Defaults to "session_id"
101
- */
102
- sidKeyName?: string;
103
-
104
- /**
105
- * Response time rounding in milliseconds to prevent timing attacks on login. Login response time should always be a multiple of this value. Defaults to 500 milliseconds
106
- */
107
- responseThrottle?: number;
108
-
109
- expressConfig?: {
110
- /**
111
- * Express app instance. If provided Prostgles will attempt to set sidKeyName to user cookie
112
- */
113
- app: Express;
114
-
115
- /**
116
- * Used in allowing logging in through express. Defaults to /login
117
- */
118
- loginRoute?: string;
119
-
120
- /**
121
- * Used in allowing logging out through express. Defaults to /logout
122
- */
123
- logoutGetPath?: string;
124
-
125
- /**
126
- * Options used in setting the cookie after a successful login
127
- */
128
- cookieOptions?: AnyObject;
129
-
130
- /**
131
- * False by default. If false and userRoutes are provided then the socket will request window.location.reload if the current url is on a user route.
132
- */
133
- disableSocketAuthGuard?: boolean;
134
-
135
- /**
136
- * If provided, any client requests to NOT these routes (or their subroutes) will be redirected to loginRoute (if logged in) and then redirected back to the initial route after logging in
137
- * If logged in the user is allowed to access these routes
138
- */
139
- publicRoutes?: string[];
140
-
141
- /**
142
- * Will attach a app.use listener and will expose getUser
143
- * Used for blocking access
144
- */
145
- use?: (args: { req: ExpressReq; res: ExpressRes, next: NextFunction } & AuthRequestParams<S, SUser>) => void | Promise<void>;
146
-
147
- /**
148
- * Will be called after a GET request is authorised
149
- * This means that
150
- */
151
- onGetRequestOK?: (
152
- req: ExpressReq,
153
- res: ExpressRes,
154
- params: AuthRequestParams<S, SUser>
155
- ) => any;
156
-
157
- /**
158
- * Name of get url parameter used in redirecting user after successful login.
159
- * Defaults to "returnURL"
160
- */
161
- returnUrlParamName?: string;
162
-
163
- magicLinks?: {
164
-
165
- /**
166
- * Will default to /magic-link
167
- */
168
- route?: string;
169
-
170
- /**
171
- * Used in creating a session/logging in using a magic link
172
- */
173
- check: (magicId: string, dbo: DBOFullyTyped<S>, db: DB, client: LoginClientInfo) => Awaitable<BasicSession | undefined>;
174
- }
175
-
176
- }
177
-
178
- /**
179
- * undefined sid is allowed to enable public users
180
- */
181
- getUser: (sid: string | undefined, dbo: DBOFullyTyped<S>, db: DB, client: AuthClientRequest & LoginClientInfo) => Awaitable<AuthResult<SUser>>;
182
-
183
- register?: (params: AnyObject, dbo: DBOFullyTyped<S>, db: DB) => Awaitable<BasicSession> | BasicSession;
184
- login?: (params: AnyObject, dbo: DBOFullyTyped<S>, db: DB, client: LoginClientInfo) => Awaitable<BasicSession> | BasicSession;
185
- logout?: (sid: string | undefined, dbo: DBOFullyTyped<S>, db: DB) => Awaitable<any>;
186
-
187
- /**
188
- * If provided then session info will be saved on socket.__prglCache and reused from there
189
- */
190
- cacheSession?: {
191
- getSession: (sid: string | undefined, dbo: DBOFullyTyped<S>, db: DB) => Awaitable<BasicSession>
192
- }
193
- }
194
-
195
- export class AuthHandler {
196
- protected prostgles: Prostgles;
197
- protected opts?: Auth;
198
- dbo: DBHandlerServer;
199
- db: DB;
200
- sidKeyName?: string;
201
-
202
- routes: {
203
- login?: string;
204
- returnUrlParamName?: string;
205
- logoutGetPath?: string;
206
- magicLinks?: {
207
- route: string;
208
- expressRoute: string;
209
- }
210
- readonly catchAll: '*';
211
- } = {
212
- catchAll: "*"
213
- }
214
-
215
- constructor(prostgles: Prostgles) {
216
- this.prostgles = prostgles;
217
- this.opts = prostgles.opts.auth as any;
218
- if (prostgles.opts.auth?.expressConfig) {
219
- const { magicLinks, returnUrlParamName, loginRoute, logoutGetPath } = prostgles.opts.auth.expressConfig;
220
- const magicLinksRoute = magicLinks?.route || "/magic-link"
221
- this.routes = {
222
- magicLinks: magicLinks? {
223
- expressRoute: `${magicLinksRoute}/:id`,
224
- route: magicLinksRoute
225
- } : undefined,
226
- returnUrlParamName: returnUrlParamName || "returnURL",
227
- login: loginRoute || "/login",
228
- logoutGetPath: logoutGetPath || "/logout",
229
- catchAll: "*"
230
- }
231
- }
232
- if(!prostgles.dbo || !prostgles.db) throw "dbo or db missing";
233
- this.dbo = prostgles.dbo;
234
- this.db = prostgles.db;
235
- }
236
-
237
- validateSid = (sid: string | undefined) => {
238
- if (!sid) return undefined;
239
- if (typeof sid !== "string") throw "sid missing or not a string";
240
- return sid;
241
- }
242
-
243
- matchesRoute = (route: string | undefined, clientFullRoute: string) => {
244
- return route && clientFullRoute && (
245
- route === clientFullRoute ||
246
- clientFullRoute.startsWith(route) && ["/", "?", "#"].includes(clientFullRoute[route.length] ?? "")
247
- )
248
- }
249
-
250
- isUserRoute = (pathname: string) => {
251
- const pubRoutes = [
252
- ...this.opts?.expressConfig?.publicRoutes || [],
253
- ];
254
- if (this.routes?.login) pubRoutes.push(this.routes.login);
255
- if (this.routes?.logoutGetPath) pubRoutes.push(this.routes.logoutGetPath);
256
- if (this.routes?.magicLinks?.route) pubRoutes.push(this.routes.magicLinks.route);
257
-
258
- return !pubRoutes.some(publicRoute => {
259
- return this.matchesRoute(publicRoute, pathname);
260
- });
261
- }
262
-
263
- private setCookieAndGoToReturnURLIFSet = (cookie: { sid: string; expires: number; }, r: { req: ExpressReq; res: ExpressRes }) => {
264
- const { sid, expires } = cookie;
265
- const { res, req } = r;
266
- if (sid) {
267
- const maxAgeOneDay = 60 * 60 * 24; // 24 hours;
268
- type CD = { maxAge: number } | { expires: Date }
269
- let cookieDuration: CD = {
270
- maxAge: maxAgeOneDay
271
- }
272
- if(expires && Number.isFinite(expires) && !isNaN(+ new Date(expires))){
273
- // const maxAge = (+new Date(expires)) - Date.now();
274
- cookieDuration = { expires: new Date(expires) };
275
- const days = (+cookieDuration.expires - Date.now())/(24 * 60 * 60e3);
276
- if(days >= 400){
277
- console.warn(`Cookie expiration is higher than the Chrome 400 day limit: ${days}days`)
278
- }
279
- }
280
-
281
- const cookieOpts = {
282
- ...cookieDuration,
283
- httpOnly: true, // The cookie only accessible by the web server
284
- //signed: true // Indicates if the cookie should be signed
285
- secure: true,
286
- sameSite: "strict" as const,
287
- ...(this.opts?.expressConfig?.cookieOptions || {})
288
- };
289
- const cookieData = sid;
290
- if(!this.sidKeyName || !this.routes?.returnUrlParamName) throw "sidKeyName or returnURL missing"
291
- res.cookie(this.sidKeyName, cookieData, cookieOpts);
292
- const successURL = this.getReturnUrl(req) || "/";
293
- res.redirect(successURL);
294
-
295
- } else {
296
- throw ("no user or session")
297
- }
298
- }
299
-
300
- getUser = async (clientReq: { httpReq: ExpressReq; }): Promise<AuthResult> => {
301
- if(!this.sidKeyName || !this.opts?.getUser) {
302
- throw "sidKeyName or this.opts.getUser missing";
303
- }
304
- const sid = clientReq.httpReq?.cookies?.[this.sidKeyName];
305
- if (!sid) return undefined;
306
-
307
- try {
308
- return this.throttledFunc(async () => {
309
- return this.opts!.getUser(this.validateSid(sid), this.dbo as any, this.db, getLoginClientInfo(clientReq));
310
- }, 50)
311
- } catch (err) {
312
- console.error(err);
313
- }
314
- return undefined;
315
- }
316
-
317
- async init() {
318
- if (!this.opts) return;
319
-
320
- this.opts.sidKeyName = this.opts.sidKeyName || "session_id";
321
- const { sidKeyName, login, getUser, expressConfig } = this.opts;
322
- this.sidKeyName = this.opts.sidKeyName;
323
-
324
- if (typeof sidKeyName !== "string" && !login) {
325
- throw "Invalid auth: Provide { sidKeyName: string } ";
326
- }
327
- /**
328
- * Why ??? Collision with socket.io ???
329
- */
330
- if (this.sidKeyName === "sid") throw "sidKeyName cannot be 'sid' please provide another name.";
331
-
332
- if (!getUser) throw "getUser missing from auth config";
333
-
334
- if (expressConfig) {
335
- const { app, publicRoutes = [], onGetRequestOK, magicLinks, use } = expressConfig;
336
- if (publicRoutes.find(r => typeof r !== "string" || !r)) {
337
- throw "Invalid or empty string provided within publicRoutes "
338
- }
339
-
340
- if(use){
341
- app.use((req, res, next) => {
342
- use({
343
- req,
344
- res,
345
- next,
346
- getUser: () => this.getUser({ httpReq: req }) as any,
347
- dbo: this.dbo as DBOFullyTyped,
348
- db: this.db,
349
- })
350
- })
351
- }
352
-
353
- if (magicLinks && this.routes.magicLinks) {
354
- const { check } = magicLinks;
355
- if (!check) throw "Check must be defined for magicLinks";
356
-
357
- app.get(this.routes.magicLinks?.expressRoute, async (req: ExpressReq, res: ExpressRes) => {
358
- const { id } = req.params ?? {};
359
-
360
- if (typeof id !== "string" || !id) {
361
- res.status(HTTPCODES.BAD_REQUEST).json({ msg: "Invalid magic-link id. Expecting a string" });
362
- } else {
363
- try {
364
- const session = await this.throttledFunc(async () => {
365
- return check(id, this.dbo as any, this.db, getLoginClientInfo({ httpReq: req }));
366
- });
367
- if (!session) {
368
- res.status(HTTPCODES.AUTH_ERROR).json({ msg: "Invalid magic-link" });
369
- } else {
370
- this.setCookieAndGoToReturnURLIFSet(session, { req, res });
371
- }
372
-
373
- } catch (e) {
374
- res.status(HTTPCODES.AUTH_ERROR).json({ msg: e });
375
- }
376
- }
377
- });
378
- }
379
-
380
- const loginRoute = this.routes?.login;
381
- if (loginRoute) {
382
-
383
-
384
- app.post(loginRoute, async (req: ExpressReq, res: ExpressRes) => {
385
- try {
386
- const start = Date.now();
387
- const { sid, expires } = await this.loginThrottled(req.body || {}, getLoginClientInfo({ httpReq: req })) || {};
388
- await this.prostgles.opts.onLog?.({
389
- type: "auth",
390
- command: "login",
391
- duration: Date.now() - start,
392
- sid,
393
- socketId: undefined,
394
- })
395
- if (sid) {
396
-
397
- this.setCookieAndGoToReturnURLIFSet({ sid, expires }, { req, res });
398
-
399
- } else {
400
- throw ("Internal error: no user or session")
401
- }
402
- } catch (err) {
403
- console.log(err)
404
- res.status(HTTPCODES.AUTH_ERROR).json({ err });
405
- }
406
-
407
- });
408
-
409
- if (this.routes.logoutGetPath && this.opts.logout) {
410
- app.get(this.routes.logoutGetPath, async (req: ExpressReq, res: ExpressRes) => {
411
- const sid = this.validateSid(req?.cookies?.[sidKeyName]);
412
- if (sid) {
413
- try {
414
- await this.throttledFunc(() => {
415
- return this.opts!.logout!(req?.cookies?.[sidKeyName], this.dbo as any, this.db);
416
- })
417
- } catch (err) {
418
- console.error(err);
419
- }
420
- }
421
- res.redirect("/")
422
- });
423
- }
424
-
425
- if (Array.isArray(publicRoutes)) {
426
-
427
- /* Redirect if not logged in and requesting non public content */
428
- app.get(this.routes.catchAll, async (req: ExpressReq, res: ExpressRes, next) => {
429
- const clientReq: AuthClientRequest = { httpReq: req }
430
- const getUser = this.getUser;
431
- if(this.prostgles.restApi){
432
- if(Object.values(this.prostgles.restApi.routes).some(restRoute => this.matchesRoute(restRoute.split("/:")[0], req.path))){
433
- next();
434
- return;
435
- }
436
- }
437
- try {
438
- const returnURL = this.getReturnUrl(req);
439
-
440
- /**
441
- * Requesting a User route
442
- */
443
- if (this.isUserRoute(req.path)) {
444
-
445
- /* Check auth. Redirect to login if unauthorized */
446
- const u = await getUser(clientReq);
447
- if (!u) {
448
- res.redirect(`${loginRoute}?returnURL=${encodeURIComponent(req.originalUrl)}`);
449
- return;
450
- }
451
-
452
- /* If authorized and going to returnUrl then redirect. Otherwise serve file */
453
- } else if (returnURL && (await getUser(clientReq))) {
454
-
455
- res.redirect(returnURL);
456
- return;
457
-
458
- /** If Logged in and requesting login then redirect to main page */
459
- } else if (this.matchesRoute(loginRoute, req.path) && (await getUser(clientReq))) {
460
-
461
- res.redirect("/");
462
- return;
463
- }
464
-
465
- onGetRequestOK?.(req, res, { getUser: () => getUser(clientReq), dbo: this.dbo as DBOFullyTyped, db: this.db })
466
-
467
- } catch (error) {
468
- console.error(error);
469
- const errorMessage = typeof error === "string" ? error : error instanceof Error ? error.message : "";
470
- res.status(HTTPCODES.AUTH_ERROR).json({ msg: "Something went wrong when processing your request" + (errorMessage? (": " + errorMessage) : "") });
471
- }
472
-
473
- });
474
- }
475
- }
476
- }
477
- }
478
-
479
- getReturnUrl = (req: ExpressReq) => {
480
- const { returnUrlParamName } = this.routes;
481
- if (returnUrlParamName && req?.query?.[returnUrlParamName]) {
482
- const returnURL = decodeURIComponent(req?.query?.[returnUrlParamName] as string);
483
-
484
- return getSafeReturnURL(returnURL, returnUrlParamName);
485
- }
486
- return null;
487
- }
488
-
489
- destroy = () => {
490
- const app = this.opts?.expressConfig?.app;
491
- const { login, logoutGetPath, magicLinks } = this.routes;
492
- removeExpressRoute(app, [login, logoutGetPath, magicLinks?.expressRoute]);
493
- }
494
-
495
- throttledFunc = <T>(func: () => Promise<T>, throttle = 500): Promise<T> => {
496
-
497
- return new Promise(async (resolve, reject) => {
498
-
499
- let result: any, error: any, finished = false;
500
-
501
- /**
502
- * Throttle reject response times to prevent timing attacks
503
- */
504
- const interval = setInterval(() => {
505
- if (finished) {
506
- clearInterval(interval);
507
- if (error) {
508
- reject(error);
509
- } else {
510
- resolve(result)
511
- }
512
- }
513
- }, throttle);
514
-
515
-
516
- try {
517
- result = await func();
518
- resolve(result);
519
- clearInterval(interval);
520
- } catch (err) {
521
- console.log(err)
522
- error = err;
523
- }
524
-
525
- finished = true;
526
- })
527
- }
528
-
529
- loginThrottled = async (params: AnyObject, client: LoginClientInfo): Promise<BasicSession> => {
530
- if (!this.opts?.login) throw "Auth login config missing";
531
- const { responseThrottle = 500 } = this.opts;
532
-
533
- return this.throttledFunc(async () => {
534
- const result = await this.opts?.login?.(params, this.dbo as DBOFullyTyped, this.db, client);
535
- const err = {
536
- msg: "Bad login result type. \nExpecting: undefined | null | { sid: string; expires: number } but got: " + JSON.stringify(result)
537
- }
538
-
539
- if(!result) throw err;
540
- if(result && (typeof result.sid !== "string" || typeof result.expires !== "number") || !result && ![undefined, null].includes(result)) {
541
- throw err
542
- }
543
- if(result && result.expires < Date.now()){
544
- throw { msg: "auth.login() is returning an expired session. Can only login with a session.expires greater than Date.now()"}
545
- }
546
-
547
- return result;
548
- }, responseThrottle);
549
-
550
- }
551
-
552
-
553
- /**
554
- * Will return first sid value found in:
555
- * Bearer header
556
- * http cookie
557
- * query params
558
- * Based on sid names in auth
559
- */
560
- getSID(localParams: LocalParams): string | undefined {
561
- if (!this.opts) return undefined;
562
-
563
- const { sidKeyName } = this.opts;
564
-
565
- if (!sidKeyName || !localParams) return undefined;
566
-
567
- if (localParams.socket) {
568
- const { handshake } = localParams.socket;
569
- const querySid = handshake?.auth?.[sidKeyName] || handshake?.query?.[sidKeyName];
570
- let rawSid = querySid;
571
- if (!rawSid) {
572
- const cookie_str = localParams.socket?.handshake?.headers?.cookie;
573
- const cookie = parseCookieStr(cookie_str);
574
- rawSid = cookie[sidKeyName];
575
- }
576
- return this.validateSid(rawSid);
577
-
578
- } else if (localParams.httpReq) {
579
- const [tokenType, base64Token] = localParams.httpReq.headers.authorization?.split(' ') ?? [];
580
- let bearerSid: string | undefined;
581
- if(tokenType && base64Token){
582
- if(tokenType.trim() !== "Bearer"){
583
- throw "Only Bearer Authorization header allowed";
584
- }
585
- bearerSid = Buffer.from(base64Token, 'base64').toString();
586
- }
587
- return this.validateSid(bearerSid ?? localParams.httpReq?.cookies?.[sidKeyName]);
588
-
589
- } else throw "socket OR httpReq missing from localParams";
590
-
591
- function parseCookieStr(cookie_str: string | undefined): any {
592
- if (!cookie_str || typeof cookie_str !== "string") {
593
- return {}
594
- }
595
-
596
- return cookie_str.replace(/\s/g, '')
597
- .split(";")
598
- .reduce<AnyObject>((prev, current) => {
599
- const [name, value] = current.split('=');
600
- prev[name!] = value;
601
- return prev;
602
- }, {});
603
- }
604
- }
605
-
606
- /**
607
- * Used for logging
608
- */
609
- getSIDNoError = (localParams: LocalParams | undefined): string | undefined => {
610
- if(!localParams) return undefined;
611
- try {
612
- return this.getSID(localParams);
613
- } catch (err) {
614
- return undefined;
615
- }
616
- }
617
-
618
- async getClientInfo(localParams: Pick<LocalParams, "socket" | "httpReq">): Promise<AuthResult> {
619
- if (!this.opts) return {};
620
-
621
- const getSession = this.opts.cacheSession?.getSession;
622
- const isSocket = "socket" in localParams;
623
- if(isSocket){
624
- if(getSession && localParams.socket?.__prglCache){
625
- const { session, user, clientUser } = localParams.socket.__prglCache;
626
- const isValid = this.isValidSocketSession(localParams.socket, session)
627
- if(isValid){
628
-
629
- return {
630
- sid: session.sid,
631
- user,
632
- clientUser,
633
- }
634
- } else return {
635
- sid: session.sid
636
- };
637
- }
638
- }
639
-
640
- const authStart = Date.now();
641
- const res = await this.throttledFunc(async () => {
642
-
643
- const { getUser } = this.opts ?? {};
644
-
645
- if (getUser && localParams && (localParams.httpReq || localParams.socket)) {
646
- const sid = this.getSID(localParams);
647
- const clientReq = localParams.httpReq? { httpReq: localParams.httpReq } : { socket: localParams.socket! };
648
- let user, clientUser;
649
- if(sid){
650
- const res = await getUser(sid, this.dbo as any, this.db, getLoginClientInfo(clientReq)) as any;
651
- user = res?.user;
652
- clientUser = res?.clientUser;
653
- }
654
- if(getSession && isSocket){
655
- const session = await getSession(sid, this.dbo as any, this.db)
656
- if(session?.expires && user && clientUser && localParams.socket){
657
- localParams.socket.__prglCache = {
658
- session,
659
- user,
660
- clientUser,
661
- }
662
- }
663
- }
664
- if(sid) {
665
- return { sid, user, clientUser }
666
- }
667
- }
668
-
669
- return {};
670
- }, 5);
671
-
672
- await this.prostgles.opts.onLog?.({
673
- type: "auth",
674
- command: "getClientInfo",
675
- duration: Date.now() - authStart,
676
- sid: res.sid,
677
- socketId: localParams.socket?.id,
678
- });
679
- return res;
680
- }
681
-
682
- isValidSocketSession = (socket: PRGLIOSocket, session: BasicSession): boolean => {
683
- const hasExpired = Boolean(session && session.expires <= Date.now())
684
- if(this.opts?.expressConfig?.publicRoutes && !this.opts.expressConfig?.disableSocketAuthGuard){
685
- const error = "Session has expired";
686
- if(hasExpired){
687
- if(session.onExpiration === "redirect")
688
- socket.emit(CHANNELS.AUTHGUARD, {
689
- shouldReload: session.onExpiration === "redirect",
690
- error
691
- });
692
- throw error;
693
- }
694
- }
695
- return Boolean(session && !hasExpired);
696
- }
697
-
698
- makeSocketAuth = async (socket: PRGLIOSocket): Promise<{ auth: AuthSocketSchema; userData: AuthResult; } | Record<string, never>> => {
699
- if (!this.opts) return {};
700
-
701
- const auth: Partial<Record<keyof Omit<AuthSocketSchema, "user">, boolean | undefined>> & { user?: AnyObject | undefined } = {};
702
-
703
- if (this.opts.expressConfig?.publicRoutes && !this.opts.expressConfig?.disableSocketAuthGuard) {
704
-
705
- auth.pathGuard = true;
706
-
707
- socket.removeAllListeners(CHANNELS.AUTHGUARD)
708
- socket.on(CHANNELS.AUTHGUARD, async (params: AuthGuardLocation, cb = (_err: any, _res?: AuthGuardLocationResponse) => { /** EMPTY */ }) => {
709
-
710
- try {
711
-
712
- const { pathname, origin } = typeof params === "string" ? JSON.parse(params) : (params || {});
713
- if (pathname && typeof pathname !== "string") {
714
- console.warn("Invalid pathname provided for AuthGuardLocation: ", pathname);
715
- }
716
-
717
- /** These origins */
718
- const IGNORED_API_ORIGINS = ["file://"]
719
- if (!IGNORED_API_ORIGINS.includes(origin) && pathname && typeof pathname === "string" && this.isUserRoute(pathname) && !(await this.getClientInfo({ socket }))?.user) {
720
- cb(null, { shouldReload: true });
721
- } else {
722
- cb(null, { shouldReload: false });
723
- }
724
-
725
- } catch (err) {
726
- console.error("AUTHGUARD err: ", err);
727
- cb(err)
728
- }
729
- });
730
- }
731
-
732
- const {
733
- register,
734
- logout
735
- } = this.opts;
736
- const login = this.loginThrottled
737
-
738
- let handlers: {
739
- name: keyof Omit<AuthSocketSchema, "user">;
740
- ch: string;
741
- func: (...args: any) => any;
742
- }[] = [
743
- { func: (params: any, dbo: any, db: DB, client: LoginClientInfo) => register?.(params, dbo, db), ch: CHANNELS.REGISTER, name: "register" as keyof Omit<AuthSocketSchema, "user"> },
744
- { func: (params: any, dbo: any, db: DB, client: LoginClientInfo) => login(params, client), ch: CHANNELS.LOGIN, name: "login" as keyof Omit<AuthSocketSchema, "user"> },
745
- { func: (params: any, dbo: any, db: DB, client: LoginClientInfo) => logout?.(this.getSID({ socket }), dbo, db), ch: CHANNELS.LOGOUT, name: "logout" as keyof Omit<AuthSocketSchema, "user">}
746
- ].filter(h => h.func);
747
-
748
- const userData = await this.getClientInfo({ socket });
749
- if (userData) {
750
- auth.user = userData.clientUser;
751
- handlers = handlers.filter(h => h.name === "logout");
752
- }
753
-
754
- handlers.map(({ func, ch, name }) => {
755
- auth[name] = true;
756
-
757
- socket.removeAllListeners(ch)
758
- socket.on(ch, async (params: any, cb = (..._callback: any) => { /** Empty */ }) => {
759
-
760
- try {
761
- if (!socket) throw "socket missing??!!";
762
- const id_address = (socket as any)?.conn?.remoteAddress;
763
- const user_agent = socket.handshake?.headers?.["user-agent"];
764
- const res = await func(params, this.dbo as any, this.db, { user_agent, id_address });
765
- if (name === "login" && res && res.sid) {
766
- /* TODO: Re-send schema to client */
767
- }
768
-
769
- cb(null, true);
770
-
771
- } catch (err) {
772
- console.error(name + " err", err);
773
- cb(err)
774
- }
775
- });
776
- });
777
-
778
- return { auth, userData };
779
- }
780
- }
781
-
782
- export const getSafeReturnURL = (returnURL: string, returnUrlParamName: string, quiet = false) => {
783
- /** Dissalow redirect to other domains */
784
- if(returnURL) {
785
- const allowedOrigin = "https://localhost";
786
- const { origin, pathname, search, searchParams } = new URL(returnURL, allowedOrigin);
787
- if(
788
- origin !== allowedOrigin ||
789
- returnURL !== `${pathname}${search}` ||
790
- searchParams.get(returnUrlParamName)
791
- ){
792
- if(!quiet){
793
- console.error(`Unsafe returnUrl: ${returnURL}. Redirecting to /`);
794
- }
795
- return "/";
796
- }
797
-
798
- return returnURL;
799
- }
800
- }
801
-
802
- const issue = ([
803
- ["https://localhost", "/"],
804
- ["//localhost.bad.com", "/"],
805
- ["//localhost.com", "/"],
806
- ["/localhost/com", "/localhost/com"],
807
- ["/localhost/com?here=there", "/localhost/com?here=there"],
808
- ["/localhost/com?returnUrl=there", "/"],
809
- ["//http://localhost.com", "/"],
810
- ["//abc.com", "/"],
811
- ["///abc.com", "/"],
812
- ] as const).find(([returnURL, expected]) => getSafeReturnURL(returnURL, "returnUrl", true) !== expected);
813
-
814
- if(issue){
815
- throw new Error(`getSafeReturnURL failed for ${issue[0]}. Expected: ${issue[1]}`);
816
- }