@abtnode/auth 1.8.68 → 1.8.69-beta-54faead3

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.
Files changed (3) hide show
  1. package/lib/auth.js +19 -3
  2. package/lib/server.js +274 -80
  3. package/package.json +15 -14
package/lib/auth.js CHANGED
@@ -156,6 +156,22 @@ const messages = {
156
156
  en: 'Invalid Params',
157
157
  zh: '无效的参数',
158
158
  },
159
+ missingKeyPair: {
160
+ en: 'Missing app key pair',
161
+ zh: '缺少应用钥匙对',
162
+ },
163
+ missingBlockletUrl: {
164
+ en: 'Missing blocklet url',
165
+ zh: '缺少应用下载地址',
166
+ },
167
+ missingBlockletDid: {
168
+ en: 'Missing blocklet did',
169
+ zh: '缺少应用 ID',
170
+ },
171
+ missingChainHost: {
172
+ en: 'Missing chain host',
173
+ zh: '缺少链的端点地址',
174
+ },
159
175
  invalidBlocklet: {
160
176
  en: 'Invalid Blocklet',
161
177
  zh: '无效的 Blocklet',
@@ -809,9 +825,9 @@ const handleIssuePassportResponse = async ({
809
825
  };
810
826
 
811
827
  const getVCFromClaims = async ({ claims, challenge, trustedIssuers, vcTypes, locale = 'en', vcId }) => {
812
- const credential = claims.find(
813
- (x) => x.type === 'verifiableCredential' && vcTypes.some((item) => x.item.includes(item))
814
- );
828
+ const credential = claims
829
+ .filter(Boolean) // FIXES: https://github.com/ArcBlock/did-connect/issues/74
830
+ .find((x) => x.type === 'verifiableCredential' && vcTypes.some((item) => x.item.includes(item)));
815
831
 
816
832
  if (!credential || !credential.presentation) {
817
833
  return {};
package/lib/server.js CHANGED
@@ -1,11 +1,16 @@
1
1
  const get = require('lodash/get');
2
+ const pick = require('lodash/pick');
2
3
  const isEmpty = require('lodash/isEmpty');
3
4
  const last = require('lodash/last');
4
5
  const { isNFTExpired, isNFTConsumed } = require('@abtnode/util/lib/nft');
5
6
  const Client = require('@ocap/client');
6
7
  const { fromPublicKey } = require('@ocap/wallet');
7
- const { fromBase58, toAddress } = require('@ocap/util');
8
+ const { fromBase58, toAddress, toHex } = require('@ocap/util');
8
9
  const { toTypeInfo, isFromPublicKey } = require('@arcblock/did');
10
+ const urlFriendly = require('@blocklet/meta/lib/url-friendly').default;
11
+ const { slugify } = require('transliteration');
12
+ const { getChainInfo } = require('@blocklet/meta/lib/util');
13
+ const getBlockletInfo = require('@blocklet/meta/lib/info');
9
14
  const formatContext = require('@abtnode/util/lib/format-context');
10
15
  const {
11
16
  ROLES,
@@ -14,6 +19,7 @@ const {
14
19
  NFT_TYPE_SERVER_OWNERSHIP,
15
20
  SERVER_ROLES,
16
21
  NFT_TYPE_SERVERLESS,
22
+ MAIN_CHAIN_ENDPOINT,
17
23
  } = require('@abtnode/constant');
18
24
  const { toExternalBlocklet } = require('@blocklet/meta/lib/did');
19
25
  const {
@@ -219,6 +225,16 @@ const authenticateByNFT = async ({ node, claims, userDid, challenge, locale, isA
219
225
  return { role: ROLES.OWNER, teamDid: info.did, nft: state, user, passport: null, ownerDid, ownerNFT: address };
220
226
  };
221
227
 
228
+ const authenticateBySession = async ({ node, userDid, locale, allowedRoles = ['owner', 'admin', 'member'] }) => {
229
+ const info = await node.getNodeInfo();
230
+ const user = await getUser(node, info.did, userDid);
231
+ if (!user) {
232
+ throw new Error(messages.userNotFound[locale]);
233
+ }
234
+ const passport = (user.passports || []).find((x) => x.status === 'valid' && allowedRoles.includes(x.role));
235
+ return { role: passport ? passport.role : ROLES.GUEST, teamDid: info.did, user, passport: null };
236
+ };
237
+
222
238
  const getAuthVcClaim =
223
239
  ({ node, launchBlocklet, blocklet, options }) =>
224
240
  async ({ extraParams: { locale, passportId }, context: { didwallet } }) => {
@@ -273,28 +289,117 @@ const getAuthNFTClaim =
273
289
  return getOwnershipNFTClaim(node, locale);
274
290
  };
275
291
 
276
- const getLaunchBlockletClaims = (node, authMethod) => {
277
- if (authMethod === 'vc') {
292
+ const getKeyPairClaim =
293
+ (node) =>
294
+ async ({ extraParams: { locale, appDid, title }, context: { didwallet } }) => {
295
+ checkWalletVersion({ didwallet, locale });
296
+
297
+ const description = {
298
+ en: 'Please generate a new key-pair for this application',
299
+ zh: '请为应用创建新的钥匙对',
300
+ };
301
+
302
+ let appName = title;
303
+ let migrateFrom = '';
304
+
305
+ // We are rotating a key-pair for existing application
306
+ if (appDid) {
307
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
308
+ if (!blocklet) {
309
+ throw new Error(messages.invalidBlocklet[locale]);
310
+ }
311
+
312
+ const info = await node.getNodeInfo();
313
+ const { name, wallet } = getBlockletInfo(blocklet, info.sk);
314
+ appName = name;
315
+ migrateFrom = wallet.address;
316
+ }
317
+
278
318
  return {
279
- serverPassport: ['verifiableCredential', getAuthVcClaim({ node, launchBlocklet: true })],
319
+ mfa: !process.env.DID_CONNECT_MFA_DISABLED,
320
+ description: description[locale] || description.en,
321
+ moniker: (urlFriendly(slugify(appName)) || 'application').toLowerCase(),
322
+ migrateFrom,
323
+ targetType: {
324
+ role: 'application',
325
+ hash: 'sha3',
326
+ key: 'ed25519',
327
+ encoding: 'base58',
328
+ },
280
329
  };
281
- }
330
+ };
282
331
 
283
- return {
284
- serverNFT: ['asset', getAuthNFTClaim({ node })],
332
+ const getRotateKeyPairClaims = (node) => {
333
+ return [
334
+ {
335
+ authPrincipal: async ({ extraParams: { locale, appDid } }) => {
336
+ const description = {
337
+ en: 'Please select ',
338
+ zh: '请为应用创建新的钥匙对',
339
+ };
340
+
341
+ let chainInfo = { host: 'none', id: 'none', type: 'arcblock' };
342
+
343
+ if (!appDid) {
344
+ throw new Error(messages.missingBlockletDid[locale]);
345
+ }
346
+
347
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
348
+ if (!blocklet) {
349
+ throw new Error(messages.invalidBlocklet[locale]);
350
+ }
351
+
352
+ // Try to use blocklet chain config if possible
353
+ // Since migration happens on the chain the app holds some actual assets
354
+ // We must ensure it happens on that chain
355
+ chainInfo = getChainInfo(blocklet.configObj);
356
+
357
+ // Fallback to main chain, since it is the default registry for all DID
358
+ if (chainInfo.host === 'none') {
359
+ chainInfo = { host: MAIN_CHAIN_ENDPOINT, id: 'main', type: 'arcblock' };
360
+ }
361
+
362
+ return {
363
+ chainInfo,
364
+ description: description[locale] || description.en,
365
+ target: blocklet.appPid,
366
+ };
367
+ },
368
+ },
369
+ {
370
+ keyPair: getKeyPairClaim(node),
371
+ },
372
+ ];
373
+ };
374
+
375
+ const getLaunchBlockletClaims = (node, authMethod) => {
376
+ const claims = {
377
+ blockletAppKeypair: ['keyPair', getKeyPairClaim(node)],
285
378
  };
379
+
380
+ if (authMethod === 'vc') {
381
+ claims.serverPassport = ['verifiableCredential', getAuthVcClaim({ node, launchBlocklet: true })];
382
+ }
383
+
384
+ if (authMethod === 'nft') {
385
+ claims.serverNFT = ['asset', getAuthNFTClaim({ node })];
386
+ }
387
+
388
+ return claims;
286
389
  };
287
390
 
391
+ // FIXME: @wangshijun should be changed to blocklet appSK owner claim?
288
392
  const getSetupBlockletClaims = (node, authMethod, blocklet) => {
393
+ const claims = {};
394
+
289
395
  if (authMethod === 'vc') {
290
- return {
291
- serverPassport: ['verifiableCredential', getAuthVcClaim({ node, blocklet })],
292
- };
396
+ claims.serverPassport = ['verifiableCredential', getAuthVcClaim({ node, blocklet })];
397
+ }
398
+ if (authMethod === 'nft') {
399
+ claims.serverNFT = ['asset', getAuthNFTClaim({ node })];
293
400
  }
294
401
 
295
- return {
296
- serverNFT: ['asset', getAuthNFTClaim({ node })],
297
- };
402
+ return claims;
298
403
  };
299
404
 
300
405
  const getOwnershipNFTClaim = async (node, locale) => {
@@ -348,6 +453,7 @@ const ensureBlockletPermission = async ({
348
453
  blocklet,
349
454
  isAuth,
350
455
  chainHost,
456
+ allowedRoles = ['owner', 'admin', 'member'],
351
457
  }) => {
352
458
  let result;
353
459
  if (authMethod === 'vc') {
@@ -361,7 +467,7 @@ const ensureBlockletPermission = async ({
361
467
  launchBlocklet: true,
362
468
  blocklet,
363
469
  });
364
- } else {
470
+ } else if (authMethod === 'nft') {
365
471
  result = await authenticateByNFT({
366
472
  node,
367
473
  locale,
@@ -371,9 +477,16 @@ const ensureBlockletPermission = async ({
371
477
  isAuth,
372
478
  chainHost,
373
479
  });
480
+ } else {
481
+ result = await authenticateBySession({
482
+ node,
483
+ userDid,
484
+ locale,
485
+ allowedRoles,
486
+ });
374
487
  }
375
- const { teamDid, role } = result;
376
488
 
489
+ const { teamDid, role } = result;
377
490
  const permissions = await node.getPermissionsByRole({ teamDid, role: { name: role } });
378
491
  if (!permissions.some((item) => ['mutate_blocklets'].includes(item.name))) {
379
492
  throw new Error(messages.notAuthorized[locale]);
@@ -385,17 +498,38 @@ const ensureBlockletPermission = async ({
385
498
  const createLaunchBlockletHandler =
386
499
  (node, authMethod) =>
387
500
  async ({ claims, challenge, userDid, updateSession, req, extraParams }) => {
388
- const { locale, blockletMetaUrl, chainHost } = extraParams;
501
+ const { locale, blockletMetaUrl, title, description, chainHost } = extraParams;
389
502
  logger.info('createLaunchBlockletHandler', extraParams);
390
503
 
391
- if (!blockletMetaUrl) {
392
- logger.error('blockletMetaUrl must be provided');
393
- throw new Error(messages.invalidParams[locale]);
504
+ const keyPair = claims.find((x) => x.type === 'keyPair');
505
+ if (!keyPair) {
506
+ logger.error('app keyPair must be provided');
507
+ throw new Error(messages.missingKeyPair[locale]);
508
+ }
509
+
510
+ if (!blockletMetaUrl && !title && !description) {
511
+ logger.error('blockletMetaUrl | title + description must be provided');
512
+ throw new Error(messages.missingBlockletUrl[locale]);
394
513
  }
395
514
 
396
515
  if (authMethod === 'nft' && !chainHost) {
397
516
  logger.error('chainHost must be provided');
398
- throw new Error(messages.invalidParams[locale]);
517
+ throw new Error(messages.missingChainHost[locale]);
518
+ }
519
+
520
+ let blocklet;
521
+ if (blockletMetaUrl) {
522
+ blocklet = await node.getBlockletMetaFromUrl({ url: blockletMetaUrl, checkPrice: true });
523
+ if (!blocklet.meta) {
524
+ throw new Error(messages.invalidBlocklet[locale]);
525
+ }
526
+
527
+ if (!blocklet.isFree) {
528
+ if (isEmpty(extraParams?.previousWorkflowData?.downloadTokenList)) {
529
+ logger.error('downloadTokenList must be provided');
530
+ throw new Error(messages.invalidParams[locale]);
531
+ }
532
+ }
399
533
  }
400
534
 
401
535
  const { role, passport, user, extra, nft } = await ensureBlockletPermission({
@@ -407,22 +541,9 @@ const createLaunchBlockletHandler =
407
541
  locale,
408
542
  isAuth: false,
409
543
  chainHost,
544
+ blocklet,
410
545
  });
411
546
 
412
- const blocklet = await node.getBlockletMetaFromUrl({ url: blockletMetaUrl, checkPrice: true });
413
- if (!blocklet.meta) {
414
- throw new Error(messages.invalidBlocklet[locale]);
415
- }
416
-
417
- if (!blocklet.isFree) {
418
- if (isEmpty(extraParams?.previousWorkflowData?.downloadTokenList)) {
419
- logger.error('downloadTokenList must be provided');
420
- throw new Error(messages.invalidParams[locale]);
421
- }
422
- }
423
-
424
- const { did } = blocklet.meta;
425
-
426
547
  let controller;
427
548
 
428
549
  let sessionToken = '';
@@ -434,76 +555,149 @@ const createLaunchBlockletHandler =
434
555
  secret,
435
556
  expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
436
557
  });
437
- } else if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
438
- controller = extra.controller;
439
- sessionToken = createBlockletControllerAuthToken({
440
- did: userDid,
441
- role,
442
- controller,
443
- secret,
444
- expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
445
- });
446
- } else {
447
- sessionToken = createAuthTokenByOwnershipNFT({
448
- did: userDid,
449
- role,
450
- secret,
451
- expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
452
- });
558
+ }
559
+ if (authMethod === 'nft') {
560
+ if (role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER) {
561
+ controller = extra.controller;
562
+ sessionToken = createBlockletControllerAuthToken({
563
+ did: userDid,
564
+ role,
565
+ controller,
566
+ secret,
567
+ expiresIn: EXTERNAL_LAUNCH_BLOCKLET_TOKEN_EXPIRE,
568
+ });
569
+ } else {
570
+ sessionToken = createAuthTokenByOwnershipNFT({
571
+ did: userDid,
572
+ role,
573
+ secret,
574
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
575
+ });
576
+ }
577
+ }
578
+
579
+ if (sessionToken) {
580
+ await updateSession({ sessionToken }, true);
453
581
  }
454
582
 
455
- await updateSession({ sessionToken }, true);
583
+ if (blocklet) {
584
+ const blockletDid =
585
+ role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER
586
+ ? toExternalBlocklet(blocklet.meta.name, controller.nftId, { didOnly: true })
587
+ : blocklet.meta.did;
456
588
 
457
- const blockletDid =
458
- role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER
459
- ? toExternalBlocklet(blocklet.meta.name, controller.nftId, { didOnly: true })
460
- : blocklet.meta.did;
589
+ await updateSession({ blockletDid });
461
590
 
462
- await updateSession({
463
- blockletDid,
464
- });
591
+ // 检查是否已安装,这里不做升级的处理
592
+ const existedBlocklet = await node.getBlocklet({ did: blockletDid, attachRuntimeInfo: false });
465
593
 
466
- // 检查是否已安装,这里不做升级的处理
467
- const existedBlocklet = await node.getBlocklet({ did: blockletDid, attachRuntimeInfo: false });
594
+ // 如果是 serverless, 并且已经消费过了,但是没有安装,则抛出异常
595
+ if (!existedBlocklet && role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && isNFTConsumed(nft)) {
596
+ throw new Error(messages.nftAlreadyConsume[locale]);
597
+ }
468
598
 
469
- // 如果是 serverless, 并且已经消费过了,但是没有安装,则抛出异常
470
- if (!existedBlocklet && role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER && isNFTConsumed(nft)) {
471
- throw new Error(messages.nftAlreadyConsume[locale]);
599
+ if (existedBlocklet) {
600
+ await updateSession({ isInstalled: true });
601
+ logger.info('blocklet already exists', { blockletDid });
602
+ return;
603
+ }
472
604
  }
473
605
 
474
- if (existedBlocklet) {
475
- await updateSession({ isInstalled: true });
476
- logger.info('blocklet already exists', { blockletDid });
477
- return;
606
+ logger.info('start install blocklet', { blockletMetaUrl, title, description });
607
+ const tmp = await node.installBlocklet(
608
+ {
609
+ url: blockletMetaUrl,
610
+ title,
611
+ description,
612
+ appSk: toHex(keyPair.secret),
613
+ delay: 1000 * 4, // delay 4 seconds to download, wait for ws connection from frontend
614
+ downloadTokenList: extraParams?.previousWorkflowData?.downloadTokenList,
615
+ controller: role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER ? controller : null,
616
+ },
617
+ formatContext(Object.assign(req, { user: { ...pick(user, ['did', 'fullName']), role } }))
618
+ );
619
+
620
+ await updateSession({ blockletDid: tmp.meta.did });
621
+ };
622
+
623
+ const getBlockletPermissionChecker =
624
+ (node, allowedRoles = ['owner', 'admin', 'member']) =>
625
+ async ({ userDid, extraParams }) => {
626
+ const { locale = 'en', connectedDid } = extraParams;
627
+
628
+ if (!connectedDid || userDid !== connectedDid) {
629
+ throw new Error(
630
+ {
631
+ en: 'Please use current connected wallet to install blocklet',
632
+ zh: '请使用当前登录的钱包来安装应用',
633
+ }[locale]
634
+ );
478
635
  }
479
636
 
480
- const tmp = await node.installBlocklet({
481
- url: blockletMetaUrl,
482
- delay: 1000 * 4, // delay 4 seconds to download, wait for ws connection from frontend
483
- downloadTokenList: extraParams?.previousWorkflowData?.downloadTokenList,
484
- controller: role === SERVER_ROLES.EXTERNAL_BLOCKLET_CONTROLLER ? controller : null,
485
- });
637
+ const info = await node.getNodeInfo();
638
+ const user = await getUser(node, info.did, userDid);
639
+ const passport = (user.passports || []).find((x) => x.status === 'valid' && allowedRoles.includes(x.role));
640
+ if (!passport) {
641
+ throw new Error(
642
+ {
643
+ en: 'You do not have permission to install blocklets on this server',
644
+ zh: '你无权在此节点上安装应用',
645
+ }[locale]
646
+ );
647
+ }
648
+ };
649
+
650
+ const createRotateKeyPairHandler =
651
+ (node) =>
652
+ async ({ claims, userDid, req, extraParams }) => {
653
+ const { locale, appDid } = extraParams;
654
+ logger.info('createRotateKeyPairHandler', extraParams);
655
+
656
+ const keyPair = claims.find((x) => x.type === 'keyPair');
657
+ if (!keyPair) {
658
+ logger.error('app keyPair must be provided');
659
+ throw new Error(messages.missingKeyPair[locale]);
660
+ }
661
+
662
+ if (!appDid) {
663
+ logger.error('appDid must be provided');
664
+ throw new Error(messages.missingBlockletDid[locale]);
665
+ }
666
+
667
+ const blocklet = await node.getBlocklet({ did: appDid, attachRuntimeInfo: false });
668
+ if (!blocklet) {
669
+ throw new Error(messages.invalidBlocklet[locale]);
670
+ }
671
+
672
+ // Only the blocklet owner(identified by appPid) can rotate key pair
673
+ if (blocklet.appPid !== userDid) {
674
+ throw new Error(messages.notAllowed[locale]);
675
+ }
486
676
 
487
- await node.createAuditLog(
677
+ await node.configBlocklet(
488
678
  {
489
- action: 'installBlocklet',
490
- args: { url: blockletMetaUrl },
491
- context: formatContext(Object.assign(req, { user })),
492
- result: tmp,
679
+ did: blocklet.meta.did,
680
+ configs: [{ key: 'BLOCKLET_APP_SK', value: toHex(keyPair.secret), secure: true }],
681
+ skipHook: true,
682
+ skipDidDocument: true,
493
683
  },
494
- node
684
+ formatContext(Object.assign(req, { user: { did: userDid, fullName: 'Owner', role: 'owner' } }))
495
685
  );
496
- logger.info('start install blocklet', { blockletDid, bundleDid: did });
497
686
  };
498
687
 
499
688
  module.exports = {
500
689
  getAuthVcClaim,
690
+ getKeyPairClaim,
501
691
  authenticateByVc,
502
692
  authenticateByNFT,
693
+ authenticateBySession,
694
+ getRotateKeyPairClaims,
503
695
  getOwnershipNFTClaim,
504
696
  getLaunchBlockletClaims,
505
697
  createLaunchBlockletHandler,
698
+ createRotateKeyPairHandler,
506
699
  ensureBlockletPermission,
700
+ getBlockletPermissionChecker,
507
701
  getSetupBlockletClaims,
508
702
  getTrustedIssuers,
509
703
  getServerlessNFTClaim,
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.8.68",
6
+ "version": "1.8.69-beta-54faead3",
7
7
  "description": "Simple lib to manage auth in ABT Node",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -20,27 +20,28 @@
20
20
  "author": "linchen <linchen1987@foxmail.com> (http://github.com/linchen1987)",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
- "@abtnode/constant": "1.8.68",
24
- "@abtnode/logger": "1.8.68",
25
- "@abtnode/util": "1.8.68",
26
- "@arcblock/did": "1.18.42",
27
- "@arcblock/jwt": "^1.18.42",
28
- "@arcblock/vc": "1.18.42",
29
- "@blocklet/constant": "1.8.68",
30
- "@blocklet/meta": "1.8.68",
31
- "@ocap/client": "1.18.42",
32
- "@ocap/mcrypto": "1.18.42",
33
- "@ocap/util": "1.18.42",
34
- "@ocap/wallet": "1.18.42",
23
+ "@abtnode/constant": "1.8.69-beta-54faead3",
24
+ "@abtnode/logger": "1.8.69-beta-54faead3",
25
+ "@abtnode/util": "1.8.69-beta-54faead3",
26
+ "@arcblock/did": "1.18.57",
27
+ "@arcblock/jwt": "^1.18.57",
28
+ "@arcblock/vc": "1.18.57",
29
+ "@blocklet/constant": "1.8.69-beta-54faead3",
30
+ "@blocklet/meta": "1.8.69-beta-54faead3",
31
+ "@ocap/client": "1.18.57",
32
+ "@ocap/mcrypto": "1.18.57",
33
+ "@ocap/util": "1.18.57",
34
+ "@ocap/wallet": "1.18.57",
35
35
  "axios": "^0.27.2",
36
36
  "joi": "17.7.0",
37
37
  "jsonwebtoken": "^9.0.0",
38
38
  "lodash": "^4.17.21",
39
39
  "semver": "^7.3.8",
40
+ "transliteration": "^2.3.5",
40
41
  "url-join": "^4.0.1"
41
42
  },
42
43
  "devDependencies": {
43
44
  "jest": "^27.5.1"
44
45
  },
45
- "gitHead": "1392044ac5677bde567797adeb9a6d3f0b9264b8"
46
+ "gitHead": "3dec0c85a77de5ba2d37c19ac769b126bfaafc86"
46
47
  }