@webtypen/webframez-core 0.3.73 → 0.3.74

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.
@@ -0,0 +1,19 @@
1
+ import type { Request } from "../Router/Request";
2
+ import type { Response } from "../Router/Response";
3
+ import { AuthScope, AuthScopeOptions, AuthOperation, AuthRouteOptions } from "./ModelAuth";
4
+ export type AuthConfig = {
5
+ /** Disable automatic main creation; explicitly configured scopes remain available. */
6
+ main?: boolean;
7
+ defaults?: Partial<AuthScopeOptions>;
8
+ scopes?: Record<string, AuthScopeOptions>;
9
+ };
10
+ /** Application-wide registry of named browser-authentication scopes. */
11
+ export declare class AuthFacade {
12
+ private readonly scopes;
13
+ private initialized;
14
+ init(): void;
15
+ scope(key?: string): AuthScope;
16
+ registerScope(key: string, config: Partial<AuthScopeOptions>): AuthScope;
17
+ }
18
+ export declare const Auth: AuthFacade;
19
+ export declare function authRoute(operation: AuthOperation, options: AuthRouteOptions): (req: Request, res: Response) => Promise<Response>;
@@ -0,0 +1,53 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.authRoute = exports.Auth = exports.AuthFacade = void 0;
4
+ const Config_1 = require("../Config");
5
+ const Model_1 = require("../Database/Model");
6
+ const ModelAuth_1 = require("./ModelAuth");
7
+ function mergeOptions(base, config) {
8
+ return Object.assign(Object.assign(Object.assign({ model: Model_1.Model }, base), config), { fields: Object.assign(Object.assign({}, base.fields), config.fields), session: Object.assign(Object.assign({}, base.session), config.session), messages: Object.assign(Object.assign({}, base.messages), config.messages) });
9
+ }
10
+ /** Application-wide registry of named browser-authentication scopes. */
11
+ class AuthFacade {
12
+ constructor() {
13
+ this.scopes = new Map();
14
+ this.initialized = false;
15
+ }
16
+ init() {
17
+ if (this.initialized)
18
+ return;
19
+ const config = Config_1.Config.get("auth") || {};
20
+ const configured = new Map();
21
+ for (const [key, options] of Object.entries(config.scopes || {})) {
22
+ configured.set(key, new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(config.defaults || {}, options)), { key })));
23
+ }
24
+ if (config.main !== false && !configured.has("main")) {
25
+ configured.set("main", new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(config.defaults || {}, {})), { key: "main" })));
26
+ }
27
+ for (const [key, scope] of configured)
28
+ this.scopes.set(key, scope);
29
+ this.initialized = true;
30
+ }
31
+ scope(key = "main") {
32
+ this.init();
33
+ const scope = this.scopes.get(key);
34
+ if (!scope)
35
+ throw new Error(`Auth scope "${key}" is not registered.`);
36
+ return scope;
37
+ }
38
+ registerScope(key, config) {
39
+ var _a;
40
+ this.init();
41
+ const defaults = Config_1.Config.get("auth.defaults") || {};
42
+ const base = ((_a = this.scopes.get(key)) === null || _a === void 0 ? void 0 : _a.configuration) || defaults;
43
+ const scope = new ModelAuth_1.AuthScope(Object.assign(Object.assign({}, mergeOptions(base, config)), { key }));
44
+ this.scopes.set(key, scope);
45
+ return scope;
46
+ }
47
+ }
48
+ exports.AuthFacade = AuthFacade;
49
+ exports.Auth = new AuthFacade();
50
+ function authRoute(operation, options) {
51
+ return (req, res) => exports.Auth.scope(options.auth || "main").handle(operation, req, res, options.loginResponse);
52
+ }
53
+ exports.authRoute = authRoute;
@@ -11,13 +11,14 @@ export type AuthSnapshot<TUser = Record<string, unknown>> = {
11
11
  user: TUser;
12
12
  session: AuthSession;
13
13
  };
14
- export type AuthErrorCode = "missing_login" | "invalid_login" | "inactive_user" | "invalid_origin" | "invalid_csrf" | "unauthorized" | "server_error";
14
+ export type AuthErrorCode = "missing_login" | "invalid_login" | "inactive_user" | "invalid_origin" | "invalid_csrf" | "unauthorized" | "server_error" | "login_blocked" | "logout_blocked";
15
15
  export declare class AuthLoginError extends WebAuthError {
16
16
  readonly code: AuthErrorCode;
17
17
  constructor(code: AuthErrorCode, status: 401 | 403, message: string);
18
18
  }
