@jskit-ai/auth-provider-local-core 0.1.15 → 0.1.17
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
"packageVersion": 1,
|
|
3
3
|
"packageId": "@jskit-ai/auth-provider-local-core",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.17",
|
|
5
5
|
"kind": "runtime",
|
|
6
6
|
"description": "Local auth provider with a file backend default and no database requirement.",
|
|
7
7
|
"dependsOn": [
|
|
@@ -54,11 +54,18 @@ export default Object.freeze({
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
+
"ci": {
|
|
58
|
+
"environment": {
|
|
59
|
+
"AUTH_PROVIDER": "local"
|
|
60
|
+
},
|
|
61
|
+
"services": [],
|
|
62
|
+
"steps": []
|
|
63
|
+
},
|
|
57
64
|
"mutations": {
|
|
58
65
|
"dependencies": {
|
|
59
66
|
"runtime": {
|
|
60
|
-
"@jskit-ai/auth-core": "0.1.
|
|
61
|
-
"@jskit-ai/kernel": "0.1.
|
|
67
|
+
"@jskit-ai/auth-core": "0.1.115",
|
|
68
|
+
"@jskit-ai/kernel": "0.1.118",
|
|
62
69
|
"nodemailer": "^7.0.10",
|
|
63
70
|
"dotenv": "^16.4.5"
|
|
64
71
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/auth-provider-local-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.17",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -11,8 +11,8 @@
|
|
|
11
11
|
"./server/lib/index": "./src/server/lib/index.js"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@jskit-ai/auth-core": "0.1.
|
|
15
|
-
"@jskit-ai/kernel": "0.1.
|
|
14
|
+
"@jskit-ai/auth-core": "0.1.115",
|
|
15
|
+
"@jskit-ai/kernel": "0.1.118",
|
|
16
16
|
"nodemailer": "^7.0.10"
|
|
17
17
|
}
|
|
18
18
|
}
|
|
@@ -6,6 +6,12 @@ import {
|
|
|
6
6
|
import { normalizeAuthCapabilities } from "@jskit-ai/auth-core/shared/authCapabilities";
|
|
7
7
|
import { buildSecurityStatusFromAuthMethodsStatus } from "@jskit-ai/auth-core/shared/authSecurityStatus";
|
|
8
8
|
import { normalizeAuthActor, normalizeAuthResult } from "@jskit-ai/auth-core/server/authActor";
|
|
9
|
+
import {
|
|
10
|
+
assertDevAuthPolicy,
|
|
11
|
+
ensureDevAuthExchangeAvailable,
|
|
12
|
+
ensureDevAuthRuntimeAvailable,
|
|
13
|
+
resolveDevAuthPolicy
|
|
14
|
+
} from "@jskit-ai/auth-core/server/devAuth";
|
|
9
15
|
import { normalizeEmail } from "@jskit-ai/auth-core/server/utils";
|
|
10
16
|
import { throwUnsupportedAuthOperation } from "@jskit-ai/auth-core/server/unsupportedOperation";
|
|
11
17
|
import { normalizePasswordStrategy } from "./passwords.js";
|
|
@@ -15,6 +21,7 @@ const ACCESS_TOKEN_COOKIE = "jskit_local_access_token";
|
|
|
15
21
|
const REFRESH_TOKEN_COOKIE = "jskit_local_refresh_token";
|
|
16
22
|
const RECOVERY_TOKEN_COOKIE = "jskit_local_recovery_token";
|
|
17
23
|
const PROVIDER_ID = "local";
|
|
24
|
+
const DEV_AUTH_SESSION_PURPOSE = "dev-auth";
|
|
18
25
|
const ACCESS_TTL_SECONDS = 15 * 60;
|
|
19
26
|
const REFRESH_TTL_SECONDS = 60 * 60 * 24 * 30;
|
|
20
27
|
const RECOVERY_SESSION_TTL_SECONDS = 15 * 60;
|
|
@@ -36,6 +43,14 @@ function isNormalSession(session) {
|
|
|
36
43
|
return (session?.purpose || "normal") === "normal";
|
|
37
44
|
}
|
|
38
45
|
|
|
46
|
+
function isDevAuthSession(session) {
|
|
47
|
+
return session?.purpose === DEV_AUTH_SESSION_PURPOSE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isAuthenticatingSession(session) {
|
|
51
|
+
return isNormalSession(session) || isDevAuthSession(session);
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
function normalizeDisplayName(value, email) {
|
|
40
55
|
const displayName = String(value || "").trim();
|
|
41
56
|
if (displayName) {
|
|
@@ -52,6 +67,15 @@ function safeRequestCookies(request) {
|
|
|
52
67
|
return request && request.cookies && typeof request.cookies === "object" ? request.cookies : {};
|
|
53
68
|
}
|
|
54
69
|
|
|
70
|
+
function unauthenticatedAuthResult(clearSession = false) {
|
|
71
|
+
return {
|
|
72
|
+
authenticated: false,
|
|
73
|
+
clearSession: Boolean(clearSession),
|
|
74
|
+
session: null,
|
|
75
|
+
transientFailure: false
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
55
79
|
function cookieOptions(isProduction, maxAge) {
|
|
56
80
|
return {
|
|
57
81
|
path: "/",
|
|
@@ -92,21 +116,59 @@ function buildActor(user, profile = null) {
|
|
|
92
116
|
});
|
|
93
117
|
}
|
|
94
118
|
|
|
95
|
-
|
|
96
|
-
let profile = buildProfile(user);
|
|
97
|
-
if (profileProjector && typeof profileProjector.syncIdentityProfile === "function") {
|
|
98
|
-
profile = await profileProjector.syncIdentityProfile(profile, profileOptions);
|
|
99
|
-
if (!profile || typeof profile !== "object") {
|
|
100
|
-
throw new Error("auth.profile.projector.syncIdentityProfile() must return a profile object.");
|
|
101
|
-
}
|
|
102
|
-
}
|
|
119
|
+
function buildAuthResult({ user, session, appProfile = null }) {
|
|
103
120
|
return normalizeAuthResult({
|
|
104
|
-
profile,
|
|
105
|
-
actor: buildActor(user,
|
|
121
|
+
profile: appProfile || buildProfile(user),
|
|
122
|
+
actor: buildActor(user, appProfile),
|
|
106
123
|
session
|
|
107
124
|
});
|
|
108
125
|
}
|
|
109
126
|
|
|
127
|
+
function requireProfileResult(profile, methodName) {
|
|
128
|
+
if (!profile || typeof profile !== "object") {
|
|
129
|
+
throw new Error(`auth.profile.projector.${methodName}() must return a profile object.`);
|
|
130
|
+
}
|
|
131
|
+
return profile;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function buildAuthPayload({ user, session, profileProjector, profileOptions = {} }) {
|
|
135
|
+
const appProfile = profileProjector
|
|
136
|
+
? requireProfileResult(
|
|
137
|
+
await profileProjector.syncIdentityProfile(buildProfile(user), profileOptions),
|
|
138
|
+
"syncIdentityProfile"
|
|
139
|
+
)
|
|
140
|
+
: null;
|
|
141
|
+
return buildAuthResult({ user, session, appProfile });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function findExistingAppProfile(user, profileProjector) {
|
|
145
|
+
if (!profileProjector) {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
if (typeof profileProjector.findByIdentity !== "function") {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"auth.profile.projector.findByIdentity() is required for dev auth impersonation."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
const profile = await profileProjector.findByIdentity(buildProfile(user));
|
|
154
|
+
return profile ? requireProfileResult(profile, "findByIdentity") : null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function buildSessionAuthPayload({ devAuth, profileProjector, request, session, user }) {
|
|
158
|
+
if (!isDevAuthSession(session)) {
|
|
159
|
+
return buildAuthPayload({ user, session: null, profileProjector });
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
ensureDevAuthRuntimeAvailable(devAuth, request);
|
|
163
|
+
} catch {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const appProfile = await findExistingAppProfile(user, profileProjector);
|
|
167
|
+
return profileProjector && !appProfile
|
|
168
|
+
? null
|
|
169
|
+
: buildAuthResult({ user, session: null, appProfile });
|
|
170
|
+
}
|
|
171
|
+
|
|
110
172
|
function buildAccessToken({ user, session, secret, ttlSeconds = ACCESS_TTL_SECONDS }) {
|
|
111
173
|
const issuedAt = nowSeconds();
|
|
112
174
|
return signToken(
|
|
@@ -166,6 +228,22 @@ function validateEmailInput(email) {
|
|
|
166
228
|
return normalized;
|
|
167
229
|
}
|
|
168
230
|
|
|
231
|
+
function devLoginAsValidationError(fieldErrors = {}) {
|
|
232
|
+
const messages = Object.values(fieldErrors).filter(Boolean);
|
|
233
|
+
return new AppError(400, messages[0] || "Provide a user id or email.", {
|
|
234
|
+
details: {
|
|
235
|
+
fieldErrors
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function devLoginAsUserNotFound({ email = "", userId = "" } = {}) {
|
|
241
|
+
return devLoginAsValidationError({
|
|
242
|
+
...(userId ? { userId: "User not found." } : {}),
|
|
243
|
+
...(email ? { email: "User not found." } : {})
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
169
247
|
function normalizeInvitationInput(value = null) {
|
|
170
248
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
171
249
|
return null;
|
|
@@ -221,6 +299,10 @@ function createLocalAuthService({
|
|
|
221
299
|
|
|
222
300
|
const passwords = normalizePasswordStrategy(passwordStrategy);
|
|
223
301
|
const isProduction = config.nodeEnv === "production";
|
|
302
|
+
const devAuth = config.devAuth || resolveDevAuthPolicy({
|
|
303
|
+
nodeEnv: config.nodeEnv
|
|
304
|
+
});
|
|
305
|
+
assertDevAuthPolicy(devAuth);
|
|
224
306
|
const profileProjectionEnabled = typeof profileProjector?.syncIdentityProfile === "function";
|
|
225
307
|
const recoveryDelivery = config.smtpConfigured
|
|
226
308
|
? "smtp"
|
|
@@ -262,7 +344,8 @@ function createLocalAuthService({
|
|
|
262
344
|
},
|
|
263
345
|
securityStatus: true,
|
|
264
346
|
signOutOtherSessions: true,
|
|
265
|
-
appProfileProjection: profileProjectionEnabled
|
|
347
|
+
appProfileProjection: profileProjectionEnabled,
|
|
348
|
+
devLoginAs: devAuth.enabled === true && devAuth.isProduction !== true
|
|
266
349
|
}
|
|
267
350
|
});
|
|
268
351
|
|
|
@@ -367,6 +450,40 @@ function createLocalAuthService({
|
|
|
367
450
|
});
|
|
368
451
|
}
|
|
369
452
|
|
|
453
|
+
function isDevAuthBootstrapEnabled() {
|
|
454
|
+
return devAuth.enabled === true && devAuth.isProduction !== true;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
async function devLoginAs(request, input = {}) {
|
|
458
|
+
ensureDevAuthExchangeAvailable(devAuth, request);
|
|
459
|
+
const userId = String(input?.userId || "").trim();
|
|
460
|
+
const email = normalizeEmail(input?.email || "");
|
|
461
|
+
if (!userId && !email) {
|
|
462
|
+
throw devLoginAsValidationError({
|
|
463
|
+
userId: "Provide a user id or email.",
|
|
464
|
+
email: "Provide a user id or email."
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return backend.withTransaction(async (tx) => {
|
|
469
|
+
let user = userId ? await tx.users.findById(userId) : null;
|
|
470
|
+
if (!user && email) {
|
|
471
|
+
user = await tx.users.findByEmail(email);
|
|
472
|
+
}
|
|
473
|
+
if (!user || user.disabled) {
|
|
474
|
+
throw devLoginAsUserNotFound({ email, userId });
|
|
475
|
+
}
|
|
476
|
+
const appProfile = await findExistingAppProfile(user, profileProjector);
|
|
477
|
+
if (profileProjector && !appProfile) {
|
|
478
|
+
throw devLoginAsUserNotFound({ email, userId });
|
|
479
|
+
}
|
|
480
|
+
const session = await createSessionForUser(tx, user, {
|
|
481
|
+
purpose: DEV_AUTH_SESSION_PURPOSE
|
|
482
|
+
});
|
|
483
|
+
return buildAuthResult({ user, session, appProfile });
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
|
|
370
487
|
async function authenticateRequest(request) {
|
|
371
488
|
const cookies = safeRequestCookies(request);
|
|
372
489
|
const accessToken = String(cookies[ACCESS_TOKEN_COOKIE] || "").trim();
|
|
@@ -386,10 +503,20 @@ function createLocalAuthService({
|
|
|
386
503
|
}
|
|
387
504
|
return { user, session };
|
|
388
505
|
});
|
|
389
|
-
if (resolved &&
|
|
506
|
+
if (resolved && isAuthenticatingSession(resolved.session)) {
|
|
507
|
+
const authPayload = await buildSessionAuthPayload({
|
|
508
|
+
devAuth,
|
|
509
|
+
profileProjector,
|
|
510
|
+
request,
|
|
511
|
+
session: resolved.session,
|
|
512
|
+
user: resolved.user
|
|
513
|
+
});
|
|
514
|
+
if (!authPayload) {
|
|
515
|
+
return unauthenticatedAuthResult(true);
|
|
516
|
+
}
|
|
390
517
|
return {
|
|
391
518
|
authenticated: true,
|
|
392
|
-
...
|
|
519
|
+
...authPayload,
|
|
393
520
|
session: null,
|
|
394
521
|
sessionPurpose: resolved.session.purpose || "normal",
|
|
395
522
|
clearSession: false,
|
|
@@ -400,36 +527,32 @@ function createLocalAuthService({
|
|
|
400
527
|
}
|
|
401
528
|
|
|
402
529
|
if (!refreshToken) {
|
|
403
|
-
return
|
|
404
|
-
authenticated: false,
|
|
405
|
-
clearSession: Boolean(accessToken),
|
|
406
|
-
session: null,
|
|
407
|
-
transientFailure: false
|
|
408
|
-
};
|
|
530
|
+
return unauthenticatedAuthResult(Boolean(accessToken));
|
|
409
531
|
}
|
|
410
532
|
|
|
411
533
|
const resolved = await resolveValidSessionByRefreshToken(refreshToken);
|
|
412
534
|
if (!resolved) {
|
|
413
|
-
return
|
|
414
|
-
authenticated: false,
|
|
415
|
-
clearSession: true,
|
|
416
|
-
session: null,
|
|
417
|
-
transientFailure: false
|
|
418
|
-
};
|
|
535
|
+
return unauthenticatedAuthResult(true);
|
|
419
536
|
}
|
|
420
537
|
|
|
421
|
-
if (!
|
|
422
|
-
return
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
538
|
+
if (!isAuthenticatingSession(resolved.session)) {
|
|
539
|
+
return unauthenticatedAuthResult();
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const authPayload = await buildSessionAuthPayload({
|
|
543
|
+
devAuth,
|
|
544
|
+
profileProjector,
|
|
545
|
+
request,
|
|
546
|
+
session: resolved.session,
|
|
547
|
+
user: resolved.user
|
|
548
|
+
});
|
|
549
|
+
if (!authPayload) {
|
|
550
|
+
return unauthenticatedAuthResult(true);
|
|
428
551
|
}
|
|
429
552
|
|
|
430
553
|
return {
|
|
431
554
|
authenticated: true,
|
|
432
|
-
...
|
|
555
|
+
...authPayload,
|
|
433
556
|
session: buildSessionPayload({
|
|
434
557
|
user: resolved.user,
|
|
435
558
|
session: resolved.session,
|
|
@@ -686,6 +809,8 @@ function createLocalAuthService({
|
|
|
686
809
|
|
|
687
810
|
return Object.freeze({
|
|
688
811
|
getCapabilities,
|
|
812
|
+
isDevAuthBootstrapEnabled,
|
|
813
|
+
devLoginAs,
|
|
689
814
|
authenticateRequest,
|
|
690
815
|
hasAccessTokenCookie(request) {
|
|
691
816
|
return Boolean(safeRequestCookies(request)[ACCESS_TOKEN_COOKIE] || safeRequestCookies(request)[RECOVERY_TOKEN_COOKIE]);
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { applyAuthServiceDecorators } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
2
|
+
import { parseBooleanFlag } from "@jskit-ai/auth-core/server/booleanFlag";
|
|
3
|
+
import {
|
|
4
|
+
assertDevAuthPolicy,
|
|
5
|
+
resolveDevAuthPolicyFromEnv
|
|
6
|
+
} from "@jskit-ai/auth-core/server/devAuth";
|
|
2
7
|
import fs from "node:fs";
|
|
3
8
|
import path from "node:path";
|
|
4
9
|
import { randomBytes } from "node:crypto";
|
|
@@ -12,20 +17,6 @@ function resolveLocalBackendMode(scope) {
|
|
|
12
17
|
return String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
|
|
13
18
|
}
|
|
14
19
|
|
|
15
|
-
function parseBoolean(value, fallback = false) {
|
|
16
|
-
const raw = String(value || "").trim().toLowerCase();
|
|
17
|
-
if (!raw) {
|
|
18
|
-
return fallback;
|
|
19
|
-
}
|
|
20
|
-
if (["1", "true", "yes", "on"].includes(raw)) {
|
|
21
|
-
return true;
|
|
22
|
-
}
|
|
23
|
-
if (["0", "false", "no", "off"].includes(raw)) {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
return fallback;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
function resolveRuntimeEnv(scope) {
|
|
30
21
|
const env = scope && typeof scope.has === "function" && scope.has("jskit.env") ? scope.make("jskit.env") : {};
|
|
31
22
|
return {
|
|
@@ -91,7 +82,7 @@ function resolveSmtpConfig(env) {
|
|
|
91
82
|
smtp: {
|
|
92
83
|
host,
|
|
93
84
|
port: Number(env.AUTH_LOCAL_SMTP_PORT || 587),
|
|
94
|
-
secure:
|
|
85
|
+
secure: parseBooleanFlag(env.AUTH_LOCAL_SMTP_SECURE, false),
|
|
95
86
|
user: String(env.AUTH_LOCAL_SMTP_USER || "").trim(),
|
|
96
87
|
password: String(env.AUTH_LOCAL_SMTP_PASSWORD || ""),
|
|
97
88
|
from,
|
|
@@ -130,10 +121,12 @@ function resolveConfig(scope) {
|
|
|
130
121
|
const isProduction = nodeEnv === "production";
|
|
131
122
|
const backend = String(env.AUTH_LOCAL_BACKEND || "file").trim().toLowerCase() || "file";
|
|
132
123
|
const storeDir = resolveStoreDir(env);
|
|
133
|
-
if (backend === "file" && isProduction && !
|
|
124
|
+
if (backend === "file" && isProduction && !parseBooleanFlag(env.AUTH_LOCAL_FILE_PRODUCTION_ACK, false)) {
|
|
134
125
|
throw new Error("AUTH_LOCAL_FILE_PRODUCTION_ACK is required to use the local file auth backend in production.");
|
|
135
126
|
}
|
|
136
127
|
const smtp = resolveSmtpConfig(env);
|
|
128
|
+
const devAuth = resolveDevAuthPolicyFromEnv(env);
|
|
129
|
+
assertDevAuthPolicy(devAuth);
|
|
137
130
|
return {
|
|
138
131
|
backend,
|
|
139
132
|
storeDir,
|
|
@@ -145,10 +138,32 @@ function resolveConfig(scope) {
|
|
|
145
138
|
isProduction
|
|
146
139
|
}),
|
|
147
140
|
recoveryDevOutput: String(env.AUTH_LOCAL_RECOVERY_DEV_OUTPUT || "log").trim().toLowerCase() || "log",
|
|
141
|
+
devAuth,
|
|
148
142
|
...smtp
|
|
149
143
|
};
|
|
150
144
|
}
|
|
151
145
|
|
|
146
|
+
function createLazyProfileProjector(scope) {
|
|
147
|
+
if (!scope.has("auth.profile.projector")) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
const resolveMethod = (methodName) => {
|
|
151
|
+
const projector = scope.make("auth.profile.projector");
|
|
152
|
+
if (!projector || typeof projector[methodName] !== "function") {
|
|
153
|
+
throw new Error(`auth.profile.projector.${methodName}() must be a function.`);
|
|
154
|
+
}
|
|
155
|
+
return projector[methodName].bind(projector);
|
|
156
|
+
};
|
|
157
|
+
return {
|
|
158
|
+
findByIdentity(identity, options = {}) {
|
|
159
|
+
return resolveMethod("findByIdentity")(identity, options);
|
|
160
|
+
},
|
|
161
|
+
syncIdentityProfile(profile, options = {}) {
|
|
162
|
+
return resolveMethod("syncIdentityProfile")(profile, options);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
152
167
|
class AuthLocalServiceProvider {
|
|
153
168
|
static id = "auth.provider.local";
|
|
154
169
|
|
|
@@ -178,17 +193,7 @@ class AuthLocalServiceProvider {
|
|
|
178
193
|
throw new Error(`AUTH_LOCAL_BACKEND="${config.backend}" requires a package or app provider that registers auth.local.backend.`);
|
|
179
194
|
}
|
|
180
195
|
const backend = scope.make("auth.local.backend");
|
|
181
|
-
const profileProjector = scope
|
|
182
|
-
? {
|
|
183
|
-
async syncIdentityProfile(profile, options = {}) {
|
|
184
|
-
const projector = scope.make("auth.profile.projector");
|
|
185
|
-
if (!projector || typeof projector.syncIdentityProfile !== "function") {
|
|
186
|
-
throw new Error("auth.profile.projector.syncIdentityProfile() must be a function.");
|
|
187
|
-
}
|
|
188
|
-
return projector.syncIdentityProfile(profile, options);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
: null;
|
|
196
|
+
const profileProjector = createLazyProfileProjector(scope);
|
|
192
197
|
const invitationContextResolver = scope.has("auth.invitationContextResolver")
|
|
193
198
|
? {
|
|
194
199
|
async resolveInvitationContext(invitation, options = {}) {
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import descriptor from "../package.descriptor.mjs";
|
|
4
|
+
|
|
5
|
+
test("local auth contributes only its provider selection to CI", () => {
|
|
6
|
+
assert.deepEqual(descriptor.ci, {
|
|
7
|
+
environment: {
|
|
8
|
+
AUTH_PROVIDER: "local"
|
|
9
|
+
},
|
|
10
|
+
services: [],
|
|
11
|
+
steps: []
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -6,6 +6,7 @@ import test from "node:test";
|
|
|
6
6
|
import { createApplication } from "@jskit-ai/kernel/_testable";
|
|
7
7
|
import { ActionRuntimeServiceProvider } from "@jskit-ai/kernel/server/actions";
|
|
8
8
|
import { registerAuthServiceDecorator } from "@jskit-ai/auth-core/server/authServiceDecoratorRegistry";
|
|
9
|
+
import { DEV_AUTH_SECRET_HEADER } from "@jskit-ai/auth-core/server/devAuth";
|
|
9
10
|
import { AuthActionsServiceProvider } from "@jskit-ai/auth-core/server/providers/AuthActionsServiceProvider";
|
|
10
11
|
import { AuthLocalServiceProvider } from "../src/server/providers/AuthLocalServiceProvider.js";
|
|
11
12
|
import { AuthProviderServiceProvider } from "../src/server/providers/AuthProviderServiceProvider.js";
|
|
@@ -18,6 +19,8 @@ import {
|
|
|
18
19
|
verifyPassword
|
|
19
20
|
} from "../src/server/lib/index.js";
|
|
20
21
|
|
|
22
|
+
const DEV_AUTH_SECRET = "local-preview-exchange-secret";
|
|
23
|
+
|
|
21
24
|
function createAppConfigFixture() {
|
|
22
25
|
return {
|
|
23
26
|
surfaceModeAll: "all",
|
|
@@ -52,7 +55,44 @@ function createReplyFixture() {
|
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
function createLocalAuthRequest({
|
|
59
|
+
cookies = {},
|
|
60
|
+
headers = {},
|
|
61
|
+
host = "localhost:4100",
|
|
62
|
+
remoteAddress = "127.0.0.1"
|
|
63
|
+
} = {}) {
|
|
64
|
+
return {
|
|
65
|
+
cookies,
|
|
66
|
+
headers: {
|
|
67
|
+
host,
|
|
68
|
+
...headers
|
|
69
|
+
},
|
|
70
|
+
socket: {
|
|
71
|
+
remoteAddress
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function createDevAuthExchangeRequest(options = {}) {
|
|
77
|
+
const request = createLocalAuthRequest(options);
|
|
78
|
+
return {
|
|
79
|
+
...request,
|
|
80
|
+
headers: {
|
|
81
|
+
...request.headers,
|
|
82
|
+
[DEV_AUTH_SECRET_HEADER]: DEV_AUTH_SECRET
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function devAuthEnv() {
|
|
88
|
+
return {
|
|
89
|
+
AUTH_DEV_BYPASS_ENABLED: "true",
|
|
90
|
+
AUTH_DEV_BYPASS_SECRET: DEV_AUTH_SECRET
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
async function createStartedApp({
|
|
95
|
+
env = {},
|
|
56
96
|
profileProjector = null,
|
|
57
97
|
profileProjectorFactory = null,
|
|
58
98
|
passwordStrategy = null,
|
|
@@ -68,7 +108,8 @@ async function createStartedApp({
|
|
|
68
108
|
AUTH_LOCAL_STORE_DIR: storeDir,
|
|
69
109
|
AUTH_LOCAL_RECOVERY_DEV_OUTPUT: "response",
|
|
70
110
|
APP_PUBLIC_URL: "http://localhost:5173",
|
|
71
|
-
NODE_ENV: "test"
|
|
111
|
+
NODE_ENV: "test",
|
|
112
|
+
...env
|
|
72
113
|
});
|
|
73
114
|
app.instance(
|
|
74
115
|
"jskit.logger",
|
|
@@ -162,6 +203,179 @@ test("local auth provider registers, logs in, reads session, and logs out with f
|
|
|
162
203
|
assert.match(usersFile, /^user:v1:/);
|
|
163
204
|
});
|
|
164
205
|
|
|
206
|
+
test("local file auth login-as issues native cookies for an existing user", async () => {
|
|
207
|
+
const { app } = await createStartedApp({
|
|
208
|
+
env: devAuthEnv()
|
|
209
|
+
});
|
|
210
|
+
const authService = app.make("authService");
|
|
211
|
+
await authService.register({
|
|
212
|
+
displayName: "Ada",
|
|
213
|
+
email: "ada@example.com",
|
|
214
|
+
password: "correct horse battery staple"
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const impersonated = await authService.devLoginAs(createDevAuthExchangeRequest(), {
|
|
218
|
+
email: "ADA@EXAMPLE.COM"
|
|
219
|
+
});
|
|
220
|
+
assert.equal(impersonated.profile.email, "ada@example.com");
|
|
221
|
+
assert.equal(impersonated.session.purpose, "dev-auth");
|
|
222
|
+
assert.equal(authService.getCapabilities().features.devLoginAs, true);
|
|
223
|
+
|
|
224
|
+
const reply = createReplyFixture();
|
|
225
|
+
authService.writeSessionCookies(reply, impersonated.session);
|
|
226
|
+
assert.equal(Boolean(reply.cookies.jskit_local_access_token), true);
|
|
227
|
+
assert.equal(Boolean(reply.cookies.jskit_local_refresh_token), true);
|
|
228
|
+
|
|
229
|
+
const authenticated = await authService.authenticateRequest(createLocalAuthRequest({
|
|
230
|
+
cookies: reply.cookies
|
|
231
|
+
}));
|
|
232
|
+
assert.equal(authenticated.authenticated, true);
|
|
233
|
+
assert.equal(authenticated.actor.email, "ada@example.com");
|
|
234
|
+
assert.equal(authenticated.sessionPurpose, "dev-auth");
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("local login-as never invokes the mutating profile projector", async () => {
|
|
238
|
+
const projectedProfile = {
|
|
239
|
+
id: "app-user-ada",
|
|
240
|
+
authProvider: "local",
|
|
241
|
+
authProviderUserSid: "provider-user-ada",
|
|
242
|
+
displayName: "Ada",
|
|
243
|
+
email: "ada@example.com"
|
|
244
|
+
};
|
|
245
|
+
let findCalls = 0;
|
|
246
|
+
let syncCalls = 0;
|
|
247
|
+
const { app } = await createStartedApp({
|
|
248
|
+
env: devAuthEnv(),
|
|
249
|
+
profileProjector: {
|
|
250
|
+
async findByIdentity() {
|
|
251
|
+
findCalls += 1;
|
|
252
|
+
return projectedProfile;
|
|
253
|
+
},
|
|
254
|
+
async syncIdentityProfile(profile) {
|
|
255
|
+
syncCalls += 1;
|
|
256
|
+
return {
|
|
257
|
+
...projectedProfile,
|
|
258
|
+
authProviderUserSid: profile.authProviderUserSid
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
const authService = app.make("authService");
|
|
264
|
+
const registered = await authService.register({
|
|
265
|
+
displayName: "Ada",
|
|
266
|
+
email: "ada@example.com",
|
|
267
|
+
password: "correct horse battery staple"
|
|
268
|
+
});
|
|
269
|
+
projectedProfile.authProviderUserSid = registered.actor.providerUserId;
|
|
270
|
+
findCalls = 0;
|
|
271
|
+
syncCalls = 0;
|
|
272
|
+
|
|
273
|
+
const impersonated = await authService.devLoginAs(createDevAuthExchangeRequest(), {
|
|
274
|
+
email: "ada@example.com"
|
|
275
|
+
});
|
|
276
|
+
const reply = createReplyFixture();
|
|
277
|
+
authService.writeSessionCookies(reply, impersonated.session);
|
|
278
|
+
const authenticated = await authService.authenticateRequest(createLocalAuthRequest({
|
|
279
|
+
cookies: reply.cookies
|
|
280
|
+
}));
|
|
281
|
+
|
|
282
|
+
assert.equal(impersonated.profile.id, "app-user-ada");
|
|
283
|
+
assert.equal(authenticated.profile.id, "app-user-ada");
|
|
284
|
+
assert.equal(findCalls, 2);
|
|
285
|
+
assert.equal(syncCalls, 0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("local login-as rejects missing, disabled, and unprojected users", async () => {
|
|
289
|
+
const { app } = await createStartedApp({
|
|
290
|
+
env: devAuthEnv(),
|
|
291
|
+
profileProjector: {
|
|
292
|
+
async findByIdentity() {
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
295
|
+
async syncIdentityProfile() {
|
|
296
|
+
throw new Error("preview login must not synchronize profiles");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
const backend = app.make("auth.local.backend");
|
|
301
|
+
const password = await hashPassword("stored password value");
|
|
302
|
+
await backend.withTransaction(async (tx) => {
|
|
303
|
+
await tx.users.create({
|
|
304
|
+
disabled: false,
|
|
305
|
+
displayName: "Unprojected",
|
|
306
|
+
email: "unprojected@example.com",
|
|
307
|
+
id: "usr_unprojected",
|
|
308
|
+
password
|
|
309
|
+
});
|
|
310
|
+
await tx.users.create({
|
|
311
|
+
disabled: true,
|
|
312
|
+
displayName: "Disabled",
|
|
313
|
+
email: "disabled@example.com",
|
|
314
|
+
id: "usr_disabled",
|
|
315
|
+
password
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
const authService = app.make("authService");
|
|
319
|
+
|
|
320
|
+
for (const email of [
|
|
321
|
+
"missing@example.com",
|
|
322
|
+
"disabled@example.com",
|
|
323
|
+
"unprojected@example.com"
|
|
324
|
+
]) {
|
|
325
|
+
await assert.rejects(
|
|
326
|
+
() => authService.devLoginAs(createDevAuthExchangeRequest(), { email }),
|
|
327
|
+
/User not found/
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("local login-as rejects missing, incorrect, and non-loopback exchange authorization", async () => {
|
|
333
|
+
const { app } = await createStartedApp({
|
|
334
|
+
env: devAuthEnv()
|
|
335
|
+
});
|
|
336
|
+
const authService = app.make("authService");
|
|
337
|
+
await authService.register({
|
|
338
|
+
displayName: "Ada",
|
|
339
|
+
email: "ada@example.com",
|
|
340
|
+
password: "correct horse battery staple"
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await assert.rejects(
|
|
344
|
+
() => authService.devLoginAs(createLocalAuthRequest(), { email: "ada@example.com" }),
|
|
345
|
+
/not authorized/
|
|
346
|
+
);
|
|
347
|
+
await assert.rejects(
|
|
348
|
+
() => authService.devLoginAs(createLocalAuthRequest({
|
|
349
|
+
headers: {
|
|
350
|
+
[DEV_AUTH_SECRET_HEADER]: "wrong-secret"
|
|
351
|
+
}
|
|
352
|
+
}), { email: "ada@example.com" }),
|
|
353
|
+
/not authorized/
|
|
354
|
+
);
|
|
355
|
+
await assert.rejects(
|
|
356
|
+
() => authService.devLoginAs(createDevAuthExchangeRequest({
|
|
357
|
+
remoteAddress: "203.0.113.5"
|
|
358
|
+
}), { email: "ada@example.com" }),
|
|
359
|
+
/localhost/
|
|
360
|
+
);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("local auth provider rejects dev login-as enablement in production", async () => {
|
|
364
|
+
await assert.rejects(
|
|
365
|
+
() => createStartedApp({
|
|
366
|
+
env: {
|
|
367
|
+
...devAuthEnv(),
|
|
368
|
+
AUTH_LOCAL_FILE_PRODUCTION_ACK: "true",
|
|
369
|
+
AUTH_LOCAL_SESSION_SECRET: "production-session-secret",
|
|
370
|
+
NODE_ENV: "production"
|
|
371
|
+
}
|
|
372
|
+
}),
|
|
373
|
+
(error) => /must not be enabled in production/.test(
|
|
374
|
+
String(error.details?.cause?.message || error.message || "")
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
});
|
|
378
|
+
|
|
165
379
|
test("local auth provider applies auth service decorators for blocking and non-blocking hooks", async () => {
|
|
166
380
|
const calls = [];
|
|
167
381
|
const errors = [];
|