@abtnode/auth 1.7.4 → 1.7.5

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/lib/server.js ADDED
@@ -0,0 +1,377 @@
1
+ const get = require('lodash/get');
2
+ const last = require('lodash/last');
3
+ const Client = require('@ocap/client');
4
+ const { fromPublicKey } = require('@ocap/wallet');
5
+ const { fromBase58, toAddress } = require('@ocap/util');
6
+ const { toTypeInfo, isFromPublicKey } = require('@arcblock/did');
7
+ const semver = require('semver');
8
+ const {
9
+ ROLES,
10
+ VC_TYPE_GENERAL_PASSPORT,
11
+ VC_TYPE_NODE_PASSPORT,
12
+ VC_TYPE_BLOCKLET_PURCHASE,
13
+ NFT_TYPE_SERVER_OWNERSHIP,
14
+ } = require('@abtnode/constant');
15
+ const {
16
+ messages,
17
+ getVCFromClaims,
18
+ getUser,
19
+ validatePassportStatus,
20
+ createAuthToken,
21
+ createAuthTokenByOwnershipNFT,
22
+ checkWalletVersion,
23
+ } = require('./auth');
24
+ const {
25
+ validatePassport,
26
+ isUserPassportRevoked,
27
+ getRoleFromLocalPassport,
28
+ getRoleFromExternalPassport,
29
+ createUserPassport,
30
+ } = require('./passport');
31
+
32
+ const logger = require('./logger');
33
+
34
+ const secret = process.env.ABT_NODE_SESSION_SECRET;
35
+ const LAUNCH_BLOCKLET_TOKEN_EXPIRE = '1d';
36
+ const abtnodeVcTypes = [VC_TYPE_GENERAL_PASSPORT, VC_TYPE_NODE_PASSPORT];
37
+ const blockletVcTypes = [VC_TYPE_BLOCKLET_PURCHASE];
38
+
39
+ const authenticateByVc = async ({ node, locale, userDid, claims, challenge, requireNodeInitialized = true }) => {
40
+ if (requireNodeInitialized) {
41
+ if ((await node.isInitialized()) === false) {
42
+ throw new Error(messages.notInitialized[locale]);
43
+ }
44
+ }
45
+
46
+ const info = await node.getNodeInfo();
47
+ const teamDid = info.did;
48
+ const { name } = info;
49
+
50
+ // check user approved
51
+ const user = await getUser(node, teamDid, userDid);
52
+ if (user && !user.approved) {
53
+ throw new Error(messages.notAllowed[locale]);
54
+ }
55
+
56
+ // Get passport
57
+ const trustedPassports = (info.trustedPassports || []).map((x) => x.issuerDid);
58
+ const trustedIssuers = [info.did, ...trustedPassports].filter(Boolean);
59
+ const { vc, types: passportTypes } = await getVCFromClaims({
60
+ claims,
61
+ challenge,
62
+ trustedIssuers,
63
+ vcTypes: abtnodeVcTypes,
64
+ locale,
65
+ });
66
+
67
+ if (!vc) {
68
+ throw new Error(messages.missingCredentialClaim[locale]);
69
+ }
70
+
71
+ // Get user passport from vc
72
+ let passport = createUserPassport(vc);
73
+ if (user && isUserPassportRevoked(user, passport)) {
74
+ throw new Error(messages.passportRevoked[locale](name));
75
+ }
76
+
77
+ // Get role from vc
78
+ let role;
79
+ if (passportTypes.some((x) => [VC_TYPE_GENERAL_PASSPORT].includes(x))) {
80
+ await validatePassport(get(vc, 'credentialSubject.passport'));
81
+ const issuerId = get(vc, 'issuer.id');
82
+ if (issuerId === teamDid) {
83
+ role = getRoleFromLocalPassport(get(vc, 'credentialSubject.passport'));
84
+ } else {
85
+ // map external passport to local role
86
+ const { mappings = [] } = (info.trustedPassports || []).find((x) => x.issuerDid === issuerId) || {};
87
+ role = await getRoleFromExternalPassport({
88
+ passport: get(vc, 'credentialSubject.passport'),
89
+ node,
90
+ teamDid,
91
+ locale,
92
+ mappings,
93
+ });
94
+
95
+ // check status of external passport if passport has an endpoint
96
+ const endpoint = get(vc, 'credentialStatus.id');
97
+ if (endpoint) {
98
+ if (endpoint) {
99
+ await validatePassportStatus({ vcId: vc.id, endpoint, locale });
100
+ }
101
+ }
102
+ }
103
+ } else if (passportTypes.some((x) => [NFT_TYPE_SERVER_OWNERSHIP].includes(x))) {
104
+ role = ROLES.OWNER;
105
+ } else {
106
+ logger.error('cannot get role from passport, use "guest" for default', { passportTypes, vcId: vc.id });
107
+ role = ROLES.GUEST;
108
+ }
109
+
110
+ // Recreate passport with correct role
111
+ passport = createUserPassport(vc, { role });
112
+
113
+ return { role, user, teamDid, passport };
114
+ };
115
+
116
+ const authenticateByNFT = async ({ node, claims, userDid, challenge, locale }) => {
117
+ const info = await node.getNodeInfo();
118
+ const client = new Client(info.launcher.chainHost);
119
+
120
+ const claim = claims.find((x) => x.type === 'asset');
121
+ if (!claim) {
122
+ throw new Error(messages.missingNftClaim[locale]);
123
+ }
124
+
125
+ const fields = ['asset', 'ownerProof', 'ownerPk', 'ownerDid'];
126
+ for (const field of fields) {
127
+ if (!claim[field]) {
128
+ throw new Error(messages.invalidNftClaim[locale]);
129
+ }
130
+ }
131
+
132
+ const address = claim.asset;
133
+ const ownerDid = toAddress(claim.ownerDid);
134
+ const ownerPk = fromBase58(claim.ownerPk);
135
+ const ownerProof = fromBase58(claim.ownerProof);
136
+ if (isFromPublicKey(ownerDid, ownerPk) === false) {
137
+ throw new Error(messages.invalidNftHolder[locale]);
138
+ }
139
+
140
+ const owner = fromPublicKey(ownerPk, toTypeInfo(ownerDid));
141
+ if (owner.verify(challenge, ownerProof) === false) {
142
+ throw new Error(messages.invalidNftProof[locale]);
143
+ }
144
+
145
+ const { state } = await client.getAssetState({ address }, { ignoreFields: ['context'] });
146
+ if (!state) {
147
+ throw new Error(messages.invalidNft[locale]);
148
+ }
149
+ if (state.owner !== ownerDid || state.owner !== info.ownerNft.holder) {
150
+ throw new Error(messages.invalidNftHolder[locale]);
151
+ }
152
+ if (state.issuer !== info.launcher.did) {
153
+ throw new Error(messages.invalidNftIssuer[locale]);
154
+ }
155
+
156
+ // enforce tag match
157
+ if (last(state.tags) !== info.launcher.tag) {
158
+ throw new Error(messages.tagNotMatch[locale]);
159
+ }
160
+
161
+ const user = await getUser(node, info.did, userDid);
162
+ return { role: ROLES.OWNER, teamDid: info.did, nft: state, user, passport: null };
163
+ };
164
+
165
+ const getAuthVcClaim =
166
+ (node) =>
167
+ async ({ extraParams: { locale }, context: { abtwallet } }) => {
168
+ checkWalletVersion({ abtwallet, locale });
169
+ const info = await node.getNodeInfo();
170
+ const trustedPassports = (info.trustedPassports || []).map((x) => x.issuerDid);
171
+ const trustedIssuers = [info.did, ...trustedPassports].filter(Boolean);
172
+ return {
173
+ description: messages.requestPassport[locale],
174
+ optional: false,
175
+ item: abtnodeVcTypes,
176
+ trustedIssuers,
177
+ };
178
+ };
179
+
180
+ const getLaunchFreeBlockletClaims = (node, authMethod) => {
181
+ if (authMethod === 'vc') {
182
+ return {
183
+ serverPassport: ['verifiableCredential', getAuthVcClaim(node)],
184
+ };
185
+ }
186
+
187
+ return {
188
+ serverNFT: [
189
+ 'asset',
190
+ async ({ extraParams: { locale }, context: { abtwallet } }) => {
191
+ checkWalletVersion({ abtwallet, locale });
192
+ return getOwnershipNFTClaim(node, locale);
193
+ },
194
+ ],
195
+ };
196
+ };
197
+
198
+ const getLaunchPaidBlockletClaims = (node, authMethod) => {
199
+ const claims = getLaunchFreeBlockletClaims(node, authMethod);
200
+
201
+ claims.blockletPurchaseNft = [
202
+ 'verifiableCredential',
203
+ async ({ extraParams: { locale, blockletMetaUrl }, context: { abtwallet } }) => {
204
+ checkWalletVersion({ abtwallet, locale });
205
+ const registryUrl = new URL(blockletMetaUrl).origin;
206
+ const [registry, { meta }] = await Promise.all([
207
+ node.getRegistryMeta(registryUrl),
208
+ node.getBlockletMetaFromUrl({ url: blockletMetaUrl }),
209
+ ]);
210
+
211
+ return {
212
+ description: messages.requestBlockletNft[locale],
213
+ item: blockletVcTypes,
214
+ trustedIssuers: [registry.id],
215
+ tag: meta.did,
216
+ };
217
+ },
218
+ ];
219
+
220
+ return claims;
221
+ };
222
+
223
+ const getOwnershipNFTClaim = async (node, locale) => {
224
+ const info = await node.getNodeInfo();
225
+ if (!info.ownerNft && !info.ownerNft.holder && !info.ownerNft.issuer) {
226
+ throw new Error(messages.noNft[locale]);
227
+ }
228
+
229
+ const tag = get(info, 'launcher.tag', '');
230
+ const chainHost = get(info, 'launcher.chainHost', '');
231
+ if (!tag) {
232
+ throw new Error(messages.noTag[locale]);
233
+ }
234
+ if (!chainHost) {
235
+ throw new Error(messages.noChainHost[locale]);
236
+ }
237
+
238
+ return {
239
+ description: messages.requestNft[locale],
240
+ trustedIssuers: [info.ownerNft.issuer],
241
+ tag, // tag is an unique identifier for the server in launcher
242
+ };
243
+ };
244
+
245
+ const ensureBlockletPermission = async ({ authMethod, node, userDid, claims, challenge, locale }) => {
246
+ let result;
247
+ if (authMethod === 'vc') {
248
+ result = await authenticateByVc({
249
+ node,
250
+ userDid,
251
+ claims,
252
+ challenge,
253
+ requireNodeInitialized: false,
254
+ locale,
255
+ });
256
+ } else {
257
+ result = await authenticateByNFT({
258
+ node,
259
+ locale,
260
+ userDid,
261
+ claims,
262
+ challenge,
263
+ });
264
+ }
265
+ const { teamDid, role } = result;
266
+
267
+ const permissions = await node.getPermissionsByRole({ teamDid, role: { name: role } });
268
+ if (!permissions.some((item) => item.name === 'mutate_blocklet')) {
269
+ throw new Error(messages.notAuthorized[locale]);
270
+ }
271
+
272
+ return result;
273
+ };
274
+
275
+ const createLaunchBlockletHandler =
276
+ (node, authMethod) =>
277
+ async ({ claims, challenge, userDid, token, storage, extraParams: { locale, blockletMetaUrl } }) => {
278
+ if (!blockletMetaUrl) {
279
+ throw new Error(messages.invalidParams[locale]);
280
+ }
281
+
282
+ const { role, passport } = await ensureBlockletPermission({ authMethod, node, userDid, claims, challenge, locale });
283
+
284
+ const result = await node.getBlockletMetaFromUrl({ url: blockletMetaUrl, checkPrice: true });
285
+ if (!result.meta) {
286
+ throw new Error(messages.invalidBlocklet[locale]);
287
+ }
288
+
289
+ const { did } = result.meta;
290
+
291
+ let blockletPurchaseVerified;
292
+ if (!result.isFree) {
293
+ const registryUrl = new URL(blockletMetaUrl).origin;
294
+ const registryMeta = await node.getRegistryMeta(registryUrl);
295
+
296
+ const { vc: blockletVc } = await getVCFromClaims({
297
+ claims,
298
+ challenge,
299
+ trustedIssuers: [registryMeta.id],
300
+ vcTypes: blockletVcTypes,
301
+ locale,
302
+ });
303
+
304
+ if (!blockletVc) {
305
+ throw new Error(messages.missingBlockletCredentialClaim[locale]);
306
+ }
307
+
308
+ if (get(blockletVc, 'credentialSubject.purchased.blocklet.id') !== did) {
309
+ throw new Error(messages.invalidBlockletVc[locale]);
310
+ }
311
+
312
+ blockletPurchaseVerified = true;
313
+ }
314
+
315
+ let sessionToken = '';
316
+ if (authMethod === 'vc') {
317
+ sessionToken = createAuthToken({
318
+ did: userDid,
319
+ passport,
320
+ role,
321
+ secret,
322
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
323
+ });
324
+ } else {
325
+ sessionToken = createAuthTokenByOwnershipNFT({
326
+ did: userDid,
327
+ role,
328
+ secret,
329
+ expiresIn: LAUNCH_BLOCKLET_TOKEN_EXPIRE,
330
+ });
331
+ }
332
+
333
+ // 检查是否已安装,这里不做升级的处理
334
+ const existedBlocklet = await node.getBlocklet({ did });
335
+ if (existedBlocklet) {
336
+ const storageData = {
337
+ did: userDid,
338
+ sessionToken,
339
+ };
340
+
341
+ if (semver.gt(result.meta.version, existedBlocklet.meta.version)) {
342
+ const appDidEnv = existedBlocklet.environments.find((e) => e.key === 'BLOCKLET_APP_ID');
343
+ storageData.upgradeAvailable = {
344
+ appDid: appDidEnv ? appDidEnv.value : '',
345
+ did: existedBlocklet.meta.did,
346
+ currentVersion: existedBlocklet.meta.version,
347
+ version: result.meta.version,
348
+ };
349
+ }
350
+
351
+ await storage.update(token, storageData);
352
+
353
+ logger.info('blocklet already exists', { did });
354
+ return;
355
+ }
356
+
357
+ await storage.update(token, { did: userDid, sessionToken });
358
+
359
+ const context = {};
360
+ if (typeof blockletPurchaseVerified !== 'undefined') {
361
+ context.blockletPurchaseVerified = blockletPurchaseVerified;
362
+ }
363
+
364
+ await node.installBlocklet({ url: blockletMetaUrl }, context);
365
+ logger.info('start install blocklet', { did });
366
+ };
367
+
368
+ module.exports = {
369
+ getAuthVcClaim,
370
+ authenticateByVc,
371
+ authenticateByNFT,
372
+ getOwnershipNFTClaim,
373
+ getLaunchFreeBlockletClaims,
374
+ getLaunchPaidBlockletClaims,
375
+ createLaunchBlockletHandler,
376
+ ensureBlockletPermission,
377
+ };
@@ -0,0 +1,21 @@
1
+ const get = require('lodash/get');
2
+
3
+ const getServerAuthMethod = (info) => {
4
+ if (info.initialized) {
5
+ return 'vc';
6
+ }
7
+
8
+ if (
9
+ get(info, 'ownerNft.holder') &&
10
+ get(info, 'ownerNft.issuer') &&
11
+ get(info, 'launcher.tag') &&
12
+ get(info, 'launcher.chainHost') &&
13
+ get(info, 'launcher.did') === get(info, 'ownerNft.issuer')
14
+ ) {
15
+ return 'nft';
16
+ }
17
+
18
+ return 'vc';
19
+ };
20
+
21
+ module.exports = { getServerAuthMethod };
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.7.4",
6
+ "version": "1.7.5",
7
7
  "description": "Simple lib to manage auth in ABT Node",