19
19
  export type ModelAuthOptions = Partial<WebAuthOptions> & {
20
20
  model: typeof Model;
21
+ key?: string;
21
22
  /** Takes precedence over session.audience; defaults to the configured origin. */
22
23
  audience?: string;
23
24
  locale?: "en" | "de";
@@ -33,31 +34,39 @@ export type ModelAuthOptions = Partial<WebAuthOptions> & {
33
34
  csrfDisabled?: false;
34
35
  verifyPassword?: (password: string, storedHash: string) => boolean | Promise<boolean>;
35
36
  isUserAllowed?: (user: Model) => boolean | Promise<boolean>;
37
+ beforeLogin?: AuthActionHook;
38
+ afterLogin?: AuthActionHook;
39
+ beforeLogout?: AuthActionHook;
40
+ afterLogout?: AuthActionHook;
36
41
  onLogin?: (session: AuthSession, req: Request) => void | Promise<void>;
37
42
  publicUserFields?: readonly string[];
38
43
  onAuthenticate?: (auth: AuthContext, req: Request) => void | Promise<void>;
39
44
  messages?: Partial<Record<AuthErrorCode, string>>;
40
45
  };
46
+ export type AuthActionHook = (req: Request, user: Model, scope: AuthScope) => void | Promise<void>;
41
47
  export type AuthOperation = "csrf" | "login" | "logout" | "refresh";
42
48
  export type AuthRouteOptions = Partial<ModelAuthOptions> & {
43
- auth?: ModelAuth | (() => ModelAuth);
49
+ auth?: string;
44
50
  loginPath?: string;
45
51
  loginResponse?: (req: Request, session: AuthSession) => object | Promise<object>;
46
52
  middleware?: string[];
47
53
  domains?: string[];
48
54
  };
49
55
  /** Model-backed browser authentication with secure default routes and revocable sessions. */
50
- export declare class ModelAuth extends WebAuth {
56
+ export declare class AuthScope extends WebAuth {
51
57
  private readonly modelOptions;
58
+ readonly key: string;
52
59
  private readonly model;
53
60
  private readonly sessionUsers;
54
61
  private readonly fields;
55
62
  constructor(modelOptions: ModelAuthOptions);
63
+ get configuration(): ModelAuthOptions;
56
64
  private message;
57
65
  /** Use this when creating users; existing bcrypt hashes are supported without migration. */
58
66
  static hashPassword(password: string): Promise<string>;
59
67
  verifyCredentials(req: Request): Promise<string>;
60
68
  login(req: Request, res: Response, verifyCredentials?: () => Promise<string | null>): Promise<AuthSession>;
69
+ logout(req: Request, res: Response): Promise<void>;
61
70
  /** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
62
71
  resolve<TUser extends Model = Model>(req: Request): Promise<AuthContext<TUser> | null>;
63
72
  resolveCookies<TUser extends Model = Model>(cookies: Record<string, string | undefined>): Promise<AuthContext<TUser> | null>;
@@ -69,4 +78,6 @@ export declare class ModelAuth extends WebAuth {
69
78
  }): (next: Function, reject: Function, req: Request, res: Response) => Promise<any>;
70
79
  handle(operation: AuthOperation, req: Request, res: Response, loginResponse?: AuthRouteOptions["loginResponse"]): Promise<Response>;
71
80
  }
72
- export declare function authRoute(operation: AuthOperation, options: AuthRouteOptions): (req: Request, res: Response) => Promise<Response>;
81
+ /** Compatibility name for existing integrations. Prefer Auth.scope(). */
82
+ export { AuthScope as ModelAuth };
83
+ export type AuthScopeOptions = ModelAuthOptions;
@@ -9,7 +9,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.authRoute = exports.ModelAuth = exports.AuthLoginError = void 0;
12
+ exports.ModelAuth = exports.AuthScope = exports.AuthLoginError = void 0;
13
13
  const bcryptjs_1 = require("bcryptjs");
14
14
  const Config_1 = require("../Config");
15
15
  const Request_1 = require("../Router/Request");
@@ -23,6 +23,11 @@ class AuthLoginError extends WebAuth_1.WebAuthError {
23
23
  }
24
24
  }
25
25
  exports.AuthLoginError = AuthLoginError;
26
+ class AuthHookError extends AuthLoginError {
27
+ constructor(code, error) {
28
+ super(code, error instanceof WebAuth_1.WebAuthError ? error.status : 403, error instanceof Error ? error.message : typeof error === "string" ? error : "Authentication action was blocked.");
29
+ }
30
+ }
26
31
  const defaultMessages = {
27
32
  en: {
28
33
  missing_login: "Please provide your login and password.",
@@ -32,6 +37,8 @@ const defaultMessages = {
32
37
  invalid_csrf: "Please reload the page and try logging in again.",
33
38
  unauthorized: "Please log in.",
34
39
  server_error: "Authentication could not be processed.",
40
+ login_blocked: "Login was blocked.",
41
+ logout_blocked: "Logout was blocked.",
35
42
  },
36
43
  de: {
37
44
  missing_login: "Bitte gib E-Mail-Adresse und Passwort an.",
@@ -41,19 +48,24 @@ const defaultMessages = {
41
48
  invalid_csrf: "Der Login konnte gerade nicht verarbeitet werden. Bitte lade die Seite neu.",
42
49
  unauthorized: "Bitte melde dich an.",
43
50
  server_error: "Die Anmeldung konnte gerade nicht verarbeitet werden.",
51
+ login_blocked: "Die Anmeldung wurde abgelehnt.",
52
+ logout_blocked: "Die Abmeldung wurde abgelehnt.",
44
53
  },
45
54
  };
46
55
  // Verify a dummy password hash for unknown accounts as well.
47
56
  let dummyHash;
48
57
  /** Model-backed browser authentication with secure default routes and revocable sessions. */
49
- class ModelAuth extends WebAuth_1.WebAuth {
58
+ class AuthScope extends WebAuth_1.WebAuth {
50
59
  constructor(modelOptions) {
51
- var _a, _b, _c, _d, _e;
60
+ var _a, _b, _c, _d, _e, _f, _g;
52
61
  if (modelOptions.csrfDisabled)
53
62
  throw new Error("Browser auth routes require CSRF protection.");
54
63
  if (!modelOptions.model)
55
64
  throw new Error("Route.auth requires a Model class.");
56
65
  const origin = new URL(modelOptions.origin || Config_1.Config.get("auth.origin") || Config_1.Config.get("website.baseUrl") || Config_1.Config.get("application.website.baseUrl") || process.env.WEBSITE_URL || process.env.PUBLIC_BASE_URL || `http://localhost:${process.env.PORT || 3035}`).origin;
66
+ const key = (_a = modelOptions.key) !== null && _a !== void 0 ? _a : "main";
67
+ if (!/^[A-Za-z0-9_-]+$/.test(key))
68
+ throw new Error("Invalid auth scope key.");
57
69
  const model = modelOptions.model;
58
70
  const fields = Object.assign({ identifier: "email", password: "password", active: "is_active", primaryKey: new model().__primaryKey || "_id" }, modelOptions.fields);
59
71
  const allowed = (user) => __awaiter(this, void 0, void 0, function* () {
@@ -61,7 +73,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
61
73
  && (!modelOptions.isUserAllowed || (yield modelOptions.isUserAllowed(user))));
62
74
  });
63
75
  const sessionUsers = new WeakMap();
64
- const sessions = new SessionAuth_1.SessionAuth(Object.assign(Object.assign({ accessTokenSeconds: 30 * 86400, sessionSeconds: 30 * 86400 }, modelOptions.session), { issuer: origin, audience: (_c = (_a = modelOptions.audience) !== null && _a !== void 0 ? _a : (_b = modelOptions.session) === null || _b === void 0 ? void 0 : _b.audience) !== null && _c !== void 0 ? _c : origin, connection: ((_d = modelOptions.session) === null || _d === void 0 ? void 0 : _d.connection) || new model().__connection, isSessionAllowed: (session) => __awaiter(this, void 0, void 0, function* () {
76
+ const sessions = new SessionAuth_1.SessionAuth(Object.assign(Object.assign({ accessTokenSeconds: 30 * 86400, sessionSeconds: 30 * 86400 }, modelOptions.session), { issuer: origin, scope: key, audience: (_d = (_b = modelOptions.audience) !== null && _b !== void 0 ? _b : (_c = modelOptions.session) === null || _c === void 0 ? void 0 : _c.audience) !== null && _d !== void 0 ? _d : origin, connection: ((_e = modelOptions.session) === null || _e === void 0 ? void 0 : _e.connection) || new model().__connection, isSessionAllowed: (session) => __awaiter(this, void 0, void 0, function* () {
65
77
  const id = fields.primaryKey === "_id" ? yield model.objectId(session.subject, { noExceptions: true }) : session.subject;
66
78
  const user = id !== null && id !== undefined ? yield model.where(fields.primaryKey, "=", id).first() : null;
67
79
  if (!(yield allowed(user)))
@@ -69,12 +81,16 @@ class ModelAuth extends WebAuth_1.WebAuth {
69
81
  sessionUsers.set(session, user);
70
82
  return true;
71
83
  }) }));
72
- super(sessions, Object.assign(Object.assign({}, modelOptions), { origin, allowInsecureLocalhost: (_e = modelOptions.allowInsecureLocalhost) !== null && _e !== void 0 ? _e : true }));
84
+ super(sessions, Object.assign(Object.assign({}, modelOptions), { origin, cookiePrefix: (_f = modelOptions.cookiePrefix) !== null && _f !== void 0 ? _f : (key === "main" ? undefined : `${origin.startsWith("https:") ? "__Host-wf_" : "wf_dev_"}${key}_`), allowInsecureLocalhost: (_g = modelOptions.allowInsecureLocalhost) !== null && _g !== void 0 ? _g : true }));
73
85
  this.modelOptions = modelOptions;
86
+ this.key = key;
74
87
  this.sessionUsers = sessionUsers;
75
88
  this.model = model;
76
89
  this.fields = fields;
77
90
  }
91
+ get configuration() {
92
+ return this.modelOptions;
93
+ }
78
94
  message(code) {
79
95
  var _a, _b;
80
96
  return (_b = (_a = this.modelOptions.messages) === null || _a === void 0 ? void 0 : _a[code]) !== null && _b !== void 0 ? _b : defaultMessages[this.modelOptions.locale || "en"][code];
@@ -112,14 +128,53 @@ class ModelAuth extends WebAuth_1.WebAuth {
112
128
  login: { get: () => super.login }
113
129
  });
114
130
  return __awaiter(this, void 0, void 0, function* () {
115
- const session = yield _super.login.call(this, req, res, verifyCredentials);
131
+ let user = null;
132
+ const session = yield _super.login.call(this, req, res, () => __awaiter(this, void 0, void 0, function* () {
133
+ const subject = yield verifyCredentials();
134
+ if (subject && (this.modelOptions.beforeLogin || this.modelOptions.afterLogin)) {
135
+ const id = this.fields.primaryKey === "_id" ? yield this.model.objectId(subject, { noExceptions: true }) : subject;
136
+ user = id == null ? null : yield this.model.where(this.fields.primaryKey, "=", id).first();
137
+ if (!user)
138
+ throw new AuthLoginError("invalid_login", 401, this.message("invalid_login"));
139
+ if (this.modelOptions.beforeLogin) {
140
+ try {
141
+ yield this.modelOptions.beforeLogin(req, user, this);
142
+ }
143
+ catch (error) {
144
+ throw new AuthHookError("login_blocked", error);
145
+ }
146
+ }
147
+ }
148
+ return subject;
149
+ }));
116
150
  if (this.modelOptions.trackActivity !== false)
117
151
  yield this.sessions.recordLogin(session, (0, AuthSecurity_1.authHeader)(req, "user-agent"));
118
152
  if (this.modelOptions.onLogin)
119
153
  yield this.modelOptions.onLogin(session, req);
154
+ if (this.modelOptions.afterLogin)
155
+ yield this.modelOptions.afterLogin(req, user, this);
120
156
  return session;
121
157
  });
122
158
  }
159
+ logout(req, res) {
160
+ return __awaiter(this, void 0, void 0, function* () {
161
+ const session = yield this.logoutSession(req);
162
+ const user = this.sessionUsers.get(session);
163
+ if (!user)
164
+ throw new AuthLoginError("unauthorized", 401, this.message("unauthorized"));
165
+ if (this.modelOptions.beforeLogout) {
166
+ try {
167
+ yield this.modelOptions.beforeLogout(req, user, this);
168
+ }
169
+ catch (error) {
170
+ throw new AuthHookError("logout_blocked", error);
171
+ }
172
+ }
173
+ yield this.finishLogout(session, res);
174
+ if (this.modelOptions.afterLogout)
175
+ yield this.modelOptions.afterLogout(req, user, this);
176
+ });
177
+ }
123
178
  /** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
124
179
  resolve(req) {
125
180
  const _super = Object.create(null, {
@@ -188,6 +243,7 @@ class ModelAuth extends WebAuth_1.WebAuth {
188
243
  });
189
244
  }
190
245
  handle(operation, req, res, loginResponse) {
246
+ var _a, _b;
191
247
  return __awaiter(this, void 0, void 0, function* () {
192
248
  res.header("Cache-Control", "no-store");
193
249
  try {
@@ -210,17 +266,10 @@ class ModelAuth extends WebAuth_1.WebAuth {
210
266
  if (!(error instanceof WebAuth_1.WebAuthError))
211
267
  console.error(error);
212
268
  return res.status(error instanceof WebAuth_1.WebAuthError ? error.status : 500).send({ status: "error", code,
213
- message: this.message(code) });
269
+ message: error instanceof AuthHookError ? (_b = (_a = this.modelOptions.messages) === null || _a === void 0 ? void 0 : _a[code]) !== null && _b !== void 0 ? _b : error.message : this.message(code) });
214
270
  }
215
271
  });
216
272
  }
217
273
  }
218
- exports.ModelAuth = ModelAuth;
219
- function authRoute(operation, options) {
220
- let auth;
221
- return (req, res) => {
222
- auth || (auth = typeof options.auth === "function" ? options.auth() : options.auth || new ModelAuth(options));
223
- return auth.handle(operation, req, res, options.loginResponse);
224
- };
225
- }
226
- exports.authRoute = authRoute;
274
+ exports.AuthScope = AuthScope;
275
+ exports.ModelAuth = AuthScope;
@@ -1,9 +1,10 @@
1
- import type { DocumentDatabase } from "../Database/DatabaseAdapter";
1
+ import type { DatabaseIdAdapter, DocumentDatabase } from "../Database/DatabaseAdapter";
2
2
  export type AuthSession = {
3
3
  id: string;
4
4
  subject: string;
5
5
  issuer: string;
6
6
  audience: string;
7
+ scope: string;
7
8
  environment: string | null;
8
9
  userAgent?: string;
9
10
  lastActiveAt?: number;
@@ -27,6 +28,7 @@ export type CreatedAuthSession = SessionTokenPair & {
27
28
  export type SessionAuthOptions = {
28
29
  issuer: string;
29
30
  audience: string;
31
+ scope?: string;
30
32
  environment?: string;
31
33
  connection?: string;
32
34
  collection?: string;
@@ -37,6 +39,8 @@ export type SessionAuthOptions = {
37
39
  isSessionAllowed: (session: AuthSession) => boolean | Promise<boolean>;
38
40
  /** Dependency injection for a document-capable driver. Atomic findOneAndUpdate is required. */
39
41
  database?: () => Promise<DocumentDatabase>;
42
+ /** ID handling for an injected document store; otherwise the selected driver owns IDs. */
43
+ idAdapter?: DatabaseIdAdapter;
40
44
  };
41
45
  /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
42
46
  export declare class SessionAuth {
@@ -45,6 +49,8 @@ export declare class SessionAuth {
45
49
  private readonly accessSeconds;
46
50
  private readonly sessionSeconds;
47
51
  constructor(options: SessionAuthOptions);
52
+ private ids;
53
+ private sessionId;
48
54
  private rows;
49
55
  private allowed;
50
56
  private pair;
@@ -13,22 +13,32 @@ exports.SessionAuth = void 0;
13
13
  const DBConnection_1 = require("../Database/DBConnection");
14
14
  const AuthSecurity_1 = require("./AuthSecurity");
15
15
  function publicSession(record) {
16
- return Object.assign(Object.assign({ id: record.id, subject: record.subject, issuer: record.issuer, audience: record.audience, environment: record.environment, createdAt: record.createdAt, expiresAt: record.expiresAt, parent: record.parent }, (record.userAgent !== undefined ? { userAgent: record.userAgent } : {})), (record.lastActiveAt !== undefined ? { lastActiveAt: record.lastActiveAt } : {}));
16
+ return Object.assign(Object.assign({ id: String(record._id), subject: String(record._subject), issuer: record.issuer, audience: record.audience, scope: record.scope, environment: record.environment, createdAt: record.createdAt, expiresAt: record.expiresAt, parent: record.parent }, (record.userAgent !== undefined ? { userAgent: record.userAgent } : {})), (record.lastActiveAt !== undefined ? { lastActiveAt: record.lastActiveAt } : {}));
17
17
  }
18
18
  function tokenSessionId(token) {
19
- return typeof token === "string" && /^[A-Za-z0-9_-]{43}\.[A-Za-z0-9_-]{43}$/.test(token) ? token.split(".")[0] : null;
19
+ return typeof token === "string" && /^[A-Za-z0-9_-]{1,128}\.[A-Za-z0-9_-]{43}$/.test(token) ? token.split(".")[0] : null;
20
20
  }
21
21
  /** Revocable, hashed, independently addressable device sessions. Legacy UserAuth is unaffected. */
22
22
  class SessionAuth {
23
23
  constructor(options) {
24
+ var _a;
24
25
  this.options = options;
25
- this.scope = { issuer: (0, AuthSecurity_1.authText)(options.issuer, "issuer"), audience: (0, AuthSecurity_1.authText)(options.audience, "audience"),
26
+ this.scope = { issuer: (0, AuthSecurity_1.authText)(options.issuer, "issuer"), audience: (0, AuthSecurity_1.authText)(options.audience, "audience"), scope: (0, AuthSecurity_1.authText)((_a = options.scope) !== null && _a !== void 0 ? _a : "main", "scope"),
26
27
  environment: options.environment === undefined ? null : (0, AuthSecurity_1.authText)(options.environment, "environment") };
27
28
  this.accessSeconds = (0, AuthSecurity_1.authLifetime)(options.accessTokenSeconds, 15 * 60);
28
29
  this.sessionSeconds = (0, AuthSecurity_1.authLifetime)(options.sessionSeconds, 30 * 86400);
29
30
  if (typeof options.isSessionAllowed !== "function")
30
31
  throw new Error("SessionAuth requires isSessionAllowed.");
31
32
  }
33
+ ids() {
34
+ return this.options.idAdapter || DBConnection_1.DBConnection.getIdAdapter(this.options.connection);
35
+ }
36
+ sessionId(value) {
37
+ const adapter = this.ids();
38
+ const native = adapter.normalize(value);
39
+ // Converted legacy MongoDB sessions keep working with their existing opaque token prefix.
40
+ return native !== null && native !== void 0 ? native : (/^[A-Za-z0-9_-]{43}$/.test(value) ? adapter.normalize((0, AuthSecurity_1.hashAuthToken)(value).slice(0, 24)) : null);
41
+ }
32
42
  rows() {
33
43
  return __awaiter(this, void 0, void 0, function* () {
34
44
  const db = yield (this.options.database ? this.options.database() : DBConnection_1.DBConnection.getDocumentStore(this.options.connection));
@@ -54,9 +64,9 @@ class SessionAuth {
54
64
  (0, AuthSecurity_1.authText)(parent.issuer, "parent issuer");
55
65
  (0, AuthSecurity_1.authText)(parent.sessionId, "parent session");
56
66
  }
57
- const now = Date.now(), id = (0, AuthSecurity_1.randomAuthToken)();
67
+ const now = Date.now(), id = this.ids().create();
58
68
  const access = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, refresh = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, csrf = (0, AuthSecurity_1.randomAuthToken)();
59
- const record = Object.assign(Object.assign({ _id: id, id, subject }, this.scope), { parent: parent ? Object.assign({}, parent) : null, createdAt: now, expiresAt: now + this.sessionSeconds * 1000, accessExpiresAt: now + Math.min(this.accessSeconds, this.sessionSeconds) * 1000, accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh), csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf), usedRefreshHashes: [], revokedAt: null });
69
+ const record = Object.assign(Object.assign({ _id: id, _subject: this.ids().create(subject) }, this.scope), { parent: parent ? Object.assign({}, parent) : null, createdAt: now, expiresAt: now + this.sessionSeconds * 1000, accessExpiresAt: now + Math.min(this.accessSeconds, this.sessionSeconds) * 1000, accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh), csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf), usedRefreshHashes: [], revokedAt: null });
60
70
  if (!(yield this.allowed(record)))
61
71
  throw new Error("Session is not allowed.");
62
72
  yield (yield this.rows()).insertOne(record);
@@ -68,16 +78,16 @@ class SessionAuth {
68
78
  const id = tokenSessionId(token);
69
79
  if (!id)
70
80
  return null;
71
- const row = yield (yield this.rows()).findOne(Object.assign(Object.assign({ _id: id }, this.scope), { accessHash: (0, AuthSecurity_1.hashAuthToken)(token), accessExpiresAt: { $gt: Date.now() } }));
81
+ const row = yield (yield this.rows()).findOne(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { accessHash: (0, AuthSecurity_1.hashAuthToken)(token), accessExpiresAt: { $gt: Date.now() } }));
72
82
  return this.allowed(row);
73
83
  });
74
84
  }
75
85
  /** Trusted-server introspection; never expose this method as an unauthenticated endpoint. */
76
86
  inspect(sessionId) {
77
87
  return __awaiter(this, void 0, void 0, function* () {
78
- if (typeof sessionId !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(sessionId))
88
+ if (typeof sessionId !== "string" || !/^[A-Za-z0-9_-]{1,128}$/.test(sessionId))
79
89
  return null;
80
- return this.allowed(yield (yield this.rows()).findOne(Object.assign({ _id: sessionId }, this.scope)));
90
+ return this.allowed(yield (yield this.rows()).findOne(Object.assign({ _id: this.sessionId(sessionId) }, this.scope)));
81
91
  });
82
92
  }
83
93
  verifyCsrf(token, csrf, kind = "access") {
@@ -85,7 +95,7 @@ class SessionAuth {
85
95
  const id = tokenSessionId(token);
86
96
  if (!id || typeof csrf !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(csrf))
87
97
  return false;
88
- const row = yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign(Object.assign({ _id: id }, this.scope), (kind === "access" ? { accessHash: (0, AuthSecurity_1.hashAuthToken)(token) } : { $or: [{ refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }, { usedRefreshHashes: (0, AuthSecurity_1.hashAuthToken)(token) }] })), { csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf) }), (kind === "access" ? { accessExpiresAt: { $gt: Date.now() } } : {})));
98
+ const row = yield (yield this.rows()).findOne(Object.assign(Object.assign(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), (kind === "access" ? { accessHash: (0, AuthSecurity_1.hashAuthToken)(token) } : { $or: [{ refreshHash: (0, AuthSecurity_1.hashAuthToken)(token) }, { usedRefreshHashes: (0, AuthSecurity_1.hashAuthToken)(token) }] })), { csrfHash: (0, AuthSecurity_1.hashAuthToken)(csrf) }), (kind === "access" ? { accessExpiresAt: { $gt: Date.now() } } : {})));
89
99
  return !!(yield this.allowed(row));
90
100
  });
91
101
  }
@@ -96,7 +106,7 @@ class SessionAuth {
96
106
  if (!id)
97
107
  return null;
98
108
  const rows = yield this.rows(), hash = (0, AuthSecurity_1.hashAuthToken)(token);
99
- const row = yield rows.findOne(Object.assign({ _id: id }, this.scope));
109
+ const row = yield rows.findOne(Object.assign({ _id: this.sessionId(id) }, this.scope));
100
110
  if (!(yield this.allowed(row)) || !row)
101
111
  return null;
102
112
  if (row.refreshHash !== hash) {
@@ -110,12 +120,12 @@ class SessionAuth {
110
120
  return null;
111
121
  }
112
122
  const access = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, refresh = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`;
113
- const next = yield rows.findOneAndUpdate(Object.assign(Object.assign({ _id: id }, this.scope), { refreshHash: hash, revokedAt: null, expiresAt: { $gt: Date.now() } }), { $set: { accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh),
123
+ const next = yield rows.findOneAndUpdate(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { refreshHash: hash, revokedAt: null, expiresAt: { $gt: Date.now() } }), { $set: { accessHash: (0, AuthSecurity_1.hashAuthToken)(access), refreshHash: (0, AuthSecurity_1.hashAuthToken)(refresh),
114
124
  accessExpiresAt: Math.min(Date.now() + this.accessSeconds * 1000, row.expiresAt),
115
125
  usedRefreshHashes: [...row.usedRefreshHashes, hash] } }, { returnDocument: "after" });
116
126
  if (!next) {
117
127
  // Also detect simultaneous replay that lost the compare-and-swap.
118
- yield rows.updateOne(Object.assign(Object.assign({ _id: id }, this.scope), { usedRefreshHashes: hash, revokedAt: null }), { $set: { revokedAt: Date.now() } });
128
+ yield rows.updateOne(Object.assign(Object.assign({ _id: this.sessionId(id) }, this.scope), { usedRefreshHashes: hash, revokedAt: null }), { $set: { revokedAt: Date.now() } });
119
129
  return null;
120
130
  }
121
131
  return this.pair(next, access, refresh);
@@ -125,7 +135,7 @@ class SessionAuth {
125
135
  recordLogin(session, userAgent) {
126
136
  return __awaiter(this, void 0, void 0, function* () {
127
137
  const metadata = { userAgent: userAgent.slice(0, 512), lastActiveAt: session.createdAt };
128
- yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: session.id }, this.scope), { revokedAt: null }), { $set: metadata });
138
+ yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: this.sessionId(session.id) }, this.scope), { revokedAt: null }), { $set: metadata });
129
139
  Object.assign(session, metadata);
130
140
  });
131
141
  }
@@ -136,26 +146,26 @@ class SessionAuth {
136
146
  const now = Date.now();
137
147
  if (((_a = session.lastActiveAt) !== null && _a !== void 0 ? _a : session.createdAt) >= now - 60000)
138
148
  return;
139
- yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: session.id }, this.scope), { revokedAt: null, expiresAt: { $gt: now } }), { $set: { lastActiveAt: now } });
149
+ yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: this.sessionId(session.id) }, this.scope), { revokedAt: null, expiresAt: { $gt: now } }), { $set: { lastActiveAt: now } });
140
150
  session.lastActiveAt = now;
141
151
  });
142
152
  }
143
153
  revoke(sessionId) {
144
154
  return __awaiter(this, void 0, void 0, function* () {
145
155
  (0, AuthSecurity_1.authText)(sessionId, "session ID");
146
- yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: sessionId }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
156
+ yield (yield this.rows()).updateOne(Object.assign(Object.assign({ _id: this.sessionId(sessionId) }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
147
157
  });
148
158
  }
149
159
  revokeAll(subject) {
150
160
  return __awaiter(this, void 0, void 0, function* () {
151
161
  (0, AuthSecurity_1.authText)(subject, "subject");
152
- yield (yield this.rows()).updateMany(Object.assign(Object.assign({ subject }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
162
+ yield (yield this.rows()).updateMany(Object.assign(Object.assign({ _subject: this.ids().create(subject) }, this.scope), { revokedAt: null }), { $set: { revokedAt: Date.now() } });
153
163
  });
154
164
  }
155
165
  list(subject) {
156
166
  return __awaiter(this, void 0, void 0, function* () {
157
167
  (0, AuthSecurity_1.authText)(subject, "subject");
158
- const rows = yield (yield this.rows()).find(Object.assign(Object.assign({ subject }, this.scope), { revokedAt: null, expiresAt: { $gt: Date.now() } })).toArray();
168
+ const rows = yield (yield this.rows()).find(Object.assign(Object.assign({ _subject: this.ids().create(subject) }, this.scope), { revokedAt: null, expiresAt: { $gt: Date.now() } })).toArray();
159
169
  return rows.map(publicSession);
160
170
  });
161
171
  }
@@ -57,6 +57,10 @@ function readSsoClientCredentials(req) {
57
57
  exports.readSsoClientCredentials = readSsoClientCredentials;
58
58
  function challenge(verifier) { return (0, crypto_1.createHash)("sha256").update(verifier).digest("base64url"); }
59
59
  function verifierValid(value) { return typeof value === "string" && /^[A-Za-z0-9._~-]{43,128}$/.test(value); }
60
+ function sessionIdValid(value) {
61
+ return typeof value === "string" && (/^[a-fA-F0-9]{24}$/.test(value) || /^[0-9]{1,20}$/.test(value)
62
+ || /^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$/.test(value) || opaqueValid(value));
63
+ }
60
64
  function opaqueValid(value) { return typeof value === "string" && /^[A-Za-z0-9_-]{43}$/.test(value); }
61
65
  function redirectAllowed(value, allowLocal = false) {
62
66
  const url = (0, AuthSecurity_1.authUrl)(value, allowLocal);
@@ -245,7 +249,7 @@ class SsoClient {
245
249
  identity(value, environment) {
246
250
  if (!value || value.issuer !== this.issuer || value.audience !== this.options.clientId || value.environment !== environment ||
247
251
  typeof value.subject !== "string" || !value.subject.length || value.subject.length > 512 ||
248
- !opaqueValid(value.authoritySessionId) || !Number.isFinite(value.expiresAt) || value.expiresAt <= Date.now())
252
+ !sessionIdValid(value.authoritySessionId) || !Number.isFinite(value.expiresAt) || value.expiresAt <= Date.now())
249
253
  return null;
250
254
  return { issuer: value.issuer, audience: value.audience, subject: value.subject, environment: value.environment,
251
255
  authoritySessionId: value.authoritySessionId, expiresAt: value.expiresAt };
@@ -272,7 +276,7 @@ class SsoClient {
272
276
  }
273
277
  introspect(sessionId, environment) {
274
278
  return __awaiter(this, void 0, void 0, function* () {
275
- if (!opaqueValid(sessionId))
279
+ if (!sessionIdValid(sessionId))
276
280
  return null;
277
281
  (0, AuthSecurity_1.authText)(environment, "environment");
278
282
  const response = yield postBackchannel(this.introspection, this.options.clientId, this.options.clientSecret, { session_id: sessionId, environment });
@@ -38,5 +38,7 @@ export declare class WebAuth {
38
38
  establishSession(subject: string, res: Response, parent?: AuthSession["parent"]): Promise<AuthSession>;
39
39
  authenticate(req: Request): Promise<AuthSession | null>;
40
40
  refresh(req: Request, res: Response): Promise<AuthSession>;
41
+ protected logoutSession(req: Request): Promise<AuthSession>;
42
+ protected finishLogout(session: AuthSession, res: Response): Promise<void>;
41
43
  logout(req: Request, res: Response): Promise<void>;
42
44
  }
@@ -130,16 +130,29 @@ class WebAuth {
130
130
  return pair.session;
131
131
  });
132
132
  }
133
- logout(req, res) {
133
+ logoutSession(req) {
134
134
  return __awaiter(this, void 0, void 0, function* () {
135
135
  this.post(req);
136
136
  const csrf = this.csrf(req), token = this.cookies.read(req, "refresh");
137
137
  if (!(yield this.sessions.verifyCsrf(token, csrf, "refresh")))
138
138
  throw new WebAuthError(403, "Invalid session CSRF token.");
139
- yield this.sessions.revoke(token.split(".")[0]);
139
+ const session = yield this.sessions.inspect(token.split(".")[0]);
140
+ if (!session)
141
+ throw new WebAuthError(401, "Invalid session.");
142
+ return session;
143
+ });
144
+ }
145
+ finishLogout(session, res) {
146
+ return __awaiter(this, void 0, void 0, function* () {
147
+ yield this.sessions.revoke(session.id);
140
148
  for (const key of ["access", "refresh", "csrf"])
141
149
  this.cookies.write(res, key, "", 0, key !== "csrf");
142
150
  });
143
151
  }
152
+ logout(req, res) {
153
+ return __awaiter(this, void 0, void 0, function* () {
154
+ yield this.finishLogout(yield this.logoutSession(req), res);
155
+ });
156
+ }
144
157
  }
145
158
  exports.WebAuth = WebAuth;
@@ -5,3 +5,4 @@ export * from "./SessionAuth";
5
5
  export * from "./WebAuth";
6
6
  export * from "./SingleSignOn";
7
7
  export * from "./ModelAuth";
8
+ export * from "./Auth";
@@ -21,3 +21,4 @@ __exportStar(require("./SessionAuth"), exports);
21
21
  __exportStar(require("./WebAuth"), exports);
22
22
  __exportStar(require("./SingleSignOn"), exports);
23
23
  __exportStar(require("./ModelAuth"), exports);
24
+ __exportStar(require("./Auth"), exports);
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.ConsoleApplication = void 0;
13
+ const Auth_1 = require("./Auth/Auth");
13
14
  /// <reference types="node" />
14
15
  const Config_1 = require("./Config");
15
16
  const ConsoleOutputHelper_1 = require("./Commands/ConsoleOutputHelper");
@@ -63,6 +64,7 @@ class ConsoleApplication {
63
64
  Config_1.Config.register(key, options.config[key]);
64
65
  }
65
66
  }
67
+ Auth_1.Auth.init();
66
68
  NotificationService_1.NotificationService.init();
67
69
  if (options && options.errorHandler) {
68
70
  if (Array.isArray(options.errorHandler)) {
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LambdaApplication = void 0;
4
+ const Auth_1 = require("./Auth/Auth");
4
5
  const Config_1 = require("./Config");
5
6
  const DatatableRegistry_1 = require("./Datatable/DatatableRegistry");
6
7
  const QueueJobsRegisty_1 = require("./Queue/QueueJobsRegisty");
@@ -26,6 +27,7 @@ class LambdaApplication {
26
27
  Config_1.Config.register(key, options.config[key]);
27
28
  }
28
29
  }
30
+ Auth_1.Auth.init();
29
31
  if (options && options.errorHandler) {
30
32
  if (Array.isArray(options.errorHandler)) {
31
33
  ErrorHandler_1.ErrorHandler.setHandlers(options.errorHandler);
@@ -30,7 +30,7 @@ export declare class RouteFacade {
30
30
  /** Register the existing DataBuilder POST protocol with request-local configuration. */
31
31
  databuilder(path: string, options?: DataBuilderRouteOptions): void;
32
32
  /** Register CSRF, model login, logout and refresh using the browser auth contract. */
33
- auth(prefix: string, options: AuthRouteOptions): void;
33
+ auth(prefix: string, options?: AuthRouteOptions): void;
34
34
  /**
35
35
  * Register a POST-Method
36
36
  *
@@ -1,10 +1,21 @@
1
1
  "use strict";
2
+ var __rest = (this && this.__rest) || function (s, e) {
3
+ var t = {};
4
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
5
+ t[p] = s[p];
6
+ if (s != null && typeof Object.getOwnPropertySymbols === "function")
7
+ for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
8
+ if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
9
+ t[p[i]] = s[p[i]];
10
+ }
11
+ return t;
12
+ };
2
13
  Object.defineProperty(exports, "__esModule", { value: true });
3
14
  exports.Route = exports.RouteFacade = void 0;
4
15
  const routing_1 = require("../routing");
5
16
  const Router_1 = require("./Router");
6
17
  const DataBuilderRoute_1 = require("../DataBuilder/DataBuilderRoute");
7
- const ModelAuth_1 = require("../Auth/ModelAuth");
18
+ const Auth_1 = require("../Auth/Auth");
8
19
  class RouteFacade {
9
20
  constructor() {
10
21
  this.tempGroupPrefix = null;
@@ -71,15 +82,19 @@ class RouteFacade {
71
82
  this.registerWithGroupContext("POST", path, (0, DataBuilderRoute_1.dataBuilderRoute)(options), { middleware: options.middleware, domains: options.domains });
72
83
  }
73
84
  /** Register CSRF, model login, logout and refresh using the browser auth contract. */
74
- auth(prefix, options) {
75
- if (!(options === null || options === void 0 ? void 0 : options.auth) && !(options === null || options === void 0 ? void 0 : options.model))
76
- throw new Error("Route.auth requires a Model class or auth instance.");
85
+ auth(prefix, options = {}) {
86
+ const { auth = "main", loginPath, loginResponse, middleware, domains } = options, overrides = __rest(options, ["auth", "loginPath", "loginResponse", "middleware", "domains"]);
87
+ if (typeof auth !== "string" || !/^[A-Za-z0-9_-]+$/.test(auth))
88
+ throw new Error("Route.auth expects an auth scope key.");
89
+ if (Object.keys(overrides).length) {
90
+ Auth_1.Auth.registerScope(auth, overrides);
91
+ }
77
92
  const base = prefix.replace(/\/+$/, "");
78
93
  const registration = { middleware: options.middleware, domains: options.domains };
79
- this.get(`${base}/csrf`, (0, ModelAuth_1.authRoute)("csrf", options), registration);
80
- this.post(options.loginPath || `${base}/login`, (0, ModelAuth_1.authRoute)("login", options), registration);
81
- this.post(`${base}/logout`, (0, ModelAuth_1.authRoute)("logout", options), registration);
82
- this.post(`${base}/refresh`, (0, ModelAuth_1.authRoute)("refresh", options), registration);
94
+ this.get(`${base}/csrf`, (0, Auth_1.authRoute)("csrf", options), registration);
95
+ this.post(options.loginPath || `${base}/login`, (0, Auth_1.authRoute)("login", options), registration);
96
+ this.post(`${base}/logout`, (0, Auth_1.authRoute)("logout", options), registration);
97
+ this.post(`${base}/refresh`, (0, Auth_1.authRoute)("refresh", options), registration);
83
98
  }
84
99
  /**
85
100
  * Register a POST-Method
@@ -13,6 +13,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
15
  exports.WebApplication = void 0;
16
+ const Auth_1 = require("./Auth/Auth");
16
17
  /// <reference types="node" />
17
18
  const http_1 = __importDefault(require("http"));
18
19
  const Config_1 = require("./Config");
@@ -47,6 +48,7 @@ class WebApplication {
47
48
  Config_1.Config.register(key, options.config[key]);
48
49
  }
49
50
  }
51
+ Auth_1.Auth.init();
50
52
  NotificationService_1.NotificationService.init();
51
53
  if (options && options.errorHandler) {
52
54
  if (Array.isArray(options.errorHandler)) {
@@ -283,7 +283,7 @@ configuration before rolling the new auth flow out to existing applications.
283
283
  ## Standard browser login with `Route.auth`
284
284
 
285
285
  ```ts
286
- import { Model, ModelAuth, Route } from "@webtypen/webframez-core";
286
+ import { Model, AuthScope, Route } from "@webtypen/webframez-core";
287
287
 
288
288
  class User extends Model {
289
289
  __table = "users";
@@ -294,7 +294,7 @@ Route.auth("/api/auth", { model: User });
294
294
  // GET /api/auth/csrf
295
295
  // POST /api/auth/login, /api/auth/logout, /api/auth/refresh
296
296
 
297
- // During registration: user.password = await ModelAuth.hashPassword(password);
297
+ // During registration: user.password = await AuthScope.hashPassword(password);
298
298
  ```
299
299
 
300
300
  Set `auth.origin` or `website.baseUrl` to the canonical public URL (including the
@@ -312,7 +312,7 @@ not trimmed. Store a bcrypt hash; existing bcrypt hashes work without migration.
312
312
  and `isUserAllowed` adds account/tenant checks on login and every session access.
313
313
  An explicit `false` in `is_active` rejects the account; `fields.active: false`
314
314
  disables this field check. Optional `session` options configure storage, scope
315
- and lifetimes. For `ModelAuth`, access and absolute session lifetimes default to
315
+ and lifetimes. For `AuthScope`, access and absolute session lifetimes default to
316
316
  30 days; both remain configurable through `session.accessTokenSeconds` and
317
317
  `session.sessionSeconds`. The lower-level `SessionAuth` retains its 15-minute
318
318
  access-token default.
@@ -331,8 +331,9 @@ CSRF before login, sends `X-CSRF-Token` on same-origin mutations, and inserts
331
331
  `cookieName` supports custom cookie prefixes. For a custom Fetch implementation,
332
332
  pass it as the second argument to `createAuthFetch`.
333
333
 
334
- Existing applications can pass `auth: instance` or `auth: () => instance` instead
335
- of `model`; use a shared `ModelAuth` for middleware and session-management APIs.
334
+ `Route.auth` selects a shared registered scope with `auth: "main"` (the default).
335
+ Model and other scope options passed to `Route.auth` configure that scope in the
336
+ same `Auth` registry, including partial nested overrides.
336
337
  `loginPath` keeps existing URLs, `loginResponse` adds application-specific response
337
338
  fields (for example a validated redirect), and `onLogin` / `onAuthenticate` add application-specific callbacks. Browser
338
339
  user-agent metadata and last activity are recorded automatically; `trackActivity: false`
@@ -342,7 +343,7 @@ be accessible before authentication; apply authentication middleware to protecte
342
343
  application routes separately.
343
344
 
344
345
 
345
- `audience` is a top-level option in both `Route.auth` and `ModelAuth`. It takes
346
+ `audience` is a top-level option in both `Route.auth` and `AuthScope`. It takes
346
347
  precedence over the still-supported `session.audience`; the default is the origin.
347
348
  Standard error messages are included in English and German (`locale: "de"`);
348
349
  `messages` overrides individual entries while retaining the remaining defaults.
@@ -354,7 +355,7 @@ Route.auth("/api/auth", {
354
355
  model: User,
355
356
  audience: "simplebis-website",
356
357
  locale: "de",
357
- session: { collection: "website_auth_sessions" },
358
+ auth: "main",
358
359
  // Optional overrides:
359
360
  messages: { invalid_login: "Bitte prüfe deine Zugangsdaten." },
360
361
  });
@@ -364,3 +365,66 @@ Session metadata stays in the existing session collection. `sessions.list(subjec
364
365
  includes `userAgent` and `lastActiveAt` when available, so applications do not need
365
366
  a second model/query or callbacks to maintain the session list. Existing records
366
367
  without metadata remain valid; last activity falls back to the creation timestamp.
368
+
369
+
370
+ ## Named auth scopes and lifecycle hooks
371
+
372
+ ```ts
373
+ import { Auth, Config, Route } from "@webtypen/webframez-core";
374
+
375
+ Config.register("auth", {
376
+ scopes: {
377
+ main: { model: User, audience: "website", locale: "de" },
378
+ admin: { model: AdminUser, audience: "administration" },
379
+ },
380
+ });
381
+ Route.auth("/api/auth", {
382
+ auth: "main",
383
+ beforeLogin: async (req, user, scope) => {
384
+ if (!await canLogIn(user, req)) throw new Error("Login denied by policy.");
385
+ },
386
+ afterLogin: async (req, user, scope) => { /* Login has completed. */ },
387
+ beforeLogout: async (req, user, scope) => { /* Throw to prevent logout. */ },
388
+ afterLogout: async (req, user, scope) => { /* Session revoked and cookies cleared. */ },
389
+ });
390
+
391
+ Auth.scope(); // same singleton scope as Auth.scope("main")
392
+ Auth.scope("admin");
393
+ Auth.registerScope("staff", { model: StaffUser });
394
+ ```
395
+
396
+ Applications pass the auth configuration to the regular boot `config` object.
397
+ Web, console and Lambda boot initialize the registry after registering Config.
398
+ An automatic `main` scope exists unless `auth.main: false`; define its model in
399
+ `auth.scopes.main` or in `Route.auth({ model: User })` before using login. Configured
400
+ scopes and scopes registered at runtime remain available when automatic main is
401
+ disabled. Unknown keys fail explicitly. `auth.defaults` supplies shared options.
402
+ Registering an existing key updates its scope; routes resolve the current registry
403
+ entry on each request. `ModelAuth` remains a compatibility alias of `AuthScope`.
404
+
405
+ Each record in the default `auth_sessions` collection contains `scope`. Scope,
406
+ issuer, audience and environment are checked for all session operations. Named
407
+ scopes use separate cookie prefixes by default; configure the browser auth client
408
+ with the selected scope's CSRF cookie name when using a non-main scope.
409
+ The selected driver's ID adapter creates `_id` and converts `_subject`; MongoDB
410
+ stores both as BSON ObjectId. Stored records contain no extra `id` or `subject`.
411
+ Public session metadata still exposes string `id` and `subject` for transport.
412
+ Injected document stores must provide `session.idAdapter` as well as `database`.
413
+
414
+ The four hooks receive `(req, user, scope)` and await async callbacks. Login hooks
415
+ receive the verified user, never a browser-supplied subject. CSRF and origin checks
416
+ happen first. Exceptions in `beforeLogin` return `login_blocked`, and exceptions
417
+ in `beforeLogout` return `logout_blocked`; the response includes the exception's
418
+ message and a 403 status (or a thrown WebAuthError's status). No session/cookie
419
+ mutation or after-hook occurs on a rejected before-hook. After-hooks run after the
420
+ operation completes; their failures do not undo an already completed operation.
421
+ Existing `onLogin(session, req)` and `onAuthenticate(auth, req)` callbacks remain
422
+ supported. Individual `messages` entries can override before-hook error messages.
423
+
424
+ When migrating old MongoDB sessions, convert the former opaque `_id` to ObjectId
425
+ using the first 24 hex characters of SHA-256 of the former ID. Convert `subject`
426
+ to `_subject`, remove duplicate `id`/`subject`, and set the correct `scope`. Keep
427
+ hashes and expiry unchanged. SessionAuth recognizes the former token prefixes,
428
+ so converted sessions retain their existing cookies. Applications should run
429
+ an explicit migration while stopping auth writes, rather than changing schemas
430
+ implicitly during a request.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.73",
3
+ "version": "0.3.74",
4
4
  "description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
5
5
  "homepage": "https://webtypen.de/",
6
6
  "license": "MIT",