@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,943 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
/* eslint-disable prefer-destructuring */
|
|
3
|
+
/* eslint-disable object-curly-newline */
|
|
4
|
+
/* eslint-disable consistent-return */
|
|
5
|
+
const url = require('url');
|
|
6
|
+
const get = require('lodash/get');
|
|
7
|
+
const set = require('lodash/set');
|
|
8
|
+
const pick = require('lodash/pick');
|
|
9
|
+
const omit = require('lodash/omit');
|
|
10
|
+
const random = require('lodash/random');
|
|
11
|
+
const cloneDeep = require('lodash/cloneDeep');
|
|
12
|
+
const isEqual = require('lodash/isEqual');
|
|
13
|
+
const isPlainObject = require('lodash/isPlainObject');
|
|
14
|
+
const semver = require('semver');
|
|
15
|
+
const Mcrypto = require('@ocap/mcrypto');
|
|
16
|
+
const SealedBox = require('tweetnacl-sealedbox-js');
|
|
17
|
+
const stringify = require('json-stable-stringify');
|
|
18
|
+
const { isValid: isValidDid } = require('@arcblock/did');
|
|
19
|
+
const { stripHexPrefix, toBase64, fromBase64 } = require('@ocap/util');
|
|
20
|
+
|
|
21
|
+
const { encrypt, decrypt, SESSION_STATUS, PROTECTED_KEYS } = require('../protocol');
|
|
22
|
+
|
|
23
|
+
// Scheme: https://tools.ietf.org/html/rfc3986#section-3.1
|
|
24
|
+
// Absolute URL: https://tools.ietf.org/html/rfc3986#section-4.3
|
|
25
|
+
const ABSOLUTE_URL_REGEX = /^[a-zA-Z][a-zA-Z\d+\-.]*?:/;
|
|
26
|
+
// Windows paths like `c:\`
|
|
27
|
+
const WINDOWS_PATH_REGEX = /^[a-zA-Z]:\\/;
|
|
28
|
+
const isUrl = (input) => {
|
|
29
|
+
if (typeof input !== 'string') {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
if (WINDOWS_PATH_REGEX.test(input)) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return ABSOLUTE_URL_REGEX.test(input);
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// eslint-disable-next-line
|
|
39
|
+
const debug = require('debug')(`${require('../../package.json').name}:handlers:util`);
|
|
40
|
+
|
|
41
|
+
const sha3 = Mcrypto.Hasher.SHA3.hash256;
|
|
42
|
+
const getLocale = (req) => (req.acceptsLanguages('en-US', 'zh-CN') || 'en-US').split('-').shift();
|
|
43
|
+
const getSessionId = () => Date.now().toString();
|
|
44
|
+
const noop = () => ({});
|
|
45
|
+
const noTouch = (x) => x;
|
|
46
|
+
|
|
47
|
+
const errors = {
|
|
48
|
+
tokenMissing: {
|
|
49
|
+
en: 'Session Id is required to check status',
|
|
50
|
+
zh: '缺少会话 ID 参数',
|
|
51
|
+
},
|
|
52
|
+
didMismatch: {
|
|
53
|
+
en: 'Login user and wallet user mismatch, please relogin and try again',
|
|
54
|
+
zh: '登录用户和扫码用户不匹配,为保障安全,请重新登录应用',
|
|
55
|
+
},
|
|
56
|
+
mfaMismatch: {
|
|
57
|
+
en: 'Dynamic verification code mismatch, please try again later',
|
|
58
|
+
zh: '动态验证码不匹配,请重试',
|
|
59
|
+
},
|
|
60
|
+
challengeMismatch: {
|
|
61
|
+
en: 'Challenge mismatch',
|
|
62
|
+
zh: '随机校验码不匹配',
|
|
63
|
+
},
|
|
64
|
+
token404: {
|
|
65
|
+
en: 'Session not found or expired',
|
|
66
|
+
zh: '会话不存在或已过期',
|
|
67
|
+
},
|
|
68
|
+
didMissing: {
|
|
69
|
+
en: 'userDid is required to start auth',
|
|
70
|
+
zh: 'userDid 参数缺失,请勿尝试连接多个不同的钱包',
|
|
71
|
+
},
|
|
72
|
+
pkMissing: {
|
|
73
|
+
en: 'userPk is required to start auth',
|
|
74
|
+
zh: 'userPk 参数缺失,请勿尝试连接多个不同的钱包',
|
|
75
|
+
},
|
|
76
|
+
authClaim: {
|
|
77
|
+
en: 'authPrincipal claim is not configured correctly',
|
|
78
|
+
zh: 'authPrincipal 声明配置不正确',
|
|
79
|
+
},
|
|
80
|
+
userDeclined: {
|
|
81
|
+
en: 'You have declined the authentication request',
|
|
82
|
+
zh: '授权请求被拒绝',
|
|
83
|
+
},
|
|
84
|
+
userBusy: {
|
|
85
|
+
en: 'Busy processing another DID Connect request',
|
|
86
|
+
zh: '正在处理其他请求',
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// This logic exist because the handlers maybe attached to a nested router
|
|
91
|
+
// pathname pattern: /:prefix/:action/auth
|
|
92
|
+
// But the group of handlers may be attached to a sub router, which has a baseUrl of `/api/login` (can only be extracted from `req.originalUrl`)
|
|
93
|
+
// We need to ensure the full url is given to DID Wallet
|
|
94
|
+
// eg: `/agent/login/auth` on the current router will be converted to `/api/login/agent/login/auth`
|
|
95
|
+
const _preparePathname = (path, req) => {
|
|
96
|
+
const delimiter = path.replace(/\/retrieve$/, '').replace(/\/auth$/, '');
|
|
97
|
+
const fullPath = url.parse(req.originalUrl).pathname;
|
|
98
|
+
const [prefix] = fullPath.split(delimiter);
|
|
99
|
+
const cleanPath = [prefix, path].join('/').replace(/\/+/g, '/');
|
|
100
|
+
// console.log('preparePathname', { path, delimiter, fullPath, prefix, cleanPath });
|
|
101
|
+
return cleanPath;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const getBaseUrl = (req) => {
|
|
105
|
+
if (req.headers['x-path-prefix']) {
|
|
106
|
+
return `/${req.headers['x-path-prefix']}/`.replace(/\/+/g, '/');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return '/';
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// This makes the lib smart enough to infer baseURL from request object
|
|
113
|
+
const prepareBaseUrl = (req, params) => {
|
|
114
|
+
const pathPrefix = getBaseUrl(req).replace(/\/$/, '');
|
|
115
|
+
const [hostname = '', port = 80] = (
|
|
116
|
+
req.get('x-forwarded-host') ||
|
|
117
|
+
req.get('x-real-hostname') ||
|
|
118
|
+
req.get('host') ||
|
|
119
|
+
''
|
|
120
|
+
).split(':');
|
|
121
|
+
// NOTE: x-real-port exist because sometimes the auth api is behind a port-forwarding proxy
|
|
122
|
+
const finalPort = get(params, 'x-real-port', null) || req.get('X-Real-Port') || port || '';
|
|
123
|
+
return url.format({
|
|
124
|
+
protocol:
|
|
125
|
+
get(params, 'x-real-protocol') || req.get('X-Real-protocol') || req.get('X-Forwarded-Proto') || req.protocol,
|
|
126
|
+
hostname,
|
|
127
|
+
port: Number(finalPort) === 80 ? '' : finalPort,
|
|
128
|
+
pathname: pathPrefix,
|
|
129
|
+
});
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// https://github.com/joaquimserafim/base64-url/blob/54d9c9ede66a8724f280cf24fd18c38b9a53915f/index.js#L10
|
|
133
|
+
const unescape = (str) => (str + '==='.slice((str.length + 3) % 4)).replace(/-/g, '+').replace(/_/g, '/');
|
|
134
|
+
const decodeEncKey = (str) => new Uint8Array(Buffer.from(unescape(str), 'base64'));
|
|
135
|
+
|
|
136
|
+
const getStepChallenge = () => stripHexPrefix(Mcrypto.getRandomBytes(16)).toUpperCase();
|
|
137
|
+
|
|
138
|
+
const parseWalletUA = (userAgent) => {
|
|
139
|
+
const ua = (userAgent || '').toString().toLowerCase();
|
|
140
|
+
let os = '';
|
|
141
|
+
let version = '';
|
|
142
|
+
if (ua.indexOf('android') > -1) {
|
|
143
|
+
os = 'android';
|
|
144
|
+
} else if (ua.indexOf('iphone') > -1) {
|
|
145
|
+
os = 'ios';
|
|
146
|
+
} else if (ua.indexOf('ipad') > -1) {
|
|
147
|
+
os = 'ios';
|
|
148
|
+
} else if (ua.indexOf('ipod') > -1) {
|
|
149
|
+
os = 'ios';
|
|
150
|
+
} else if (ua.indexOf('arcwallet') === 0) {
|
|
151
|
+
os = 'web';
|
|
152
|
+
} else if (ua.indexOf('abtwallet') === 0) {
|
|
153
|
+
os = 'web';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const match = ua.split(/\s+/).find((x) => x.startsWith('arcwallet/') || x.startsWith('abtwallet/'));
|
|
157
|
+
if (match) {
|
|
158
|
+
const tmp = match.split('/');
|
|
159
|
+
if (tmp.length > 1 && semver.coerce(tmp[1])) {
|
|
160
|
+
version = semver.coerce(tmp[1]).version;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return { os, version, jwt: '1.1.0' };
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const isDeepLink = (str) => str.startsWith('https://abtwallet.io/i/') || str.startsWith('https://didwallet.io/i/');
|
|
168
|
+
|
|
169
|
+
// whether we should force did-wallet to connect with userDid from session
|
|
170
|
+
const isConnectedOnly = (params, sessionUserDid = '') => {
|
|
171
|
+
if (typeof params.forceConnected === 'string' && isValidDid(params.forceConnected)) {
|
|
172
|
+
return params.forceConnected;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!sessionUserDid) {
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (isValidDid(sessionUserDid) === false) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (params.connectedDid !== sessionUserDid) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// auto
|
|
188
|
+
if (typeof params.forceConnected === 'undefined') {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// query string from client: `true` | `false` | did
|
|
193
|
+
if (typeof params.forceConnected === 'string') {
|
|
194
|
+
try {
|
|
195
|
+
return !!JSON.parse(params.forceConnected);
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return !!params.forceConnected;
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
// If we treat an did-connect roundtrip as a session, then action token is the session id
|
|
205
|
+
module.exports = function createHandlers({
|
|
206
|
+
action,
|
|
207
|
+
pathname,
|
|
208
|
+
claims,
|
|
209
|
+
onStart,
|
|
210
|
+
onConnect,
|
|
211
|
+
onAuth,
|
|
212
|
+
onDecline,
|
|
213
|
+
onComplete,
|
|
214
|
+
onExpire,
|
|
215
|
+
onError,
|
|
216
|
+
pathTransformer,
|
|
217
|
+
tokenStorage,
|
|
218
|
+
authenticator,
|
|
219
|
+
authPrincipal,
|
|
220
|
+
persistentDynamicClaims = false,
|
|
221
|
+
getSignParams = noop,
|
|
222
|
+
getPathName = noTouch,
|
|
223
|
+
options,
|
|
224
|
+
}) {
|
|
225
|
+
const { tokenKey, encKey, versionKey, cleanupDelay } = options;
|
|
226
|
+
|
|
227
|
+
const defaultSteps = (Array.isArray(claims) ? claims : [claims]).filter(Boolean);
|
|
228
|
+
|
|
229
|
+
// Smart detection of user-defined authPrincipal claim
|
|
230
|
+
if (defaultSteps.length > 0) {
|
|
231
|
+
const keys = Object.keys(defaultSteps[0]);
|
|
232
|
+
const firstClaim = defaultSteps[0][keys[0]];
|
|
233
|
+
if (Array.isArray(firstClaim)) {
|
|
234
|
+
if (firstClaim[0] === 'authPrincipal') {
|
|
235
|
+
// eslint-disable-next-line no-param-reassign
|
|
236
|
+
authPrincipal = false;
|
|
237
|
+
}
|
|
238
|
+
} else if (keys[0] === 'authPrincipal') {
|
|
239
|
+
// eslint-disable-next-line no-param-reassign
|
|
240
|
+
authPrincipal = false;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Prepend default authPrincipal claim if not set
|
|
245
|
+
if (authPrincipal) {
|
|
246
|
+
let target = '';
|
|
247
|
+
let description = 'Please continue with your account';
|
|
248
|
+
let chainInfo;
|
|
249
|
+
let targetType;
|
|
250
|
+
|
|
251
|
+
if (typeof authPrincipal === 'string') {
|
|
252
|
+
if (isValidDid(authPrincipal)) {
|
|
253
|
+
// If auth principal is provided as a did
|
|
254
|
+
target = authPrincipal;
|
|
255
|
+
} else {
|
|
256
|
+
// If auth principal is provided as a string
|
|
257
|
+
description = authPrincipal;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (typeof authPrincipal === 'object') {
|
|
261
|
+
target = get(authPrincipal, 'target', target);
|
|
262
|
+
description = get(authPrincipal, 'description', description);
|
|
263
|
+
targetType = get(authPrincipal, 'targetType', targetType);
|
|
264
|
+
|
|
265
|
+
// If provided a chainInfo
|
|
266
|
+
if (authPrincipal.chainInfo && authenticator._isValidChainInfo(authPrincipal.chainInfo)) {
|
|
267
|
+
chainInfo = authPrincipal.chainInfo;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const supervised = defaultSteps.length === 0;
|
|
272
|
+
defaultSteps.unshift({
|
|
273
|
+
authPrincipal: {
|
|
274
|
+
skippable: true,
|
|
275
|
+
description,
|
|
276
|
+
target,
|
|
277
|
+
chainInfo,
|
|
278
|
+
targetType,
|
|
279
|
+
supervised,
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Whether we can skip the authPrincipal step safely
|
|
285
|
+
const canSkipConnect = defaultSteps[0] && defaultSteps[0].authPrincipal && defaultSteps[0].authPrincipal.skippable;
|
|
286
|
+
|
|
287
|
+
const createExtraParams = (locale, params, extra = {}) => {
|
|
288
|
+
const finalParams = { ...params, ...(extra || {}) };
|
|
289
|
+
return {
|
|
290
|
+
locale,
|
|
291
|
+
action,
|
|
292
|
+
...Object.keys(finalParams)
|
|
293
|
+
.filter((x) => !['userDid', 'userInfo', 'userSession', 'appSession', 'userPk', 'token'].includes(x))
|
|
294
|
+
.reduce((obj, x) => {
|
|
295
|
+
obj[x] = finalParams[x];
|
|
296
|
+
return obj;
|
|
297
|
+
}, {}),
|
|
298
|
+
};
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
const createSessionUpdater =
|
|
302
|
+
(token, params) =>
|
|
303
|
+
// eslint-disable-next-line require-await
|
|
304
|
+
async (key, value, secure = false) => {
|
|
305
|
+
const getUpdate = (k, v) => {
|
|
306
|
+
if (secure && params[encKey]) {
|
|
307
|
+
const encrypted = SealedBox.seal(Buffer.from(stringify(v)), decodeEncKey(params[encKey]));
|
|
308
|
+
return { [k]: Buffer.from(encrypted).toString('base64') };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return { [k]: v };
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// If key is an object, update multiple keys
|
|
315
|
+
if (typeof key === 'object') {
|
|
316
|
+
secure = value; // eslint-disable-line no-param-reassign
|
|
317
|
+
const keys = Object.keys(key);
|
|
318
|
+
const updates = Object.assign(...keys.map((k) => getUpdate(k, key[k])));
|
|
319
|
+
return tokenStorage.update(token, updates);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return tokenStorage.update(token, omit(getUpdate(key, value), PROTECTED_KEYS));
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
// used for multi-factor authentication
|
|
326
|
+
const createMfaCodeGenerator = (token) => async () => {
|
|
327
|
+
const mfaCode = random(10, 99);
|
|
328
|
+
await tokenStorage.update(token, { mfaCode });
|
|
329
|
+
return mfaCode;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const onProcessError = ({ req, res, stage, err }) => {
|
|
333
|
+
const { token, store } = req.context || {};
|
|
334
|
+
if (token) {
|
|
335
|
+
tokenStorage.update(token, { status: SESSION_STATUS.ERROR, error: err.message, mfaCode: 0 });
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
res.jsonp({ error: err.message });
|
|
339
|
+
onError({ token, extraParams: get(store, 'extraParams', {}), stage, err });
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const preparePathname = (str, req) => {
|
|
343
|
+
const auto = _preparePathname(str, req);
|
|
344
|
+
const custom = pathTransformer(auto);
|
|
345
|
+
// console.log('preparePathname', { str, auto, custom });
|
|
346
|
+
return custom;
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
// For web app
|
|
350
|
+
const generateSession = async (req, res) => {
|
|
351
|
+
try {
|
|
352
|
+
const params = {
|
|
353
|
+
'x-real-port': req.get('x-real-port'),
|
|
354
|
+
'x-real-protocol': req.get('x-real-protocol'),
|
|
355
|
+
deviceDid: req.get('x-device-did'),
|
|
356
|
+
connectedDid: get(req, 'cookies.connected_did', ''),
|
|
357
|
+
connectedPk: get(req, 'cookies.connected_pk', ''),
|
|
358
|
+
...req.body,
|
|
359
|
+
...req.query,
|
|
360
|
+
...req.params,
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
// force to connected user if we are during a session, and this behavior is customizable
|
|
364
|
+
params.forceConnected = isConnectedOnly(params, req.get('x-user-did'));
|
|
365
|
+
|
|
366
|
+
const token = sha3(getSessionId({ req, action, pathname })).replace(/^0x/, '').slice(0, 8);
|
|
367
|
+
await tokenStorage.create(token, SESSION_STATUS.CREATED);
|
|
368
|
+
|
|
369
|
+
// These fields are used to track the source session
|
|
370
|
+
// - sourceToken is the token of previous session
|
|
371
|
+
// - destToken is the token of final target session
|
|
372
|
+
let sourceToken = params.sourceToken || '';
|
|
373
|
+
const sourceTokenState = sourceToken ? await tokenStorage.read(sourceToken) : null;
|
|
374
|
+
if (sourceTokenState) {
|
|
375
|
+
if ([SESSION_STATUS.SUCCEED].includes(sourceTokenState.status)) {
|
|
376
|
+
sourceToken = '';
|
|
377
|
+
} else {
|
|
378
|
+
await tokenStorage.update(sourceToken, { destToken: token });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const finalPath = preparePathname(getPathName(pathname, req), req);
|
|
383
|
+
const baseUrl = prepareBaseUrl(req, params);
|
|
384
|
+
const uri = await authenticator.uri({ token, pathname: finalPath, baseUrl, query: {} });
|
|
385
|
+
|
|
386
|
+
// Always set currentStep to 0 when generate a new token
|
|
387
|
+
// Since the did of logged in user may be different of the auth did
|
|
388
|
+
const challenge = getStepChallenge();
|
|
389
|
+
const didwallet = parseWalletUA(req.query['user-agent'] || req.headers['user-agent']);
|
|
390
|
+
const extraParams = createExtraParams(getLocale(req), params);
|
|
391
|
+
const hookParams = {
|
|
392
|
+
req,
|
|
393
|
+
request: req,
|
|
394
|
+
challenge,
|
|
395
|
+
baseUrl,
|
|
396
|
+
deepLink: uri,
|
|
397
|
+
extraParams,
|
|
398
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
399
|
+
didwallet,
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
const [wallet, delegator] = await Promise.all([
|
|
403
|
+
authenticator.getWalletInfo({ baseUrl, request: req, extraParams }),
|
|
404
|
+
authenticator.getDelegator({ baseUrl, request: req, extraParams }),
|
|
405
|
+
]);
|
|
406
|
+
const [appInfo, memberAppInfo] = await Promise.all([
|
|
407
|
+
authenticator.getAppInfo({ baseUrl, request: req, wallet, delegator, extraParams }, 'appInfo'),
|
|
408
|
+
authenticator.getAppInfo({ baseUrl, request: req, wallet, delegator, extraParams }, 'memberAppInfo'),
|
|
409
|
+
]);
|
|
410
|
+
await tokenStorage.update(token, {
|
|
411
|
+
currentStep: 0,
|
|
412
|
+
mfaSupported: !didwallet.os, // If we are mobile (both webview and mobile browsers , os is truthy), mfa should be disabled
|
|
413
|
+
challenge,
|
|
414
|
+
sharedKey: getStepChallenge(), // used for wallet to encrypt userInfo
|
|
415
|
+
extraParams: params,
|
|
416
|
+
appInfo,
|
|
417
|
+
memberAppInfo,
|
|
418
|
+
sourceToken,
|
|
419
|
+
});
|
|
420
|
+
// debug('generate token', { action, pathname, token });
|
|
421
|
+
|
|
422
|
+
// The data returned by onStart will be set to extra of response data
|
|
423
|
+
// {String} extra.connectedDid:The server will notify the connectedDid in wallet to automatically connect (no code scanning is required) (notification is unreliable)
|
|
424
|
+
// {Boolean} extra.saveConnect: The server tells the app web side to remember the connect session did after the connect is complete
|
|
425
|
+
const extra = await onStart(hookParams);
|
|
426
|
+
|
|
427
|
+
res.jsonp({ token, status: SESSION_STATUS.CREATED, url: uri, appInfo, memberAppInfo, extra: extra || {} });
|
|
428
|
+
} catch (err) {
|
|
429
|
+
onProcessError({ req, res, stage: 'generate-token', err });
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
// For web app
|
|
434
|
+
const checkSession = async (req, res) => {
|
|
435
|
+
try {
|
|
436
|
+
const { locale, token, store, params } = req.context;
|
|
437
|
+
if (!token) {
|
|
438
|
+
res.status(400).json({ error: errors.tokenMissing[locale] });
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
if (!store) {
|
|
442
|
+
res.status(400).json({ error: errors.token404[locale] });
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (store.status === SESSION_STATUS.FORBIDDEN) {
|
|
447
|
+
res.status(403).json({ error: errors.didMismatch[locale] });
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (store.status === SESSION_STATUS.SUCCEED) {
|
|
452
|
+
setTimeout(() => {
|
|
453
|
+
tokenStorage.delete(token).catch(console.error);
|
|
454
|
+
}, cleanupDelay);
|
|
455
|
+
const extraParams = createExtraParams(locale, params, get(store, 'extraParams', {}));
|
|
456
|
+
await onComplete({
|
|
457
|
+
req,
|
|
458
|
+
request: req,
|
|
459
|
+
userDid: store.did,
|
|
460
|
+
userPk: store.pk,
|
|
461
|
+
extraParams,
|
|
462
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
res.status(200).json(
|
|
467
|
+
Object.keys(store)
|
|
468
|
+
.filter((x) => PROTECTED_KEYS.includes(x) === false)
|
|
469
|
+
.reduce((acc, key) => {
|
|
470
|
+
acc[key] = store[key];
|
|
471
|
+
return acc;
|
|
472
|
+
}, {})
|
|
473
|
+
);
|
|
474
|
+
} catch (err) {
|
|
475
|
+
onProcessError({ req, res, stage: 'check-token-status', err });
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
// For web app
|
|
480
|
+
const expireSession = async (req, res) => {
|
|
481
|
+
try {
|
|
482
|
+
const { locale, token, store } = req.context;
|
|
483
|
+
if (!token) {
|
|
484
|
+
res.status(400).json({ error: errors.tokenMissing[locale] });
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
if (!store) {
|
|
488
|
+
res.status(400).json({ error: errors.token404[locale] });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
onExpire({ token, extraParams: get(store, 'extraParams', {}), status: 'expired' });
|
|
493
|
+
|
|
494
|
+
// We do not delete tokens that are scanned by wallet since it will cause confusing
|
|
495
|
+
if (store.status !== SESSION_STATUS.SCANNED) {
|
|
496
|
+
await tokenStorage.delete(token);
|
|
497
|
+
}
|
|
498
|
+
res.status(200).json({ token });
|
|
499
|
+
} catch (err) {
|
|
500
|
+
onProcessError({ req, res, stage: 'mark-token-timeout', err });
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
// Only check userDid and userPk if we have done auth principal
|
|
505
|
+
const checkUser = async ({ context, userDid, userPk }) => {
|
|
506
|
+
const { locale, token, store } = context;
|
|
507
|
+
const isConnected = store.currentStep > 0;
|
|
508
|
+
|
|
509
|
+
// Only check userDid and userPk if we have done auth principal
|
|
510
|
+
if (isConnected) {
|
|
511
|
+
if (!userDid) {
|
|
512
|
+
return errors.didMissing[locale];
|
|
513
|
+
}
|
|
514
|
+
if (!userPk) {
|
|
515
|
+
return errors.pkMissing[locale];
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
// check userDid mismatch
|
|
519
|
+
if (userDid !== store.did) {
|
|
520
|
+
await tokenStorage.update(token, { status: SESSION_STATUS.FORBIDDEN });
|
|
521
|
+
return errors.didMismatch[locale];
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return false;
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
// eslint-disable-next-line consistent-return
|
|
529
|
+
const onAuthRequest = async (req, res) => {
|
|
530
|
+
const { locale, token, store, params, didwallet } = req.context;
|
|
531
|
+
const extraParams = createExtraParams(locale, params, get(store, 'extraParams', {}));
|
|
532
|
+
const userDid = params.userDid || store.did || extraParams.connectedDid;
|
|
533
|
+
const userPk = params.userPk || store.pk || extraParams.connectedPk;
|
|
534
|
+
|
|
535
|
+
const error = await checkUser({ context: req.context, userDid, userPk });
|
|
536
|
+
if (error) {
|
|
537
|
+
return res.jsonp({ error });
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
if (params[versionKey] && store.clientVersion !== params[versionKey]) {
|
|
541
|
+
store.clientVersion = params[versionKey];
|
|
542
|
+
store.encryptionKey = params[encKey];
|
|
543
|
+
await tokenStorage.update(token, { clientVersion: params[versionKey], encryptionKey: params[encKey] });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const steps = [...cloneDeep(defaultSteps)];
|
|
548
|
+
const shouldSkipConnect = canSkipConnect && !!extraParams.connectedDid;
|
|
549
|
+
if (shouldSkipConnect) {
|
|
550
|
+
set(steps, '[0].authPrincipal.supervised', false);
|
|
551
|
+
}
|
|
552
|
+
if (extraParams.forceConnected) {
|
|
553
|
+
let target = extraParams.connectedDid;
|
|
554
|
+
if (typeof extraParams.forceConnected === 'string' && isValidDid(extraParams.forceConnected)) {
|
|
555
|
+
target = extraParams.forceConnected;
|
|
556
|
+
}
|
|
557
|
+
if (isValidDid(target)) {
|
|
558
|
+
set(steps, '[0].authPrincipal.target', target);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
if (store.status !== SESSION_STATUS.SCANNED) {
|
|
563
|
+
await tokenStorage.update(token, { status: SESSION_STATUS.SCANNED, connectedWallet: didwallet });
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Since we can not store dynamic claims anywhere, we should calculate it on the fly
|
|
567
|
+
if (store.dynamic || shouldSkipConnect) {
|
|
568
|
+
const newClaims = await onConnect({
|
|
569
|
+
req,
|
|
570
|
+
request: req,
|
|
571
|
+
userDid,
|
|
572
|
+
userPk,
|
|
573
|
+
didwallet,
|
|
574
|
+
challenge: store.challenge,
|
|
575
|
+
pathname: preparePathname(getPathName(pathname, req), req),
|
|
576
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
577
|
+
extraParams,
|
|
578
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
579
|
+
});
|
|
580
|
+
if (newClaims) {
|
|
581
|
+
if (Array.isArray(newClaims)) {
|
|
582
|
+
steps.push(...newClaims);
|
|
583
|
+
} else {
|
|
584
|
+
steps.push(newClaims);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const signParams = await getSignParams(req);
|
|
590
|
+
const signedClaim = await authenticator.sign(
|
|
591
|
+
Object.assign(signParams, {
|
|
592
|
+
context: {
|
|
593
|
+
token,
|
|
594
|
+
userDid,
|
|
595
|
+
userPk,
|
|
596
|
+
didwallet,
|
|
597
|
+
...pick(store, ['currentStep', 'sharedKey', 'encryptionKey']),
|
|
598
|
+
mfaCode: store.mfaSupported ? createMfaCodeGenerator(token) : undefined,
|
|
599
|
+
},
|
|
600
|
+
claims: steps[store.currentStep],
|
|
601
|
+
pathname: preparePathname(getPathName(pathname, req), req),
|
|
602
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
603
|
+
extraParams,
|
|
604
|
+
challenge: store.challenge,
|
|
605
|
+
appInfo: store.appInfo,
|
|
606
|
+
memberAppInfo: store.memberAppInfo,
|
|
607
|
+
request: req,
|
|
608
|
+
})
|
|
609
|
+
);
|
|
610
|
+
|
|
611
|
+
res.jsonp(encrypt(signedClaim, store));
|
|
612
|
+
} catch (err) {
|
|
613
|
+
onProcessError({ req, res, stage: 'send-auth-claim', err });
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
// eslint-disable-next-line consistent-return
|
|
618
|
+
const onAuthResponse = async (req, res) => {
|
|
619
|
+
const { locale, token, store, params, didwallet } = req.context;
|
|
620
|
+
|
|
621
|
+
try {
|
|
622
|
+
const {
|
|
623
|
+
userDid,
|
|
624
|
+
userPk,
|
|
625
|
+
action: userAction,
|
|
626
|
+
challenge: userChallenge,
|
|
627
|
+
claims: claimResponse,
|
|
628
|
+
timestamp,
|
|
629
|
+
} = await authenticator.verify(decrypt(params, store), locale);
|
|
630
|
+
// debug('onAuthResponse.verify', { userDid, token, claims: claimResponse });
|
|
631
|
+
|
|
632
|
+
if (!store.did || !store.pk) {
|
|
633
|
+
await tokenStorage.update(token, { did: userDid, pk: userPk });
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const extraParams = createExtraParams(locale, params, get(store, 'extraParams', {}));
|
|
637
|
+
const cbParams = {
|
|
638
|
+
step: store.currentStep,
|
|
639
|
+
req,
|
|
640
|
+
request: req,
|
|
641
|
+
userDid,
|
|
642
|
+
userPk,
|
|
643
|
+
challenge: store.challenge,
|
|
644
|
+
didwallet,
|
|
645
|
+
claims: claimResponse,
|
|
646
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
647
|
+
extraParams,
|
|
648
|
+
updateSession: createSessionUpdater(token, extraParams),
|
|
649
|
+
timestamp,
|
|
650
|
+
};
|
|
651
|
+
|
|
652
|
+
const steps = [...defaultSteps];
|
|
653
|
+
const shouldSkipConnect = canSkipConnect && !!extraParams.connectedDid;
|
|
654
|
+
|
|
655
|
+
// Since we can not store dynamic claims anywhere, we should calculate it on the fly
|
|
656
|
+
if (store.dynamic || shouldSkipConnect) {
|
|
657
|
+
const newClaims = await onConnect(cbParams);
|
|
658
|
+
if (newClaims) {
|
|
659
|
+
if (Array.isArray(newClaims)) {
|
|
660
|
+
steps.push(...newClaims);
|
|
661
|
+
} else {
|
|
662
|
+
steps.push(newClaims);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
} else if (persistentDynamicClaims && Array.isArray(store.dynamicClaims)) {
|
|
666
|
+
steps.push(...store.dynamicClaims);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Ensure user approval
|
|
670
|
+
if (userAction === 'declineAuth') {
|
|
671
|
+
await tokenStorage.update(token, {
|
|
672
|
+
status: SESSION_STATUS.ERROR,
|
|
673
|
+
error: errors.userDeclined[locale],
|
|
674
|
+
mfaCode: 0,
|
|
675
|
+
currentStep: steps.length - 1,
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const result = await onDecline(cbParams);
|
|
679
|
+
return res.jsonp({ ...(result || {}) });
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
if (userAction === 'busy') {
|
|
683
|
+
await tokenStorage.update(token, {
|
|
684
|
+
status: SESSION_STATUS.BUSY,
|
|
685
|
+
error: errors.userBusy[locale],
|
|
686
|
+
mfaCode: 0,
|
|
687
|
+
currentStep: steps.length - 1,
|
|
688
|
+
});
|
|
689
|
+
|
|
690
|
+
return res.jsonp({});
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// Since only 1 MFA is allowed when multiple claims is requested, we just need to check the first one
|
|
694
|
+
if (store.mfaCode && !claimResponse.some((x) => isEqual(x.mfaCode, [store.mfaCode]))) {
|
|
695
|
+
return onProcessError({ req, res, stage: 'verify-mfa-code', err: new Error(errors.mfaMismatch[locale]) });
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Ensure JWT challenge match
|
|
699
|
+
if (!userChallenge) {
|
|
700
|
+
return res.jsonp({ error: errors.challengeMismatch[locale] });
|
|
701
|
+
}
|
|
702
|
+
if (userChallenge !== store.challenge) {
|
|
703
|
+
return res.jsonp({ error: errors.challengeMismatch[locale] });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Ensure userDid match between authPrincipal and later process
|
|
707
|
+
const error = await checkUser({ context: req.context, userDid, userPk });
|
|
708
|
+
if (error) {
|
|
709
|
+
return res.jsonp({ error });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const isConnected = store.currentStep > 0;
|
|
713
|
+
if (isConnected === false) {
|
|
714
|
+
// Some permission check login can be done here
|
|
715
|
+
// Error thrown from this callback will terminate the process
|
|
716
|
+
const newClaims = await onConnect(cbParams);
|
|
717
|
+
if (newClaims) {
|
|
718
|
+
await tokenStorage.update(token, {
|
|
719
|
+
dynamic: !persistentDynamicClaims,
|
|
720
|
+
dynamicClaims: persistentDynamicClaims ? newClaims : undefined,
|
|
721
|
+
});
|
|
722
|
+
if (Array.isArray(newClaims)) {
|
|
723
|
+
steps.push(...newClaims);
|
|
724
|
+
} else {
|
|
725
|
+
steps.push(newClaims);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
const onLastStep = async (result) => {
|
|
731
|
+
let nextWorkflow = isUrl(extraParams.nw) ? extraParams.nw : '';
|
|
732
|
+
if (nextWorkflow && result && result.nextWorkflowData) {
|
|
733
|
+
if (isPlainObject(result.nextWorkflowData) === false) {
|
|
734
|
+
const err = new Error(`expect nextWorkflowData should be a plain object, got: ${result.nextWorkflowData}`);
|
|
735
|
+
return onProcessError({ req, res, stage: 'validate-next-workflow-data', err });
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
const tmp = new URL(nextWorkflow);
|
|
739
|
+
const merged = Object.assign(extraParams.previousWorkflowData || {}, result.nextWorkflowData);
|
|
740
|
+
const previousWorkflowData = toBase64(JSON.stringify(merged));
|
|
741
|
+
// For max url length please refer to discussion at https://stackoverflow.com/a/417184/686854
|
|
742
|
+
if (previousWorkflowData.length > 8192) {
|
|
743
|
+
const err = new Error('base64 encoded nextWorkflowData should be less than 8192 characters');
|
|
744
|
+
return onProcessError({ req, res, stage: 'append-next-workflow', err });
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
if (isDeepLink(nextWorkflow)) {
|
|
748
|
+
const actualUrl = decodeURIComponent(tmp.searchParams.get('url'));
|
|
749
|
+
const obj = new URL(actualUrl);
|
|
750
|
+
obj.searchParams.set('previousWorkflowData', previousWorkflowData);
|
|
751
|
+
tmp.searchParams.set('url', obj.href);
|
|
752
|
+
} else {
|
|
753
|
+
tmp.searchParams.set('previousWorkflowData', previousWorkflowData);
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
nextWorkflow = tmp.href;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
const updates = {};
|
|
760
|
+
|
|
761
|
+
// If we have nextWorkflow, return it to browser
|
|
762
|
+
let actualNw = nextWorkflow || result?.nextWorkflow || '';
|
|
763
|
+
if (actualNw) {
|
|
764
|
+
if (isDeepLink(actualNw)) {
|
|
765
|
+
actualNw = new URL(actualNw).searchParams.get('url');
|
|
766
|
+
}
|
|
767
|
+
if (actualNw) {
|
|
768
|
+
updates.nextWorkflow = decodeURIComponent(actualNw);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// If we have nextWorkflow, do not mark current session as complete
|
|
773
|
+
// Instead, save the relationship between the two
|
|
774
|
+
// Then, mark both session as complete on nextWorkflow complete
|
|
775
|
+
// In theory, we can use this mechanism to concat infinite sessions
|
|
776
|
+
if (result && result.nextToken && result.nextWorkflow) {
|
|
777
|
+
try {
|
|
778
|
+
await tokenStorage.update(result.nextToken, { prevToken: token });
|
|
779
|
+
} catch (err) {
|
|
780
|
+
console.error('DIDAuth: failed to to update nextToken', err);
|
|
781
|
+
updates.status = SESSION_STATUS.SUCCEED;
|
|
782
|
+
}
|
|
783
|
+
} else {
|
|
784
|
+
if (store.prevToken) {
|
|
785
|
+
try {
|
|
786
|
+
await tokenStorage.update(store.prevToken, { status: SESSION_STATUS.SUCCEED });
|
|
787
|
+
} catch (err) {
|
|
788
|
+
console.error('DIDAuth: failed to to update prevToken', err);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
updates.status = SESSION_STATUS.SUCCEED;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
await tokenStorage.update(token, updates);
|
|
795
|
+
return res.jsonp({ ...Object.assign({ nextWorkflow }, result || {}) });
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
// If we are only requesting the authPrincipal claim
|
|
799
|
+
// We make such assertion here, because the onConnect callback can modify the steps
|
|
800
|
+
if (steps.length === 1) {
|
|
801
|
+
const result = await onAuth(cbParams);
|
|
802
|
+
return onLastStep(result);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
// If we got requestedClaims other than the authPrincipal
|
|
806
|
+
if (isConnected && store.currentStep < steps.length) {
|
|
807
|
+
// Call onAuth on each step, since we do not hold all results until complete
|
|
808
|
+
const result = await onAuth(cbParams);
|
|
809
|
+
|
|
810
|
+
// Only return if we are walked through all steps
|
|
811
|
+
const isLastStep = store.currentStep === steps.length - 1;
|
|
812
|
+
if (isLastStep) {
|
|
813
|
+
return onLastStep(result);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// Move to next step: nextStep is persisted here to avoid an memory storage error
|
|
818
|
+
const nextStep = store.currentStep + 1;
|
|
819
|
+
const nextChallenge = getStepChallenge();
|
|
820
|
+
await tokenStorage.update(token, { currentStep: nextStep, challenge: nextChallenge, mfaCode: 0 });
|
|
821
|
+
const signParams = await getSignParams(req);
|
|
822
|
+
|
|
823
|
+
try {
|
|
824
|
+
const nextSignedClaim = await authenticator.sign(
|
|
825
|
+
Object.assign(signParams, {
|
|
826
|
+
context: {
|
|
827
|
+
token,
|
|
828
|
+
userDid,
|
|
829
|
+
userPk,
|
|
830
|
+
didwallet,
|
|
831
|
+
...pick(store, ['currentStep', 'sharedKey', 'encryptionKey']),
|
|
832
|
+
mfaCode: store.mfaSupported ? createMfaCodeGenerator(token) : undefined,
|
|
833
|
+
},
|
|
834
|
+
claims: steps[nextStep],
|
|
835
|
+
pathname: preparePathname(getPathName(pathname, req), req),
|
|
836
|
+
baseUrl: prepareBaseUrl(req, extraParams),
|
|
837
|
+
extraParams,
|
|
838
|
+
challenge: nextChallenge,
|
|
839
|
+
appInfo: store.appInfo,
|
|
840
|
+
memberAppInfo: store.memberAppInfo,
|
|
841
|
+
request: req,
|
|
842
|
+
})
|
|
843
|
+
);
|
|
844
|
+
return res.jsonp(encrypt(nextSignedClaim, store));
|
|
845
|
+
} catch (err) {
|
|
846
|
+
return onProcessError({ req, res, stage: 'next-auth-claim', err });
|
|
847
|
+
}
|
|
848
|
+
} catch (err) {
|
|
849
|
+
onProcessError({ req, res, stage: 'verify-auth-claim', err });
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
const ensureContext = async (req, res, next) => {
|
|
854
|
+
const didwallet = parseWalletUA(req.query['user-agent'] || req.headers['user-agent']);
|
|
855
|
+
const params = { ...req.body, ...req.query, ...req.params };
|
|
856
|
+
const token = params[tokenKey];
|
|
857
|
+
const locale = getLocale(req);
|
|
858
|
+
|
|
859
|
+
let store = null;
|
|
860
|
+
if (token) {
|
|
861
|
+
store = await tokenStorage.read(token);
|
|
862
|
+
if (params.previousWorkflowData) {
|
|
863
|
+
try {
|
|
864
|
+
store.extraParams.previousWorkflowData = JSON.parse(fromBase64(params.previousWorkflowData));
|
|
865
|
+
await tokenStorage.update(token, { extraParams: store.extraParams });
|
|
866
|
+
} catch (e) {
|
|
867
|
+
console.warn('Could not parse previousWorkflowData', params.previousWorkflowData, e);
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
if (store?.destToken && typeof params.notrace === 'undefined') {
|
|
871
|
+
const result = await tokenStorage.read(store.destToken);
|
|
872
|
+
if (result) {
|
|
873
|
+
store = result;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
req.context = { locale, token, didwallet, params, store };
|
|
879
|
+
return next();
|
|
880
|
+
};
|
|
881
|
+
|
|
882
|
+
const ensureSignedJson = (req, res, next) => {
|
|
883
|
+
if (req.ensureSignedJson === undefined) {
|
|
884
|
+
req.ensureSignedJson = true;
|
|
885
|
+
const originJsonp = res.jsonp;
|
|
886
|
+
res.jsonp = async (payload) => {
|
|
887
|
+
if (payload.appPk && payload.authInfo) {
|
|
888
|
+
return originJsonp.call(res, payload);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
const data = payload.response ? { response: payload.response } : { response: payload };
|
|
892
|
+
const fields = ['error', 'errorMessage', 'successMessage', 'nextWorkflow', 'nextUrl', 'cookies', 'storages'];
|
|
893
|
+
|
|
894
|
+
// Attach protocol fields to the root
|
|
895
|
+
fields.forEach((x) => {
|
|
896
|
+
if (payload[x]) {
|
|
897
|
+
data[x] = payload[x];
|
|
898
|
+
}
|
|
899
|
+
});
|
|
900
|
+
data.errorMessage = data.error || data.errorMessage || '';
|
|
901
|
+
|
|
902
|
+
// Remove protocol fields from the response
|
|
903
|
+
if (typeof data.response === 'object') {
|
|
904
|
+
data.response = omit(data.response, fields);
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
const params = { ...req.body, ...req.query, ...req.params };
|
|
908
|
+
const token = params[tokenKey];
|
|
909
|
+
const store = token ? await tokenStorage.read(token) : null;
|
|
910
|
+
|
|
911
|
+
const extraParams = get(store, 'extraParams', {});
|
|
912
|
+
const signedData = await authenticator.signResponse(data, prepareBaseUrl(req, extraParams), req, extraParams);
|
|
913
|
+
// debug('ensureSignedJson.do', signed);
|
|
914
|
+
originJsonp.call(res, encrypt(signedData, store));
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const { token, store, locale } = req.context;
|
|
919
|
+
if (!token || !store) {
|
|
920
|
+
return res.jsonp({ error: errors.token404[locale] });
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
next();
|
|
924
|
+
};
|
|
925
|
+
|
|
926
|
+
return {
|
|
927
|
+
generateSession,
|
|
928
|
+
expireSession,
|
|
929
|
+
checkSession,
|
|
930
|
+
onAuthRequest,
|
|
931
|
+
onAuthResponse,
|
|
932
|
+
ensureContext,
|
|
933
|
+
ensureSignedJson,
|
|
934
|
+
createExtraParams,
|
|
935
|
+
};
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
module.exports.isDeepLink = isDeepLink;
|
|
939
|
+
module.exports.isConnectedOnly = isConnectedOnly;
|
|
940
|
+
module.exports.parseWalletUA = parseWalletUA;
|
|
941
|
+
module.exports.preparePathname = _preparePathname;
|
|
942
|
+
module.exports.prepareBaseUrl = prepareBaseUrl;
|
|
943
|
+
module.exports.getStepChallenge = getStepChallenge;
|