@abtnode/blocklet-services 1.6.23
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 +84 -0
- package/build/asset-manifest.json +36 -0
- package/build/favicon.ico +0 -0
- package/build/images/blocklet.png +0 -0
- package/build/index.html +1 -0
- package/build/manifest.json +15 -0
- package/build/precache-manifest.b61da416864625d7da6840b710c5fd05.js +150 -0
- package/build/service-worker.js +39 -0
- package/build/static/css/2.73d52aac.chunk.css +2 -0
- package/build/static/css/2.73d52aac.chunk.css.map +1 -0
- package/build/static/js/2.adca7e30.chunk.js +3 -0
- package/build/static/js/2.adca7e30.chunk.js.LICENSE.txt +113 -0
- package/build/static/js/2.adca7e30.chunk.js.map +1 -0
- package/build/static/js/3.9cf8cde1.chunk.js +3 -0
- package/build/static/js/3.9cf8cde1.chunk.js.LICENSE.txt +19 -0
- package/build/static/js/3.9cf8cde1.chunk.js.map +1 -0
- package/build/static/js/4.3bcd2384.chunk.js +2 -0
- package/build/static/js/4.3bcd2384.chunk.js.map +1 -0
- package/build/static/js/5.a0b39d30.chunk.js +2 -0
- package/build/static/js/5.a0b39d30.chunk.js.map +1 -0
- package/build/static/js/6.91e31d64.chunk.js +2 -0
- package/build/static/js/6.91e31d64.chunk.js.map +1 -0
- package/build/static/js/7.aa6a9bc1.chunk.js +2 -0
- package/build/static/js/7.aa6a9bc1.chunk.js.map +1 -0
- package/build/static/js/main.0fef8e40.chunk.js +2 -0
- package/build/static/js/main.0fef8e40.chunk.js.map +1 -0
- package/build/static/js/runtime-main.8d9f8ef7.js +2 -0
- package/build/static/js/runtime-main.8d9f8ef7.js.map +1 -0
- package/build/static/media/rubik-all-400-normal.c2324103.woff +0 -0
- package/build/static/media/rubik-all-500-normal.000c3408.woff +0 -0
- package/build/static/media/rubik-all-600-normal.30df2fd0.woff +0 -0
- package/build/static/media/rubik-cyrillic-400-normal.aa383bbd.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-500-normal.27a1ebd4.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-600-normal.74d5cdba.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-400-normal.b8647475.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-500-normal.860932d9.woff2 +0 -0
- package/build/static/media/rubik-cyrillic-ext-600-normal.942f240f.woff2 +0 -0
- package/build/static/media/rubik-hebrew-400-normal.2c9e3c2a.woff2 +0 -0
- package/build/static/media/rubik-hebrew-500-normal.08d6e502.woff2 +0 -0
- package/build/static/media/rubik-hebrew-600-normal.bfa32e44.woff2 +0 -0
- package/build/static/media/rubik-latin-400-normal.b8fd53c5.woff2 +0 -0
- package/build/static/media/rubik-latin-500-normal.595f1a98.woff2 +0 -0
- package/build/static/media/rubik-latin-600-normal.5f06934f.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-400-normal.3c5c378e.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-500-normal.5663c731.woff2 +0 -0
- package/build/static/media/rubik-latin-ext-600-normal.ff159cb8.woff2 +0 -0
- package/build/static/media/ubuntu-mono-all-400-normal.e39678a8.woff +0 -0
- package/build/static/media/ubuntu-mono-cyrillic-400-normal.e09900e7.woff2 +0 -0
- package/build/static/media/ubuntu-mono-cyrillic-ext-400-normal.762d0b06.woff2 +0 -0
- package/build/static/media/ubuntu-mono-greek-400-normal.81c98658.woff2 +0 -0
- package/build/static/media/ubuntu-mono-greek-ext-400-normal.1af17851.woff2 +0 -0
- package/build/static/media/ubuntu-mono-latin-400-normal.469ee478.woff2 +0 -0
- package/build/static/media/ubuntu-mono-latin-ext-400-normal.e28ff028.woff2 +0 -0
- package/lib/cache.js +25 -0
- package/lib/index.js +443 -0
- package/lib/mount.js +52 -0
- package/lib/util.js +17 -0
- package/package.json +119 -0
- package/services/auth/index.js +290 -0
- package/services/auth/libs/auth.js +76 -0
- package/services/auth/libs/jwt.js +73 -0
- package/services/auth/meta.json +61 -0
- package/services/auth/routes/blocklet-info.js +33 -0
- package/services/auth/routes/env.js +33 -0
- package/services/auth/routes/invite.js +80 -0
- package/services/auth/routes/issue-passport.js +58 -0
- package/services/auth/routes/login.js +230 -0
- package/services/auth/routes/lost-passport-issue.js +5 -0
- package/services/auth/routes/lost-passport-list.js +6 -0
- package/services/auth/routes/notification.js +257 -0
- package/services/auth/routes/passport.js +18 -0
- package/services/auth/routes/session.js +27 -0
- package/services/auth/state/index.js +30 -0
- package/services/auth/state/message.js +32 -0
- package/services/auth/util/index.js +35 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
/* eslint-disable arrow-parens */
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const get = require('lodash/get');
|
|
4
|
+
const minimatch = require('minimatch');
|
|
5
|
+
const bearerToken = require('express-bearer-token');
|
|
6
|
+
const validators = require('@blocklet/sdk/lib/validators');
|
|
7
|
+
const {
|
|
8
|
+
genPermissionName,
|
|
9
|
+
NODE_SERVICES,
|
|
10
|
+
NODE_SERVICES_PREFIX,
|
|
11
|
+
WELLKNOWN_AUTH_PATH_PREFIX,
|
|
12
|
+
} = require('@abtnode/constant');
|
|
13
|
+
const { BLOCKLET_MODES, BlockletStatus } = require('@blocklet/meta/lib/constants');
|
|
14
|
+
|
|
15
|
+
const getBlockletNotRunningTemplate = require('@abtnode/router-templates/lib/blocklet-not-running');
|
|
16
|
+
const getBlockletMaintenanceTemplate = require('@abtnode/router-templates/lib/blocklet-maintenance');
|
|
17
|
+
const { getBaseUrl } = require('@abtnode/router-adapter');
|
|
18
|
+
const { setUserInfoHeaders } = require('@abtnode/auth/lib/auth');
|
|
19
|
+
const logger = require('@abtnode/logger')(require('./meta.json').name);
|
|
20
|
+
|
|
21
|
+
const { name, description, version } = require('./meta.json');
|
|
22
|
+
|
|
23
|
+
const initJwt = require('./libs/jwt');
|
|
24
|
+
const initAuth = require('./libs/auth');
|
|
25
|
+
const createLoginRoutes = require('./routes/login');
|
|
26
|
+
const createInviteRoutes = require('./routes/invite');
|
|
27
|
+
const createIssuePassportAuth = require('./routes/issue-passport');
|
|
28
|
+
const createLostPassportListAuth = require('./routes/lost-passport-list');
|
|
29
|
+
const createLostPassportIssueAuth = require('./routes/lost-passport-issue');
|
|
30
|
+
const createSessionRoutes = require('./routes/session');
|
|
31
|
+
const createEnvRoutes = require('./routes/env');
|
|
32
|
+
const createBlockletRoutes = require('./routes/blocklet-info');
|
|
33
|
+
const createPassportRoutes = require('./routes/passport');
|
|
34
|
+
const { init: initNotification } = require('./routes/notification');
|
|
35
|
+
const { init: initStates } = require('./state/index');
|
|
36
|
+
|
|
37
|
+
const defaultOptions = {
|
|
38
|
+
dataDir: '',
|
|
39
|
+
sessionSecret: 'your_should_change_this',
|
|
40
|
+
sessionTtl: '7d',
|
|
41
|
+
webWalletUrl: 'https://web.abtwallet.io',
|
|
42
|
+
loginTokenKey: 'login_token',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const init = ({ node, httpRouter, serviceHttpRouter, wsRouter, options }) => {
|
|
46
|
+
if (!options.dataDir) {
|
|
47
|
+
throw new Error('Auth service requires dataDir to start');
|
|
48
|
+
}
|
|
49
|
+
if (!options.sessionSecret) {
|
|
50
|
+
throw new Error('Auth service requires sessionSecret to start');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const isProduction = process.env.NODE_ENV === 'production' || process.env.ABT_NODE_SERVICE_ENV === 'production';
|
|
54
|
+
const isE2E = process.env.NODE_ENV === 'e2e';
|
|
55
|
+
const distDir = path.resolve(__dirname, '../../build');
|
|
56
|
+
|
|
57
|
+
logger.info('init params', { isProduction, distDir });
|
|
58
|
+
|
|
59
|
+
const opts = { ...defaultOptions, ...options, name, description, version, prefix: NODE_SERVICES_PREFIX.AUTH_SERVICE };
|
|
60
|
+
if (opts.sessionSecret === defaultOptions.sessionSecret) {
|
|
61
|
+
logger.warn('session secret should be set to make things secure');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* set user info to req.user
|
|
66
|
+
*/
|
|
67
|
+
const ensureUser = async ({ req, token } = {}) => {
|
|
68
|
+
try {
|
|
69
|
+
if (token) {
|
|
70
|
+
const teamDid = req.getBlockletDid();
|
|
71
|
+
const result = await verify(token, teamDid);
|
|
72
|
+
req.user = result;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Do nothing
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* @returns {object} res
|
|
81
|
+
* {boolean} res.blocked
|
|
82
|
+
* {boolean} res.authenticated
|
|
83
|
+
* {boolean} res.authorized
|
|
84
|
+
*/
|
|
85
|
+
const checkAuth = async ({ req } = {}) => {
|
|
86
|
+
if (!isProduction && !isE2E) {
|
|
87
|
+
return {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const config = (await req.getServiceConfig(NODE_SERVICES.AUTH_SERVICE)) || {};
|
|
91
|
+
|
|
92
|
+
const ignoreUrls = (get(config, 'ignoreUrls') || []).filter(Boolean);
|
|
93
|
+
|
|
94
|
+
const shouldIgnore = ignoreUrls.some((s) => minimatch(req.url, s)) || req.url.startsWith(`${opts.prefix}/api`);
|
|
95
|
+
if (shouldIgnore) {
|
|
96
|
+
return {};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (config.blockUnauthenticated === false) {
|
|
100
|
+
return {};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!req.user) {
|
|
104
|
+
return { blocked: true, authenticated: false };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!config.blockUnauthorized) {
|
|
108
|
+
return {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rule = await req.getRoutingRule();
|
|
112
|
+
if (rule.to && rule.to.interfaceName) {
|
|
113
|
+
const permissionName = genPermissionName(rule.to.interfaceName);
|
|
114
|
+
const teamDid = req.getBlockletDid();
|
|
115
|
+
const rbac = await node.getRBAC(teamDid);
|
|
116
|
+
|
|
117
|
+
if (await rbac.can(req.user.role, ...permissionName.split('_'))) {
|
|
118
|
+
return {};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return { blocked: true, authenticated: true, authorized: false };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {};
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
initStates(opts.dataDir);
|
|
128
|
+
|
|
129
|
+
initNotification(node, httpRouter, wsRouter, opts);
|
|
130
|
+
|
|
131
|
+
const { login, verify } = initJwt(node, opts);
|
|
132
|
+
const { authenticator, handlers } = initAuth(node, opts);
|
|
133
|
+
|
|
134
|
+
// auth middleware for http request
|
|
135
|
+
serviceHttpRouter.use(
|
|
136
|
+
bearerToken({
|
|
137
|
+
queryKey: opts.loginTokenKey,
|
|
138
|
+
bodyKey: opts.loginTokenKey,
|
|
139
|
+
headerKey: 'Bearer',
|
|
140
|
+
cookie: {
|
|
141
|
+
signed: true,
|
|
142
|
+
secret: opts.sessionSecret,
|
|
143
|
+
key: opts.loginTokenKey,
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
// set user info to req.user
|
|
149
|
+
serviceHttpRouter.use(async (req, res, next) => {
|
|
150
|
+
const { token } = req;
|
|
151
|
+
await ensureUser({ req, token });
|
|
152
|
+
next();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// set user info to req.headers
|
|
156
|
+
serviceHttpRouter.use(async (req, res, next) => {
|
|
157
|
+
setUserInfoHeaders(req);
|
|
158
|
+
next();
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
// public did-auth api
|
|
162
|
+
handlers.attach(Object.assign({ app: serviceHttpRouter }, createLoginRoutes(node, authenticator, login, opts)));
|
|
163
|
+
handlers.attach(Object.assign({ app: serviceHttpRouter }, createInviteRoutes(node, authenticator, login, opts)));
|
|
164
|
+
handlers.attach(Object.assign({ app: serviceHttpRouter }, createIssuePassportAuth(node, authenticator, login, opts)));
|
|
165
|
+
handlers.attach(
|
|
166
|
+
Object.assign({ app: serviceHttpRouter }, createLostPassportListAuth(node, authenticator, login, opts))
|
|
167
|
+
);
|
|
168
|
+
handlers.attach(
|
|
169
|
+
Object.assign({ app: serviceHttpRouter }, createLostPassportIssueAuth(node, authenticator, login, opts))
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
// public page
|
|
173
|
+
[`${WELLKNOWN_AUTH_PATH_PREFIX}/issue-passport`, `${WELLKNOWN_AUTH_PATH_PREFIX}/lost-passport`].forEach((x) => {
|
|
174
|
+
serviceHttpRouter.get(x, (req, res) => {
|
|
175
|
+
res.sendFile(path.join(distDir, 'index.html'));
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// public http api
|
|
180
|
+
createEnvRoutes.init(httpRouter, node, opts);
|
|
181
|
+
createBlockletRoutes.init(httpRouter, node, opts);
|
|
182
|
+
createPassportRoutes.init(httpRouter, node, opts);
|
|
183
|
+
|
|
184
|
+
// check blocklet running
|
|
185
|
+
// Request would not arrive here before blocklet is installed, because there is no config in router provider(nginx)
|
|
186
|
+
serviceHttpRouter.use(async (req, res, next) => {
|
|
187
|
+
const blocklet = await req.getBlocklet();
|
|
188
|
+
if (
|
|
189
|
+
![
|
|
190
|
+
BlockletStatus.running,
|
|
191
|
+
// Waiting, Downloading should be allowed because blocklet is currently being upgrading
|
|
192
|
+
BlockletStatus.waiting,
|
|
193
|
+
BlockletStatus.downloading,
|
|
194
|
+
].includes(blocklet.status)
|
|
195
|
+
) {
|
|
196
|
+
const nodeInfo = await req.getNodeInfo();
|
|
197
|
+
res.status(500);
|
|
198
|
+
// return json if json has high priority, e.g. "*/*,application/json"
|
|
199
|
+
if (req.accepts(['html', 'json']) === 'json') {
|
|
200
|
+
res.json({ code: 'error', error: 'blocklet is not running' });
|
|
201
|
+
} else if (blocklet.status === BlockletStatus.starting) {
|
|
202
|
+
res.send(getBlockletMaintenanceTemplate(blocklet, nodeInfo));
|
|
203
|
+
} else {
|
|
204
|
+
res.send(getBlockletNotRunningTemplate(blocklet, nodeInfo));
|
|
205
|
+
}
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
next();
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// auth middleware for websocket connect
|
|
213
|
+
wsRouter.use('**', async (req, socket, head, next) => {
|
|
214
|
+
const { searchParams } = new URL(req.url, `http://${req.headers.host || 'unknown'}`);
|
|
215
|
+
const token = searchParams.get('token');
|
|
216
|
+
|
|
217
|
+
// ignore some dev path in dev mode
|
|
218
|
+
const blocklet = await req.getBlocklet();
|
|
219
|
+
const devUrls = ['/sockjs-node'];
|
|
220
|
+
const mode = get(blocklet, 'mode');
|
|
221
|
+
if (mode === BLOCKLET_MODES.DEVELOPMENT && devUrls.includes(req.url)) {
|
|
222
|
+
next();
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
await ensureUser({ req, token });
|
|
227
|
+
|
|
228
|
+
setUserInfoHeaders(req);
|
|
229
|
+
|
|
230
|
+
const { blocked, authenticated, authorized } = await checkAuth({ req });
|
|
231
|
+
if (blocked) {
|
|
232
|
+
let message = '401 Unauthorized';
|
|
233
|
+
if (authenticated && !authorized) {
|
|
234
|
+
message = '403 Forbidden';
|
|
235
|
+
}
|
|
236
|
+
socket.write(`HTTP/1.1 ${message}\r\n\r\n`);
|
|
237
|
+
socket.destroy();
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
next();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
serviceHttpRouter.use(async (req, res, next) => {
|
|
245
|
+
const { blocked, authenticated, authorized } = await checkAuth({ req });
|
|
246
|
+
|
|
247
|
+
if (blocked) {
|
|
248
|
+
if (!authenticated) {
|
|
249
|
+
if (req.path !== '/') {
|
|
250
|
+
// redirect to root path for login
|
|
251
|
+
// redirect to target path after login
|
|
252
|
+
const baseUrl = getBaseUrl(req);
|
|
253
|
+
const redirect = encodeURIComponent(`${baseUrl.replace(/\/+$/, '')}/${req.url.replace(/^\/+/, '')}`);
|
|
254
|
+
res.redirect(`${baseUrl.replace(/\/+$/, '')}/?redirect=${redirect}`);
|
|
255
|
+
} else {
|
|
256
|
+
res.sendFile(path.join(distDir, 'index.html'));
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!authorized) {
|
|
262
|
+
res.status(403).json({ code: 'forbidden', error: 'not allowed' });
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
next();
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Note: this api depends on the user decoding logic
|
|
271
|
+
createSessionRoutes.init(serviceHttpRouter, node, opts);
|
|
272
|
+
|
|
273
|
+
// Block /.service/@abtnode/auth-service/<invalid_path>
|
|
274
|
+
serviceHttpRouter.use((req, res, next) => {
|
|
275
|
+
if (req.path.startsWith(opts.prefix)) {
|
|
276
|
+
res.status(400).send('Bad request');
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
next();
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
module.exports = {
|
|
285
|
+
name,
|
|
286
|
+
description,
|
|
287
|
+
version,
|
|
288
|
+
init,
|
|
289
|
+
validators,
|
|
290
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const get = require('lodash/get');
|
|
3
|
+
const joinUrl = require('url-join');
|
|
4
|
+
const DiskStorage = require('@arcblock/did-auth-storage-nedb');
|
|
5
|
+
const { WalletAuthenticator } = require('@arcblock/did-auth');
|
|
6
|
+
const WalletHandlers = require('@blocklet/sdk/lib/wallet-handler');
|
|
7
|
+
const sendNotification = require('@blocklet/sdk/lib/util/send-notification');
|
|
8
|
+
|
|
9
|
+
const { getBlockletLogo } = require('../util');
|
|
10
|
+
|
|
11
|
+
module.exports = (node, opts) => {
|
|
12
|
+
const authenticator = new WalletAuthenticator({
|
|
13
|
+
wallet: async ({ request }) => {
|
|
14
|
+
const { wallet } = await request.getBlockletInfo();
|
|
15
|
+
return wallet.toJSON();
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
appInfo: async ({ request, baseUrl }) => {
|
|
19
|
+
const groupPathPrefix = request.headers['x-group-path-prefix'] || '/';
|
|
20
|
+
|
|
21
|
+
const [blocklet, meta, info] = await Promise.all([
|
|
22
|
+
request.getBlocklet(),
|
|
23
|
+
request.getBlockletInfo(),
|
|
24
|
+
request.getNodeInfo(),
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// logo
|
|
28
|
+
const logo = getBlockletLogo({ baseUrl: joinUrl(baseUrl, opts.prefix), blocklet, nodeInfo: info });
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: meta.name,
|
|
32
|
+
description: meta.description || `Login to ${meta.name}`,
|
|
33
|
+
icon: logo,
|
|
34
|
+
updateSubEndpoint: true,
|
|
35
|
+
subscriptionEndpoint: joinUrl(groupPathPrefix, opts.prefix, 'websocket'),
|
|
36
|
+
nodeDid: info.did,
|
|
37
|
+
};
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
chainInfo: async ({ request } = {}) => {
|
|
41
|
+
if (!request || !request.getBlocklet) {
|
|
42
|
+
return { host: 'none', id: 'none' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const blocklet = await request.getBlocklet();
|
|
46
|
+
const chainHost = get(blocklet, 'configObj.CHAIN_HOST');
|
|
47
|
+
return chainHost ? { host: chainHost, id: 'chain' } : { host: 'none', id: 'none' };
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const handlers = new WalletHandlers({
|
|
52
|
+
authenticator,
|
|
53
|
+
options: {
|
|
54
|
+
prefix: `${opts.prefix}/api/did`,
|
|
55
|
+
},
|
|
56
|
+
tokenGenerator: () => Date.now().toString(),
|
|
57
|
+
tokenStorage: new DiskStorage({
|
|
58
|
+
dbPath: path.join(opts.dataDir, 'auth.db'),
|
|
59
|
+
dbPort: process.env.NODE_ENV === 'test' ? null : Number(process.env.NEDB_MULTI_PORT),
|
|
60
|
+
}),
|
|
61
|
+
sendNotificationFn: async (connectedDid, message, { req }) => {
|
|
62
|
+
const { wallet } = await req.getBlockletInfo();
|
|
63
|
+
const sender = {
|
|
64
|
+
appId: wallet.address,
|
|
65
|
+
appSk: wallet.secretKey,
|
|
66
|
+
did: req.getBlockletDid(),
|
|
67
|
+
};
|
|
68
|
+
return sendNotification(connectedDid, message, sender, process.env.ABT_NODE_SERVICE_PORT);
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
authenticator,
|
|
74
|
+
handlers,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/* eslint-disable no-underscore-dangle */
|
|
2
|
+
const jwt = require('jsonwebtoken');
|
|
3
|
+
const { createAuthToken } = require('@abtnode/auth/lib/auth');
|
|
4
|
+
const { isUserPassportRevoked } = require('@abtnode/auth/lib/passport');
|
|
5
|
+
|
|
6
|
+
const getUser = async (node, teamDid, userDid) => {
|
|
7
|
+
const user = await node.getUser({ teamDid, user: { did: userDid } });
|
|
8
|
+
if (user && user.approved) {
|
|
9
|
+
return user;
|
|
10
|
+
}
|
|
11
|
+
return null;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line no-unused-vars
|
|
15
|
+
const initJwt = (node, options) => {
|
|
16
|
+
const secret = options.sessionSecret;
|
|
17
|
+
const ttl = options.sessionTtl || '1d';
|
|
18
|
+
|
|
19
|
+
if (!secret) {
|
|
20
|
+
throw new Error('Auth service require a non-empty session secret to start');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const login = async (did, { role, passport }) =>
|
|
24
|
+
createAuthToken({
|
|
25
|
+
did,
|
|
26
|
+
passport,
|
|
27
|
+
role,
|
|
28
|
+
secret,
|
|
29
|
+
expiresIn: ttl,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const verify = (token, teamDid) =>
|
|
33
|
+
// eslint-disable-next-line implicit-arrow-linebreak
|
|
34
|
+
new Promise((resolve, reject) => {
|
|
35
|
+
jwt.verify(token, secret, async (err, decoded) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
return reject(err);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const { did, role, passport } = decoded;
|
|
41
|
+
if (!did) {
|
|
42
|
+
return reject(new Error('Invalid jwt token: invalid did'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const user = await getUser(node, teamDid, did);
|
|
46
|
+
if (!user) {
|
|
47
|
+
return reject(new Error('Invalid jwt token: invalid user'));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (passport && passport.id && isUserPassportRevoked(user, passport)) {
|
|
51
|
+
return reject(new Error(`Passport ${passport.name} has been revoked`));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
user.role = role;
|
|
55
|
+
|
|
56
|
+
// backward compatible
|
|
57
|
+
if (!role && passport && !passport.id) {
|
|
58
|
+
user.role = passport.name;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolve(user);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
login,
|
|
67
|
+
verify,
|
|
68
|
+
};
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
initJwt.getUser = getUser;
|
|
72
|
+
|
|
73
|
+
module.exports = initJwt;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@abtnode/auth-service",
|
|
3
|
+
"description": "Neat and easy to use user authentication and authorization service for blocklets",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"schema": {
|
|
6
|
+
"JSONSchema": {
|
|
7
|
+
"title": "Config Auth Service",
|
|
8
|
+
"description": "Customize how Auth Service works",
|
|
9
|
+
"type": "object",
|
|
10
|
+
"required": ["profileFields", "webWalletUrl"],
|
|
11
|
+
"properties": {
|
|
12
|
+
"invitedUserOnly": {
|
|
13
|
+
"type": "string",
|
|
14
|
+
"title": "Only invited users are allowed to login",
|
|
15
|
+
"enum": ["yes", "no", "not-first"],
|
|
16
|
+
"default": "no"
|
|
17
|
+
},
|
|
18
|
+
"profileFields": {
|
|
19
|
+
"type": "array",
|
|
20
|
+
"title": "What info do you want user to provide when login?",
|
|
21
|
+
"items": {
|
|
22
|
+
"type": "string",
|
|
23
|
+
"enum": ["fullName", "email", "avatar", "phone"]
|
|
24
|
+
},
|
|
25
|
+
"default": ["fullName", "email", "avatar"],
|
|
26
|
+
"uniqueItems": true
|
|
27
|
+
},
|
|
28
|
+
"webWalletUrl": {
|
|
29
|
+
"type": "string",
|
|
30
|
+
"title": "The URL of your preferred web wallet instance",
|
|
31
|
+
"pattern": "^https?://",
|
|
32
|
+
"default": "https://web.abtwallet.io"
|
|
33
|
+
},
|
|
34
|
+
"ignoreUrls": {
|
|
35
|
+
"type": "array",
|
|
36
|
+
"title": "Which URLs do not need to be protected. e.g: /public/**",
|
|
37
|
+
"items": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"minLength": 1
|
|
40
|
+
},
|
|
41
|
+
"default": []
|
|
42
|
+
},
|
|
43
|
+
"blockUnauthenticated": {
|
|
44
|
+
"type": "boolean",
|
|
45
|
+
"title": "Do you want Auth Service block unauthenticated requests for you?",
|
|
46
|
+
"default": true
|
|
47
|
+
},
|
|
48
|
+
"blockUnauthorized": {
|
|
49
|
+
"type": "boolean",
|
|
50
|
+
"title": "Do you want Auth Service block unauthorized requests for you?",
|
|
51
|
+
"default": false
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
"UISchema": {
|
|
56
|
+
"profileFields": {
|
|
57
|
+
"ui:widget": "checkboxes"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const get = require('lodash/get');
|
|
5
|
+
const logger = require('@abtnode/logger')(require('../meta.json').name);
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
init(server, node, opts) {
|
|
9
|
+
server.get(`${opts.prefix}/blocklet/logo/:did`, async (req, res) => {
|
|
10
|
+
const staticDir = path.resolve(__dirname, './build');
|
|
11
|
+
const sendOptions = { maxAge: '1d' };
|
|
12
|
+
|
|
13
|
+
let blocklet = null;
|
|
14
|
+
try {
|
|
15
|
+
blocklet = await node.ensureBlockletIntegrity(req.params.did);
|
|
16
|
+
|
|
17
|
+
if (blocklet && get(blocklet, 'env.appDir') && blocklet.meta.logo) {
|
|
18
|
+
const logoFile = path.join(get(blocklet, 'env.appDir'), blocklet.meta.logo);
|
|
19
|
+
|
|
20
|
+
if (fs.existsSync(logoFile)) {
|
|
21
|
+
res.sendFile(logoFile, sendOptions);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
res.sendFile('/images/blocklet.png', { root: staticDir, ...sendOptions });
|
|
27
|
+
} catch (err) {
|
|
28
|
+
logger.error('failed to send blocklet logo', { did: req.params.did, error: err });
|
|
29
|
+
res.sendFile('/images/blocklet.png', { root: staticDir, ...sendOptions });
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const { WELLKNOWN_AUTH_PATH_PREFIX } = require('@abtnode/constant');
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
init(server, node, opts) {
|
|
5
|
+
server.get(`**${opts.prefix}/api/env`, async (req, res) => {
|
|
6
|
+
res.type('js');
|
|
7
|
+
|
|
8
|
+
const [blocklet, config, info] = await Promise.all([
|
|
9
|
+
req.getBlockletInfo(),
|
|
10
|
+
req.getServiceConfig(opts.name),
|
|
11
|
+
req.getNodeInfo(),
|
|
12
|
+
]);
|
|
13
|
+
let pathPrefix = req.headers['x-path-prefix'] || '/';
|
|
14
|
+
let groupPathPrefix = req.headers['x-group-path-prefix'];
|
|
15
|
+
|
|
16
|
+
// fix pathPrefix && groupPathPrefix
|
|
17
|
+
// FIXME: pathPrefix and groupPathPrefix should be correct in service gateway
|
|
18
|
+
pathPrefix = pathPrefix.replace(WELLKNOWN_AUTH_PATH_PREFIX, '') || '/';
|
|
19
|
+
if (groupPathPrefix) {
|
|
20
|
+
groupPathPrefix = groupPathPrefix.replace(WELLKNOWN_AUTH_PATH_PREFIX, '') || '/';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
res.send(`window.env = {
|
|
24
|
+
appId: "${blocklet.did}",
|
|
25
|
+
appName: "${blocklet.name}",
|
|
26
|
+
pathPrefix: "${pathPrefix}",
|
|
27
|
+
apiPrefix: "${pathPrefix.replace(/\/+$/, '')}${opts.prefix}",
|
|
28
|
+
${groupPathPrefix ? `groupPathPrefix: "${groupPathPrefix}",` : ''}
|
|
29
|
+
webWalletUrl: "${info.webWalletUrl || config.webWalletUrl || opts.webWalletUrl}",
|
|
30
|
+
}`);
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const get = require('lodash/get');
|
|
2
|
+
const joinUrl = require('url-join');
|
|
3
|
+
const {
|
|
4
|
+
createInvitationRequest,
|
|
5
|
+
handleInvitationResponse,
|
|
6
|
+
messages,
|
|
7
|
+
checkWalletVersion,
|
|
8
|
+
beforeInvitationRequest,
|
|
9
|
+
} = require('@abtnode/auth/lib/auth');
|
|
10
|
+
const logger = require('@abtnode/logger')(require('../meta.json').name);
|
|
11
|
+
|
|
12
|
+
module.exports = function createRoutes(node, authenticator, login, opts) {
|
|
13
|
+
return {
|
|
14
|
+
action: 'invite',
|
|
15
|
+
|
|
16
|
+
onStart: async ({ extraParams, req }) => {
|
|
17
|
+
const { locale = 'en', inviteId } = extraParams;
|
|
18
|
+
const teamDid = req.headers['x-blocklet-did'];
|
|
19
|
+
|
|
20
|
+
await beforeInvitationRequest({ node, teamDid, locale, inviteId });
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
claims: {
|
|
24
|
+
profile: async ({ extraParams, context }) => {
|
|
25
|
+
const { locale } = extraParams;
|
|
26
|
+
|
|
27
|
+
const config = await context.request.getServiceConfig();
|
|
28
|
+
const profileFields = get(config, 'profileFields');
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
fields: profileFields || ['fullName', 'avatar'],
|
|
32
|
+
description: messages.description[locale],
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
signature: async ({ extraParams, context: { request, abtwallet } }) => {
|
|
37
|
+
const { locale, inviteId } = extraParams;
|
|
38
|
+
checkWalletVersion({ abtwallet, locale });
|
|
39
|
+
const nodeInfo = await request.getNodeInfo();
|
|
40
|
+
const teamDid = request.headers['x-blocklet-did'];
|
|
41
|
+
|
|
42
|
+
return createInvitationRequest({
|
|
43
|
+
node,
|
|
44
|
+
nodeInfo,
|
|
45
|
+
teamDid,
|
|
46
|
+
inviteId,
|
|
47
|
+
locale,
|
|
48
|
+
});
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
onAuth: async ({ claims, userDid, userPk, token, storage, extraParams, req, baseUrl }) => {
|
|
53
|
+
const { locale, inviteId } = extraParams;
|
|
54
|
+
const nodeInfo = await req.getNodeInfo();
|
|
55
|
+
const teamDid = req.headers['x-blocklet-did'];
|
|
56
|
+
const statusEndpointBaseUrl = joinUrl(baseUrl, opts.prefix);
|
|
57
|
+
const endpoint = baseUrl;
|
|
58
|
+
|
|
59
|
+
const { passport, response, role } = await handleInvitationResponse({
|
|
60
|
+
node,
|
|
61
|
+
nodeInfo,
|
|
62
|
+
teamDid,
|
|
63
|
+
userDid,
|
|
64
|
+
userPk,
|
|
65
|
+
inviteId,
|
|
66
|
+
locale,
|
|
67
|
+
claims,
|
|
68
|
+
statusEndpointBaseUrl,
|
|
69
|
+
endpoint,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Generate new session token that client can save to localStorage
|
|
73
|
+
const loginToken = await login(userDid, { passport, role });
|
|
74
|
+
await storage.update(token, { did: userDid, loginToken });
|
|
75
|
+
logger.info('login.success', { userDid });
|
|
76
|
+
|
|
77
|
+
return response;
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|