@arcblock/did-connect-js 1.21.2
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/LICENSE +13 -0
- package/README.md +211 -0
- package/lib/authenticator/base.js +98 -0
- package/lib/authenticator/wallet.js +768 -0
- package/lib/handlers/base.js +49 -0
- package/lib/handlers/util.js +943 -0
- package/lib/handlers/wallet.js +168 -0
- package/lib/index.d.ts +384 -0
- package/lib/index.js +7 -0
- package/lib/protocol.js +46 -0
- package/lib/schema/claims.js +256 -0
- package/lib/schema/index.js +56 -0
- package/package.json +86 -0
|
@@ -0,0 +1,768 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
/* eslint-disable indent */
|
|
3
|
+
/* eslint-disable object-curly-newline */
|
|
4
|
+
const qs = require('querystring');
|
|
5
|
+
const pick = require('lodash/pick');
|
|
6
|
+
const random = require('lodash/random');
|
|
7
|
+
const shuffle = require('lodash/shuffle');
|
|
8
|
+
const isEqual = require('lodash/isEqual');
|
|
9
|
+
const Client = require('@ocap/client');
|
|
10
|
+
const Jwt = require('@arcblock/jwt');
|
|
11
|
+
const RSA = require('@ocap/mcrypto/lib/crypter/rsa').default;
|
|
12
|
+
const { toDid, toBase58, fromBase58 } = require('@ocap/util');
|
|
13
|
+
const { fromAddress } = require('@ocap/wallet');
|
|
14
|
+
const { toAddress } = require('@arcblock/did');
|
|
15
|
+
|
|
16
|
+
const BaseAuthenticator = require('./base');
|
|
17
|
+
|
|
18
|
+
// eslint-disable-next-line
|
|
19
|
+
const debug = require('debug')(`${require('../../package.json').name}:authenticator:wallet`);
|
|
20
|
+
|
|
21
|
+
const { DEFAULT_CHAIN_INFO } = BaseAuthenticator;
|
|
22
|
+
const DEFAULT_TIMEOUT = 8000;
|
|
23
|
+
const MFA_CODE_COUNT = 3;
|
|
24
|
+
|
|
25
|
+
const schema = require('../schema');
|
|
26
|
+
|
|
27
|
+
const formatDisplay = (display) => {
|
|
28
|
+
// empty
|
|
29
|
+
if (!display) {
|
|
30
|
+
return '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// object like
|
|
34
|
+
if (display && display.type && display.content) {
|
|
35
|
+
return JSON.stringify(pick(display, ['type', 'content']));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// string like
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(display);
|
|
41
|
+
if (parsed && parsed.type && parsed.content) {
|
|
42
|
+
return display;
|
|
43
|
+
}
|
|
44
|
+
return '';
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return '';
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
class WalletAuthenticator extends BaseAuthenticator {
|
|
51
|
+
/**
|
|
52
|
+
* @typedef ApplicationInfo
|
|
53
|
+
* @prop {string} name - application name
|
|
54
|
+
* @prop {string} description - application description
|
|
55
|
+
* @prop {string} icon - application icon/logo url
|
|
56
|
+
* @prop {string} link - application home page, with which user can return application from wallet
|
|
57
|
+
* @prop {string} path - deep link url
|
|
58
|
+
* @prop {string} publisher - application did with `did:abt:` prefix
|
|
59
|
+
*/
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @typedef ChainInfo
|
|
63
|
+
* @prop {string} id - application chain id
|
|
64
|
+
* @prop {string} type - application chain type
|
|
65
|
+
* @prop {string} host - graphql endpoint of the application chain
|
|
66
|
+
*/
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Creates an instance of DID Authenticator.
|
|
70
|
+
*
|
|
71
|
+
* @class
|
|
72
|
+
* @param {object} config
|
|
73
|
+
* @param {WalletObject|Function} config.wallet - wallet instance {@see @ocap/wallet} or a function that returns wallet instance
|
|
74
|
+
* @param {WalletObject|Function} [config.delegator] - the party that authorizes `wallet` to perform actions on behalf of `wallet`
|
|
75
|
+
* @param {string|Function} [config.delegation] - the jwt token that proves delegation relationship
|
|
76
|
+
* @param {ApplicationInfo|Function} config.appInfo - application basic info or a function that returns application info
|
|
77
|
+
* @param {ChainInfo|Function} config.chainInfo - application chain info or a function that returns chain info
|
|
78
|
+
* @param {Number} [config.timeout=8000] - timeout in milliseconds when generating claim
|
|
79
|
+
* @param {object} [config.baseUrl] - url to assemble wallet request uri, can be inferred from request object
|
|
80
|
+
* @param {string} [config.tokenKey='_t_'] - query param key for `token`
|
|
81
|
+
* @example
|
|
82
|
+
* const { fromRandom } = require('@ocap/wallet');
|
|
83
|
+
*
|
|
84
|
+
* const wallet = fromRandom().toJSON();
|
|
85
|
+
* const chainHost = 'https://beta.abtnetwork.io/api';
|
|
86
|
+
* const chainId = 'beta';
|
|
87
|
+
* const auth = new Authenticator({
|
|
88
|
+
* wallet,
|
|
89
|
+
* baseUrl: 'http://beta.abtnetwork.io/webapp',
|
|
90
|
+
* appInfo: {
|
|
91
|
+
* name: 'DID Wallet Demo',
|
|
92
|
+
* description: 'Demo application to show the potential of DID Wallet',
|
|
93
|
+
* icon: 'https://arcblock.oss-cn-beijing.aliyuncs.com/images/wallet-round.png',
|
|
94
|
+
* },
|
|
95
|
+
* memberAppInfo: null,
|
|
96
|
+
* chainInfo: {
|
|
97
|
+
* host: chainHost,
|
|
98
|
+
* id: chainId,
|
|
99
|
+
* },
|
|
100
|
+
* timeout: 8000,
|
|
101
|
+
* });
|
|
102
|
+
*/
|
|
103
|
+
constructor({
|
|
104
|
+
wallet,
|
|
105
|
+
appInfo,
|
|
106
|
+
memberAppInfo,
|
|
107
|
+
delegator,
|
|
108
|
+
delegation,
|
|
109
|
+
timeout = DEFAULT_TIMEOUT,
|
|
110
|
+
chainInfo = DEFAULT_CHAIN_INFO,
|
|
111
|
+
baseUrl = '',
|
|
112
|
+
tokenKey = '_t_',
|
|
113
|
+
}) {
|
|
114
|
+
super();
|
|
115
|
+
|
|
116
|
+
this.wallet = this._validateWallet(wallet);
|
|
117
|
+
this.appInfo = this._validateAppInfo(appInfo);
|
|
118
|
+
this.memberAppInfo = this._validateAppInfo(memberAppInfo, true);
|
|
119
|
+
this.chainInfo = chainInfo;
|
|
120
|
+
|
|
121
|
+
this.delegator = delegator;
|
|
122
|
+
this.delegation = delegation;
|
|
123
|
+
|
|
124
|
+
this.baseUrl = baseUrl;
|
|
125
|
+
this.tokenKey = tokenKey;
|
|
126
|
+
this.timeout = timeout;
|
|
127
|
+
|
|
128
|
+
if (!this.appInfo.link) {
|
|
129
|
+
this.appInfo.link = this.baseUrl;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Generate a deep link url that can be displayed as QRCode for DID Wallet to consume
|
|
135
|
+
*
|
|
136
|
+
* @method
|
|
137
|
+
* @param {object} params
|
|
138
|
+
* @param {string} params.baseUrl - baseUrl inferred from request object
|
|
139
|
+
* @param {string} params.pathname - wallet callback pathname
|
|
140
|
+
* @param {string} params.token - action token
|
|
141
|
+
* @param {object} params.query - params that should be persisted in wallet callback url
|
|
142
|
+
* @returns {string}
|
|
143
|
+
*/
|
|
144
|
+
uri({ baseUrl, pathname = '', token = '', query = {} } = {}) {
|
|
145
|
+
const params = { ...query, [this.tokenKey]: token };
|
|
146
|
+
const payload = {
|
|
147
|
+
action: 'requestAuth',
|
|
148
|
+
url: encodeURIComponent(`${this.baseUrl || baseUrl}${pathname}?${qs.stringify(params)}`),
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const uri = `https://abtwallet.io/i/?${qs.stringify(payload)}`;
|
|
152
|
+
debug('uri', { token, pathname, uri, params, payload });
|
|
153
|
+
return uri;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Compute public url to return to wallet
|
|
158
|
+
*
|
|
159
|
+
* @method
|
|
160
|
+
* @param {string} pathname
|
|
161
|
+
* @param {object} params
|
|
162
|
+
* @returns {string}
|
|
163
|
+
*/
|
|
164
|
+
getPublicUrl(pathname, params = {}, baseUrl = '') {
|
|
165
|
+
return `${this.baseUrl || baseUrl}${pathname}?${qs.stringify(params)}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Sign a plain response, usually on auth success or error
|
|
170
|
+
*
|
|
171
|
+
* @method
|
|
172
|
+
* @param {object} params
|
|
173
|
+
* @param {object} params.response - response
|
|
174
|
+
* @param {string} params.errorMessage - error message, default to empty
|
|
175
|
+
* @param {string} params.successMessage - success message, default to empty
|
|
176
|
+
* @param {string} params.nextWorkflow - https://github.com/ArcBlock/ABT-DID-Protocol#concatenate-multiple-workflow
|
|
177
|
+
* @param {string} params.nextUrl - tell wallet do open this url in webview
|
|
178
|
+
* @param {object} params.cookies - key-value pairs to be set as cookie before open nextUrl
|
|
179
|
+
* @param {object} params.storages - key-value pairs to be set as localStorage before open nextUrl
|
|
180
|
+
* @param {string} baseUrl
|
|
181
|
+
* @param {object} request
|
|
182
|
+
* @returns {Promise<object>} { appPk, agentPk, authInfo }
|
|
183
|
+
*/
|
|
184
|
+
async signResponse(
|
|
185
|
+
{
|
|
186
|
+
response = {},
|
|
187
|
+
errorMessage = '',
|
|
188
|
+
successMessage = '',
|
|
189
|
+
nextWorkflow = '',
|
|
190
|
+
nextUrl = '',
|
|
191
|
+
cookies = {},
|
|
192
|
+
storages = {},
|
|
193
|
+
},
|
|
194
|
+
baseUrl,
|
|
195
|
+
request,
|
|
196
|
+
extraParams = {}
|
|
197
|
+
) {
|
|
198
|
+
const context = request.context || {};
|
|
199
|
+
const infoParams = { baseUrl, request, ...context, extraParams };
|
|
200
|
+
const [wallet, delegator, delegation] = await Promise.all([
|
|
201
|
+
this.getWalletInfo(infoParams),
|
|
202
|
+
this.getDelegator(infoParams),
|
|
203
|
+
this.getDelegation(infoParams),
|
|
204
|
+
]);
|
|
205
|
+
|
|
206
|
+
const [appInfo, memberAppInfo] = await Promise.all([
|
|
207
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, 'appInfo'),
|
|
208
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, 'memberAppInfo'),
|
|
209
|
+
]);
|
|
210
|
+
const didwallet = request.context.wallet;
|
|
211
|
+
|
|
212
|
+
const payload = {
|
|
213
|
+
appInfo,
|
|
214
|
+
memberAppInfo,
|
|
215
|
+
status: errorMessage ? 'error' : 'ok',
|
|
216
|
+
errorMessage: errorMessage || '',
|
|
217
|
+
successMessage: successMessage || '',
|
|
218
|
+
nextWorkflow: nextWorkflow || '',
|
|
219
|
+
nextUrl: nextUrl || '',
|
|
220
|
+
cookies: cookies || {},
|
|
221
|
+
storages: storages || '',
|
|
222
|
+
response,
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if (delegator) {
|
|
226
|
+
payload.iss = toDid(delegator.address);
|
|
227
|
+
payload.agentDid = toDid(wallet.address);
|
|
228
|
+
payload.verifiableClaims = [{ type: 'certificate', content: delegation }];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const result = {
|
|
232
|
+
appPk: toBase58(wallet.pk),
|
|
233
|
+
authInfo: Jwt.sign(wallet.address, wallet.sk, payload, true, didwallet ? didwallet.jwt : undefined),
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
if (delegator) {
|
|
237
|
+
result.appPk = toBase58(delegator.pk);
|
|
238
|
+
result.agentPk = toBase58(wallet.pk);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Sign a auth response that returned to wallet: tell the wallet the appInfo/chainInfo
|
|
246
|
+
*
|
|
247
|
+
* @method
|
|
248
|
+
* @param {object} params
|
|
249
|
+
* @param {object} params.claims - info required by application to complete the auth
|
|
250
|
+
* @param {string} params.pathname - pathname to assemble callback url
|
|
251
|
+
* @param {string} params.baseUrl - baseUrl
|
|
252
|
+
* @param {object} params.challenge - random challenge to be included in the body
|
|
253
|
+
* @param {object} params.extraParams - extra query params and locale
|
|
254
|
+
* @param {object} params.request
|
|
255
|
+
* @param {object} params.context
|
|
256
|
+
* @param {string} params.context.token - action token
|
|
257
|
+
* @param {number} params.context.currentStep - current step
|
|
258
|
+
* @param {string} [params.context.sharedKey] - shared key between app and wallet
|
|
259
|
+
* @param {string} [params.context.encryptionKey] - encryption key from wallet
|
|
260
|
+
* @param {Function} [params.context.mfaCode] - function used to generate mfa code
|
|
261
|
+
* @param {string} params.context.userDid - decoded from req.query, base58
|
|
262
|
+
* @param {string} params.context.userPk - decoded from req.query, base58
|
|
263
|
+
* @param {string} params.context.didwallet - DID Wallet os and version
|
|
264
|
+
* @returns {Promise<object>} { appPk, agentPk, sharedKey, authInfo }
|
|
265
|
+
*/
|
|
266
|
+
async sign({ context, request, claims, pathname = '', baseUrl = '', challenge = '', extraParams = {} }) {
|
|
267
|
+
// debug('sign.context', context);
|
|
268
|
+
// debug('sign.params', extraParams);
|
|
269
|
+
|
|
270
|
+
const claimsInfo = await this.tryWithTimeout(() =>
|
|
271
|
+
this.genRequestedClaims({
|
|
272
|
+
claims,
|
|
273
|
+
context: { baseUrl, request, ...context },
|
|
274
|
+
extraParams,
|
|
275
|
+
})
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
if (claimsInfo.filter((x) => x.mfaCode && x.mfaCode.length > 0).length > 1) {
|
|
279
|
+
throw new Error('Multiple MFA is not supported when sending more than 1 claim');
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// FIXME: this maybe buggy if user provided multiple claims
|
|
283
|
+
const tmp = claimsInfo.find((x) => isEqual(this._isValidChainInfo(x.chainInfo), DEFAULT_CHAIN_INFO) === false);
|
|
284
|
+
|
|
285
|
+
const infoParams = { baseUrl, request, ...context, extraParams };
|
|
286
|
+
const [wallet, delegator, delegation, chainInfo] = await Promise.all([
|
|
287
|
+
this.getWalletInfo(infoParams),
|
|
288
|
+
this.getDelegator(infoParams),
|
|
289
|
+
this.getDelegation(infoParams),
|
|
290
|
+
this.getChainInfo(infoParams, tmp?.chainInfo),
|
|
291
|
+
]);
|
|
292
|
+
|
|
293
|
+
const [appInfo, memberAppInfo] = await Promise.all([
|
|
294
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, 'appInfo'),
|
|
295
|
+
this.getAppInfo({ ...infoParams, wallet, delegator }, 'memberAppInfo'),
|
|
296
|
+
]);
|
|
297
|
+
|
|
298
|
+
const payload = {
|
|
299
|
+
action: 'responseAuth',
|
|
300
|
+
challenge,
|
|
301
|
+
appInfo,
|
|
302
|
+
memberAppInfo,
|
|
303
|
+
chainInfo,
|
|
304
|
+
requestedClaims: claimsInfo.map((x) => {
|
|
305
|
+
delete x.chainInfo;
|
|
306
|
+
return x;
|
|
307
|
+
}),
|
|
308
|
+
url: `${this.baseUrl || baseUrl}${pathname}?${qs.stringify({ [this.tokenKey]: context.token })}`,
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
if (delegator) {
|
|
312
|
+
payload.iss = toDid(delegator.address);
|
|
313
|
+
payload.agentDid = toDid(wallet.address);
|
|
314
|
+
payload.verifiableClaims = [{ type: 'certificate', content: delegation }];
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// debug('sign.payload', payload);
|
|
318
|
+
|
|
319
|
+
const version = context.didwallet ? context.didwallet.jwt : undefined;
|
|
320
|
+
const result = {
|
|
321
|
+
appPk: toBase58(wallet.pk),
|
|
322
|
+
authInfo: Jwt.sign(wallet.address, wallet.sk, payload, true, version),
|
|
323
|
+
sensitive: claimsInfo.every((x) => ['keyPair', 'encryptionKey'].includes(x.type)),
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
// encrypt context.encKey with user pk here
|
|
327
|
+
if (result.sensitive && context.sharedKey && context.encryptionKey) {
|
|
328
|
+
try {
|
|
329
|
+
const pk = fromBase58(context.encryptionKey).toString('utf8');
|
|
330
|
+
result.sharedKey = RSA.encrypt(context.sharedKey, pk, 'base58');
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error('Failed to encrypt shared key', err);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (delegator) {
|
|
337
|
+
result.appPk = toBase58(delegator.pk);
|
|
338
|
+
result.agentPk = toBase58(wallet.pk);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return result;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Determine chainInfo on the fly
|
|
346
|
+
*
|
|
347
|
+
* @param {object} params - contains the context of this request
|
|
348
|
+
* @param {object|undefined} [info=undefined] - chain info object or function
|
|
349
|
+
* @returns {Promise<ChainInfo>}
|
|
350
|
+
* @memberof WalletAuthenticator
|
|
351
|
+
*/
|
|
352
|
+
async getChainInfo(params, info) {
|
|
353
|
+
if (info && this._isValidChainInfo(info)) {
|
|
354
|
+
return info;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (typeof this.chainInfo === 'function') {
|
|
358
|
+
const result = await this.tryWithTimeout(() => this.chainInfo(params));
|
|
359
|
+
if (this._isValidChainInfo(result)) {
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (this.chainInfo && this._isValidChainInfo(this.chainInfo)) {
|
|
365
|
+
return this.chainInfo;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return DEFAULT_CHAIN_INFO;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Determine appInfo/memberAppInfo on the fly
|
|
373
|
+
*
|
|
374
|
+
* @param {object} params - contains the context of this request
|
|
375
|
+
* @param {string} key - appInfo | memberAppInfo
|
|
376
|
+
* @returns {Promise<ApplicationInfo>}
|
|
377
|
+
* @memberof WalletAuthenticator
|
|
378
|
+
*/
|
|
379
|
+
async getAppInfo(params, key = 'appInfo') {
|
|
380
|
+
if (typeof this[key] === 'function') {
|
|
381
|
+
const info = await this.tryWithTimeout(() => this[key](params));
|
|
382
|
+
if (info) {
|
|
383
|
+
if (!info.link) {
|
|
384
|
+
info.link = params.baseUrl;
|
|
385
|
+
}
|
|
386
|
+
if (!info.publisher) {
|
|
387
|
+
info.publisher = toDid(params.delegator ? params.delegator.address : params.wallet.address);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return this._validateAppInfo(info, key === 'memberAppInfo');
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if (this[key] && !this[key].publisher) {
|
|
395
|
+
this[key].publisher = toDid(params.delegator ? params.delegator.address : params.wallet.address);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return this[key];
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
async getWalletInfo(params) {
|
|
402
|
+
if (typeof this.wallet === 'function') {
|
|
403
|
+
const result = await this.tryWithTimeout(() => this.wallet(params));
|
|
404
|
+
return this._validateWallet(result, true);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
return this.wallet;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async getDelegator(params) {
|
|
411
|
+
if (typeof this.delegator === 'function') {
|
|
412
|
+
const result = await this.tryWithTimeout(() => this.delegator(params));
|
|
413
|
+
return result ? this._validateWallet(result, false) : null;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return this.delegator;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
async getDelegation(params) {
|
|
420
|
+
if (typeof this.delegation === 'function') {
|
|
421
|
+
const result = await this.tryWithTimeout(() => this.delegation(params));
|
|
422
|
+
return result;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return this.delegation;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Verify a DID auth response sent from DID Wallet
|
|
430
|
+
*
|
|
431
|
+
* @method
|
|
432
|
+
* @param {object} data
|
|
433
|
+
* @param {string} [locale=en]
|
|
434
|
+
* @param {boolean} [enforceTimestamp=true]
|
|
435
|
+
* @returns Promise<boolean>
|
|
436
|
+
*/
|
|
437
|
+
async verify(data, locale = 'en', enforceTimestamp = true) {
|
|
438
|
+
const {
|
|
439
|
+
iss,
|
|
440
|
+
iat,
|
|
441
|
+
challenge = '',
|
|
442
|
+
action = 'responseAuth',
|
|
443
|
+
requestedClaims,
|
|
444
|
+
} = await this._verify(data, 'userPk', 'userInfo', locale, enforceTimestamp);
|
|
445
|
+
|
|
446
|
+
debug('verify.context', { userPk: data.userPk, userDid: toAddress(iss), action, challenge });
|
|
447
|
+
debug('verify.claims', requestedClaims);
|
|
448
|
+
return {
|
|
449
|
+
token: data.token,
|
|
450
|
+
userDid: toAddress(iss),
|
|
451
|
+
userPk: data.userPk,
|
|
452
|
+
claims: requestedClaims,
|
|
453
|
+
action,
|
|
454
|
+
challenge,
|
|
455
|
+
timestamp: iat,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
// ---------------------------------------
|
|
460
|
+
// Request claim related methods
|
|
461
|
+
// ---------------------------------------
|
|
462
|
+
genRequestedClaims({ claims, context, extraParams }) {
|
|
463
|
+
return Promise.all(
|
|
464
|
+
Object.keys(claims).map(async (x) => {
|
|
465
|
+
let name = x;
|
|
466
|
+
let claim = claims[x];
|
|
467
|
+
|
|
468
|
+
if (Array.isArray(claims[x])) {
|
|
469
|
+
[name, claim] = claims[x];
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!schema.claims[name]) {
|
|
473
|
+
throw new Error(`Unsupported claim type ${name}`);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const fn = typeof this[name] === 'function' ? name : 'getClaimInfo';
|
|
477
|
+
const result = await this[fn]({ claim, context, extraParams });
|
|
478
|
+
|
|
479
|
+
if (result.mfa && typeof context.mfaCode === 'function') {
|
|
480
|
+
result.mfaCode = [await context.mfaCode()];
|
|
481
|
+
while (result.mfaCode.length < MFA_CODE_COUNT) {
|
|
482
|
+
const noise = random(10, 99);
|
|
483
|
+
if (result.mfaCode.includes(noise) === false) {
|
|
484
|
+
result.mfaCode.push(noise);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
result.mfaCode = shuffle(result.mfaCode);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const { value, error } = schema.claims[name].validate(result);
|
|
491
|
+
if (error) {
|
|
492
|
+
throw new Error(`Invalid ${name} claim: ${error.message}`);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
return value;
|
|
496
|
+
})
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
async getClaimInfo({ claim, context, extraParams }) {
|
|
501
|
+
const { userDid, userPk, didwallet } = context;
|
|
502
|
+
const result =
|
|
503
|
+
typeof claim === 'function'
|
|
504
|
+
? await claim({
|
|
505
|
+
userDid: userDid ? toAddress(userDid) : '',
|
|
506
|
+
userPk: userPk || '',
|
|
507
|
+
didwallet,
|
|
508
|
+
extraParams,
|
|
509
|
+
context,
|
|
510
|
+
})
|
|
511
|
+
: claim;
|
|
512
|
+
|
|
513
|
+
const infoParams = { ...context, ...extraParams };
|
|
514
|
+
const chainInfo = await this.getChainInfo(infoParams, result.chainInfo);
|
|
515
|
+
|
|
516
|
+
result.chainInfo = chainInfo;
|
|
517
|
+
|
|
518
|
+
return result;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// Request wallet to sign something: transaction/text/html/image
|
|
522
|
+
async signature({ claim, context, extraParams }) {
|
|
523
|
+
const {
|
|
524
|
+
data,
|
|
525
|
+
type = 'mime:text/plain',
|
|
526
|
+
digest = '',
|
|
527
|
+
method = 'sha3', // set this to `none` to instruct wallet not to hash before signing
|
|
528
|
+
wallet,
|
|
529
|
+
sender,
|
|
530
|
+
display,
|
|
531
|
+
description: desc,
|
|
532
|
+
chainInfo,
|
|
533
|
+
meta = {},
|
|
534
|
+
mfa = false,
|
|
535
|
+
nonce = '',
|
|
536
|
+
requirement = { tokens: [], assets: {} },
|
|
537
|
+
} = await this.getClaimInfo({
|
|
538
|
+
claim,
|
|
539
|
+
context,
|
|
540
|
+
extraParams,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
debug('claim.signature', { data, digest, type, sender, context, nonce, requirement });
|
|
544
|
+
|
|
545
|
+
if (!data && !digest) {
|
|
546
|
+
throw new Error('Signature claim requires either data or digest to be provided');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const description = desc || 'Sign this transaction to continue.';
|
|
550
|
+
|
|
551
|
+
// We have to encode the transaction
|
|
552
|
+
if (type.endsWith('Tx')) {
|
|
553
|
+
if (!chainInfo.host) {
|
|
554
|
+
throw new Error('Invalid chainInfo when trying to encoding transaction');
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
const client = new Client(chainInfo.host);
|
|
558
|
+
|
|
559
|
+
if (typeof client[`encode${type}`] !== 'function') {
|
|
560
|
+
throw new Error(`Unsupported transaction type ${type}`);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!data.pk) {
|
|
564
|
+
data.pk = context.userPk;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
const { buffer: txBuffer } = await client[`encode${type}`]({
|
|
569
|
+
tx: data,
|
|
570
|
+
wallet: wallet || fromAddress(sender || context.userDid),
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
return {
|
|
574
|
+
type: 'signature',
|
|
575
|
+
description,
|
|
576
|
+
typeUrl: 'fg:t:transaction',
|
|
577
|
+
origin: toBase58(txBuffer),
|
|
578
|
+
method,
|
|
579
|
+
display: formatDisplay(display),
|
|
580
|
+
digest: '',
|
|
581
|
+
chainInfo,
|
|
582
|
+
meta,
|
|
583
|
+
mfa,
|
|
584
|
+
nonce,
|
|
585
|
+
requirement,
|
|
586
|
+
};
|
|
587
|
+
} catch (err) {
|
|
588
|
+
throw new Error(`Failed to encode transaction: ${err.message}`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// We have en encoded transaction
|
|
593
|
+
if (type === 'fg:t:transaction') {
|
|
594
|
+
return {
|
|
595
|
+
type: 'signature',
|
|
596
|
+
description,
|
|
597
|
+
typeUrl: 'fg:t:transaction',
|
|
598
|
+
origin: toBase58(data),
|
|
599
|
+
display: formatDisplay(display),
|
|
600
|
+
method,
|
|
601
|
+
digest: '',
|
|
602
|
+
chainInfo,
|
|
603
|
+
meta,
|
|
604
|
+
mfa,
|
|
605
|
+
nonce,
|
|
606
|
+
requirement,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// If we are ask user to sign anything just pass the data
|
|
611
|
+
// Wallet should not hash the data if `method` is empty
|
|
612
|
+
// If we are asking user to sign a very large piece of data
|
|
613
|
+
// Just hash the data and show him the digest
|
|
614
|
+
return {
|
|
615
|
+
type: 'signature',
|
|
616
|
+
description: desc || 'Sign this message to continue.',
|
|
617
|
+
origin: data ? toBase58(data) : '',
|
|
618
|
+
typeUrl: type,
|
|
619
|
+
display: formatDisplay(display),
|
|
620
|
+
method,
|
|
621
|
+
digest,
|
|
622
|
+
chainInfo,
|
|
623
|
+
meta,
|
|
624
|
+
mfa,
|
|
625
|
+
nonce,
|
|
626
|
+
requirement,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Request wallet to complete and sign a partial tx to broadcasting
|
|
631
|
+
// Usually used in payment scenarios
|
|
632
|
+
// The wallet can leverage multiple input capabilities of the chain
|
|
633
|
+
async prepareTx({ claim, context, extraParams }) {
|
|
634
|
+
const {
|
|
635
|
+
partialTx,
|
|
636
|
+
requirement = { tokens: [], assets: {} },
|
|
637
|
+
type,
|
|
638
|
+
display,
|
|
639
|
+
wallet,
|
|
640
|
+
sender,
|
|
641
|
+
description: desc,
|
|
642
|
+
chainInfo,
|
|
643
|
+
meta = {},
|
|
644
|
+
mfa = false,
|
|
645
|
+
nonce = '',
|
|
646
|
+
} = await this.getClaimInfo({
|
|
647
|
+
claim,
|
|
648
|
+
context,
|
|
649
|
+
extraParams,
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
debug('claim.prepareTx', { partialTx, requirement, type, sender, context });
|
|
653
|
+
|
|
654
|
+
if (!partialTx || !requirement) {
|
|
655
|
+
throw new Error('prepareTx claim requires both partialTx and requirement to be provided');
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
const description = desc || 'Prepare and sign this transaction to continue.';
|
|
659
|
+
|
|
660
|
+
// We have to encode the transaction
|
|
661
|
+
if (type && type.endsWith('Tx')) {
|
|
662
|
+
if (!chainInfo.host) {
|
|
663
|
+
throw new Error('Invalid chainInfo when trying to encoding partial transaction');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
const client = new Client(chainInfo.host);
|
|
667
|
+
|
|
668
|
+
if (typeof client[`encode${type}`] !== 'function') {
|
|
669
|
+
throw new Error(`Unsupported transaction type ${type} when encoding partial transaction`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (!partialTx.pk) {
|
|
673
|
+
partialTx.pk = context.userPk;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
try {
|
|
677
|
+
const { buffer: txBuffer } = await client[`encode${type}`]({
|
|
678
|
+
tx: partialTx,
|
|
679
|
+
wallet: wallet || fromAddress(sender || context.userDid),
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
return {
|
|
683
|
+
type: 'prepareTx',
|
|
684
|
+
description,
|
|
685
|
+
partialTx: toBase58(txBuffer),
|
|
686
|
+
display: formatDisplay(display),
|
|
687
|
+
requirement,
|
|
688
|
+
chainInfo,
|
|
689
|
+
meta,
|
|
690
|
+
mfa,
|
|
691
|
+
nonce,
|
|
692
|
+
};
|
|
693
|
+
} catch (err) {
|
|
694
|
+
throw new Error(`Failed to encode partial transaction: ${err.message}`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// We have en encoded transaction
|
|
699
|
+
return {
|
|
700
|
+
type: 'prepareTx',
|
|
701
|
+
description,
|
|
702
|
+
partialTx: toBase58(partialTx),
|
|
703
|
+
requirement,
|
|
704
|
+
display: formatDisplay(display),
|
|
705
|
+
chainInfo,
|
|
706
|
+
meta,
|
|
707
|
+
mfa,
|
|
708
|
+
nonce,
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
_validateAppInfo(info, allowEmpty = false) {
|
|
713
|
+
if (typeof info === 'function') {
|
|
714
|
+
return info;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (!info) {
|
|
718
|
+
if (allowEmpty === false) {
|
|
719
|
+
throw new Error('Wallet authenticator can not work with invalid appInfo: empty');
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return null;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
const { value, error } = schema.appInfo.validate(info);
|
|
726
|
+
if (error) {
|
|
727
|
+
throw new Error(`Wallet authenticator can not work with invalid appInfo: ${error.message}`);
|
|
728
|
+
}
|
|
729
|
+
return value;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
_isValidChainInfo(x) {
|
|
733
|
+
const { error } = schema.chainInfo.validate(x);
|
|
734
|
+
return !error;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
tryWithTimeout(asyncFn, label = '') {
|
|
738
|
+
if (typeof asyncFn !== 'function') {
|
|
739
|
+
throw new Error('asyncFn must be a valid function when calling tryWithTimeout');
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
const timeout = Number(this.timeout) || DEFAULT_TIMEOUT;
|
|
743
|
+
const inferredLabel = label || asyncFn.name || asyncFn.toString();
|
|
744
|
+
const invocationStack = new Error(`Timeout at: ${inferredLabel}`).stack;
|
|
745
|
+
|
|
746
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
747
|
+
return new Promise(async (resolve, reject) => {
|
|
748
|
+
const timer = setTimeout(() => {
|
|
749
|
+
const error = new Error(`Async operation (${inferredLabel}) did not complete within ${timeout} ms`);
|
|
750
|
+
error.stack = invocationStack;
|
|
751
|
+
error.name = 'TIMEOUT';
|
|
752
|
+
reject(error);
|
|
753
|
+
}, timeout);
|
|
754
|
+
|
|
755
|
+
try {
|
|
756
|
+
const result = await asyncFn();
|
|
757
|
+
resolve(result);
|
|
758
|
+
} catch (err) {
|
|
759
|
+
reject(err);
|
|
760
|
+
} finally {
|
|
761
|
+
clearTimeout(timer);
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
module.exports = WalletAuthenticator;
|
|
768
|
+
module.exports.formatDisplay = formatDisplay;
|