@webtypen/webframez-core 0.3.76 → 0.3.78
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/README.md +13 -0
- package/bearer-auth-client.d.ts +13 -0
- package/bearer-auth-client.js +67 -0
- package/dist/Auth/Auth.js +1 -1
- package/dist/Auth/AuthHandoff.d.ts +16 -0
- package/dist/Auth/AuthHandoff.js +64 -0
- package/dist/Auth/AuthMembership.d.ts +39 -0
- package/dist/Auth/AuthMembership.js +62 -0
- package/dist/Auth/ModelAuth.d.ts +23 -0
- package/dist/Auth/ModelAuth.js +151 -3
- package/dist/Auth/SessionAuth.d.ts +3 -1
- package/dist/Auth/SessionAuth.js +8 -4
- package/dist/Auth/SingleSignOn.js +3 -2
- package/dist/Auth/index.d.ts +2 -0
- package/dist/Auth/index.js +2 -0
- package/dist/Hooks/WebframezHooks.d.ts +1 -0
- package/dist/Hooks/WebframezHooks.js +8 -4
- package/dist/Router/Router.d.ts +4 -0
- package/dist/Router/Router.js +174 -124
- package/docs/authentication.md +67 -0
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -925,3 +925,16 @@ the text representation; their separate `icon` property controls navigation icon
|
|
|
925
925
|
## DataBuilder model forms
|
|
926
926
|
|
|
927
927
|
DataBuilder supports `@Field()`, parameterized `@Validates()`, server-side `ModelForm` classes and `Route.databuilder()` while retaining the v1 request protocol and legacy controller registrations. See [DataBuilder documentation](docs/databuilder.md).
|
|
928
|
+
|
|
929
|
+
## Routing and lifecycle performance
|
|
930
|
+
|
|
931
|
+
Route and domain patterns are compiled at registration and reused while matching.
|
|
932
|
+
`Router.init()` resets their caches; runtime registration and legacy direct route
|
|
933
|
+
stores still work. Exact-route precedence, domain ordering, parameters and wildcards
|
|
934
|
+
retain their existing behavior. Public regex builders still produce fresh regexes.
|
|
935
|
+
|
|
936
|
+
Lifecycle hooks skip event normalization when no listeners are registered. Use
|
|
937
|
+
`WebframezHooks.hasListeners(eventName)` before constructing an expensive payload.
|
|
938
|
+
Registered handlers retain their ordered, awaited execution and snapshot behavior.
|
|
939
|
+
The router applies this check before building telemetry payloads. Middleware keeps
|
|
940
|
+
its existing `next`/`reject` callback contract.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BearerAuthTokens = {
|
|
2
|
+
auth_token: string;
|
|
3
|
+
refresh_token: string;
|
|
4
|
+
auth_expires_at: number;
|
|
5
|
+
refresh_expires_at: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function createBearerAuthFetch(options: {
|
|
9
|
+
configuration: (url: URL) => { key: string; refreshUrl: string } | null;
|
|
10
|
+
load: (key: string) => Promise<BearerAuthTokens | null>;
|
|
11
|
+
clear?: (key: string) => Promise<void>;
|
|
12
|
+
save: (key: string, tokens: BearerAuthTokens) => Promise<void>;
|
|
13
|
+
}, originalFetch?: typeof fetch): typeof fetch;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/** Browser/native entry point; imports no server or database code. */
|
|
2
|
+
exports.createBearerAuthFetch = function createBearerAuthFetch(options, originalFetch = globalThis.fetch.bind(globalThis)) {
|
|
3
|
+
const pending = new Map();
|
|
4
|
+
const refresh = (configuration, failedToken) => {
|
|
5
|
+
const key = configuration.key;
|
|
6
|
+
const perform = async () => {
|
|
7
|
+
const current = await options.load(key);
|
|
8
|
+
if (!current) return null;
|
|
9
|
+
if (failedToken && current.auth_token !== failedToken) return current;
|
|
10
|
+
let response;
|
|
11
|
+
try {
|
|
12
|
+
response = await originalFetch(configuration.refreshUrl, { method: "POST", credentials: "omit",
|
|
13
|
+
headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: current.refresh_token }),
|
|
14
|
+
redirect: "error", signal: typeof AbortSignal !== "undefined" && typeof AbortSignal.timeout === "function" ? AbortSignal.timeout(15000) : undefined });
|
|
15
|
+
} catch (error) {
|
|
16
|
+
// A timeout may have consumed the rotating token; never replay it automatically.
|
|
17
|
+
if (options.clear) await options.clear(key);
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (!response.ok) {
|
|
21
|
+
if (options.clear) await options.clear(key);
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
let pair;
|
|
25
|
+
try { pair = await response.json(); }
|
|
26
|
+
catch { if (options.clear) await options.clear(key); return null; }
|
|
27
|
+
if (pair.status !== "success" || typeof pair.auth_token !== "string" || typeof pair.refresh_token !== "string"
|
|
28
|
+
|| !Number.isFinite(pair.auth_expires_at) || !Number.isFinite(pair.refresh_expires_at)) {
|
|
29
|
+
if (options.clear) await options.clear(key);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
const latest = await options.load(key);
|
|
33
|
+
if (!latest || latest.refresh_token !== current.refresh_token) return latest;
|
|
34
|
+
await options.save(key, pair);
|
|
35
|
+
return pair;
|
|
36
|
+
};
|
|
37
|
+
if (!pending.has(key)) {
|
|
38
|
+
const task = typeof navigator !== "undefined" && navigator.locks
|
|
39
|
+
? navigator.locks.request(`webframez-bearer:${key}`, perform) : perform();
|
|
40
|
+
pending.set(key, task.finally(() => pending.delete(key)));
|
|
41
|
+
}
|
|
42
|
+
return pending.get(key);
|
|
43
|
+
};
|
|
44
|
+
return async (input, init) => {
|
|
45
|
+
let url;
|
|
46
|
+
try { url = new URL(typeof Request !== "undefined" && input instanceof Request ? input.url : String(input), globalThis.location?.href); }
|
|
47
|
+
catch { return originalFetch(input, init); }
|
|
48
|
+
const configuration = options.configuration(url);
|
|
49
|
+
if (!configuration) return originalFetch(input, init);
|
|
50
|
+
const refreshUrl = new URL(configuration.refreshUrl);
|
|
51
|
+
if (refreshUrl.origin !== url.origin) throw new Error("Bearer refresh must use the request origin.");
|
|
52
|
+
let session = await options.load(configuration.key);
|
|
53
|
+
if (!session || url.href === refreshUrl.href) return originalFetch(input, init);
|
|
54
|
+
if (session.auth_expires_at <= Date.now() + 30000) session = await refresh(configuration, session.auth_token) || session;
|
|
55
|
+
const headers = new Headers(init?.headers || (typeof Request !== "undefined" && input instanceof Request ? input.headers : undefined));
|
|
56
|
+
headers.set("Authorization", `Bearer ${session.auth_token}`);
|
|
57
|
+
const retryInput = typeof Request !== "undefined" && input instanceof Request ? input.clone() : input;
|
|
58
|
+
const response = await originalFetch(input, { ...init, headers, credentials: "omit", redirect: "error" });
|
|
59
|
+
if (response.status !== 401) return response;
|
|
60
|
+
const failure = await response.clone().json().catch(() => null);
|
|
61
|
+
if (failure?.code !== "unauthorized") return response;
|
|
62
|
+
const renewed = await refresh(configuration, session.auth_token);
|
|
63
|
+
if (!renewed) return response;
|
|
64
|
+
headers.set("Authorization", `Bearer ${renewed.auth_token}`);
|
|
65
|
+
return originalFetch(retryInput, { ...init, headers, credentials: "omit", redirect: "error" });
|
|
66
|
+
};
|
|
67
|
+
};
|
package/dist/Auth/Auth.js
CHANGED
|
@@ -15,7 +15,7 @@ const Model_1 = require("../Database/Model");
|
|
|
15
15
|
const ModelAuth_1 = require("./ModelAuth");
|
|
16
16
|
function mergeOptions(base, config) {
|
|
17
17
|
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), passwordReset: config.passwordReset === false ? false : config.passwordReset === undefined && base.passwordReset === false ? false
|
|
18
|
-
: Object.assign(Object.assign({}, (base.passwordReset || {})), (config.passwordReset || {})), messages: Object.assign(Object.assign({}, base.messages), config.messages) });
|
|
18
|
+
: Object.assign(Object.assign({}, (base.passwordReset || {})), (config.passwordReset || {})), memberships: Object.assign(Object.assign({}, base.memberships), config.memberships), messages: Object.assign(Object.assign({}, base.messages), config.messages) });
|
|
19
19
|
}
|
|
20
20
|
/** Application-wide registry of named browser-authentication scopes. */
|
|
21
21
|
class AuthFacade {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { DocumentDatabase } from "../Database/DatabaseAdapter";
|
|
2
|
+
/** Single-use delivery to a separate frontend origin after a server-side SSO callback. */
|
|
3
|
+
export declare class AuthHandoff {
|
|
4
|
+
private readonly options;
|
|
5
|
+
private readonly key;
|
|
6
|
+
constructor(options: {
|
|
7
|
+
secret: string;
|
|
8
|
+
audience: string;
|
|
9
|
+
connection?: string;
|
|
10
|
+
database?: () => Promise<DocumentDatabase>;
|
|
11
|
+
});
|
|
12
|
+
private rows;
|
|
13
|
+
create(payload: object): Promise<string>;
|
|
14
|
+
consume<T = unknown>(code: string): Promise<T | null>;
|
|
15
|
+
cleanup(): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AuthHandoff = void 0;
|
|
13
|
+
const crypto_1 = require("crypto");
|
|
14
|
+
const DBConnection_1 = require("../Database/DBConnection");
|
|
15
|
+
const AuthSecurity_1 = require("./AuthSecurity");
|
|
16
|
+
/** Single-use delivery to a separate frontend origin after a server-side SSO callback. */
|
|
17
|
+
class AuthHandoff {
|
|
18
|
+
constructor(options) {
|
|
19
|
+
this.options = options;
|
|
20
|
+
if (typeof options.secret !== "string" || options.secret.length < 32)
|
|
21
|
+
throw new Error("Auth handoff requires a strong server secret.");
|
|
22
|
+
if (!options.audience)
|
|
23
|
+
throw new Error("Auth handoff requires an audience.");
|
|
24
|
+
this.key = (0, crypto_1.createHash)("sha256").update(`webframez-handoff:${options.secret}`).digest();
|
|
25
|
+
}
|
|
26
|
+
rows() {
|
|
27
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
28
|
+
const db = yield (this.options.database ? this.options.database() : DBConnection_1.DBConnection.getDocumentStore(this.options.connection));
|
|
29
|
+
return db.collection("auth_handoffs");
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
create(payload) {
|
|
33
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
34
|
+
const code = (0, AuthSecurity_1.randomAuthToken)(), iv = (0, crypto_1.randomBytes)(12);
|
|
35
|
+
const cipher = (0, crypto_1.createCipheriv)("aes-256-gcm", this.key, iv);
|
|
36
|
+
cipher.setAAD(Buffer.from(this.options.audience));
|
|
37
|
+
const encrypted = Buffer.concat([cipher.update(JSON.stringify(payload), "utf8"), cipher.final()]);
|
|
38
|
+
yield (yield this.rows()).insertOne({ _id: (0, AuthSecurity_1.hashAuthToken)(code), audience: this.options.audience,
|
|
39
|
+
iv: iv.toString("base64"), tag: cipher.getAuthTag().toString("base64"), payload: encrypted.toString("base64"),
|
|
40
|
+
expiresAt: Date.now() + 30000, purgeAt: new Date(Date.now() + 60000), consumedAt: null });
|
|
41
|
+
return code;
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
consume(code) {
|
|
45
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
46
|
+
if (typeof code !== "string" || !/^[A-Za-z0-9_-]{43}$/.test(code))
|
|
47
|
+
return null;
|
|
48
|
+
const row = yield (yield this.rows()).findOneAndUpdate({ _id: (0, AuthSecurity_1.hashAuthToken)(code), audience: this.options.audience,
|
|
49
|
+
expiresAt: { $gt: Date.now() }, consumedAt: null }, { $set: { consumedAt: Date.now(), payload: "" } }, { returnDocument: "before" });
|
|
50
|
+
if (!row)
|
|
51
|
+
return null;
|
|
52
|
+
const decipher = (0, crypto_1.createDecipheriv)("aes-256-gcm", this.key, Buffer.from(row.iv, "base64"));
|
|
53
|
+
decipher.setAAD(Buffer.from(this.options.audience));
|
|
54
|
+
decipher.setAuthTag(Buffer.from(row.tag, "base64"));
|
|
55
|
+
return JSON.parse(Buffer.concat([decipher.update(Buffer.from(row.payload, "base64")), decipher.final()]).toString("utf8"));
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
cleanup() {
|
|
59
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
60
|
+
yield (yield this.rows()).deleteMany({ audience: this.options.audience, expiresAt: { $lte: Date.now() } });
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.AuthHandoff = AuthHandoff;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Model } from "../Database/Model";
|
|
2
|
+
export type AuthMembershipCondition = {
|
|
3
|
+
field: string;
|
|
4
|
+
operator: "=" | "!=";
|
|
5
|
+
value: unknown;
|
|
6
|
+
};
|
|
7
|
+
export type AuthMembershipBinding = {
|
|
8
|
+
user: {
|
|
9
|
+
foreignKey: string;
|
|
10
|
+
localKeys: readonly string[];
|
|
11
|
+
firstAvailable?: boolean;
|
|
12
|
+
};
|
|
13
|
+
resource: {
|
|
14
|
+
foreignKey: string;
|
|
15
|
+
localKeys: readonly string[];
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** Only server-loaded models and server-configured fields participate in authorization. */
|
|
19
|
+
export type AuthMembershipOptions = {
|
|
20
|
+
model: typeof Model;
|
|
21
|
+
bindings: readonly AuthMembershipBinding[];
|
|
22
|
+
/** Optionally bind this membership to the environment selected by a federated session. */
|
|
23
|
+
sessionEnvironmentKey?: string;
|
|
24
|
+
conditions?: readonly AuthMembershipCondition[];
|
|
25
|
+
userConditions?: readonly AuthMembershipCondition[];
|
|
26
|
+
resourceConditions?: readonly AuthMembershipCondition[];
|
|
27
|
+
};
|
|
28
|
+
export type AuthMembershipContext<TMembership extends Model = Model, TResource extends Model = Model> = {
|
|
29
|
+
membership: TMembership;
|
|
30
|
+
resource: TResource;
|
|
31
|
+
};
|
|
32
|
+
/** Resolves fresh membership records without caching authorization between requests. */
|
|
33
|
+
export declare class AuthMembership {
|
|
34
|
+
private readonly options;
|
|
35
|
+
constructor(options: AuthMembershipOptions);
|
|
36
|
+
private matches;
|
|
37
|
+
matchesEnvironment(resource: Model, environment: string | null): boolean;
|
|
38
|
+
find<TMembership extends Model = Model>(user: Model, resource: Model): Promise<TMembership | null>;
|
|
39
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.AuthMembership = void 0;
|
|
13
|
+
const Model_1 = require("../Database/Model");
|
|
14
|
+
/** Resolves fresh membership records without caching authorization between requests. */
|
|
15
|
+
class AuthMembership {
|
|
16
|
+
constructor(options) {
|
|
17
|
+
var _a;
|
|
18
|
+
this.options = options;
|
|
19
|
+
if (!options.model || !((_a = options.bindings) === null || _a === void 0 ? void 0 : _a.length))
|
|
20
|
+
throw new Error("Auth membership requires a model and bindings.");
|
|
21
|
+
for (const binding of options.bindings) {
|
|
22
|
+
if (!binding.user.foreignKey || !binding.resource.foreignKey
|
|
23
|
+
|| !binding.user.localKeys.length || !binding.resource.localKeys.length) {
|
|
24
|
+
throw new Error("Auth membership requires user and resource reference fields.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
matches(model, conditions = []) {
|
|
29
|
+
return conditions.every(condition => condition.operator === "="
|
|
30
|
+
? model[condition.field] === condition.value : model[condition.field] !== condition.value);
|
|
31
|
+
}
|
|
32
|
+
matchesEnvironment(resource, environment) {
|
|
33
|
+
const key = this.options.sessionEnvironmentKey;
|
|
34
|
+
return !environment || !key || String(resource === null || resource === void 0 ? void 0 : resource[key]) === environment;
|
|
35
|
+
}
|
|
36
|
+
find(user, resource) {
|
|
37
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
38
|
+
if (!(user instanceof Model_1.Model) || !(resource instanceof Model_1.Model)
|
|
39
|
+
|| !this.matches(user, this.options.userConditions) || !this.matches(resource, this.options.resourceConditions))
|
|
40
|
+
return null;
|
|
41
|
+
for (const binding of this.options.bindings) {
|
|
42
|
+
let userIds = binding.user.localKeys.map(key => user[key]).filter(value => value !== null && value !== undefined && value !== "");
|
|
43
|
+
if (binding.user.firstAvailable)
|
|
44
|
+
userIds = userIds.slice(0, 1);
|
|
45
|
+
const resourceIds = binding.resource.localKeys.map(key => resource[key]).filter(value => value !== null && value !== undefined && value !== "");
|
|
46
|
+
// Missing references must never turn into an unbounded membership lookup.
|
|
47
|
+
if (!userIds.length || !resourceIds.length)
|
|
48
|
+
continue;
|
|
49
|
+
let query = this.options.model.where(binding.user.foreignKey, "=", { $in: userIds })
|
|
50
|
+
.where(binding.resource.foreignKey, "=", { $in: resourceIds });
|
|
51
|
+
for (const condition of this.options.conditions || []) {
|
|
52
|
+
query = query.where(condition.field, condition.operator, condition.value);
|
|
53
|
+
}
|
|
54
|
+
const membership = yield query.first();
|
|
55
|
+
if (membership)
|
|
56
|
+
return membership;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
exports.AuthMembership = AuthMembership;
|
package/dist/Auth/ModelAuth.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Model } from "../Database/Model";
|
|
2
2
|
import { Request } from "../Router/Request";
|
|
3
3
|
import type { Response } from "../Router/Response";
|
|
4
|
+
import { AuthMembershipContext, AuthMembershipOptions } from "./AuthMembership";
|
|
4
5
|
import { PasswordReset, PasswordResetOptions } from "./PasswordReset";
|
|
5
6
|
import { AuthSession, SessionAuthOptions } from "./SessionAuth";
|
|
6
7
|
import { WebAuth, WebAuthError, WebAuthOptions } from "./WebAuth";
|
|
7
8
|
export type AuthContext<TUser extends Model = Model> = {
|
|
8
9
|
user: TUser;
|
|
9
10
|
session: AuthSession;
|
|
11
|
+
memberships?: Record<string, AuthMembershipContext>;
|
|
10
12
|
};
|
|
11
13
|
export type AuthSnapshot<TUser = Record<string, unknown>> = {
|
|
12
14
|
user: TUser;
|
|
@@ -19,6 +21,8 @@ export declare class AuthLoginError extends WebAuthError {
|
|
|
19
21
|
}
|
|
20
22
|
export type ModelAuthOptions = Partial<WebAuthOptions> & {
|
|
21
23
|
model: typeof Model;
|
|
24
|
+
/** Bearer scopes return opaque tokens and never use ambient authentication cookies. */
|
|
25
|
+
transport?: "cookie" | "bearer";
|
|
22
26
|
key?: string;
|
|
23
27
|
/** Takes precedence over session.audience; defaults to the configured origin. */
|
|
24
28
|
audience?: string;
|
|
@@ -35,6 +39,8 @@ export type ModelAuthOptions = Partial<WebAuthOptions> & {
|
|
|
35
39
|
session?: Partial<Omit<SessionAuthOptions, "issuer" | "isSessionAllowed">>;
|
|
36
40
|
csrfDisabled?: false;
|
|
37
41
|
verifyPassword?: (password: string, storedHash: string) => boolean | Promise<boolean>;
|
|
42
|
+
memberships?: Record<string, AuthMembershipOptions>;
|
|
43
|
+
isSessionAllowed?: (session: AuthSession, user: Model) => boolean | Promise<boolean>;
|
|
38
44
|
isUserAllowed?: (user: Model) => boolean | Promise<boolean>;
|
|
39
45
|
beforeLogin?: AuthActionHook;
|
|
40
46
|
afterLogin?: AuthActionHook;
|
|
@@ -60,6 +66,8 @@ export declare class AuthScope extends WebAuth {
|
|
|
60
66
|
readonly key: string;
|
|
61
67
|
readonly passwordReset: PasswordReset | null;
|
|
62
68
|
private readonly model;
|
|
69
|
+
private readonly memberships;
|
|
70
|
+
private readonly contexts;
|
|
63
71
|
private authPaths;
|
|
64
72
|
private readonly sessionUsers;
|
|
65
73
|
private readonly fields;
|
|
@@ -77,8 +85,23 @@ export declare class AuthScope extends WebAuth {
|
|
|
77
85
|
verifyCredentials(req: Request): Promise<string>;
|
|
78
86
|
login(req: Request, res: Response, verifyCredentials?: () => Promise<string | null>): Promise<AuthSession>;
|
|
79
87
|
logout(req: Request, res: Response): Promise<void>;
|
|
88
|
+
/** Trusted SSO/server entry point; the subject must already be verified by the caller. */
|
|
89
|
+
establishBearerSession(req: Request, subject: string, parent?: AuthSession["parent"], environment?: string): Promise<{
|
|
90
|
+
auth_token: string;
|
|
91
|
+
refresh_token: string;
|
|
92
|
+
auth_expires_at: number;
|
|
93
|
+
refresh_expires_at: number;
|
|
94
|
+
session: AuthSession;
|
|
95
|
+
}>;
|
|
96
|
+
logoutBearer(req: Request): Promise<void>;
|
|
97
|
+
/** Explicit Authorization credentials require no cookie CSRF and are never read from query parameters. */
|
|
98
|
+
resolveBearer<TUser extends Model = Model>(req: Request, res?: Response): Promise<AuthContext<TUser> | null>;
|
|
80
99
|
/** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
|
|
81
100
|
resolve<TUser extends Model = Model>(req: Request, res?: Response): Promise<AuthContext<TUser> | null>;
|
|
101
|
+
/** For integrations whose authenticated User was established by another trusted transport. */
|
|
102
|
+
findMembership<TMembership extends Model = Model>(name: string, user: Model, resource: Model): Promise<TMembership | null>;
|
|
103
|
+
/** Attaches a fresh membership only to a context authenticated by this scope. */
|
|
104
|
+
authorizeMembership<TMembership extends Model = Model>(req: Request, name: string, resource: Model): Promise<TMembership | null>;
|
|
82
105
|
resolveCookies<TUser extends Model = Model>(cookies: Record<string, string | undefined>): Promise<AuthContext<TUser> | null>;
|
|
83
106
|
/** Only explicit public fields are sent to browsers; Model instances remain on the server. */
|
|
84
107
|
snapshot(auth: AuthContext | null | undefined): AuthSnapshot | null;
|
package/dist/Auth/ModelAuth.js
CHANGED
|
@@ -8,6 +8,17 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
|
8
8
|
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
9
|
});
|
|
10
10
|
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
11
22
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
23
|
exports.ModelAuth = exports.AuthScope = exports.AuthLoginError = void 0;
|
|
13
24
|
const bcryptjs_1 = require("bcryptjs");
|
|
@@ -15,6 +26,7 @@ const routing_1 = require("../routing");
|
|
|
15
26
|
const Config_1 = require("../Config");
|
|
16
27
|
const Request_1 = require("../Router/Request");
|
|
17
28
|
const Auth_1 = require("./Auth");
|
|
29
|
+
const AuthMembership_1 = require("./AuthMembership");
|
|
18
30
|
const PasswordReset_1 = require("./PasswordReset");
|
|
19
31
|
const AuthSecurity_1 = require("./AuthSecurity");
|
|
20
32
|
const SessionAuth_1 = require("./SessionAuth");
|
|
@@ -79,14 +91,21 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
79
91
|
const sessions = new SessionAuth_1.SessionAuth(Object.assign(Object.assign({ accessTokenSeconds: 15 * 60, sessionSeconds: 30 * 86400, idleTimeoutSeconds: 7 * 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* () {
|
|
80
92
|
const id = fields.primaryKey === "_id" ? yield model.objectId(session.subject, { noExceptions: true }) : session.subject;
|
|
81
93
|
const user = id !== null && id !== undefined ? yield model.where(fields.primaryKey, "=", id).first() : null;
|
|
82
|
-
if (!(yield allowed(user)))
|
|
94
|
+
if (!(yield allowed(user)) || (modelOptions.isSessionAllowed && !(yield modelOptions.isSessionAllowed(session, user))))
|
|
83
95
|
return false;
|
|
84
96
|
sessionUsers.set(session, user);
|
|
85
97
|
return true;
|
|
86
98
|
}) }));
|
|
87
99
|
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 }));
|
|
88
100
|
this.modelOptions = modelOptions;
|
|
101
|
+
this.memberships = new Map();
|
|
102
|
+
this.contexts = new WeakSet();
|
|
89
103
|
this.authPaths = { basePath: "/api/auth", loginPath: "/api/auth/login" };
|
|
104
|
+
for (const [name, options] of Object.entries(modelOptions.memberships || {})) {
|
|
105
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
106
|
+
throw new Error("Invalid auth membership key.");
|
|
107
|
+
this.memberships.set(name, new AuthMembership_1.AuthMembership(options));
|
|
108
|
+
}
|
|
90
109
|
this.key = key;
|
|
91
110
|
this.sessionUsers = sessionUsers;
|
|
92
111
|
this.model = model;
|
|
@@ -191,6 +210,82 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
191
210
|
yield this.modelOptions.afterLogout(req, user, this);
|
|
192
211
|
});
|
|
193
212
|
}
|
|
213
|
+
/** Trusted SSO/server entry point; the subject must already be verified by the caller. */
|
|
214
|
+
establishBearerSession(req, subject, parent = null, environment) {
|
|
215
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
216
|
+
if (this.modelOptions.transport !== "bearer")
|
|
217
|
+
throw new Error("Bearer transport is not enabled for this auth scope.");
|
|
218
|
+
const id = this.fields.primaryKey === "_id" ? yield this.model.objectId(subject, { noExceptions: true }) : subject;
|
|
219
|
+
const user = id == null ? null : yield this.model.where(this.fields.primaryKey, "=", id).first();
|
|
220
|
+
if (!user || (this.fields.active !== false && user[this.fields.active] === false)
|
|
221
|
+
|| (this.modelOptions.isUserAllowed && !(yield this.modelOptions.isUserAllowed(user)))) {
|
|
222
|
+
throw new AuthLoginError("inactive_user", 403, this.message("inactive_user"));
|
|
223
|
+
}
|
|
224
|
+
if (this.modelOptions.beforeLogin) {
|
|
225
|
+
try {
|
|
226
|
+
yield this.modelOptions.beforeLogin(req, user, this);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
throw new AuthHookError("login_blocked", error);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const pair = yield this.sessions.create(subject, parent, environment);
|
|
233
|
+
if (this.modelOptions.trackActivity !== false)
|
|
234
|
+
yield this.sessions.recordLogin(pair.session, (0, AuthSecurity_1.authHeader)(req, "user-agent"));
|
|
235
|
+
if (this.modelOptions.onLogin)
|
|
236
|
+
yield this.modelOptions.onLogin(pair.session, req);
|
|
237
|
+
if (this.modelOptions.afterLogin)
|
|
238
|
+
yield this.modelOptions.afterLogin(req, user, this);
|
|
239
|
+
const { csrf_token } = pair, tokens = __rest(pair, ["csrf_token"]);
|
|
240
|
+
return tokens;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
logoutBearer(req) {
|
|
244
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
245
|
+
if (req.method !== "POST")
|
|
246
|
+
throw new WebAuth_1.WebAuthError(403, "Logout requires POST.");
|
|
247
|
+
const auth = yield this.resolveBearer(req);
|
|
248
|
+
if (!auth)
|
|
249
|
+
throw new AuthLoginError("unauthorized", 401, this.message("unauthorized"));
|
|
250
|
+
if (this.modelOptions.beforeLogout) {
|
|
251
|
+
try {
|
|
252
|
+
yield this.modelOptions.beforeLogout(req, auth.user, this);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
throw new AuthHookError("logout_blocked", error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
yield this.sessions.revoke(auth.session.id);
|
|
259
|
+
if (this.modelOptions.afterLogout)
|
|
260
|
+
yield this.modelOptions.afterLogout(req, auth.user, this);
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
/** Explicit Authorization credentials require no cookie CSRF and are never read from query parameters. */
|
|
264
|
+
resolveBearer(req, res) {
|
|
265
|
+
var _a;
|
|
266
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
267
|
+
if (this.modelOptions.transport !== "bearer")
|
|
268
|
+
throw new Error("Bearer transport is not enabled for this auth scope.");
|
|
269
|
+
res === null || res === void 0 ? void 0 : res.header("Cache-Control", "no-store");
|
|
270
|
+
Object.defineProperty(req, "auth", { value: null, writable: true, configurable: true, enumerable: false });
|
|
271
|
+
const header = (0, AuthSecurity_1.authHeader)(req, "authorization");
|
|
272
|
+
const token = (_a = /^Bearer ([A-Za-z0-9_-]{1,128}\.[A-Za-z0-9_-]{43})$/i.exec(header)) === null || _a === void 0 ? void 0 : _a[1];
|
|
273
|
+
if (!token)
|
|
274
|
+
return null;
|
|
275
|
+
const session = yield this.sessions.authenticate(token);
|
|
276
|
+
const user = session && this.sessionUsers.get(session);
|
|
277
|
+
if (!session || !user)
|
|
278
|
+
return null;
|
|
279
|
+
if (this.modelOptions.trackActivity !== false)
|
|
280
|
+
yield this.sessions.touch(session);
|
|
281
|
+
const auth = { user, session };
|
|
282
|
+
this.contexts.add(auth);
|
|
283
|
+
if (this.modelOptions.onAuthenticate)
|
|
284
|
+
yield this.modelOptions.onAuthenticate(auth, req);
|
|
285
|
+
req.auth = auth;
|
|
286
|
+
return auth;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
194
289
|
/** Resolve a fresh validated session and its model; never trusts a client-supplied req.auth. */
|
|
195
290
|
resolve(req, res) {
|
|
196
291
|
const _super = Object.create(null, {
|
|
@@ -198,6 +293,8 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
198
293
|
resume: { get: () => super.resume }
|
|
199
294
|
});
|
|
200
295
|
return __awaiter(this, void 0, void 0, function* () {
|
|
296
|
+
if (this.modelOptions.transport === "bearer")
|
|
297
|
+
return this.resolveBearer(req, res);
|
|
201
298
|
res === null || res === void 0 ? void 0 : res.header("Cache-Control", "no-store");
|
|
202
299
|
Object.defineProperty(req, "auth", { value: null, writable: true, configurable: true, enumerable: false });
|
|
203
300
|
const session = (yield _super.authenticate.call(this, req)) || (res ? yield _super.resume.call(this, req, res) : null);
|
|
@@ -209,12 +306,44 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
209
306
|
if (this.modelOptions.trackActivity !== false)
|
|
210
307
|
yield this.sessions.touch(session);
|
|
211
308
|
const auth = { user, session };
|
|
309
|
+
this.contexts.add(auth);
|
|
212
310
|
if (this.modelOptions.onAuthenticate)
|
|
213
311
|
yield this.modelOptions.onAuthenticate(auth, req);
|
|
214
312
|
req.auth = auth;
|
|
215
313
|
return auth;
|
|
216
314
|
});
|
|
217
315
|
}
|
|
316
|
+
/** For integrations whose authenticated User was established by another trusted transport. */
|
|
317
|
+
findMembership(name, user, resource) {
|
|
318
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
319
|
+
const resolver = this.memberships.get(name);
|
|
320
|
+
if (!resolver)
|
|
321
|
+
throw new Error(`Auth membership "${name}" is not configured.`);
|
|
322
|
+
return resolver.find(user, resource);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
/** Attaches a fresh membership only to a context authenticated by this scope. */
|
|
326
|
+
authorizeMembership(req, name, resource) {
|
|
327
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
328
|
+
const auth = req.auth;
|
|
329
|
+
if (!auth || !this.contexts.has(auth))
|
|
330
|
+
return null;
|
|
331
|
+
if (auth.memberships)
|
|
332
|
+
delete auth.memberships[name];
|
|
333
|
+
const resolver = this.memberships.get(name);
|
|
334
|
+
if (!resolver)
|
|
335
|
+
throw new Error(`Auth membership "${name}" is not configured.`);
|
|
336
|
+
if (!resolver.matchesEnvironment(resource, auth.session.environment))
|
|
337
|
+
return null;
|
|
338
|
+
const membership = yield resolver.find(auth.user, resource);
|
|
339
|
+
if (!membership)
|
|
340
|
+
return null;
|
|
341
|
+
if (!auth.memberships)
|
|
342
|
+
auth.memberships = Object.create(null);
|
|
343
|
+
auth.memberships[name] = { resource, membership };
|
|
344
|
+
return membership;
|
|
345
|
+
});
|
|
346
|
+
}
|
|
218
347
|
resolveCookies(cookies) {
|
|
219
348
|
return __awaiter(this, void 0, void 0, function* () {
|
|
220
349
|
const req = Object.assign(new Request_1.Request(), { method: "GET", headers: { cookie: `${this.cookies.prefix}access=${encodeURIComponent(cookies[`${this.cookies.prefix}access`] || "")}` } });
|
|
@@ -261,10 +390,29 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
261
390
|
});
|
|
262
391
|
}
|
|
263
392
|
handle(operation, req, res, loginResponse) {
|
|
264
|
-
var _a, _b;
|
|
393
|
+
var _a, _b, _c;
|
|
265
394
|
return __awaiter(this, void 0, void 0, function* () {
|
|
266
395
|
res.header("Cache-Control", "no-store");
|
|
267
396
|
try {
|
|
397
|
+
if (this.modelOptions.transport === "bearer") {
|
|
398
|
+
if (operation === "csrf")
|
|
399
|
+
return res.status(405).send({ status: "error", code: "unsupported_operation" });
|
|
400
|
+
if (req.method !== "POST")
|
|
401
|
+
throw new WebAuth_1.WebAuthError(403, "Authentication requires POST.");
|
|
402
|
+
if (operation === "logout") {
|
|
403
|
+
yield this.logoutBearer(req);
|
|
404
|
+
return res.send({ status: "success" });
|
|
405
|
+
}
|
|
406
|
+
if (operation === "refresh") {
|
|
407
|
+
const token = (_a = req.body) === null || _a === void 0 ? void 0 : _a.refresh_token;
|
|
408
|
+
const pair = typeof token === "string" ? yield this.sessions.refresh(token) : null;
|
|
409
|
+
if (!pair)
|
|
410
|
+
throw new AuthLoginError("unauthorized", 401, this.message("unauthorized"));
|
|
411
|
+
return res.send(Object.assign({ status: "success" }, pair));
|
|
412
|
+
}
|
|
413
|
+
const pair = yield this.establishBearerSession(req, yield this.verifyCredentials(req));
|
|
414
|
+
return res.send(Object.assign(Object.assign(Object.assign({}, (loginResponse ? yield loginResponse(req, pair.session) : {})), { status: "success" }), pair));
|
|
415
|
+
}
|
|
268
416
|
if (operation === "csrf")
|
|
269
417
|
return res.send({ csrf: this.bootstrap(req, res) });
|
|
270
418
|
if (operation === "logout") {
|
|
@@ -284,7 +432,7 @@ class AuthScope extends WebAuth_1.WebAuth {
|
|
|
284
432
|
if (!(error instanceof WebAuth_1.WebAuthError))
|
|
285
433
|
console.error(error);
|
|
286
434
|
return res.status(error instanceof WebAuth_1.WebAuthError ? error.status : 500).send({ status: "error", code,
|
|
287
|
-
message: error instanceof AuthHookError ? (
|
|
435
|
+
message: error instanceof AuthHookError ? (_c = (_b = this.modelOptions.messages) === null || _b === void 0 ? void 0 : _b[code]) !== null && _c !== void 0 ? _c : error.message : this.message(code) });
|
|
288
436
|
}
|
|
289
437
|
});
|
|
290
438
|
}
|
|
@@ -30,6 +30,8 @@ export type SessionAuthOptions = {
|
|
|
30
30
|
audience: string;
|
|
31
31
|
scope?: string;
|
|
32
32
|
environment?: string;
|
|
33
|
+
/** Allow server-selected environments per session instead of one fixed environment. */
|
|
34
|
+
allowDynamicEnvironment?: boolean;
|
|
33
35
|
connection?: string;
|
|
34
36
|
collection?: string;
|
|
35
37
|
accessTokenSeconds?: number;
|
|
@@ -58,7 +60,7 @@ export declare class SessionAuth {
|
|
|
58
60
|
private liveFilter;
|
|
59
61
|
private allowed;
|
|
60
62
|
private pair;
|
|
61
|
-
create(subject: string, parent?: AuthSession["parent"]): Promise<CreatedAuthSession>;
|
|
63
|
+
create(subject: string, parent?: AuthSession["parent"], environment?: string): Promise<CreatedAuthSession>;
|
|
62
64
|
authenticate(token: string): Promise<AuthSession | null>;
|
|
63
65
|
/** Trusted-server introspection; never expose this method as an unauthenticated endpoint. */
|
|
64
66
|
inspect(sessionId: string): Promise<AuthSession | null>;
|
package/dist/Auth/SessionAuth.js
CHANGED
|
@@ -23,8 +23,8 @@ class SessionAuth {
|
|
|
23
23
|
constructor(options) {
|
|
24
24
|
var _a;
|
|
25
25
|
this.options = options;
|
|
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"),
|
|
27
|
-
environment: options.environment === undefined ? null : (0, AuthSecurity_1.authText)(options.environment, "environment") };
|
|
26
|
+
this.scope = Object.assign({ 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") }, (options.allowDynamicEnvironment && options.environment === undefined ? {}
|
|
27
|
+
: { environment: options.environment === undefined ? null : (0, AuthSecurity_1.authText)(options.environment, "environment") }));
|
|
28
28
|
this.accessSeconds = (0, AuthSecurity_1.authLifetime)(options.accessTokenSeconds, 15 * 60);
|
|
29
29
|
this.sessionSeconds = (0, AuthSecurity_1.authLifetime)(options.sessionSeconds, 30 * 86400);
|
|
30
30
|
this.idleSeconds = options.idleTimeoutSeconds === undefined || options.idleTimeoutSeconds === false ? false : (0, AuthSecurity_1.authLifetime)(options.idleTimeoutSeconds, 7 * 86400);
|
|
@@ -66,16 +66,20 @@ class SessionAuth {
|
|
|
66
66
|
return { auth_token: access, refresh_token: refresh, auth_expires_at: record.accessExpiresAt,
|
|
67
67
|
refresh_expires_at: Math.min(record.expiresAt, record.createdAt + this.sessionSeconds * 1000), session: publicSession(record) };
|
|
68
68
|
}
|
|
69
|
-
create(subject, parent = null) {
|
|
69
|
+
create(subject, parent = null, environment) {
|
|
70
|
+
var _a;
|
|
70
71
|
return __awaiter(this, void 0, void 0, function* () {
|
|
71
72
|
(0, AuthSecurity_1.authText)(subject, "subject");
|
|
73
|
+
if (environment !== undefined && (!this.options.allowDynamicEnvironment || this.options.environment !== undefined)) {
|
|
74
|
+
throw new Error("Dynamic session environments are not enabled.");
|
|
75
|
+
}
|
|
72
76
|
if (parent) {
|
|
73
77
|
(0, AuthSecurity_1.authText)(parent.issuer, "parent issuer");
|
|
74
78
|
(0, AuthSecurity_1.authText)(parent.sessionId, "parent session");
|
|
75
79
|
}
|
|
76
80
|
const now = Date.now(), id = this.ids().create();
|
|
77
81
|
const access = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, refresh = `${id}.${(0, AuthSecurity_1.randomAuthToken)()}`, csrf = (0, AuthSecurity_1.randomAuthToken)();
|
|
78
|
-
const record = Object.assign(Object.assign({ _id: id, _subject: this.ids().create(subject) }, this.scope), { parent: parent ? Object.assign({}, parent) : null, createdAt: now, lastActiveAt: 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 });
|
|
82
|
+
const record = Object.assign(Object.assign({ _id: id, _subject: this.ids().create(subject) }, this.scope), { environment: environment === undefined ? (_a = this.scope.environment) !== null && _a !== void 0 ? _a : null : (0, AuthSecurity_1.authText)(environment, "environment"), parent: parent ? Object.assign({}, parent) : null, createdAt: now, lastActiveAt: 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 });
|
|
79
83
|
if (!(yield this.allowed(record)))
|
|
80
84
|
throw new Error("Session is not allowed.");
|
|
81
85
|
yield (yield this.rows()).insertOne(record);
|
|
@@ -118,7 +118,7 @@ class SsoAuthority {
|
|
|
118
118
|
yield (yield this.rows()).insertOne({ _id: (0, AuthSecurity_1.hashAuthToken)(code), issuer: this.issuer,
|
|
119
119
|
audience: client.id, subject: session.subject, environment: request.environment,
|
|
120
120
|
authoritySessionId: session.id, redirectUri: request.redirectUri, challenge: request.codeChallenge,
|
|
121
|
-
expiresAt: Math.min(Date.now() + 60000, session.expiresAt), consumedAt: null });
|
|
121
|
+
expiresAt: Math.min(Date.now() + 60000, session.expiresAt), purgeAt: new Date(Date.now() + 60000), consumedAt: null });
|
|
122
122
|
const redirect = new URL(request.redirectUri);
|
|
123
123
|
redirect.searchParams.set("code", code);
|
|
124
124
|
redirect.searchParams.set("state", request.state);
|
|
@@ -160,6 +160,7 @@ class SsoAuthority {
|
|
|
160
160
|
const session = client ? yield this.options.sessions.inspect(sessionId) : null;
|
|
161
161
|
if (!client || !session || !(yield this.options.authorize(session, client, environment)))
|
|
162
162
|
return null;
|
|
163
|
+
yield this.options.sessions.touch(session);
|
|
163
164
|
return { issuer: this.issuer, audience: client.id, subject: session.subject, environment,
|
|
164
165
|
authoritySessionId: session.id, expiresAt: session.expiresAt };
|
|
165
166
|
});
|
|
@@ -236,7 +237,7 @@ class SsoClient {
|
|
|
236
237
|
(0, AuthSecurity_1.authText)(environment, "environment");
|
|
237
238
|
const state = (0, AuthSecurity_1.randomAuthToken)(), binding = (0, AuthSecurity_1.randomAuthToken)(), verifier = (0, AuthSecurity_1.randomAuthToken)();
|
|
238
239
|
yield (yield this.rows()).insertOne({ _id: (0, AuthSecurity_1.hashAuthToken)(state), issuer: this.issuer, clientId: this.options.clientId,
|
|
239
|
-
bindingHash: (0, AuthSecurity_1.hashAuthToken)(binding), verifier, environment, expiresAt: Date.now() + 300000, consumedAt: null });
|
|
240
|
+
bindingHash: (0, AuthSecurity_1.hashAuthToken)(binding), verifier, environment, expiresAt: Date.now() + 300000, purgeAt: new Date(Date.now() + 300000), consumedAt: null });
|
|
240
241
|
this.cookies.write(res, "sso", binding, 300);
|
|
241
242
|
res.header("Referrer-Policy", "no-referrer");
|
|
242
243
|
const url = new URL(this.authorization.href);
|
package/dist/Auth/index.d.ts
CHANGED
package/dist/Auth/index.js
CHANGED
|
@@ -23,3 +23,5 @@ __exportStar(require("./SingleSignOn"), exports);
|
|
|
23
23
|
__exportStar(require("./ModelAuth"), exports);
|
|
24
24
|
__exportStar(require("./Auth"), exports);
|
|
25
25
|
__exportStar(require("./PasswordReset"), exports);
|
|
26
|
+
__exportStar(require("./AuthMembership"), exports);
|
|
27
|
+
__exportStar(require("./AuthHandoff"), exports);
|
|
@@ -32,6 +32,7 @@ declare class WebframezHooksFacade {
|
|
|
32
32
|
on<TEventName extends WebframezHookEventName>(eventName: TEventName, handler: WebframezHookHandler<TEventName>): () => this;
|
|
33
33
|
off<TEventName extends WebframezHookEventName>(eventName: TEventName, handler: WebframezHookHandler<TEventName>): this;
|
|
34
34
|
clear(eventName?: WebframezHookEventName): this;
|
|
35
|
+
hasListeners(eventName: WebframezHookEventName): boolean;
|
|
35
36
|
emit<TEventName extends WebframezHookEventName>(eventName: TEventName, context: Omit<Partial<WebframezOperationContext>, "operation" | "phase"> & {
|
|
36
37
|
operationId: string;
|
|
37
38
|
}): Promise<void>;
|
|
@@ -56,8 +56,16 @@ class WebframezHooksFacade {
|
|
|
56
56
|
this.handlers = {};
|
|
57
57
|
return this;
|
|
58
58
|
}
|
|
59
|
+
hasListeners(eventName) {
|
|
60
|
+
var _a;
|
|
61
|
+
return (((_a = this.handlers[eventName]) === null || _a === void 0 ? void 0 : _a.size) || 0) > 0;
|
|
62
|
+
}
|
|
59
63
|
emit(eventName, context) {
|
|
60
64
|
return __awaiter(this, void 0, void 0, function* () {
|
|
65
|
+
const handlers = this.handlers[eventName];
|
|
66
|
+
if (!handlers || handlers.size < 1) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
61
69
|
const parsed = parseEventName(eventName);
|
|
62
70
|
const attributes = Object.assign({}, (context.attributes || {}));
|
|
63
71
|
if (context.error && attributes["error.type"] === undefined) {
|
|
@@ -67,10 +75,6 @@ class WebframezHooksFacade {
|
|
|
67
75
|
type: eventName,
|
|
68
76
|
context: Object.assign(Object.assign({}, context), { operation: parsed.operation, phase: parsed.phase, timestamp: context.timestamp || new Date(), attributes }),
|
|
69
77
|
};
|
|
70
|
-
const handlers = this.handlers[eventName];
|
|
71
|
-
if (!handlers || handlers.size < 1) {
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
78
|
for (const handler of Array.from(handlers)) {
|
|
75
79
|
try {
|
|
76
80
|
yield handler(event);
|
package/dist/Router/Router.d.ts
CHANGED
|
@@ -65,6 +65,8 @@ declare class RouterFacade {
|
|
|
65
65
|
routesDELETE: {
|
|
66
66
|
[key: string]: RouteObject[];
|
|
67
67
|
};
|
|
68
|
+
private routeRegexCache;
|
|
69
|
+
private domainRegexCache;
|
|
68
70
|
/**
|
|
69
71
|
* Load the application-routes
|
|
70
72
|
*/
|
|
@@ -103,6 +105,8 @@ declare class RouterFacade {
|
|
|
103
105
|
extractParams(path: string, match: any): any;
|
|
104
106
|
escapeRegexPart(value: string): string;
|
|
105
107
|
buildRouteRegex(path: string): RegExp;
|
|
108
|
+
private getRouteRegex;
|
|
109
|
+
private getDomainRegex;
|
|
106
110
|
normalizeDomainString(value: string): string | null;
|
|
107
111
|
getRequestHostCandidates(request?: Request): string[];
|
|
108
112
|
getRouteDomainFilters(routeObj: RouteObject): string[];
|
package/dist/Router/Router.js
CHANGED
|
@@ -69,6 +69,8 @@ class RouterFacade {
|
|
|
69
69
|
* DELETE Route Store
|
|
70
70
|
*/
|
|
71
71
|
this.routesDELETE = {};
|
|
72
|
+
this.routeRegexCache = new Map();
|
|
73
|
+
this.domainRegexCache = new Map();
|
|
72
74
|
}
|
|
73
75
|
/**
|
|
74
76
|
* Load the application-routes
|
|
@@ -88,6 +90,8 @@ class RouterFacade {
|
|
|
88
90
|
this.routesPUT = {};
|
|
89
91
|
this.routesPATCH = {};
|
|
90
92
|
this.routesDELETE = {};
|
|
93
|
+
this.routeRegexCache.clear();
|
|
94
|
+
this.domainRegexCache.clear();
|
|
91
95
|
// Load routes
|
|
92
96
|
if (options.routesFunction) {
|
|
93
97
|
options.routesFunction();
|
|
@@ -139,6 +143,10 @@ class RouterFacade {
|
|
|
139
143
|
else {
|
|
140
144
|
routeConf.component = component;
|
|
141
145
|
}
|
|
146
|
+
this.getRouteRegex(path);
|
|
147
|
+
for (const domain of this.getRouteDomainFilters(routeConf)) {
|
|
148
|
+
this.getDomainRegex(domain);
|
|
149
|
+
}
|
|
142
150
|
if (type === "GET") {
|
|
143
151
|
if (!this.routesGET[path]) {
|
|
144
152
|
this.routesGET[path] = [];
|
|
@@ -273,6 +281,22 @@ class RouterFacade {
|
|
|
273
281
|
const regexString = "^" + escaped.replace(/__WFZ_TOKEN_(\d+)__/g, (all, tokenIndex) => placeholders[tokenIndex]) + "$";
|
|
274
282
|
return new RegExp(regexString);
|
|
275
283
|
}
|
|
284
|
+
getRouteRegex(path) {
|
|
285
|
+
let regex = this.routeRegexCache.get(path);
|
|
286
|
+
if (!regex) {
|
|
287
|
+
regex = this.buildRouteRegex(path);
|
|
288
|
+
this.routeRegexCache.set(path, regex);
|
|
289
|
+
}
|
|
290
|
+
return regex;
|
|
291
|
+
}
|
|
292
|
+
getDomainRegex(domain) {
|
|
293
|
+
let regex = this.domainRegexCache.get(domain);
|
|
294
|
+
if (!regex) {
|
|
295
|
+
regex = this.buildDomainRegex(domain);
|
|
296
|
+
this.domainRegexCache.set(domain, regex);
|
|
297
|
+
}
|
|
298
|
+
return regex;
|
|
299
|
+
}
|
|
276
300
|
normalizeDomainString(value) {
|
|
277
301
|
if (!value || value.trim() === "") {
|
|
278
302
|
return null;
|
|
@@ -342,7 +366,7 @@ class RouterFacade {
|
|
|
342
366
|
return new RegExp("^" + regexBody + "$", "i");
|
|
343
367
|
}
|
|
344
368
|
matchDomainFilter(hostname, domainFilter) {
|
|
345
|
-
const regex = this.
|
|
369
|
+
const regex = this.getDomainRegex(domainFilter);
|
|
346
370
|
const match = hostname.match(regex);
|
|
347
371
|
if (!match) {
|
|
348
372
|
return { matches: false, matchedDomain: null, wildcard: null };
|
|
@@ -411,7 +435,7 @@ class RouterFacade {
|
|
|
411
435
|
continue;
|
|
412
436
|
}
|
|
413
437
|
for (const routeObj of routeCandidates) {
|
|
414
|
-
const match = url.match(this.
|
|
438
|
+
const match = url.match(this.getRouteRegex(routeObj.path));
|
|
415
439
|
if (!match) {
|
|
416
440
|
continue;
|
|
417
441
|
}
|
|
@@ -442,14 +466,16 @@ class RouterFacade {
|
|
|
442
466
|
: this.mode === "aws-lambda" && options && options.event && options.event.requestContext && options.event.requestContext.http
|
|
443
467
|
? options.event.requestContext.http.method
|
|
444
468
|
: null;
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
469
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.start")) {
|
|
470
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.start", {
|
|
471
|
+
operationId: httpOperationId,
|
|
472
|
+
name: initialMethod || (this.mode === "aws-lambda" ? "lambda" : "request"),
|
|
473
|
+
attributes: {
|
|
474
|
+
"http.request.method": initialMethod,
|
|
475
|
+
"webframez.mode": this.mode,
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
}
|
|
453
479
|
let request;
|
|
454
480
|
let routeOperationId = null;
|
|
455
481
|
try {
|
|
@@ -478,15 +504,17 @@ class RouterFacade {
|
|
|
478
504
|
},
|
|
479
505
|
});
|
|
480
506
|
const errorResult = this.handleError(fallbackRequest, response, statusCode, e);
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
507
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
508
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
509
|
+
operationId: httpOperationId,
|
|
510
|
+
name: `${fallbackRequest.method || "HTTP"} unmatched`,
|
|
511
|
+
status: "error",
|
|
512
|
+
error: e,
|
|
513
|
+
request: fallbackRequest,
|
|
514
|
+
response,
|
|
515
|
+
attributes: Object.assign(Object.assign({ "http.request.method": fallbackRequest.method || null, "http.response.status_code": statusCode }, requestTelemetryAttributes(fallbackRequest)), { "webframez.mode": this.mode }),
|
|
516
|
+
});
|
|
517
|
+
}
|
|
490
518
|
return errorResult;
|
|
491
519
|
}
|
|
492
520
|
const route = this.dissolve(request);
|
|
@@ -496,83 +524,95 @@ class RouterFacade {
|
|
|
496
524
|
}
|
|
497
525
|
if (!route) {
|
|
498
526
|
const notFoundResult = this.handleError(request, response, 404, "Not found '" + request.url + "' ...");
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
527
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
528
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
529
|
+
operationId: httpOperationId,
|
|
530
|
+
name: `${request.method} unmatched`,
|
|
531
|
+
status: "ok",
|
|
532
|
+
request,
|
|
533
|
+
response,
|
|
534
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404 }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
535
|
+
});
|
|
536
|
+
}
|
|
507
537
|
return notFoundResult;
|
|
508
538
|
}
|
|
509
539
|
if (!(route.component && typeof route.component === "function") && !(route.controller && route.method_name)) {
|
|
510
540
|
const missingRouteResult = this.handleError(request, response, 404, "Missing route function ...");
|
|
511
|
-
|
|
512
|
-
operationId: httpOperationId,
|
|
513
|
-
name: `${request.method} ${route.path}`,
|
|
514
|
-
status: "ok",
|
|
515
|
-
request,
|
|
516
|
-
response,
|
|
517
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
518
|
-
});
|
|
519
|
-
return missingRouteResult;
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
yield this.handleMiddleware(route, request, response);
|
|
523
|
-
if (request.method === "OPTIONS" && !request.skipOptionsForward) {
|
|
524
|
-
const optionsResult = this.handleReturn(request, response, "");
|
|
541
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
525
542
|
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
526
543
|
operationId: httpOperationId,
|
|
527
544
|
name: `${request.method} ${route.path}`,
|
|
528
545
|
status: "ok",
|
|
529
546
|
request,
|
|
530
547
|
response,
|
|
531
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
548
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
532
549
|
});
|
|
533
|
-
return optionsResult;
|
|
534
550
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
"http.request.method": request.method,
|
|
543
|
-
"url.template": route.path,
|
|
544
|
-
"webframez.route": route.path,
|
|
545
|
-
"webframez.route.handler": route.controller && route.method_name
|
|
546
|
-
? `${route.controller.name || "Controller"}@${route.method_name}`
|
|
547
|
-
: "component",
|
|
548
|
-
},
|
|
549
|
-
});
|
|
550
|
-
if (route.controller && route.method_name) {
|
|
551
|
-
const controllerInstance = new route.controller();
|
|
552
|
-
if (!controllerInstance[route.method_name]) {
|
|
553
|
-
const unknownMethodResult = this.handleError(request, response, 404, "Unknown method '" + route.method_name + "'.");
|
|
554
|
-
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
555
|
-
operationId: routeOperationId,
|
|
556
|
-
parentOperationId: httpOperationId,
|
|
557
|
-
name: `${request.method} ${route.path}`,
|
|
558
|
-
status: "error",
|
|
559
|
-
error: new Error("Unknown route method"),
|
|
560
|
-
attributes: {
|
|
561
|
-
"http.request.method": request.method,
|
|
562
|
-
"http.response.status_code": 404,
|
|
563
|
-
"url.template": route.path,
|
|
564
|
-
"webframez.route": route.path,
|
|
565
|
-
"webframez.route.handler": `${route.controller.name || "Controller"}@${route.method_name}`,
|
|
566
|
-
},
|
|
567
|
-
});
|
|
551
|
+
return missingRouteResult;
|
|
552
|
+
}
|
|
553
|
+
try {
|
|
554
|
+
yield this.handleMiddleware(route, request, response);
|
|
555
|
+
if (request.method === "OPTIONS" && !request.skipOptionsForward) {
|
|
556
|
+
const optionsResult = this.handleReturn(request, response, "");
|
|
557
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
568
558
|
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
569
559
|
operationId: httpOperationId,
|
|
570
560
|
name: `${request.method} ${route.path}`,
|
|
571
561
|
status: "ok",
|
|
572
562
|
request,
|
|
573
563
|
response,
|
|
574
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
564
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
575
565
|
});
|
|
566
|
+
}
|
|
567
|
+
return optionsResult;
|
|
568
|
+
}
|
|
569
|
+
let result = null;
|
|
570
|
+
routeOperationId = WebframezHooks_1.WebframezHooks.createOperationId("route");
|
|
571
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.start")) {
|
|
572
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.start", {
|
|
573
|
+
operationId: routeOperationId,
|
|
574
|
+
parentOperationId: httpOperationId,
|
|
575
|
+
name: `${request.method} ${route.path}`,
|
|
576
|
+
attributes: {
|
|
577
|
+
"http.request.method": request.method,
|
|
578
|
+
"url.template": route.path,
|
|
579
|
+
"webframez.route": route.path,
|
|
580
|
+
"webframez.route.handler": route.controller && route.method_name
|
|
581
|
+
? `${route.controller.name || "Controller"}@${route.method_name}`
|
|
582
|
+
: "component",
|
|
583
|
+
},
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
if (route.controller && route.method_name) {
|
|
587
|
+
const controllerInstance = new route.controller();
|
|
588
|
+
if (!controllerInstance[route.method_name]) {
|
|
589
|
+
const unknownMethodResult = this.handleError(request, response, 404, "Unknown method '" + route.method_name + "'.");
|
|
590
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.error")) {
|
|
591
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
592
|
+
operationId: routeOperationId,
|
|
593
|
+
parentOperationId: httpOperationId,
|
|
594
|
+
name: `${request.method} ${route.path}`,
|
|
595
|
+
status: "error",
|
|
596
|
+
error: new Error("Unknown route method"),
|
|
597
|
+
attributes: {
|
|
598
|
+
"http.request.method": request.method,
|
|
599
|
+
"http.response.status_code": 404,
|
|
600
|
+
"url.template": route.path,
|
|
601
|
+
"webframez.route": route.path,
|
|
602
|
+
"webframez.route.handler": `${route.controller.name || "Controller"}@${route.method_name}`,
|
|
603
|
+
},
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
607
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
608
|
+
operationId: httpOperationId,
|
|
609
|
+
name: `${request.method} ${route.path}`,
|
|
610
|
+
status: "ok",
|
|
611
|
+
request,
|
|
612
|
+
response,
|
|
613
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 404, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
614
|
+
});
|
|
615
|
+
}
|
|
576
616
|
return unknownMethodResult;
|
|
577
617
|
}
|
|
578
618
|
result = yield controllerInstance[route.method_name].bind(controllerInstance)(request, response);
|
|
@@ -581,40 +621,46 @@ class RouterFacade {
|
|
|
581
621
|
result = yield route.component(request, response);
|
|
582
622
|
}
|
|
583
623
|
const returnResult = this.handleReturn(request, response, result);
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
request,
|
|
601
|
-
response,
|
|
602
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
603
|
-
});
|
|
604
|
-
return returnResult;
|
|
605
|
-
}
|
|
606
|
-
catch (e) {
|
|
607
|
-
if (this.isMiddlewareRejectSignal(e)) {
|
|
608
|
-
const middlewareErrorResult = this.handleError(request, response, 500, e.reason);
|
|
609
|
-
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
624
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.end")) {
|
|
625
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.end", {
|
|
626
|
+
operationId: routeOperationId,
|
|
627
|
+
parentOperationId: httpOperationId,
|
|
628
|
+
name: `${request.method} ${route.path}`,
|
|
629
|
+
status: "ok",
|
|
630
|
+
attributes: {
|
|
631
|
+
"http.request.method": request.method,
|
|
632
|
+
"http.response.status_code": response.statusCode || 200,
|
|
633
|
+
"url.template": route.path,
|
|
634
|
+
"webframez.route": route.path,
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.end")) {
|
|
639
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.end", {
|
|
610
640
|
operationId: httpOperationId,
|
|
611
641
|
name: `${request.method} ${route.path}`,
|
|
612
|
-
status: "
|
|
613
|
-
error: e.reason || e,
|
|
642
|
+
status: "ok",
|
|
614
643
|
request,
|
|
615
644
|
response,
|
|
616
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code":
|
|
645
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": response.statusCode || 200, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
617
646
|
});
|
|
647
|
+
}
|
|
648
|
+
return returnResult;
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
if (this.isMiddlewareRejectSignal(e)) {
|
|
652
|
+
const middlewareErrorResult = this.handleError(request, response, 500, e.reason);
|
|
653
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
654
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
655
|
+
operationId: httpOperationId,
|
|
656
|
+
name: `${request.method} ${route.path}`,
|
|
657
|
+
status: "error",
|
|
658
|
+
error: e.reason || e,
|
|
659
|
+
request,
|
|
660
|
+
response,
|
|
661
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
662
|
+
});
|
|
663
|
+
}
|
|
618
664
|
return middlewareErrorResult;
|
|
619
665
|
}
|
|
620
666
|
yield ErrorHandler_1.ErrorHandler.report(e, {
|
|
@@ -632,29 +678,33 @@ class RouterFacade {
|
|
|
632
678
|
});
|
|
633
679
|
const errorResult = this.handleError(request, response, 500, e);
|
|
634
680
|
if (routeOperationId) {
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
681
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("route.handler.error")) {
|
|
682
|
+
yield WebframezHooks_1.WebframezHooks.emit("route.handler.error", {
|
|
683
|
+
operationId: routeOperationId,
|
|
684
|
+
parentOperationId: httpOperationId,
|
|
685
|
+
name: `${request.method} ${route.path}`,
|
|
686
|
+
status: "error",
|
|
687
|
+
error: e,
|
|
688
|
+
attributes: {
|
|
689
|
+
"http.request.method": request.method,
|
|
690
|
+
"http.response.status_code": 500,
|
|
691
|
+
"url.template": route.path,
|
|
692
|
+
"webframez.route": route.path,
|
|
693
|
+
},
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
if (WebframezHooks_1.WebframezHooks.hasListeners("http.request.error")) {
|
|
698
|
+
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
699
|
+
operationId: httpOperationId,
|
|
638
700
|
name: `${request.method} ${route.path}`,
|
|
639
701
|
status: "error",
|
|
640
702
|
error: e,
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
"url.template": route.path,
|
|
645
|
-
"webframez.route": route.path,
|
|
646
|
-
},
|
|
703
|
+
request,
|
|
704
|
+
response,
|
|
705
|
+
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
647
706
|
});
|
|
648
707
|
}
|
|
649
|
-
yield WebframezHooks_1.WebframezHooks.emit("http.request.error", {
|
|
650
|
-
operationId: httpOperationId,
|
|
651
|
-
name: `${request.method} ${route.path}`,
|
|
652
|
-
status: "error",
|
|
653
|
-
error: e,
|
|
654
|
-
request,
|
|
655
|
-
response,
|
|
656
|
-
attributes: Object.assign(Object.assign({ "http.request.method": request.method, "http.response.status_code": 500, "url.template": route.path, "webframez.route": route.path }, requestTelemetryAttributes(request)), { "webframez.mode": this.mode }),
|
|
657
|
-
});
|
|
658
708
|
return errorResult;
|
|
659
709
|
}
|
|
660
710
|
});
|
package/docs/authentication.md
CHANGED
|
@@ -495,3 +495,70 @@ when serializing entire models. Auth snapshots always exclude these fields.
|
|
|
495
495
|
user automatically. Existing application plaintext reset links are intentionally
|
|
496
496
|
not accepted; users must request a new link after migration. Reset pages should
|
|
497
497
|
use `Referrer-Policy: no-referrer` and avoid response caching.
|
|
498
|
+
|
|
499
|
+
## Declarative memberships
|
|
500
|
+
|
|
501
|
+
Authentication identifies the user; memberships authorize access to a server-loaded resource.
|
|
502
|
+
Configure named membership models on a scope:
|
|
503
|
+
|
|
504
|
+
```ts
|
|
505
|
+
const scope = Auth.registerScope("manager", {
|
|
506
|
+
model: User,
|
|
507
|
+
memberships: {
|
|
508
|
+
environment: {
|
|
509
|
+
model: EnvironmentUser,
|
|
510
|
+
bindings: [{
|
|
511
|
+
user: { foreignKey: "_user", localKeys: ["_id"] },
|
|
512
|
+
resource: { foreignKey: "_env", localKeys: ["_id"] },
|
|
513
|
+
}],
|
|
514
|
+
conditions: [{ field: "is_deleted", operator: "!=", value: true }],
|
|
515
|
+
},
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
await scope.resolve(req, res);
|
|
520
|
+
const membership = await scope.authorizeMembership(req, "environment", environment);
|
|
521
|
+
if (!membership) return res.status(403).send({ code: "forbidden" });
|
|
522
|
+
// Actual server-side Models; excluded from browser snapshots:
|
|
523
|
+
req.auth.memberships.environment.membership;
|
|
524
|
+
req.auth.memberships.environment.resource;
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
Every authorization performs a fresh lookup. Missing IDs and unvalidated request contexts
|
|
528
|
+
fail closed. `userConditions` and `resourceConditions` optionally constrain model status.
|
|
529
|
+
`sessionEnvironmentKey` optionally binds resource authorization to a federated session environment.
|
|
530
|
+
Multiple `bindings` support legacy schemas; `localKeys` can accept local and synchronized
|
|
531
|
+
resource references. Set `user.firstAvailable: true` to prefer the first available user
|
|
532
|
+
reference rather than matching all aliases. `findMembership(name, user, resource)` is the
|
|
533
|
+
trusted-server adapter for integrations that already authenticate through another transport.
|
|
534
|
+
It does not authenticate the supplied user or create a request auth context.
|
|
535
|
+
|
|
536
|
+
## Bearer scopes for separate API origins
|
|
537
|
+
|
|
538
|
+
Cookie scopes remain the default. An explicitly configured `transport: "bearer"` scope
|
|
539
|
+
uses opaque, hashed Core access/refresh credentials through Authorization instead of cookies.
|
|
540
|
+
`Route.auth()` returns `auth_token`, `refresh_token` and their expiry timestamps. Refresh
|
|
541
|
+
requires POST with `refresh_token`; logout requires POST with the access Authorization header.
|
|
542
|
+
Cookie/query credentials are never accepted by `scope.resolve()` in bearer mode. Cookie CSRF
|
|
543
|
+
behavior remains unchanged; bearer endpoints do not use ambient browser credentials.
|
|
544
|
+
|
|
545
|
+
Use the lightweight `@webtypen/webframez-core/bearer-auth-client` entry point's
|
|
546
|
+
`createBearerAuthFetch()` with application-owned `configuration`, `load`, `save` and `clear`
|
|
547
|
+
callbacks. Configure only trusted API targets and store credentials separately per instance.
|
|
548
|
+
It renews expired access credentials, shares refresh requests within a client, uses Web Locks
|
|
549
|
+
across browser tabs when available, and retries explicit unauthorized responses once.
|
|
550
|
+
It clears credentials after an ambiguous network failure instead of replaying a rotating secret.
|
|
551
|
+
Without Web Locks, cross-tab coordination must be provided by the host application.
|
|
552
|
+
|
|
553
|
+
`establishBearerSession(req, subject, parent?, environment?)` is a trusted server entry point
|
|
554
|
+
following verified SSO; never expose a caller-supplied subject as public login input. Lifecycle
|
|
555
|
+
hooks apply. Set `session.allowDynamicEnvironment: true` to bind individual sessions to a
|
|
556
|
+
server-selected environment; fixed/default session environment behavior stays unchanged.
|
|
557
|
+
`isSessionAllowed(session, user)` can validate a parent SSO session and fails closed on denial.
|
|
558
|
+
|
|
559
|
+
`AuthHandoff` delivers a short-lived one-use code to a separate frontend after the API callback.
|
|
560
|
+
Its payload is encrypted with AES-GCM, its code is hashed, and redemption is atomic. Neither
|
|
561
|
+
access nor refresh credentials need appear in navigation URLs. Configure a strong server secret
|
|
562
|
+
and an instance-specific audience. Create TTL indexes on `auth_handoffs.purgeAt`,
|
|
563
|
+
`auth_sso_transactions.purgeAt` and `auth_sso_codes.purgeAt`, or schedule their cleanup methods.
|
|
564
|
+
SSO introspection records successful central session activity, respecting its absolute expiry.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webtypen/webframez-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.78",
|
|
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",
|
|
@@ -30,7 +30,9 @@
|
|
|
30
30
|
"routing.js",
|
|
31
31
|
"routing.d.ts",
|
|
32
32
|
"auth-client.js",
|
|
33
|
-
"auth-client.d.ts"
|
|
33
|
+
"auth-client.d.ts",
|
|
34
|
+
"bearer-auth-client.js",
|
|
35
|
+
"bearer-auth-client.d.ts"
|
|
34
36
|
],
|
|
35
37
|
"scripts": {
|
|
36
38
|
"build": "tsc",
|