@jskit-ai/auth-provider-local-core 0.1.34 → 0.1.36

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.
@@ -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.34",
4
+ "version": "0.1.36",
5
5
  "kind": "runtime",
6
6
  "description": "Local auth provider with a file backend default and no database requirement.",
7
7
  "dependsOn": [
@@ -64,8 +64,8 @@ export default Object.freeze({
64
64
  "mutations": {
65
65
  "dependencies": {
66
66
  "runtime": {
67
- "@jskit-ai/auth-core": "0.1.130",
68
- "@jskit-ai/kernel": "0.1.133",
67
+ "@jskit-ai/auth-core": "0.1.132",
68
+ "@jskit-ai/kernel": "0.1.134",
69
69
  "nodemailer": "^9.0.3",
70
70
  "dotenv": "^16.4.5"
71
71
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/auth-provider-local-core",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
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.130",
15
- "@jskit-ai/kernel": "0.1.133",
14
+ "@jskit-ai/auth-core": "0.1.132",
15
+ "@jskit-ai/kernel": "0.1.134",
16
16
  "nodemailer": "^9.0.3"
17
17
  }
18
18
  }
@@ -154,6 +154,23 @@ async function findExistingAppProfile(user, profileProjector) {
154
154
  return profile ? requireProfileResult(profile, "findByIdentity") : null;
155
155
  }
156
156
 
157
+ async function findExistingAppProfileByLogin(login, profileProjector) {
158
+ if (!profileProjector || typeof profileProjector.findByLogin !== "function") {
159
+ return null;
160
+ }
161
+ const profile = await profileProjector.findByLogin(login);
162
+ return profile ? requireProfileResult(profile, "findByLogin") : null;
163
+ }
164
+
165
+ function appProfileLocalUserId(profile = null) {
166
+ if (
167
+ String(profile?.authProvider || "").trim().toLowerCase() !== PROVIDER_ID
168
+ ) {
169
+ return "";
170
+ }
171
+ return String(profile?.authProviderUserSid || "").trim();
172
+ }
173
+
157
174
  async function buildSessionAuthPayload({ devAuth, profileProjector, request, session, user }) {
158
175
  if (!isDevAuthSession(session)) {
159
176
  return buildAuthPayload({ user, session: null, profileProjector });
@@ -230,17 +247,18 @@ function validateEmailInput(email) {
230
247
 
231
248
  function devLoginAsValidationError(fieldErrors = {}) {
232
249
  const messages = Object.values(fieldErrors).filter(Boolean);
233
- return new AppError(400, messages[0] || "Provide a user id or email.", {
250
+ return new AppError(400, messages[0] || "Provide a user id, email, or login name.", {
234
251
  details: {
235
252
  fieldErrors
236
253
  }
237
254
  });
238
255
  }
239
256
 
240
- function devLoginAsUserNotFound({ email = "", userId = "" } = {}) {
257
+ function devLoginAsUserNotFound({ email = "", login = "", userId = "" } = {}) {
241
258
  return devLoginAsValidationError({
242
259
  ...(userId ? { userId: "User not found." } : {}),
243
- ...(email ? { email: "User not found." } : {})
260
+ ...(email ? { email: "User not found." } : {}),
261
+ ...(login ? { login: "User not found." } : {})
244
262
  });
245
263
  }
246
264
 
@@ -458,10 +476,12 @@ function createLocalAuthService({
458
476
  ensureDevAuthExchangeAvailable(devAuth, request);
459
477
  const userId = String(input?.userId || "").trim();
460
478
  const email = normalizeEmail(input?.email || "");
461
- if (!userId && !email) {
479
+ const login = String(input?.login || "").trim();
480
+ if (!userId && !email && !login) {
462
481
  throw devLoginAsValidationError({
463
- userId: "Provide a user id or email.",
464
- email: "Provide a user id or email."
482
+ userId: "Provide a user id, email, or login name.",
483
+ email: "Provide a user id, email, or login name.",
484
+ login: "Provide a user id, email, or login name."
465
485
  });
466
486
  }
467
487
 
@@ -470,12 +490,18 @@ function createLocalAuthService({
470
490
  if (!user && email) {
471
491
  user = await tx.users.findByEmail(email);
472
492
  }
493
+ let appProfile = null;
494
+ if (!user && login) {
495
+ appProfile = await findExistingAppProfileByLogin(login, profileProjector);
496
+ const localUserId = appProfileLocalUserId(appProfile);
497
+ user = localUserId ? await tx.users.findById(localUserId) : null;
498
+ }
473
499
  if (!user || user.disabled) {
474
- throw devLoginAsUserNotFound({ email, userId });
500
+ throw devLoginAsUserNotFound({ email, login, userId });
475
501
  }
476
- const appProfile = await findExistingAppProfile(user, profileProjector);
502
+ appProfile ||= await findExistingAppProfile(user, profileProjector);
477
503
  if (profileProjector && !appProfile) {
478
- throw devLoginAsUserNotFound({ email, userId });
504
+ throw devLoginAsUserNotFound({ email, login, userId });
479
505
  }
480
506
  const session = await createSessionForUser(tx, user, {
481
507
  purpose: DEV_AUTH_SESSION_PURPOSE
@@ -158,6 +158,9 @@ function createLazyProfileProjector(scope) {
158
158
  findByIdentity(identity, options = {}) {
159
159
  return resolveMethod("findByIdentity")(identity, options);
160
160
  },
161
+ findByLogin(login, options = {}) {
162
+ return resolveMethod("findByLogin")(login, options);
163
+ },
161
164
  syncIdentityProfile(profile, options = {}) {
162
165
  return resolveMethod("syncIdentityProfile")(profile, options);
163
166
  }
@@ -234,6 +234,109 @@ test("local file auth login-as issues native cookies for an existing user", asyn
234
234
  assert.equal(authenticated.sessionPurpose, "dev-auth");
235
235
  });
236
236
 
237
+ test("local login-as resolves an application login through its existing auth profile link", async () => {
238
+ const projectedProfile = {
239
+ id: "app-user-ada",
240
+ authProvider: "local",
241
+ authProviderUserSid: "",
242
+ displayName: "Ada",
243
+ email: "ada@example.com",
244
+ username: "merc"
245
+ };
246
+ let syncCalls = 0;
247
+ const { app } = await createStartedApp({
248
+ env: devAuthEnv(),
249
+ profileProjector: {
250
+ async findByIdentity() {
251
+ return projectedProfile;
252
+ },
253
+ async findByLogin(login) {
254
+ return login === "merc" ? projectedProfile : null;
255
+ },
256
+ async syncIdentityProfile(profile) {
257
+ syncCalls += 1;
258
+ return {
259
+ ...projectedProfile,
260
+ authProviderUserSid: profile.authProviderUserSid
261
+ };
262
+ }
263
+ }
264
+ });
265
+ const authService = app.make("authService");
266
+ const registered = await authService.register({
267
+ displayName: "Ada",
268
+ email: "ada@example.com",
269
+ password: "correct horse battery staple"
270
+ });
271
+ projectedProfile.authProviderUserSid = registered.actor.providerUserId;
272
+ syncCalls = 0;
273
+
274
+ const impersonated = await authService.devLoginAs(createDevAuthExchangeRequest(), {
275
+ login: "merc"
276
+ });
277
+
278
+ assert.equal(impersonated.profile.id, "app-user-ada");
279
+ assert.equal(impersonated.actor.providerUserId, registered.actor.providerUserId);
280
+ assert.equal(impersonated.session.purpose, "dev-auth");
281
+ assert.equal(syncCalls, 0);
282
+ });
283
+
284
+ test("local login-as rejects unknown, disabled, and non-local application logins", async () => {
285
+ const disabledProfile = {
286
+ id: "app-user-disabled",
287
+ authProvider: "local",
288
+ authProviderUserSid: "local-user-disabled",
289
+ displayName: "Disabled",
290
+ email: "disabled-login@example.com",
291
+ username: "disabled"
292
+ };
293
+ const { app } = await createStartedApp({
294
+ env: devAuthEnv(),
295
+ profileProjector: {
296
+ async findByIdentity() {
297
+ return null;
298
+ },
299
+ async findByLogin(login) {
300
+ if (login === "disabled") {
301
+ return disabledProfile;
302
+ }
303
+ if (login === "external") {
304
+ return {
305
+ id: "app-user-external",
306
+ authProvider: "external",
307
+ authProviderUserSid: "external-user-1",
308
+ displayName: "External",
309
+ email: "external@example.com",
310
+ username: login
311
+ };
312
+ }
313
+ return null;
314
+ },
315
+ async syncIdentityProfile() {
316
+ throw new Error("preview login must not synchronize profiles");
317
+ }
318
+ }
319
+ });
320
+ const backend = app.make("auth.local.backend");
321
+ await backend.withTransaction(async (tx) => {
322
+ await tx.users.create({
323
+ disabled: true,
324
+ displayName: "Disabled",
325
+ email: disabledProfile.email,
326
+ id: disabledProfile.authProviderUserSid,
327
+ password: await hashPassword("stored password value")
328
+ });
329
+ });
330
+ const authService = app.make("authService");
331
+
332
+ for (const login of ["missing", "disabled", "external"]) {
333
+ await assert.rejects(
334
+ () => authService.devLoginAs(createDevAuthExchangeRequest(), { login }),
335
+ /User not found/
336
+ );
337
+ }
338
+ });
339
+
237
340
  test("local login-as never invokes the mutating profile projector", async () => {
238
341
  const projectedProfile = {
239
342
  id: "app-user-ada",