@nocobase/auth 2.2.0-alpha.7 → 2.2.0-alpha.9

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/lib/actions.d.ts CHANGED
@@ -6,5 +6,5 @@
6
6
  * This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
7
7
  * For more information, please refer to: https://www.nocobase.com/agreement.
8
8
  */
9
- import { Handlers } from '@nocobase/resourcer';
9
+ import type { Handlers } from '@nocobase/resourcer';
10
10
  export declare const actions: Handlers;
package/lib/actions.js CHANGED
@@ -30,7 +30,28 @@ __export(actions_exports, {
30
30
  actions: () => actions
31
31
  });
32
32
  module.exports = __toCommonJS(actions_exports);
33
+ var import_utils = require("@nocobase/utils");
33
34
  /* istanbul ignore file -- @preserve */
35
+ const localeNamespace = "auth";
36
+ function assertTrustedSignInOrigin(ctx) {
37
+ const originContext = {
38
+ protocol: ctx.protocol,
39
+ headers: ctx.headers,
40
+ get: /* @__PURE__ */ __name((name) => ctx.get(name), "get")
41
+ };
42
+ const origin = ctx.get("origin");
43
+ if (origin) {
44
+ if (!(0, import_utils.isTrustedOrigin)(originContext, origin)) {
45
+ ctx.throw(403, ctx.t("Invalid sign-in origin", { ns: localeNamespace }));
46
+ }
47
+ return;
48
+ }
49
+ const refererOrigin = (0, import_utils.getOrigin)(ctx.get("referer"));
50
+ if (refererOrigin && !(0, import_utils.isTrustedOrigin)(originContext, refererOrigin)) {
51
+ ctx.throw(403, ctx.t("Invalid sign-in origin", { ns: localeNamespace }));
52
+ }
53
+ }
54
+ __name(assertTrustedSignInOrigin, "assertTrustedSignInOrigin");
34
55
  function filterHiddenFields(ctx, user) {
35
56
  var _a, _b;
36
57
  if (!user) {
@@ -51,6 +72,7 @@ function filterHiddenFields(ctx, user) {
51
72
  __name(filterHiddenFields, "filterHiddenFields");
52
73
  const actions = {
53
74
  signIn: /* @__PURE__ */ __name(async (ctx, next) => {
75
+ assertTrustedSignInOrigin(ctx);
54
76
  ctx.body = await ctx.auth.signIn();
55
77
  await next();
56
78
  }, "signIn"),
@@ -66,7 +88,11 @@ const actions = {
66
88
  check: /* @__PURE__ */ __name(async (ctx, next) => {
67
89
  ctx.body = filterHiddenFields(ctx, ctx.auth.user);
68
90
  await next();
69
- }, "check")
91
+ }, "check"),
92
+ syncCookies: /* @__PURE__ */ __name(async (ctx, next) => {
93
+ ctx.body = await ctx.auth.syncCookies();
94
+ await next();
95
+ }, "syncCookies")
70
96
  };
71
97
  // Annotate the CommonJS export names for ESM import in node:
72
98
  0 && (module.exports = {
@@ -83,4 +83,5 @@ export declare class AuthManager {
83
83
  }, next: Next) => Promise<any>;
84
84
  private getDefaultJWTSecret;
85
85
  }
86
+ export declare function csrfMiddleware(ctx: Context, next: Next): Promise<any>;
86
87
  export {};
@@ -37,7 +37,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
37
37
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
38
38
  var auth_manager_exports = {};
39
39
  __export(auth_manager_exports, {
40
- AuthManager: () => AuthManager
40
+ AuthManager: () => AuthManager,
41
+ csrfMiddleware: () => csrfMiddleware
41
42
  });
42
43
  module.exports = __toCommonJS(auth_manager_exports);
43
44
  var import_utils = require("@nocobase/utils");
@@ -45,6 +46,7 @@ var import_jwt_service = require("./base/jwt-service");
45
46
  var import_path = __toESM(require("path"));
46
47
  var import_fs = __toESM(require("fs"));
47
48
  var import_crypto = __toESM(require("crypto"));
49
+ const SAFE_METHODS = /* @__PURE__ */ new Set(["GET", "HEAD", "OPTIONS"]);
48
50
  const _AuthManager = class _AuthManager {
49
51
  /**
50
52
  * @internal
@@ -144,23 +146,66 @@ const _AuthManager = class _AuthManager {
144
146
  middleware() {
145
147
  const self = this;
146
148
  return /* @__PURE__ */ __name(async function AuthManagerMiddleware(ctx, next) {
147
- const name = ctx.get(self.options.authKey) || self.options.default;
149
+ var _a, _b;
150
+ const headerAuthenticator = ctx.get(self.options.authKey);
151
+ const cookieName = (0, import_utils.getAuthCookieName)("authenticator", ctx.app.name);
152
+ const cookieAuthenticator = headerAuthenticator ? null : ctx.cookies.get(cookieName);
153
+ const name = headerAuthenticator || cookieAuthenticator || self.options.default;
148
154
  let authenticator;
149
155
  try {
150
156
  authenticator = await ctx.app.authManager.get(name, ctx);
151
157
  ctx.auth = authenticator;
152
158
  } catch (err) {
153
- ctx.auth = {};
154
- ctx.logger.warn(err.message, { method: "check", authenticator: name });
155
- return next();
159
+ if (cookieAuthenticator && !headerAuthenticator && self.options.default && name !== self.options.default) {
160
+ ctx.cookies.set(cookieName, null, (0, import_utils.getAuthCookieOptions)(ctx));
161
+ try {
162
+ authenticator = await ctx.app.authManager.get(self.options.default, ctx);
163
+ ctx.auth = authenticator;
164
+ } catch (defaultErr) {
165
+ ctx.auth = {};
166
+ ctx.logger.warn(defaultErr.message, { method: "check", authenticator: self.options.default });
167
+ return next();
168
+ }
169
+ } else {
170
+ ctx.auth = {};
171
+ ctx.logger.warn(err.message, { method: "check", authenticator: name });
172
+ return next();
173
+ }
156
174
  }
157
175
  if (!authenticator) {
176
+ ctx.auth = {};
158
177
  return next();
159
178
  }
160
179
  if (await ctx.auth.skipCheck()) {
161
180
  return next();
162
181
  }
163
- const user = await ctx.auth.check();
182
+ let user;
183
+ try {
184
+ user = await ctx.auth.check();
185
+ } catch (err) {
186
+ if (((_a = ctx.state) == null ? void 0 : _a.optionalAuth) === true && ((_b = ctx.state) == null ? void 0 : _b.pendingAuthTokenSource) === "cookie" && (err.status === 401 || err.statusCode === 401)) {
187
+ const authTokenCookieName = (0, import_utils.getAuthCookieName)("authToken", ctx.app.name);
188
+ const authCookieOptions = (0, import_utils.getAuthCookieOptions)(ctx);
189
+ ctx.cookies.set(authTokenCookieName, null, authCookieOptions);
190
+ const rawPublicPath = process.env.APP_PUBLIC_PATH;
191
+ if (rawPublicPath && rawPublicPath !== authCookieOptions.path) {
192
+ ctx.cookies.set(authTokenCookieName, null, { ...authCookieOptions, path: rawPublicPath });
193
+ }
194
+ ctx.state.currentUser = void 0;
195
+ ctx.state.authTokenSource = void 0;
196
+ ctx.state.pendingAuthTokenSource = void 0;
197
+ try {
198
+ return await next();
199
+ } catch (anonymousErr) {
200
+ const setCookieHeader = ctx.res.getHeader("set-cookie");
201
+ if (setCookieHeader) {
202
+ anonymousErr.headers = { ...anonymousErr.headers, "set-cookie": setCookieHeader };
203
+ }
204
+ throw anonymousErr;
205
+ }
206
+ }
207
+ throw err;
208
+ }
164
209
  if (user) {
165
210
  ctx.auth.user = user;
166
211
  }
@@ -192,7 +237,24 @@ const _AuthManager = class _AuthManager {
192
237
  };
193
238
  __name(_AuthManager, "AuthManager");
194
239
  let AuthManager = _AuthManager;
240
+ async function csrfMiddleware(ctx, next) {
241
+ var _a, _b;
242
+ if (SAFE_METHODS.has(ctx.method)) {
243
+ return next();
244
+ }
245
+ if (((_a = ctx.state) == null ? void 0 : _a.authTokenSource) !== "cookie" || !((_b = ctx.state) == null ? void 0 : _b.currentUser)) {
246
+ return next();
247
+ }
248
+ const cookieToken = ctx.cookies.get((0, import_utils.getAuthCookieName)("csrfToken", ctx.app.name));
249
+ const headerToken = ctx.get("X-CSRF-Token");
250
+ if (!cookieToken || cookieToken.length < 16 || headerToken !== cookieToken) {
251
+ return ctx.throw(403, ctx.t("Invalid CSRF token", { ns: "auth" }));
252
+ }
253
+ await next();
254
+ }
255
+ __name(csrfMiddleware, "csrfMiddleware");
195
256
  // Annotate the CommonJS export names for ESM import in node:
196
257
  0 && (module.exports = {
197
- AuthManager
258
+ AuthManager,
259
+ csrfMiddleware
198
260
  });
package/lib/auth.d.ts CHANGED
@@ -41,6 +41,7 @@ interface IAuth {
41
41
  signIn(): Promise<any>;
42
42
  signUp(): Promise<any>;
43
43
  signOut(): Promise<any>;
44
+ syncCookies(): Promise<unknown>;
44
45
  }
45
46
  export declare abstract class Auth implements IAuth {
46
47
  /**
@@ -54,7 +55,7 @@ export declare abstract class Auth implements IAuth {
54
55
  };
55
56
  protected ctx: Context;
56
57
  constructor(config: AuthConfig);
57
- skipCheck(): Promise<any>;
58
+ skipCheck(): Promise<boolean>;
58
59
  abstract check(): Promise<Model>;
59
60
  abstract checkToken(): Promise<{
60
61
  tokenStatus: 'valid' | 'expired' | 'invalid';
@@ -67,5 +68,6 @@ export declare abstract class Auth implements IAuth {
67
68
  signIn(): Promise<any>;
68
69
  signUp(): Promise<any>;
69
70
  signOut(): Promise<any>;
71
+ syncCookies(): Promise<unknown>;
70
72
  }
71
73
  export {};
package/lib/auth.js CHANGED
@@ -64,17 +64,28 @@ const _Auth = class _Auth {
64
64
  this.ctx = ctx;
65
65
  }
66
66
  async skipCheck() {
67
+ var _a, _b;
67
68
  if (this.ctx.skipAuthCheck === true) {
68
69
  return true;
69
70
  }
71
+ const { resourceName, actionName } = this.ctx.action;
72
+ const acl = this.ctx.dataSource.acl;
73
+ const isPublic = await acl.allowManager.isPublic(resourceName, actionName, this.ctx);
74
+ if (isPublic) {
75
+ return true;
76
+ }
77
+ const hasExplicitToken = Boolean(this.ctx.get("Authorization") || this.ctx.query.token);
78
+ if (!hasExplicitToken && this.ctx.app.options.acl === false && ((_a = this.ctx.state) == null ? void 0 : _a.optionalAuth) !== true) {
79
+ return true;
80
+ }
70
81
  const token = this.ctx.getBearerToken();
82
+ if (!token && ((_b = this.ctx.state) == null ? void 0 : _b.optionalAuth) === true) {
83
+ return true;
84
+ }
71
85
  if (!token && this.ctx.app.options.acl === false) {
72
86
  return true;
73
87
  }
74
- const { resourceName, actionName } = this.ctx.action;
75
- const acl = this.ctx.dataSource.acl;
76
- const isPublic = await acl.allowManager.isPublic(resourceName, actionName, this.ctx);
77
- return isPublic;
88
+ return false;
78
89
  }
79
90
  // The following methods are mainly designed for user authentications.
80
91
  async signIn() {
@@ -83,6 +94,9 @@ const _Auth = class _Auth {
83
94
  }
84
95
  async signOut() {
85
96
  }
97
+ async syncCookies() {
98
+ return void 0;
99
+ }
86
100
  };
87
101
  __name(_Auth, "Auth");
88
102
  /**
@@ -25,6 +25,11 @@ export declare class BaseAuth extends Auth {
25
25
  */
26
26
  get jwt(): JwtService;
27
27
  get tokenController(): ITokenControlService;
28
+ protected get appName(): any;
29
+ protected getSessionCookieMaxAge(): Promise<number>;
30
+ protected setAuthCookies(token: string, maxAge?: number): void;
31
+ protected setSessionCookies(user: Model, maxAge?: number): Promise<void>;
32
+ protected clearAuthCookies(): void;
28
33
  set user(user: Model);
29
34
  get user(): Model;
30
35
  /**
@@ -50,5 +55,8 @@ export declare class BaseAuth extends Auth {
50
55
  user: Model<any, any>;
51
56
  token: string;
52
57
  }>;
58
+ syncCookies(): Promise<{
59
+ synced: true;
60
+ }>;
53
61
  signOut(): Promise<any>;
54
62
  }
package/lib/base/auth.js CHANGED
@@ -40,6 +40,8 @@ __export(auth_exports, {
40
40
  BaseAuth: () => BaseAuth
41
41
  });
42
42
  module.exports = __toCommonJS(auth_exports);
43
+ var import_utils = require("@nocobase/utils");
44
+ var import_crypto = __toESM(require("crypto"));
43
45
  var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
44
46
  var import_auth = require("../auth");
45
47
  const localeNamespace = "auth";
@@ -62,6 +64,100 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
62
64
  get tokenController() {
63
65
  return this.ctx.app.authManager.tokenController;
64
66
  }
67
+ get appName() {
68
+ var _a;
69
+ return ((_a = this.ctx.app) == null ? void 0 : _a.name) || "main";
70
+ }
71
+ async getSessionCookieMaxAge() {
72
+ var _a, _b, _c;
73
+ try {
74
+ return (_a = await this.tokenController.getConfig()) == null ? void 0 : _a.sessionExpirationTime;
75
+ } catch (error) {
76
+ (_c = (_b = this.ctx.logger) == null ? void 0 : _b.warn) == null ? void 0 : _c.call(_b, error, { method: "auth.getSessionCookieMaxAge" });
77
+ return void 0;
78
+ }
79
+ }
80
+ setAuthCookies(token, maxAge) {
81
+ var _a;
82
+ if (!this.ctx.cookies) {
83
+ return;
84
+ }
85
+ const options = (0, import_utils.getAuthCookieOptions)(this.ctx, true, maxAge);
86
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("authToken", this.appName), token, options);
87
+ if ((_a = this.authenticator) == null ? void 0 : _a.name) {
88
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("authenticator", this.appName), this.authenticator.name, options);
89
+ }
90
+ }
91
+ async setSessionCookies(user, maxAge) {
92
+ var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q;
93
+ if (!this.ctx.cookies) {
94
+ return;
95
+ }
96
+ this.ctx.cookies.set(
97
+ (0, import_utils.getAuthCookieName)("csrfToken", this.appName),
98
+ import_crypto.default.randomBytes(24).toString("base64url"),
99
+ (0, import_utils.getAuthCookieOptions)(this.ctx, false, maxAge)
100
+ );
101
+ const currentRole = (_a = this.ctx.state) == null ? void 0 : _a.currentRole;
102
+ let roleName = typeof currentRole === "string" ? currentRole : (_c = (_b = this.ctx).get) == null ? void 0 : _c.call(_b, "X-Role");
103
+ const userId = typeof user.get === "function" ? user.get("id") : user.id;
104
+ if (!roleName) {
105
+ try {
106
+ const rolesUser = await ((_f = (_e = (_d = this.ctx.db) == null ? void 0 : _d.getRepository) == null ? void 0 : _e.call(_d, "rolesUsers")) == null ? void 0 : _f.findOne({
107
+ where: {
108
+ userId,
109
+ default: true
110
+ },
111
+ raw: true
112
+ }));
113
+ roleName = rolesUser == null ? void 0 : rolesUser.roleName;
114
+ } catch {
115
+ roleName = void 0;
116
+ }
117
+ }
118
+ if (!roleName) {
119
+ try {
120
+ const roles = await ((_i = (_h = (_g = this.ctx.db) == null ? void 0 : _g.getRepository) == null ? void 0 : _h.call(_g, "users.roles", userId)) == null ? void 0 : _i.find({
121
+ raw: true
122
+ }));
123
+ roleName = (_j = roles == null ? void 0 : roles[0]) == null ? void 0 : _j.name;
124
+ } catch {
125
+ roleName = void 0;
126
+ }
127
+ }
128
+ if (!roleName) {
129
+ try {
130
+ if (typeof user.getRoles === "function") {
131
+ roleName = (_l = (_k = await user.getRoles({ raw: true })) == null ? void 0 : _k[0]) == null ? void 0 : _l.name;
132
+ } else if (userId) {
133
+ const userModel = await this.userRepository.findOne({
134
+ filter: { id: userId }
135
+ });
136
+ roleName = (_o = (_n = await ((_m = userModel == null ? void 0 : userModel.getRoles) == null ? void 0 : _m.call(userModel, { raw: true }))) == null ? void 0 : _n[0]) == null ? void 0 : _o.name;
137
+ }
138
+ } catch (error) {
139
+ (_q = (_p = this.ctx.logger) == null ? void 0 : _p.warn) == null ? void 0 : _q.call(_p, error, { method: "auth.setSessionCookies" });
140
+ roleName = void 0;
141
+ }
142
+ }
143
+ if (roleName) {
144
+ this.ctx.cookies.set(
145
+ (0, import_utils.getAuthCookieName)("role", this.appName),
146
+ roleName,
147
+ (0, import_utils.getAuthCookieOptions)(this.ctx, false, maxAge)
148
+ );
149
+ }
150
+ }
151
+ clearAuthCookies() {
152
+ if (!this.ctx.cookies) {
153
+ return;
154
+ }
155
+ const options = (0, import_utils.getAuthCookieOptions)(this.ctx);
156
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("authToken", this.appName), null, options);
157
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("authenticator", this.appName), null, options);
158
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("role", this.appName), null, (0, import_utils.getAuthCookieOptions)(this.ctx, false));
159
+ this.ctx.cookies.set((0, import_utils.getAuthCookieName)("csrfToken", this.appName), null, (0, import_utils.getAuthCookieOptions)(this.ctx, false));
160
+ }
65
161
  set user(user) {
66
162
  this.ctx.state.currentUser = user;
67
163
  }
@@ -83,7 +179,9 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
83
179
  async checkToken() {
84
180
  var _a, _b, _c;
85
181
  const cache = this.ctx.cache;
182
+ this.ctx.state = this.ctx.state || {};
86
183
  const token = this.ctx.getBearerToken();
184
+ const tokenSource = this.ctx.state.pendingAuthTokenSource;
87
185
  if (!token) {
88
186
  this.ctx.throw(401, {
89
187
  message: this.ctx.t("Unauthenticated. Please sign in to continue.", { ns: localeNamespace }),
@@ -136,6 +234,7 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
136
234
  }
137
235
  if (!temp) {
138
236
  if (tokenStatus === "valid") {
237
+ this.ctx.state.authTokenSource = tokenSource;
139
238
  return { tokenStatus, user, temp };
140
239
  } else {
141
240
  this.ctx.throw(401, {
@@ -185,8 +284,10 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
185
284
  code: import_auth.AuthErrorCode.INVALID_TOKEN
186
285
  });
187
286
  }
287
+ this.ctx.state.authTokenSource = tokenSource;
188
288
  return { tokenStatus, user, jti, signInTime, temp };
189
289
  }
290
+ this.ctx.state.authTokenSource = tokenSource;
190
291
  return { tokenStatus, user, jti, signInTime, temp };
191
292
  }
192
293
  async check() {
@@ -224,6 +325,7 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
224
325
  { jwtid: renewedResult.jti, expiresIn }
225
326
  );
226
327
  this.ctx.res.setHeader("x-new-token", newToken);
328
+ this.setAuthCookies(newToken, tokenPolicy.sessionExpirationTime);
227
329
  } catch (err) {
228
330
  this.ctx.logger.error("token renew failed", {
229
331
  method: "auth.check",
@@ -275,12 +377,31 @@ const _BaseAuth = class _BaseAuth extends import_auth.Auth {
275
377
  });
276
378
  }
277
379
  const token = await this.signNewToken(user.id);
380
+ const maxAge = await this.getSessionCookieMaxAge();
381
+ this.setAuthCookies(token, maxAge);
382
+ await this.setSessionCookies(user, maxAge);
278
383
  return {
279
384
  user,
280
385
  token
281
386
  };
282
387
  }
388
+ async syncCookies() {
389
+ const tokenHeader = this.ctx.res.getHeader("x-new-token");
390
+ const token = typeof tokenHeader === "string" ? tokenHeader : this.ctx.getBearerToken();
391
+ const user = this.user || this.ctx.state.currentUser;
392
+ if (!token || !user) {
393
+ this.ctx.throw(401, {
394
+ message: this.ctx.t("Unauthenticated. Please sign in to continue.", { ns: localeNamespace }),
395
+ code: import_auth.AuthErrorCode.EMPTY_TOKEN
396
+ });
397
+ }
398
+ const maxAge = await this.getSessionCookieMaxAge();
399
+ this.setAuthCookies(token, maxAge);
400
+ await this.setSessionCookies(user, maxAge);
401
+ return { synced: true };
402
+ }
283
403
  async signOut() {
404
+ this.clearAuthCookies();
284
405
  const token = this.ctx.getBearerToken();
285
406
  if (!token) {
286
407
  return;
package/package.json CHANGED
@@ -1,16 +1,16 @@
1
1
  {
2
2
  "name": "@nocobase/auth",
3
- "version": "2.2.0-alpha.7",
3
+ "version": "2.2.0-alpha.9",
4
4
  "description": "",
5
5
  "license": "Apache-2.0",
6
6
  "main": "./lib/index.js",
7
7
  "types": "./lib/index.d.ts",
8
8
  "dependencies": {
9
- "@nocobase/actions": "2.2.0-alpha.7",
10
- "@nocobase/cache": "2.2.0-alpha.7",
11
- "@nocobase/database": "2.2.0-alpha.7",
12
- "@nocobase/resourcer": "2.2.0-alpha.7",
13
- "@nocobase/utils": "2.2.0-alpha.7",
9
+ "@nocobase/actions": "2.2.0-alpha.9",
10
+ "@nocobase/cache": "2.2.0-alpha.9",
11
+ "@nocobase/database": "2.2.0-alpha.9",
12
+ "@nocobase/resourcer": "2.2.0-alpha.9",
13
+ "@nocobase/utils": "2.2.0-alpha.9",
14
14
  "@types/jsonwebtoken": "^9.0.9",
15
15
  "jsonwebtoken": "^9.0.2"
16
16
  },
@@ -19,5 +19,5 @@
19
19
  "url": "git+https://github.com/nocobase/nocobase.git",
20
20
  "directory": "packages/auth"
21
21
  },
22
- "gitHead": "34ba02960fca9ab0b6881d3db7040a8a64067bba"
22
+ "gitHead": "4949bf5ebcd92d77a3a0a718ed578c270e4bd570"
23
23
  }