8
8
  "main": "lib/index.js",
9
9
  "files": [
@@ -20,15 +20,16 @@
20
20
  "author": "linchen <linchen1987@foxmail.com> (http://github.com/linchen1987)",
21
21
  "license": "MIT",
22
22
  "dependencies": {
23
- "@abtnode/constant": "1.7.4",
24
- "@abtnode/logger": "1.7.4",
25
- "@abtnode/util": "1.7.4",
26
- "@arcblock/did": "^1.15.3",
27
- "@arcblock/vc": "^1.15.3",
28
- "@blocklet/meta": "1.7.4",
29
- "@ocap/mcrypto": "^1.15.3",
30
- "@ocap/util": "^1.15.3",
31
- "@ocap/wallet": "^1.15.3",
23
+ "@abtnode/constant": "1.7.5",
24
+ "@abtnode/logger": "1.7.5",
25
+ "@abtnode/util": "1.7.5",
26
+ "@arcblock/did": "^1.15.7",
27
+ "@arcblock/vc": "^1.15.7",
28
+ "@blocklet/meta": "1.7.5",
29
+ "@ocap/client": "1.15.7",
30
+ "@ocap/mcrypto": "^1.15.7",
31
+ "@ocap/util": "^1.15.7",
32
+ "@ocap/wallet": "^1.15.7",
32
33
  "axios": "^0.25.0",
33
34
  "joi": "^17.6.0",
34
35
  "jsonwebtoken": "^8.5.1",
@@ -39,5 +40,5 @@
39
40
  "devDependencies": {
40
41
  "jest": "^27.4.5"
41
42
  },
42
- "gitHead": "02b25a877e5b56b389ab318a851bea01212e10df"
43
+ "gitHead": "b17d83773e5a4c06bae390fc70398d49d6dd86b3"
43
44
  }