@semapps/auth 1.1.0 → 1.1.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/index.js +0 -1
- package/mixins/auth.js +108 -102
- package/mixins/auth.sso.js +1 -1
- package/package.json +7 -6
- package/services/account.js +68 -26
- package/services/auth.local.js +6 -7
- package/services/jwt.js +15 -2
- package/services/capabilities.js +0 -69
package/index.js
CHANGED
@@ -3,7 +3,6 @@ module.exports = {
|
|
3
3
|
AuthLocalService: require('./services/auth.local'),
|
4
4
|
AuthOIDCService: require('./services/auth.oidc'),
|
5
5
|
AuthAccountService: require('./services/account'),
|
6
|
-
AuthCapabilitiesService: require('./services/capabilities'),
|
7
6
|
AuthJWTService: require('./services/jwt'),
|
8
7
|
AuthMigrationService: require('./services/migration'),
|
9
8
|
AuthMailService: require('./services/mail')
|
package/mixins/auth.js
CHANGED
@@ -1,12 +1,62 @@
|
|
1
1
|
const passport = require('passport');
|
2
2
|
const { Errors: E } = require('moleculer-web');
|
3
3
|
const { TripleStoreAdapter } = require('@semapps/triplestore');
|
4
|
-
const urlJoin = require('url-join');
|
5
4
|
const AuthAccountService = require('../services/account');
|
6
5
|
const AuthJWTService = require('../services/jwt');
|
7
|
-
const CapabilitiesService = require('../services/capabilities');
|
8
6
|
|
9
|
-
/**
|
7
|
+
/**
|
8
|
+
* Auth Mixin that handles authentication and authorization for routes
|
9
|
+
* that requested this.
|
10
|
+
*
|
11
|
+
* The authorization and authentication actions check for a valid `authorization` header.
|
12
|
+
* If the bearer token is a server-signed JWT identifying the user, `ctx.meta.tokenPayload` and
|
13
|
+
* `ctx.meta.webId` are set. Setting either `authorization` or `authentication` suffices.
|
14
|
+
*
|
15
|
+
* # Authentication
|
16
|
+
* In the `authenticate` action, the webId is set to `anon`, if no `authorization` header is present.
|
17
|
+
*
|
18
|
+
* # Authorization
|
19
|
+
* In contrast, the `authorize` action throws an unauthorized error,
|
20
|
+
* if no `authorization` header is present.
|
21
|
+
* @see https://moleculer.services/docs/0.13/moleculer-web.html#Authentication
|
22
|
+
*
|
23
|
+
* ## Capability Authorization
|
24
|
+
* Additionally, the `authorize` action supports capability authorization based on
|
25
|
+
* Verifiable Credentials (VCs), if `opts.authorizeWithCapability` is set to `true`.
|
26
|
+
*
|
27
|
+
* **WARNING**: This does not make any assertions about the validity of the capabilities'
|
28
|
+
* content (`credentialSubject`). What *is* checked:
|
29
|
+
* - the delegation chain was correct
|
30
|
+
* - all signatures are valid
|
31
|
+
* - the `controller`s of the keys (`verificationMethod`) used in the proofs to sign
|
32
|
+
* the VC capabilities and presentation are correct. I.e. the controller resolves to
|
33
|
+
* the WebId/controller identifier document (CID) which lists the key.\
|
34
|
+
* This means that *you know who signed the presentation and capabilities* on the way.
|
35
|
+
*
|
36
|
+
* NO BUSINESS LOGIC IS CHECKED.\
|
37
|
+
* It is still necessary to verify if the request itself is valid.
|
38
|
+
* There would be no error when the `credentialSubject` says: "A is allowed to read B"
|
39
|
+
* while the statement is actually made by "C" and not by "B".\
|
40
|
+
*
|
41
|
+
* @see https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
|
42
|
+
*
|
43
|
+
* @example Configuration for a new route
|
44
|
+
* ```js
|
45
|
+
* ctx.call('api.addRoute', {
|
46
|
+
* path: path.join(basePath, '/your/route'),
|
47
|
+
* name: 'your-route-name',
|
48
|
+
* aliases: {
|
49
|
+
* 'GET /': 'your.action.here',
|
50
|
+
* },
|
51
|
+
* // Set to true, to run authorization action.
|
52
|
+
* authorization: true,
|
53
|
+
* // Set to true, to run authenticate action.
|
54
|
+
* authentication: false,
|
55
|
+
* });
|
56
|
+
* ```
|
57
|
+
*
|
58
|
+
* @type {import('moleculer').ServiceSchema}
|
59
|
+
*/
|
10
60
|
const AuthMixin = {
|
11
61
|
settings: {
|
12
62
|
baseUrl: null,
|
@@ -14,6 +64,8 @@ const AuthMixin = {
|
|
14
64
|
capabilitiesPath: undefined,
|
15
65
|
registrationAllowed: true,
|
16
66
|
reservedUsernames: [],
|
67
|
+
minPasswordLength: 1,
|
68
|
+
minUsernameLength: 1,
|
17
69
|
webIdSelection: [],
|
18
70
|
accountSelection: [],
|
19
71
|
accountsDataset: 'settings',
|
@@ -21,7 +73,8 @@ const AuthMixin = {
|
|
21
73
|
},
|
22
74
|
dependencies: ['api'],
|
23
75
|
async created() {
|
24
|
-
const { jwtPath, reservedUsernames, accountsDataset, podProvider } =
|
76
|
+
const { jwtPath, reservedUsernames, minPasswordLength, minUsernameLength, accountsDataset, podProvider } =
|
77
|
+
this.settings;
|
25
78
|
|
26
79
|
this.broker.createService({
|
27
80
|
mixins: [AuthJWTService],
|
@@ -30,13 +83,9 @@ const AuthMixin = {
|
|
30
83
|
|
31
84
|
this.broker.createService({
|
32
85
|
mixins: [AuthAccountService],
|
33
|
-
settings: { reservedUsernames },
|
86
|
+
settings: { reservedUsernames, minPasswordLength, minUsernameLength },
|
34
87
|
adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: accountsDataset })
|
35
88
|
});
|
36
|
-
|
37
|
-
if (podProvider) {
|
38
|
-
this.broker.createService({ mixins: [CapabilitiesService], settings: { path: this.settings.capabilitiesPath } });
|
39
|
-
}
|
40
89
|
},
|
41
90
|
async started() {
|
42
91
|
if (!this.passportId) throw new Error('this.passportId must be set in the service creation.');
|
@@ -73,33 +122,28 @@ const AuthMixin = {
|
|
73
122
|
}
|
74
123
|
|
75
124
|
if (method === 'Bearer') {
|
76
|
-
const payload = await ctx.call('auth.jwt.
|
125
|
+
const payload = await ctx.call('auth.jwt.verifyServerSignedToken', { token });
|
77
126
|
if (payload) {
|
78
127
|
ctx.meta.tokenPayload = payload;
|
79
128
|
ctx.meta.webId = payload.webId;
|
80
129
|
return Promise.resolve(payload);
|
81
130
|
}
|
131
|
+
|
132
|
+
// Check if token is a capability.
|
133
|
+
if (route.opts.authorizeWithCapability) {
|
134
|
+
return this.validateCapability(ctx, token);
|
135
|
+
}
|
136
|
+
|
82
137
|
// Invalid token
|
83
138
|
// TODO make sure token is deleted client-side
|
84
|
-
ctx.meta.webId = 'anon';
|
85
139
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
86
|
-
} else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
|
87
|
-
const capabilityUri = token || req.$params.capability;
|
88
|
-
const capability = await this.actions.getValidateCapability({
|
89
|
-
capabilityUri,
|
90
|
-
username: req.parsedUrl.match(/[^/]+/)[0]
|
91
|
-
});
|
92
|
-
|
93
|
-
ctx.meta.webId = 'anon';
|
94
|
-
req.$ctx.meta.webId = 'anon';
|
95
|
-
req.$ctx.meta.authorization = { capability };
|
96
|
-
return Promise.resolve(null);
|
97
140
|
}
|
98
141
|
|
99
142
|
// No valid auth method given.
|
100
143
|
ctx.meta.webId = 'anon';
|
101
144
|
return Promise.resolve(null);
|
102
145
|
},
|
146
|
+
|
103
147
|
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
|
104
148
|
async authorize(ctx) {
|
105
149
|
const { route, req, res } = ctx.params;
|
@@ -108,97 +152,31 @@ const AuthMixin = {
|
|
108
152
|
const [method, token] = req.headers.authorization && req.headers.authorization.split(' ');
|
109
153
|
|
110
154
|
if (!token) {
|
111
|
-
ctx.meta.webId = 'anon';
|
112
155
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
|
113
156
|
}
|
157
|
+
if (method !== 'Bearer') {
|
158
|
+
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
159
|
+
}
|
114
160
|
|
115
|
-
if (
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
} else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
|
123
|
-
const capabilityUri = token || req.$params.capability;
|
124
|
-
const capability = await this.actions.getValidateCapability({
|
125
|
-
capabilityUri,
|
126
|
-
username: req.parsedUrl.match(/[^/]+/)[0]
|
127
|
-
});
|
128
|
-
|
129
|
-
ctx.meta.webId = 'anon';
|
161
|
+
// Validate if the token was signed by server (registered user).
|
162
|
+
const serverSignedPayload = await ctx.call('auth.jwt.verifyServerSignedToken', { token });
|
163
|
+
if (serverSignedPayload) {
|
164
|
+
ctx.meta.tokenPayload = serverSignedPayload;
|
165
|
+
ctx.meta.webId = serverSignedPayload.webId;
|
166
|
+
return Promise.resolve(serverSignedPayload);
|
167
|
+
}
|
130
168
|
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
return Promise.resolve(null);
|
135
|
-
}
|
169
|
+
// Check if token is a capability.
|
170
|
+
if (route.opts.authorizeWithCapability) {
|
171
|
+
return this.validateCapability(ctx, token);
|
136
172
|
}
|
137
173
|
|
138
174
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
139
175
|
},
|
140
|
-
getValidateCapability: {
|
141
|
-
params: {
|
142
|
-
capabilityUri: {
|
143
|
-
type: 'string',
|
144
|
-
required: true
|
145
|
-
},
|
146
|
-
webId: {
|
147
|
-
type: 'string',
|
148
|
-
optional: true
|
149
|
-
},
|
150
|
-
username: {
|
151
|
-
type: 'string',
|
152
|
-
optional: true
|
153
|
-
}
|
154
|
-
},
|
155
|
-
/**
|
156
|
-
* Checks, if the provided capabilityUri is a valid URI and within the resource owner's cap container.
|
157
|
-
* @returns {Promise<object>} The stored capability object or undefined, if the capability is not valid.
|
158
|
-
*/
|
159
|
-
async handler(ctx) {
|
160
|
-
let { capabilityUri, webId, username } = ctx.params;
|
161
|
-
/** @type {string} */
|
162
|
-
const baseUrlTrailing = urlJoin(this.settings.baseUrl, '/');
|
163
|
-
webId = webId || baseUrlTrailing + username;
|
164
|
-
|
165
|
-
const podUrl = await ctx.call('solid-storage.getUrl', { webId });
|
166
|
-
|
167
|
-
// Check if capabilityUri is within the resource owner's pod
|
168
|
-
if (!webId?.startsWith(baseUrlTrailing) || !capabilityUri?.startsWith(podUrl)) {
|
169
|
-
return undefined;
|
170
|
-
}
|
171
|
-
|
172
|
-
// Check, if capUri is a valid URI.
|
173
|
-
try {
|
174
|
-
// eslint-disable-next-line no-new
|
175
|
-
new URL(capabilityUri);
|
176
|
-
} catch {
|
177
|
-
return undefined;
|
178
|
-
}
|
179
|
-
|
180
|
-
// Check if capabilityUri is within the resource owner's cap container.
|
181
|
-
const resourceCapContainerUri = await ctx.call('capabilities.getContainerUri', {
|
182
|
-
webId
|
183
|
-
});
|
184
|
-
|
185
|
-
if (
|
186
|
-
!(await ctx.call('ldp.container.includes', {
|
187
|
-
containerUri: resourceCapContainerUri,
|
188
|
-
resourceUri: capabilityUri,
|
189
|
-
webId: 'system'
|
190
|
-
}))
|
191
|
-
) {
|
192
|
-
return undefined;
|
193
|
-
}
|
194
|
-
|
195
|
-
return await ctx.call('capabilities.get', { resourceUri: capabilityUri });
|
196
|
-
}
|
197
|
-
},
|
198
176
|
|
199
177
|
async impersonate(ctx) {
|
200
178
|
const { webId } = ctx.params;
|
201
|
-
return await ctx.call('auth.jwt.
|
179
|
+
return await ctx.call('auth.jwt.generateServerSignedToken', {
|
202
180
|
payload: {
|
203
181
|
webId
|
204
182
|
}
|
@@ -206,6 +184,34 @@ const AuthMixin = {
|
|
206
184
|
}
|
207
185
|
},
|
208
186
|
methods: {
|
187
|
+
async validateCapability(ctx, token) {
|
188
|
+
// We accept VC Presentations to invoke capabilities here. It must be encoded as JWT.
|
189
|
+
// We do not use the VC-JOSE spec to sign and envelop presentations. Instead we go
|
190
|
+
// with embedded signatures. This way, the signature persists within the resource.
|
191
|
+
|
192
|
+
// Decode JTW to JSON.
|
193
|
+
const decodedToken = await ctx.call('auth.jwt.decodeToken', { token });
|
194
|
+
if (!decodedToken) return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
195
|
+
|
196
|
+
// Verify that decoded JSON token is a valid VC presentation.
|
197
|
+
const {
|
198
|
+
verified: isCapSignatureVerified,
|
199
|
+
presentation: verifiedPresentation,
|
200
|
+
presentationResult
|
201
|
+
} = await ctx.call('crypto.vc.verifier.verifyCapabilityPresentation', {
|
202
|
+
verifiablePresentation: decodedToken,
|
203
|
+
options: {
|
204
|
+
maxChainLength: ctx.params.route.opts.maxChainLength
|
205
|
+
}
|
206
|
+
});
|
207
|
+
if (!isCapSignatureVerified) return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
208
|
+
|
209
|
+
// VC Capability is verified.
|
210
|
+
ctx.meta.webId = presentationResult?.results?.[0]?.purposeResult?.holder || 'anon';
|
211
|
+
ctx.meta.authorization = { capabilityPresentation: verifiedPresentation };
|
212
|
+
|
213
|
+
return Promise.resolve(verifiedPresentation);
|
214
|
+
},
|
209
215
|
getStrategy() {
|
210
216
|
throw new Error('getStrategy must be implemented by the service');
|
211
217
|
},
|
package/mixins/auth.sso.js
CHANGED
package/package.json
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
{
|
2
2
|
"name": "@semapps/auth",
|
3
|
-
"version": "1.1.
|
3
|
+
"version": "1.1.2",
|
4
4
|
"description": "Authentification module for SemApps",
|
5
5
|
"license": "Apache-2.0",
|
6
6
|
"author": "Virtual Assembly",
|
7
7
|
"dependencies": {
|
8
|
-
"@semapps/ldp": "1.1.
|
9
|
-
"@semapps/middlewares": "1.1.
|
10
|
-
"@semapps/mime-types": "1.1.
|
11
|
-
"@semapps/triplestore": "1.1.
|
8
|
+
"@semapps/ldp": "1.1.2",
|
9
|
+
"@semapps/middlewares": "1.1.2",
|
10
|
+
"@semapps/mime-types": "1.1.2",
|
11
|
+
"@semapps/triplestore": "1.1.2",
|
12
12
|
"bcrypt": "^5.0.1",
|
13
13
|
"express-session": "^1.17.0",
|
14
14
|
"jsonwebtoken": "^8.5.1",
|
@@ -21,6 +21,7 @@
|
|
21
21
|
"passport-cas2": "0.0.12",
|
22
22
|
"passport-local": "^1.0.0",
|
23
23
|
"pug": "^3.0.2",
|
24
|
+
"speakingurl": "^14.0.1",
|
24
25
|
"url-join": "^4.0.1"
|
25
26
|
},
|
26
27
|
"publishConfig": {
|
@@ -29,5 +30,5 @@
|
|
29
30
|
"engines": {
|
30
31
|
"node": ">=14"
|
31
32
|
},
|
32
|
-
"gitHead": "
|
33
|
+
"gitHead": "35ec922a369a84225e0b7761ef36dbeb8c327316"
|
33
34
|
}
|
package/services/account.js
CHANGED
@@ -1,53 +1,88 @@
|
|
1
1
|
const bcrypt = require('bcrypt');
|
2
|
+
const createSlug = require('speakingurl');
|
2
3
|
const DbService = require('moleculer-db');
|
3
4
|
const { TripleStoreAdapter } = require('@semapps/triplestore');
|
4
5
|
const crypto = require('crypto');
|
5
6
|
|
7
|
+
// Taken from https://stackoverflow.com/a/9204568/7900695
|
8
|
+
const emailRegexp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
9
|
+
|
6
10
|
module.exports = {
|
7
11
|
name: 'auth.account',
|
8
12
|
mixins: [DbService],
|
9
13
|
adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: 'settings' }),
|
10
14
|
settings: {
|
11
15
|
idField: '@id',
|
12
|
-
reservedUsernames: ['relay']
|
16
|
+
reservedUsernames: ['relay'],
|
17
|
+
minPasswordLength: 1,
|
18
|
+
minUsernameLength: 1
|
13
19
|
},
|
14
20
|
dependencies: ['triplestore'],
|
15
21
|
actions: {
|
16
22
|
async create(ctx) {
|
17
23
|
let { uuid, username, password, email, webId, ...rest } = ctx.params;
|
18
|
-
const hashedPassword = password ? await this.hashPassword(password) : undefined;
|
19
24
|
|
20
|
-
|
25
|
+
// FORMAT AND VERIFY PASSWORD
|
26
|
+
|
27
|
+
if (password) {
|
28
|
+
if (password.length < this.settings.minPasswordLength) {
|
29
|
+
throw new Error('password.too-short');
|
30
|
+
}
|
21
31
|
|
22
|
-
|
23
|
-
if (emailExists) {
|
24
|
-
throw new Error('email.already.exists');
|
32
|
+
password = await this.hashPassword(password);
|
25
33
|
}
|
26
34
|
|
35
|
+
// FORMAT AND VERIFY EMAIL
|
36
|
+
|
37
|
+
if (email) {
|
38
|
+
email = email.toLowerCase();
|
39
|
+
|
40
|
+
const emailExists = await ctx.call('auth.account.emailExists', { email });
|
41
|
+
if (emailExists) {
|
42
|
+
throw new Error('email.already.exists');
|
43
|
+
}
|
44
|
+
|
45
|
+
if (!emailRegexp.test(email)) {
|
46
|
+
throw new Error('email.invalid');
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// FORMAT AND VERIFY USERNAME
|
51
|
+
|
27
52
|
if (username) {
|
28
|
-
if (!ctx.meta.isSystemCall)
|
53
|
+
if (!ctx.meta.isSystemCall) {
|
54
|
+
const { isValid, error } = await this.isValidUsername(ctx, username);
|
55
|
+
if (!isValid) throw new Error(error);
|
56
|
+
}
|
29
57
|
} else if (email) {
|
30
|
-
// If username is not provided, find
|
31
|
-
|
32
|
-
|
33
|
-
let
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
// Do nothing, the loop will continue
|
58
|
+
// If username is not provided, find one automatically from the email (without errors)
|
59
|
+
username = createSlug(email.split('@')[0].toLowerCase());
|
60
|
+
|
61
|
+
let { isValid, error } = await this.isValidUsername(ctx, username);
|
62
|
+
|
63
|
+
if (!isValid) {
|
64
|
+
if (error === 'username.invalid' || error === 'username.too-short') {
|
65
|
+
// If username generated from email is invalid, use a generic name
|
66
|
+
username = 'user';
|
40
67
|
}
|
41
|
-
|
42
|
-
|
43
|
-
|
68
|
+
|
69
|
+
// If necessary, add a number after the username
|
70
|
+
let i = 0;
|
71
|
+
do {
|
72
|
+
username = i === 0 ? username : username + i;
|
73
|
+
({ isValid } = await this.isValidUsername(ctx, username));
|
74
|
+
} while (!isValid);
|
75
|
+
}
|
76
|
+
} else {
|
77
|
+
throw new Error('You must provide at least a username or an email address');
|
78
|
+
}
|
44
79
|
|
45
80
|
return await this._create(ctx, {
|
46
81
|
...rest,
|
47
82
|
uuid,
|
48
83
|
username,
|
49
84
|
email,
|
50
|
-
hashedPassword,
|
85
|
+
hashedPassword: password,
|
51
86
|
webId
|
52
87
|
});
|
53
88
|
},
|
@@ -151,7 +186,8 @@ module.exports = {
|
|
151
186
|
return account?.username;
|
152
187
|
},
|
153
188
|
async findSettingsByWebId(ctx) {
|
154
|
-
const webId = ctx.
|
189
|
+
const webId = ctx.meta.webId;
|
190
|
+
|
155
191
|
const account = await ctx.call('auth.account.findByWebId', { webId });
|
156
192
|
|
157
193
|
return {
|
@@ -219,23 +255,29 @@ module.exports = {
|
|
219
255
|
},
|
220
256
|
methods: {
|
221
257
|
async isValidUsername(ctx, username) {
|
258
|
+
let error;
|
259
|
+
|
222
260
|
// Ensure the username has no space or special characters
|
223
261
|
if (!/^[a-z0-9\-+_.]+$/.exec(username)) {
|
224
|
-
|
262
|
+
error = 'username.invalid';
|
263
|
+
}
|
264
|
+
|
265
|
+
if (username.length < this.settings.minUsernameLength) {
|
266
|
+
error = 'username.too-short';
|
225
267
|
}
|
226
268
|
|
227
269
|
// Ensure we don't use reservedUsernames
|
228
270
|
if (this.settings.reservedUsernames.includes(username)) {
|
229
|
-
|
271
|
+
error = 'username.reserved';
|
230
272
|
}
|
231
273
|
|
232
274
|
// Ensure username doesn't already exist
|
233
275
|
const usernameExists = await ctx.call('auth.account.usernameExists', { username });
|
234
276
|
if (usernameExists) {
|
235
|
-
|
277
|
+
error = 'username.already.exists';
|
236
278
|
}
|
237
279
|
|
238
|
-
return
|
280
|
+
return { isValid: !error, error };
|
239
281
|
},
|
240
282
|
async hashPassword(password) {
|
241
283
|
return new Promise((resolve, reject) => {
|
package/services/auth.local.js
CHANGED
@@ -14,6 +14,8 @@ const AuthLocalService = {
|
|
14
14
|
jwtPath: null,
|
15
15
|
registrationAllowed: true,
|
16
16
|
reservedUsernames: [],
|
17
|
+
minPasswordLength: 1,
|
18
|
+
minUsernameLength: 1,
|
17
19
|
webIdSelection: [],
|
18
20
|
accountSelection: [],
|
19
21
|
formUrl: null,
|
@@ -47,13 +49,10 @@ const AuthLocalService = {
|
|
47
49
|
actions: {
|
48
50
|
async signup(ctx) {
|
49
51
|
const { username, email, password, ...rest } = ctx.params;
|
52
|
+
|
50
53
|
// This is going to get in our way otherwise when waiting for completions.
|
51
54
|
ctx.meta.skipObjectsWatcher = true;
|
52
55
|
|
53
|
-
if (username && username.length < 2) {
|
54
|
-
throw new MoleculerError('The username must be at least 2 characters long', 400, 'BAD_REQUEST');
|
55
|
-
}
|
56
|
-
|
57
56
|
let accountData = await ctx.call('auth.account.create', {
|
58
57
|
username,
|
59
58
|
email,
|
@@ -62,7 +61,7 @@ const AuthLocalService = {
|
|
62
61
|
});
|
63
62
|
|
64
63
|
try {
|
65
|
-
const profileData = { nick: username, email, ...rest };
|
64
|
+
const profileData = { nick: accountData.username, email: accountData.email, ...rest };
|
66
65
|
const webId = await ctx.call('webid.createWebId', this.pickWebIdData(profileData), {
|
67
66
|
meta: {
|
68
67
|
isSignup: true // Allow services to handle directly the webId creation if it is generated by the AuthService
|
@@ -74,7 +73,7 @@ const AuthLocalService = {
|
|
74
73
|
|
75
74
|
ctx.emit('auth.registered', { webId, profileData, accountData });
|
76
75
|
|
77
|
-
const token = await ctx.call('auth.jwt.
|
76
|
+
const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId } });
|
78
77
|
|
79
78
|
return { token, webId, newUser: true };
|
80
79
|
} catch (e) {
|
@@ -90,7 +89,7 @@ const AuthLocalService = {
|
|
90
89
|
|
91
90
|
ctx.emit('auth.connected', { webId: accountData.webId, accountData }, { meta: { webId: null, dataset: null } });
|
92
91
|
|
93
|
-
const token = await ctx.call('auth.jwt.
|
92
|
+
const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId: accountData.webId } });
|
94
93
|
|
95
94
|
return { token, webId: accountData.webId, newUser: false };
|
96
95
|
},
|
package/services/jwt.js
CHANGED
@@ -3,6 +3,13 @@ const path = require('path');
|
|
3
3
|
const jwt = require('jsonwebtoken');
|
4
4
|
const crypto = require('crypto');
|
5
5
|
|
6
|
+
/**
|
7
|
+
* Service that creates and validates JSON web tokens(JWT).
|
8
|
+
* Tokens are signed against this server's keys.
|
9
|
+
* This is useful for generating/validating authentication tokens.
|
10
|
+
*
|
11
|
+
* TODO: Tokens do not expire.
|
12
|
+
*/
|
6
13
|
module.exports = {
|
7
14
|
name: 'auth.jwt',
|
8
15
|
settings: {
|
@@ -63,11 +70,12 @@ module.exports = {
|
|
63
70
|
);
|
64
71
|
});
|
65
72
|
},
|
66
|
-
async
|
73
|
+
async generateServerSignedToken(ctx) {
|
67
74
|
const { payload } = ctx.params;
|
68
75
|
return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });
|
69
76
|
},
|
70
|
-
|
77
|
+
/** Verifies that the token was signed by this server. */
|
78
|
+
async verifyServerSignedToken(ctx) {
|
71
79
|
const { token } = ctx.params;
|
72
80
|
try {
|
73
81
|
return jwt.verify(token, this.publicKey, { algorithms: ['RS256'] });
|
@@ -75,6 +83,11 @@ module.exports = {
|
|
75
83
|
return false;
|
76
84
|
}
|
77
85
|
},
|
86
|
+
async generateUnsignedToken(ctx) {
|
87
|
+
const { payload } = ctx.params;
|
88
|
+
const token = jwt.sign(payload, null, { algorithm: 'none' });
|
89
|
+
return token;
|
90
|
+
},
|
78
91
|
// Warning, this does NOT verify if signature is valid
|
79
92
|
async decodeToken(ctx) {
|
80
93
|
const { token } = ctx.params;
|
package/services/capabilities.js
DELETED
@@ -1,69 +0,0 @@
|
|
1
|
-
const { ControlledContainerMixin } = require('@semapps/ldp');
|
2
|
-
const { MIME_TYPES } = require('@semapps/mime-types');
|
3
|
-
|
4
|
-
const CAPABILITIES_ROUTE = '/capabilities';
|
5
|
-
|
6
|
-
/**
|
7
|
-
* Service to host the capabilities container.
|
8
|
-
* @type {import('moleculer').ServiceSchema}
|
9
|
-
*/
|
10
|
-
const CapabilitiesService = {
|
11
|
-
name: 'capabilities',
|
12
|
-
mixins: [ControlledContainerMixin],
|
13
|
-
settings: {
|
14
|
-
path: CAPABILITIES_ROUTE,
|
15
|
-
acceptedTypes: ['acl:Authorization'],
|
16
|
-
excludeFromMirror: true,
|
17
|
-
activateTombstones: false,
|
18
|
-
permissions: {},
|
19
|
-
newResourcesPermissions: {},
|
20
|
-
typeIndex: 'private'
|
21
|
-
},
|
22
|
-
hooks: {
|
23
|
-
before: {
|
24
|
-
/**
|
25
|
-
* Bypass authorization when getting the resource.
|
26
|
-
*
|
27
|
-
* The URI itself is considered the secret. So no more
|
28
|
-
* authorization is necessary at this point.
|
29
|
-
* If we decide to support capabilities with multiple
|
30
|
-
* authorization factors, this will have to change in
|
31
|
-
* the future.
|
32
|
-
*/
|
33
|
-
get: ctx => {
|
34
|
-
ctx.params.webId = 'system';
|
35
|
-
}
|
36
|
-
}
|
37
|
-
},
|
38
|
-
actions: {
|
39
|
-
createCapability: {
|
40
|
-
params: {
|
41
|
-
webId: { type: 'string', optional: false },
|
42
|
-
accessTo: { type: 'string', optional: false },
|
43
|
-
mode: { type: 'string', optional: false }
|
44
|
-
},
|
45
|
-
async handler(ctx) {
|
46
|
-
const { accessTo, mode, webId } = ctx.params;
|
47
|
-
|
48
|
-
const capContainerUri = await this.actions.getContainerUri({ webId }, { parentCtx: ctx });
|
49
|
-
|
50
|
-
const capUri = await this.actions.post(
|
51
|
-
{
|
52
|
-
containerUri: capContainerUri,
|
53
|
-
resource: {
|
54
|
-
'@type': 'acl:Authorization',
|
55
|
-
'acl:accessTo': accessTo,
|
56
|
-
'acl:mode': mode
|
57
|
-
},
|
58
|
-
contentType: MIME_TYPES.JSON,
|
59
|
-
webId
|
60
|
-
},
|
61
|
-
{ parentCtx: ctx }
|
62
|
-
);
|
63
|
-
return capUri;
|
64
|
-
}
|
65
|
-
}
|
66
|
-
}
|
67
|
-
};
|
68
|
-
|
69
|
-
module.exports = CapabilitiesService;
|