@jskit-ai/auth-core 0.1.4
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/package.descriptor.mjs +95 -0
- package/package.json +51 -0
- package/src/client/authApi.js +1 -0
- package/src/client/index.js +2 -0
- package/src/client/providers/AccessCoreClientProvider.js +23 -0
- package/src/client/providers/FastifyAuthPolicyClientProvider.js +13 -0
- package/src/client/signOutFlow.js +1 -0
- package/src/server/inviteTokens.js +41 -0
- package/src/server/lib/actionContextContributor.js +36 -0
- package/src/server/lib/authPolicySupport.js +38 -0
- package/src/server/lib/errors.js +20 -0
- package/src/server/lib/index.js +3 -0
- package/src/server/lib/objectUtils.js +5 -0
- package/src/server/lib/plugin.js +247 -0
- package/src/server/lib/routeMeta.js +64 -0
- package/src/server/lib/routeVisibilityResolver.js +25 -0
- package/src/server/lib/tokens.js +3 -0
- package/src/server/membershipAccess.js +67 -0
- package/src/server/providers/AccessCoreServiceProvider.js +35 -0
- package/src/server/providers/FastifyAuthPolicyServiceProvider.js +124 -0
- package/src/server/utils.js +26 -0
- package/src/server/validators.js +183 -0
- package/src/shared/authApi.js +50 -0
- package/src/shared/authConstraints.js +13 -0
- package/src/shared/authMethods.js +170 -0
- package/src/shared/authPaths.js +24 -0
- package/src/shared/commands/authCommandValidators.js +255 -0
- package/src/shared/commands/authLoginOAuthCompleteCommand.js +68 -0
- package/src/shared/commands/authLoginOAuthStartCommand.js +72 -0
- package/src/shared/commands/authLoginOtpRequestCommand.js +56 -0
- package/src/shared/commands/authLoginOtpVerifyCommand.js +64 -0
- package/src/shared/commands/authLoginPasswordCommand.js +57 -0
- package/src/shared/commands/authLogoutCommand.js +23 -0
- package/src/shared/commands/authPasswordRecoveryCompleteCommand.js +67 -0
- package/src/shared/commands/authPasswordResetCommand.js +49 -0
- package/src/shared/commands/authPasswordResetRequestCommand.js +50 -0
- package/src/shared/commands/authRegisterCommand.js +57 -0
- package/src/shared/commands/authSessionReadCommand.js +26 -0
- package/src/shared/index.js +3 -0
- package/src/shared/inputNormalization.js +1 -0
- package/src/shared/inviteTokens.js +38 -0
- package/src/shared/oauthCallbackParams.js +5 -0
- package/src/shared/oauthProviders.js +66 -0
- package/src/shared/signOutFlow.js +28 -0
- package/test/actionContextContributor.test.js +44 -0
- package/test/authApi.test.js +47 -0
- package/test/authMethods.test.js +95 -0
- package/test/authPaths.test.js +17 -0
- package/test/commandValidators.test.js +33 -0
- package/test/plugin.test.js +250 -0
- package/test/providerRuntime.test.js +114 -0
- package/test/routeMeta.test.js +95 -0
- package/test/routeVisibilityResolver.test.js +34 -0
- package/test/serverUtils.test.js +28 -0
- package/test/signOutFlow.test.js +67 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
export default Object.freeze({
|
|
2
|
+
"packageVersion": 1,
|
|
3
|
+
"packageId": "@jskit-ai/auth-core",
|
|
4
|
+
"version": "0.1.4",
|
|
5
|
+
"dependsOn": [
|
|
6
|
+
"@jskit-ai/value-app-config-shared"
|
|
7
|
+
],
|
|
8
|
+
"capabilities": {
|
|
9
|
+
"provides": [
|
|
10
|
+
"auth.access",
|
|
11
|
+
"auth.policy"
|
|
12
|
+
],
|
|
13
|
+
"requires": []
|
|
14
|
+
},
|
|
15
|
+
"runtime": {
|
|
16
|
+
"server": {
|
|
17
|
+
"providerEntrypoint": "src/server/providers/AccessCoreServiceProvider.js",
|
|
18
|
+
"providers": [
|
|
19
|
+
{
|
|
20
|
+
"entrypoint": "src/server/providers/AccessCoreServiceProvider.js",
|
|
21
|
+
"export": "AccessCoreServiceProvider"
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"entrypoint": "src/server/providers/FastifyAuthPolicyServiceProvider.js",
|
|
25
|
+
"export": "FastifyAuthPolicyServiceProvider"
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
"client": {
|
|
30
|
+
"providers": [
|
|
31
|
+
{
|
|
32
|
+
"entrypoint": "src/client/providers/AccessCoreClientProvider.js",
|
|
33
|
+
"export": "AccessCoreClientProvider"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"entrypoint": "src/client/providers/FastifyAuthPolicyClientProvider.js",
|
|
37
|
+
"export": "FastifyAuthPolicyClientProvider"
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"metadata": {
|
|
43
|
+
"apiSummary": {
|
|
44
|
+
"surfaces": [
|
|
45
|
+
{
|
|
46
|
+
"subpath": "./client",
|
|
47
|
+
"summary": "Exports client auth access APIs plus AccessCoreClientProvider/FastifyAuthPolicyClientProvider."
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"subpath": "./server",
|
|
51
|
+
"summary": "Exports server auth access and Fastify auth policy providers plus server auth utility modules."
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"subpath": "./shared",
|
|
55
|
+
"summary": "Exports shared auth client helpers (createApi and runAuthSignOutFlow), with structured subpaths at ./shared/authApi and ./shared/signOutFlow."
|
|
56
|
+
}
|
|
57
|
+
],
|
|
58
|
+
"containerTokens": {
|
|
59
|
+
"server": [
|
|
60
|
+
"auth.access"
|
|
61
|
+
],
|
|
62
|
+
"client": [
|
|
63
|
+
"auth.access.client"
|
|
64
|
+
]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
"mutations": {
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"runtime": {
|
|
71
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
72
|
+
"@fastify/cookie": "^11.0.2",
|
|
73
|
+
"@fastify/csrf-protection": "^7.1.0",
|
|
74
|
+
"@fastify/rate-limit": "^10.3.0"
|
|
75
|
+
},
|
|
76
|
+
"dev": {}
|
|
77
|
+
},
|
|
78
|
+
"packageJson": {
|
|
79
|
+
"scripts": {}
|
|
80
|
+
},
|
|
81
|
+
"procfile": {},
|
|
82
|
+
"files": [],
|
|
83
|
+
"text": [
|
|
84
|
+
{
|
|
85
|
+
"file": ".env",
|
|
86
|
+
"op": "upsert-env",
|
|
87
|
+
"key": "AUTH_PROFILE_MODE",
|
|
88
|
+
"value": "standalone",
|
|
89
|
+
"reason": "Default auth profile mode to standalone when users-core is not installed.",
|
|
90
|
+
"category": "runtime-config",
|
|
91
|
+
"id": "auth-profile-mode"
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jskit-ai/auth-core",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"scripts": {
|
|
6
|
+
"test": "node --test"
|
|
7
|
+
},
|
|
8
|
+
"exports": {
|
|
9
|
+
"./server/providers/AccessCoreServiceProvider": "./src/server/providers/AccessCoreServiceProvider.js",
|
|
10
|
+
"./server/providers/FastifyAuthPolicyServiceProvider": "./src/server/providers/FastifyAuthPolicyServiceProvider.js",
|
|
11
|
+
"./server/utils": "./src/server/utils.js",
|
|
12
|
+
"./server/validators": "./src/server/validators.js",
|
|
13
|
+
"./server/inviteTokens": "./src/server/inviteTokens.js",
|
|
14
|
+
"./server/membershipAccess": "./src/server/membershipAccess.js",
|
|
15
|
+
"./server/lib/index": "./src/server/lib/index.js",
|
|
16
|
+
"./server/lib/plugin": "./src/server/lib/plugin.js",
|
|
17
|
+
"./server/lib/routeMeta": "./src/server/lib/routeMeta.js",
|
|
18
|
+
"./server/lib/tokens": "./src/server/lib/tokens.js",
|
|
19
|
+
"./client": "./src/client/index.js",
|
|
20
|
+
"./client/authApi": "./src/client/authApi.js",
|
|
21
|
+
"./client/signOutFlow": "./src/client/signOutFlow.js",
|
|
22
|
+
"./shared": "./src/shared/index.js",
|
|
23
|
+
"./shared/authApi": "./src/shared/authApi.js",
|
|
24
|
+
"./shared/signOutFlow": "./src/shared/signOutFlow.js",
|
|
25
|
+
"./shared/authPaths": "./src/shared/authPaths.js",
|
|
26
|
+
"./shared/authConstraints": "./src/shared/authConstraints.js",
|
|
27
|
+
"./shared/authMethods": "./src/shared/authMethods.js",
|
|
28
|
+
"./shared/oauthProviders": "./src/shared/oauthProviders.js",
|
|
29
|
+
"./shared/oauthCallbackParams": "./src/shared/oauthCallbackParams.js",
|
|
30
|
+
"./shared/inputNormalization": "./src/shared/inputNormalization.js",
|
|
31
|
+
"./shared/inviteTokens": "./src/shared/inviteTokens.js",
|
|
32
|
+
"./shared/commands/authRegisterCommand": "./src/shared/commands/authRegisterCommand.js",
|
|
33
|
+
"./shared/commands/authLoginPasswordCommand": "./src/shared/commands/authLoginPasswordCommand.js",
|
|
34
|
+
"./shared/commands/authLoginOtpRequestCommand": "./src/shared/commands/authLoginOtpRequestCommand.js",
|
|
35
|
+
"./shared/commands/authLoginOtpVerifyCommand": "./src/shared/commands/authLoginOtpVerifyCommand.js",
|
|
36
|
+
"./shared/commands/authLoginOAuthStartCommand": "./src/shared/commands/authLoginOAuthStartCommand.js",
|
|
37
|
+
"./shared/commands/authLoginOAuthCompleteCommand": "./src/shared/commands/authLoginOAuthCompleteCommand.js",
|
|
38
|
+
"./shared/commands/authPasswordResetRequestCommand": "./src/shared/commands/authPasswordResetRequestCommand.js",
|
|
39
|
+
"./shared/commands/authPasswordRecoveryCompleteCommand": "./src/shared/commands/authPasswordRecoveryCompleteCommand.js",
|
|
40
|
+
"./shared/commands/authPasswordResetCommand": "./src/shared/commands/authPasswordResetCommand.js",
|
|
41
|
+
"./shared/commands/authLogoutCommand": "./src/shared/commands/authLogoutCommand.js",
|
|
42
|
+
"./shared/commands/authSessionReadCommand": "./src/shared/commands/authSessionReadCommand.js"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@jskit-ai/kernel": "0.1.4",
|
|
46
|
+
"@fastify/cookie": "^11.0.2",
|
|
47
|
+
"@fastify/csrf-protection": "^7.1.0",
|
|
48
|
+
"@fastify/rate-limit": "^10.3.0",
|
|
49
|
+
"typebox": "^1.0.81"
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createApi } from "../shared/authApi.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { createApi as createAuthApi } from "../authApi.js";
|
|
2
|
+
import { runAuthSignOutFlow } from "../signOutFlow.js";
|
|
3
|
+
|
|
4
|
+
const CLIENT_API = Object.freeze({
|
|
5
|
+
createAuthApi,
|
|
6
|
+
runAuthSignOutFlow
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
class AccessCoreClientProvider {
|
|
10
|
+
static id = "auth.access.client";
|
|
11
|
+
|
|
12
|
+
register(app) {
|
|
13
|
+
if (!app || typeof app.singleton !== "function") {
|
|
14
|
+
throw new Error("AccessCoreClientProvider requires application singleton().");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
app.singleton("auth.access.client", () => CLIENT_API);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
boot() {}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { AccessCoreClientProvider };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
class FastifyAuthPolicyClientProvider {
|
|
2
|
+
static id = "auth.policy.client";
|
|
3
|
+
|
|
4
|
+
register(app) {
|
|
5
|
+
if (!app || typeof app.singleton !== "function") {
|
|
6
|
+
throw new Error("FastifyAuthPolicyClientProvider requires application singleton().");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
boot() {}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export { FastifyAuthPolicyClientProvider };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { runAuthSignOutFlow } from "../shared/signOutFlow.js";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
OPAQUE_INVITE_TOKEN_HASH_PREFIX,
|
|
4
|
+
normalizeInviteToken,
|
|
5
|
+
isSha256Hex,
|
|
6
|
+
encodeInviteTokenHash,
|
|
7
|
+
decodeInviteTokenHash
|
|
8
|
+
} from "../shared/inviteTokens.js";
|
|
9
|
+
|
|
10
|
+
function buildInviteToken() {
|
|
11
|
+
return crypto.randomBytes(24).toString("hex");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hashInviteToken(token) {
|
|
15
|
+
return crypto.createHash("sha256").update(normalizeInviteToken(token)).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveInviteTokenHash(inviteToken) {
|
|
19
|
+
const normalizedToken = normalizeInviteToken(inviteToken);
|
|
20
|
+
if (!normalizedToken) {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const decodedTokenHash = decodeInviteTokenHash(normalizedToken);
|
|
25
|
+
if (decodedTokenHash) {
|
|
26
|
+
return decodedTokenHash;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return hashInviteToken(normalizedToken);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
OPAQUE_INVITE_TOKEN_HASH_PREFIX,
|
|
34
|
+
normalizeInviteToken,
|
|
35
|
+
isSha256Hex,
|
|
36
|
+
buildInviteToken,
|
|
37
|
+
hashInviteToken,
|
|
38
|
+
encodeInviteTokenHash,
|
|
39
|
+
decodeInviteTokenHash,
|
|
40
|
+
resolveInviteTokenHash
|
|
41
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { normalizeText } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function normalizePermissions(value) {
|
|
4
|
+
const source = Array.isArray(value) ? value : [];
|
|
5
|
+
return source.map((entry) => normalizeText(entry)).filter(Boolean);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function createAuthActionContextContributor() {
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
contributorId: "auth.policy.request-context",
|
|
11
|
+
contribute({ request } = {}) {
|
|
12
|
+
const contribution = {};
|
|
13
|
+
const permissions = normalizePermissions(request?.permissions);
|
|
14
|
+
|
|
15
|
+
if (request?.user) {
|
|
16
|
+
contribution.actor = request.user;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (request?.workspace) {
|
|
20
|
+
contribution.workspace = request.workspace;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (request?.membership) {
|
|
24
|
+
contribution.membership = request.membership;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (permissions.length > 0) {
|
|
28
|
+
contribution.permissions = permissions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return contribution;
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { createAuthActionContextContributor };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { asObject } from "./objectUtils.js";
|
|
2
|
+
|
|
3
|
+
function assertFunction(value, name) {
|
|
4
|
+
if (typeof value !== "function") {
|
|
5
|
+
throw new Error(`${name} is required.`);
|
|
6
|
+
}
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function assertAuthPolicyDeps(deps = {}) {
|
|
11
|
+
const source = asObject(deps);
|
|
12
|
+
const resolveActor = assertFunction(source.resolveActor, "resolveActor");
|
|
13
|
+
const hasPermission = assertFunction(source.hasPermission, "hasPermission");
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
resolveActor,
|
|
17
|
+
resolveContext: typeof source.resolveContext === "function" ? source.resolveContext : null,
|
|
18
|
+
hasPermission,
|
|
19
|
+
onPolicyDenied: typeof source.onPolicyDenied === "function" ? source.onPolicyDenied : null
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function normalizeActorResolution(result) {
|
|
24
|
+
const source = asObject(result);
|
|
25
|
+
const actor = Object.hasOwn(source, "actor")
|
|
26
|
+
? source.actor
|
|
27
|
+
: Object.hasOwn(source, "profile")
|
|
28
|
+
? source.profile
|
|
29
|
+
: null;
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
authenticated: Boolean(source.authenticated),
|
|
33
|
+
actor,
|
|
34
|
+
transientFailure: Boolean(source.transientFailure)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { assertAuthPolicyDeps, normalizeActorResolution };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const AUTH_POLICY_DENY_REASONS = Object.freeze({
|
|
2
|
+
AUTH_UPSTREAM_UNAVAILABLE: "auth_upstream_unavailable",
|
|
3
|
+
UNAUTHENTICATED: "unauthenticated",
|
|
4
|
+
OWNER_UNRESOLVED: "owner_unresolved",
|
|
5
|
+
FORBIDDEN_OWNER_MISMATCH: "forbidden_owner_mismatch",
|
|
6
|
+
INVALID_AUTH_POLICY: "invalid_auth_policy",
|
|
7
|
+
FORBIDDEN_PERMISSION: "forbidden_permission"
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function createAuthPolicyError(status, message, { code = "AUTH_POLICY_ERROR" } = {}) {
|
|
11
|
+
const normalizedStatus = Number(status) || 500;
|
|
12
|
+
const error = new Error(String(message || "Request failed."));
|
|
13
|
+
error.name = "AuthPolicyError";
|
|
14
|
+
error.status = normalizedStatus;
|
|
15
|
+
error.statusCode = normalizedStatus;
|
|
16
|
+
error.code = String(code || "AUTH_POLICY_ERROR");
|
|
17
|
+
return error;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { AUTH_POLICY_DENY_REASONS, createAuthPolicyError };
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
import fastifyCookie from "@fastify/cookie";
|
|
2
|
+
import fastifyCsrfProtection from "@fastify/csrf-protection";
|
|
3
|
+
import fastifyRateLimit from "@fastify/rate-limit";
|
|
4
|
+
import { safeRequestUrl } from "@jskit-ai/kernel/server/runtime/requestUrl";
|
|
5
|
+
|
|
6
|
+
import { assertAuthPolicyDeps, normalizeActorResolution } from "./authPolicySupport.js";
|
|
7
|
+
import { AUTH_POLICY_DENY_REASONS, createAuthPolicyError } from "./errors.js";
|
|
8
|
+
import { AUTH_POLICIES, CONTEXT_POLICIES, resolveAuthPolicyMeta } from "./routeMeta.js";
|
|
9
|
+
|
|
10
|
+
const DEFAULT_API_PREFIX = "/api/";
|
|
11
|
+
const DEFAULT_RATE_LIMIT_PLUGIN_OPTIONS = Object.freeze({
|
|
12
|
+
global: false
|
|
13
|
+
});
|
|
14
|
+
const DEFAULT_UNSAFE_METHODS = Object.freeze(["POST", "PUT", "PATCH", "DELETE"]);
|
|
15
|
+
|
|
16
|
+
function normalizeUnsafeMethods(methodsValue) {
|
|
17
|
+
const source = Array.isArray(methodsValue) ? methodsValue : DEFAULT_UNSAFE_METHODS;
|
|
18
|
+
return new Set(source.map((method) => String(method || "").trim().toUpperCase()).filter(Boolean));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeApiPrefix(value) {
|
|
22
|
+
const normalized = String(value || "").trim();
|
|
23
|
+
return normalized || DEFAULT_API_PREFIX;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function normalizeRateLimitPluginOptions(value) {
|
|
27
|
+
if (!value || typeof value !== "object") {
|
|
28
|
+
return {
|
|
29
|
+
...DEFAULT_RATE_LIMIT_PLUGIN_OPTIONS
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveRouteConfig(request) {
|
|
37
|
+
if (!request?.routeOptions || typeof request.routeOptions !== "object") {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const routeConfig = request.routeOptions.config;
|
|
42
|
+
if (!routeConfig || typeof routeConfig !== "object") {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return routeConfig;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function enforceCsrfProtection(fastify, request, reply) {
|
|
50
|
+
return new Promise((resolve, reject) => {
|
|
51
|
+
fastify.csrfProtection(request, reply, (error) => {
|
|
52
|
+
if (error) {
|
|
53
|
+
reject(error);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
resolve();
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function resolveCsrfToken(request) {
|
|
62
|
+
return request.headers["csrf-token"] || request.headers["x-csrf-token"] || request.headers["x-xsrf-token"] || null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function resolveOwnerValue(meta, request) {
|
|
66
|
+
if (typeof meta.ownerResolver === "function") {
|
|
67
|
+
return meta.ownerResolver({
|
|
68
|
+
req: request,
|
|
69
|
+
res: request.raw,
|
|
70
|
+
url: safeRequestUrl(request),
|
|
71
|
+
params: request.params || {},
|
|
72
|
+
user: request.user || null
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (typeof meta.ownerParam === "string" && meta.ownerParam) {
|
|
77
|
+
return request.params ? request.params[meta.ownerParam] : null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function notifyPolicyDenied(onPolicyDenied, payload) {
|
|
84
|
+
if (typeof onPolicyDenied !== "function") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
onPolicyDenied(payload);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function authPolicyPlugin(deps = {}, options = {}) {
|
|
91
|
+
const { resolveActor, resolveContext, hasPermission, onPolicyDenied } = assertAuthPolicyDeps(deps);
|
|
92
|
+
const apiPrefix = normalizeApiPrefix(options.apiPrefix);
|
|
93
|
+
const unsafeMethods = normalizeUnsafeMethods(options.unsafeMethods);
|
|
94
|
+
const rateLimitPluginOptions = normalizeRateLimitPluginOptions(options.rateLimitPluginOptions);
|
|
95
|
+
const nodeEnv = String(options.nodeEnv || "").trim();
|
|
96
|
+
const csrfCookieOpts =
|
|
97
|
+
options.csrfCookieOpts && typeof options.csrfCookieOpts === "object" ? options.csrfCookieOpts : {};
|
|
98
|
+
const resolveCsrfTokenFn = typeof options.resolveCsrfToken === "function" ? options.resolveCsrfToken : resolveCsrfToken;
|
|
99
|
+
const createError =
|
|
100
|
+
typeof options.createError === "function"
|
|
101
|
+
? options.createError
|
|
102
|
+
: (status, message) => createAuthPolicyError(status, message);
|
|
103
|
+
|
|
104
|
+
return async function registerAuthPolicyPlugin(fastify) {
|
|
105
|
+
await fastify.register(fastifyCookie);
|
|
106
|
+
await fastify.register(fastifyRateLimit, rateLimitPluginOptions);
|
|
107
|
+
await fastify.register(fastifyCsrfProtection, {
|
|
108
|
+
getToken: resolveCsrfTokenFn,
|
|
109
|
+
cookieOpts: {
|
|
110
|
+
path: "/",
|
|
111
|
+
sameSite: "lax",
|
|
112
|
+
secure: nodeEnv === "production",
|
|
113
|
+
httpOnly: true,
|
|
114
|
+
...csrfCookieOpts
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
fastify.decorateRequest("user", null);
|
|
119
|
+
fastify.decorateRequest("workspace", null);
|
|
120
|
+
fastify.decorateRequest("membership", null);
|
|
121
|
+
fastify.decorateRequest("permissions", null);
|
|
122
|
+
|
|
123
|
+
fastify.addHook("preHandler", async (request, reply) => {
|
|
124
|
+
const pathname = request?.raw?.url || request?.url || "/";
|
|
125
|
+
if (!String(pathname).startsWith(apiPrefix)) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const meta = resolveAuthPolicyMeta(resolveRouteConfig(request));
|
|
130
|
+
if (meta.csrfProtection && unsafeMethods.has(String(request?.method || "").toUpperCase())) {
|
|
131
|
+
await enforceCsrfProtection(fastify, request, reply);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (meta.authPolicy === AUTH_POLICIES.PUBLIC) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const actorResolution = normalizeActorResolution(await resolveActor(request, reply, meta));
|
|
139
|
+
if (actorResolution.transientFailure) {
|
|
140
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
141
|
+
reason: AUTH_POLICY_DENY_REASONS.AUTH_UPSTREAM_UNAVAILABLE,
|
|
142
|
+
statusCode: 503,
|
|
143
|
+
request,
|
|
144
|
+
meta,
|
|
145
|
+
actor: actorResolution.actor,
|
|
146
|
+
context: null
|
|
147
|
+
});
|
|
148
|
+
throw createError(503, "Authentication service temporarily unavailable. Please retry.");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (!actorResolution.authenticated) {
|
|
152
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
153
|
+
reason: AUTH_POLICY_DENY_REASONS.UNAUTHENTICATED,
|
|
154
|
+
statusCode: 401,
|
|
155
|
+
request,
|
|
156
|
+
meta,
|
|
157
|
+
actor: actorResolution.actor,
|
|
158
|
+
context: null
|
|
159
|
+
});
|
|
160
|
+
throw createError(401, "Authentication required.");
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
request.user = actorResolution.actor;
|
|
164
|
+
request.workspace = null;
|
|
165
|
+
request.membership = null;
|
|
166
|
+
request.permissions = [];
|
|
167
|
+
|
|
168
|
+
if (meta.authPolicy === AUTH_POLICIES.OWN) {
|
|
169
|
+
const ownerValue = await resolveOwnerValue(meta, request);
|
|
170
|
+
const userValue = request?.user ? request.user[meta.userField] : null;
|
|
171
|
+
|
|
172
|
+
if (ownerValue == null || userValue == null) {
|
|
173
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
174
|
+
reason: AUTH_POLICY_DENY_REASONS.OWNER_UNRESOLVED,
|
|
175
|
+
statusCode: 400,
|
|
176
|
+
request,
|
|
177
|
+
meta,
|
|
178
|
+
actor: actorResolution.actor,
|
|
179
|
+
context: null
|
|
180
|
+
});
|
|
181
|
+
throw createError(400, "Route owner could not be resolved.");
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (String(ownerValue) !== String(userValue)) {
|
|
185
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
186
|
+
reason: AUTH_POLICY_DENY_REASONS.FORBIDDEN_OWNER_MISMATCH,
|
|
187
|
+
statusCode: 403,
|
|
188
|
+
request,
|
|
189
|
+
meta,
|
|
190
|
+
actor: actorResolution.actor,
|
|
191
|
+
context: null
|
|
192
|
+
});
|
|
193
|
+
throw createError(403, "Forbidden.");
|
|
194
|
+
}
|
|
195
|
+
} else if (meta.authPolicy !== AUTH_POLICIES.REQUIRED) {
|
|
196
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
197
|
+
reason: AUTH_POLICY_DENY_REASONS.INVALID_AUTH_POLICY,
|
|
198
|
+
statusCode: 500,
|
|
199
|
+
request,
|
|
200
|
+
meta,
|
|
201
|
+
actor: actorResolution.actor,
|
|
202
|
+
context: null
|
|
203
|
+
});
|
|
204
|
+
throw createError(500, "Invalid route auth policy configuration.");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let context = null;
|
|
208
|
+
if (resolveContext && (meta.contextPolicy !== CONTEXT_POLICIES.NONE || meta.permission)) {
|
|
209
|
+
context = await resolveContext({
|
|
210
|
+
request,
|
|
211
|
+
actor: actorResolution.actor,
|
|
212
|
+
meta
|
|
213
|
+
});
|
|
214
|
+
const normalizedContext = context && typeof context === "object" ? context : {};
|
|
215
|
+
request.workspace = normalizedContext.workspace || null;
|
|
216
|
+
request.membership = normalizedContext.membership || null;
|
|
217
|
+
request.permissions = Array.isArray(normalizedContext.permissions) ? normalizedContext.permissions : [];
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (meta.permission) {
|
|
221
|
+
const allowed = await Promise.resolve(
|
|
222
|
+
hasPermission({
|
|
223
|
+
permission: meta.permission,
|
|
224
|
+
permissions: request.permissions,
|
|
225
|
+
request,
|
|
226
|
+
actor: actorResolution.actor,
|
|
227
|
+
context,
|
|
228
|
+
meta
|
|
229
|
+
})
|
|
230
|
+
);
|
|
231
|
+
if (!allowed) {
|
|
232
|
+
notifyPolicyDenied(onPolicyDenied, {
|
|
233
|
+
reason: AUTH_POLICY_DENY_REASONS.FORBIDDEN_PERMISSION,
|
|
234
|
+
statusCode: 403,
|
|
235
|
+
request,
|
|
236
|
+
meta,
|
|
237
|
+
actor: actorResolution.actor,
|
|
238
|
+
context
|
|
239
|
+
});
|
|
240
|
+
throw createError(403, "Forbidden.");
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export { authPolicyPlugin };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { asObject } from "./objectUtils.js";
|
|
2
|
+
|
|
3
|
+
const AUTH_POLICIES = Object.freeze({
|
|
4
|
+
PUBLIC: "public",
|
|
5
|
+
REQUIRED: "required",
|
|
6
|
+
OWN: "own"
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
const CONTEXT_POLICIES = Object.freeze({
|
|
10
|
+
NONE: "none",
|
|
11
|
+
OPTIONAL: "optional",
|
|
12
|
+
REQUIRED: "required"
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const DEFAULT_AUTH_POLICY_META = Object.freeze({
|
|
16
|
+
authPolicy: AUTH_POLICIES.PUBLIC,
|
|
17
|
+
contextPolicy: CONTEXT_POLICIES.NONE,
|
|
18
|
+
surface: "",
|
|
19
|
+
permission: "",
|
|
20
|
+
ownerParam: null,
|
|
21
|
+
userField: "id",
|
|
22
|
+
ownerResolver: null,
|
|
23
|
+
csrfProtection: true
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
function resolveAuthPolicyMeta(input = {}) {
|
|
27
|
+
const source = asObject(input);
|
|
28
|
+
return {
|
|
29
|
+
authPolicy: source.authPolicy || AUTH_POLICIES.PUBLIC,
|
|
30
|
+
contextPolicy: source.contextPolicy || CONTEXT_POLICIES.NONE,
|
|
31
|
+
surface: String(source.surface || "").trim(),
|
|
32
|
+
permission: String(source.permission || "").trim(),
|
|
33
|
+
ownerParam: typeof source.ownerParam === "string" && source.ownerParam ? source.ownerParam : null,
|
|
34
|
+
userField: source.userField || "id",
|
|
35
|
+
ownerResolver: typeof source.ownerResolver === "function" ? source.ownerResolver : null,
|
|
36
|
+
csrfProtection: source.csrfProtection !== false
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function withAuthPolicy(meta = {}) {
|
|
41
|
+
return {
|
|
42
|
+
config: resolveAuthPolicyMeta(meta)
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function mergeAuthPolicy(routeOptions = {}, meta = {}) {
|
|
47
|
+
const sourceRouteOptions = asObject(routeOptions);
|
|
48
|
+
const sourceConfig = asObject(sourceRouteOptions.config);
|
|
49
|
+
const sourceMeta = asObject(meta);
|
|
50
|
+
const normalizedMeta = resolveAuthPolicyMeta({
|
|
51
|
+
...sourceConfig,
|
|
52
|
+
...sourceMeta
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...sourceRouteOptions,
|
|
57
|
+
config: {
|
|
58
|
+
...sourceConfig,
|
|
59
|
+
...normalizedMeta
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export { AUTH_POLICIES, CONTEXT_POLICIES, DEFAULT_AUTH_POLICY_META, resolveAuthPolicyMeta, withAuthPolicy, mergeAuthPolicy };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { normalizeOpaqueId } from "@jskit-ai/kernel/shared/support/normalize";
|
|
2
|
+
|
|
3
|
+
function createAuthRouteVisibilityResolver() {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
resolverId: "auth.policy.visibility",
|
|
6
|
+
resolve({ visibility, context, request } = {}) {
|
|
7
|
+
if (visibility !== "user") {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const actor = context?.actor || request?.user || null;
|
|
12
|
+
const userOwnerId = normalizeOpaqueId(actor?.id);
|
|
13
|
+
if (userOwnerId == null) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return {
|
|
18
|
+
userOwnerId,
|
|
19
|
+
requiresActorScope: true
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { createAuthRouteVisibilityResolver };
|