@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.
- package/dist/Auth/Auth.d.ts +19 -0
- package/dist/Auth/Auth.js +53 -0
- package/dist/Auth/ModelAuth.d.ts +15 -4
- package/dist/Auth/ModelAuth.js +65 -16
- package/dist/Auth/SessionAuth.d.ts +7 -1
- package/dist/Auth/SessionAuth.js +27 -17
- package/dist/Auth/SingleSignOn.js +6 -2
- package/dist/Auth/WebAuth.d.ts +2 -0
- package/dist/Auth/WebAuth.js +15 -2
- package/dist/Auth/index.d.ts +1 -0
- package/dist/Auth/index.js +1 -0
- package/dist/ConsoleApplication.js +2 -0
- package/dist/LambdaApplication.js +2 -0
- package/dist/Router/Route.d.ts +1 -1
- package/dist/Router/Route.js +23 -8
- package/dist/WebApplication.js +2 -0
- package/docs/authentication.md +71 -7
- package/package.json +1 -1
|
@@ -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;
|
package/dist/Auth/ModelAuth.d.ts
CHANGED
|
@@ -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?:
|
|
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
|
|
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
|
-
|
|
81
|
+
/** Compatibility name for existing integrations. Prefer Auth.scope(). */
|
|
82
|
+
export { AuthScope as ModelAuth };
|
|
83
|
+
export type AuthScopeOptions = ModelAuthOptions;
|
package/dist/Auth/ModelAuth.js
CHANGED
|
@@ -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.
|
|
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
|
|
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: (
|
|
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: (
|
|
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
|
-
|
|
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.
|
|
219
|
-
|
|
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;
|
package/dist/Auth/SessionAuth.js
CHANGED
|
@@ -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.
|
|
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_-]{
|
|
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 = (
|
|
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,
|
|
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_-]{
|
|
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
|
-
!
|
|
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 (!
|
|
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 });
|
package/dist/Auth/WebAuth.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/Auth/WebAuth.js
CHANGED
|
@@ -130,16 +130,29 @@ class WebAuth {
|
|
|
130
130
|
return pair.session;
|
|
131
131
|
});
|
|
132
132
|
}
|
|
133
|
-
|
|
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.
|
|
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;
|
package/dist/Auth/index.d.ts
CHANGED
package/dist/Auth/index.js
CHANGED
|
@@ -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);
|
package/dist/Router/Route.d.ts
CHANGED
|
@@ -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
|
|
33
|
+
auth(prefix: string, options?: AuthRouteOptions): void;
|
|
34
34
|
/**
|
|
35
35
|
* Register a POST-Method
|
|
36
36
|
*
|
package/dist/Router/Route.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
76
|
-
|
|
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,
|
|
80
|
-
this.post(options.loginPath || `${base}/login`, (0,
|
|
81
|
-
this.post(`${base}/logout`, (0,
|
|
82
|
-
this.post(`${base}/refresh`, (0,
|
|
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
|
package/dist/WebApplication.js
CHANGED
|
@@ -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)) {
|
package/docs/authentication.md
CHANGED
|
@@ -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,
|
|
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
|
|
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 `
|
|
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
|
-
|
|
335
|
-
|
|
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 `
|
|
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
|
-
|
|
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