@semapps/auth 1.1.1 → 1.1.3
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 +103 -100
- package/mixins/auth.sso.js +1 -1
- package/package.json +6 -6
- package/services/account.js +2 -1
- package/services/auth.local.js +2 -2
- 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,
|
@@ -36,10 +86,6 @@ const AuthMixin = {
|
|
36
86
|
settings: { reservedUsernames, minPasswordLength, minUsernameLength },
|
37
87
|
adapter: new TripleStoreAdapter({ type: 'AuthAccount', dataset: accountsDataset })
|
38
88
|
});
|
39
|
-
|
40
|
-
if (podProvider) {
|
41
|
-
this.broker.createService({ mixins: [CapabilitiesService], settings: { path: this.settings.capabilitiesPath } });
|
42
|
-
}
|
43
89
|
},
|
44
90
|
async started() {
|
45
91
|
if (!this.passportId) throw new Error('this.passportId must be set in the service creation.');
|
@@ -76,33 +122,28 @@ const AuthMixin = {
|
|
76
122
|
}
|
77
123
|
|
78
124
|
if (method === 'Bearer') {
|
79
|
-
const payload = await ctx.call('auth.jwt.
|
125
|
+
const payload = await ctx.call('auth.jwt.verifyServerSignedToken', { token });
|
80
126
|
if (payload) {
|
81
127
|
ctx.meta.tokenPayload = payload;
|
82
128
|
ctx.meta.webId = payload.webId;
|
83
129
|
return Promise.resolve(payload);
|
84
130
|
}
|
131
|
+
|
132
|
+
// Check if token is a capability.
|
133
|
+
if (route.opts.authorizeWithCapability) {
|
134
|
+
return this.validateCapability(ctx, token);
|
135
|
+
}
|
136
|
+
|
85
137
|
// Invalid token
|
86
138
|
// TODO make sure token is deleted client-side
|
87
|
-
ctx.meta.webId = 'anon';
|
88
139
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
89
|
-
} else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
|
90
|
-
const capabilityUri = token || req.$params.capability;
|
91
|
-
const capability = await this.actions.getValidateCapability({
|
92
|
-
capabilityUri,
|
93
|
-
username: req.parsedUrl.match(/[^/]+/)[0]
|
94
|
-
});
|
95
|
-
|
96
|
-
ctx.meta.webId = 'anon';
|
97
|
-
req.$ctx.meta.webId = 'anon';
|
98
|
-
req.$ctx.meta.authorization = { capability };
|
99
|
-
return Promise.resolve(null);
|
100
140
|
}
|
101
141
|
|
102
142
|
// No valid auth method given.
|
103
143
|
ctx.meta.webId = 'anon';
|
104
144
|
return Promise.resolve(null);
|
105
145
|
},
|
146
|
+
|
106
147
|
// See https://moleculer.services/docs/0.13/moleculer-web.html#Authorization
|
107
148
|
async authorize(ctx) {
|
108
149
|
const { route, req, res } = ctx.params;
|
@@ -111,97 +152,31 @@ const AuthMixin = {
|
|
111
152
|
const [method, token] = req.headers.authorization && req.headers.authorization.split(' ');
|
112
153
|
|
113
154
|
if (!token) {
|
114
|
-
ctx.meta.webId = 'anon';
|
115
155
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_NO_TOKEN));
|
116
156
|
}
|
157
|
+
if (method !== 'Bearer') {
|
158
|
+
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
159
|
+
}
|
117
160
|
|
118
|
-
if (
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
} else if ((method === 'Capability' || req.$params.capability) && this.settings.podProvider) {
|
126
|
-
const capabilityUri = token || req.$params.capability;
|
127
|
-
const capability = await this.actions.getValidateCapability({
|
128
|
-
capabilityUri,
|
129
|
-
username: req.parsedUrl.match(/[^/]+/)[0]
|
130
|
-
});
|
131
|
-
|
132
|
-
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
|
+
}
|
133
168
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
return Promise.resolve(null);
|
138
|
-
}
|
169
|
+
// Check if token is a capability.
|
170
|
+
if (route.opts.authorizeWithCapability) {
|
171
|
+
return this.validateCapability(ctx, token);
|
139
172
|
}
|
140
173
|
|
141
174
|
return Promise.reject(new E.UnAuthorizedError(E.ERR_INVALID_TOKEN));
|
142
175
|
},
|
143
|
-
getValidateCapability: {
|
144
|
-
params: {
|
145
|
-
capabilityUri: {
|
146
|
-
type: 'string',
|
147
|
-
required: true
|
148
|
-
},
|
149
|
-
webId: {
|
150
|
-
type: 'string',
|
151
|
-
optional: true
|
152
|
-
},
|
153
|
-
username: {
|
154
|
-
type: 'string',
|
155
|
-
optional: true
|
156
|
-
}
|
157
|
-
},
|
158
|
-
/**
|
159
|
-
* Checks, if the provided capabilityUri is a valid URI and within the resource owner's cap container.
|
160
|
-
* @returns {Promise<object>} The stored capability object or undefined, if the capability is not valid.
|
161
|
-
*/
|
162
|
-
async handler(ctx) {
|
163
|
-
let { capabilityUri, webId, username } = ctx.params;
|
164
|
-
/** @type {string} */
|
165
|
-
const baseUrlTrailing = urlJoin(this.settings.baseUrl, '/');
|
166
|
-
webId = webId || baseUrlTrailing + username;
|
167
|
-
|
168
|
-
const podUrl = await ctx.call('solid-storage.getUrl', { webId });
|
169
|
-
|
170
|
-
// Check if capabilityUri is within the resource owner's pod
|
171
|
-
if (!webId?.startsWith(baseUrlTrailing) || !capabilityUri?.startsWith(podUrl)) {
|
172
|
-
return undefined;
|
173
|
-
}
|
174
|
-
|
175
|
-
// Check, if capUri is a valid URI.
|
176
|
-
try {
|
177
|
-
// eslint-disable-next-line no-new
|
178
|
-
new URL(capabilityUri);
|
179
|
-
} catch {
|
180
|
-
return undefined;
|
181
|
-
}
|
182
|
-
|
183
|
-
// Check if capabilityUri is within the resource owner's cap container.
|
184
|
-
const resourceCapContainerUri = await ctx.call('capabilities.getContainerUri', {
|
185
|
-
webId
|
186
|
-
});
|
187
|
-
|
188
|
-
if (
|
189
|
-
!(await ctx.call('ldp.container.includes', {
|
190
|
-
containerUri: resourceCapContainerUri,
|
191
|
-
resourceUri: capabilityUri,
|
192
|
-
webId: 'system'
|
193
|
-
}))
|
194
|
-
) {
|
195
|
-
return undefined;
|
196
|
-
}
|
197
|
-
|
198
|
-
return await ctx.call('capabilities.get', { resourceUri: capabilityUri });
|
199
|
-
}
|
200
|
-
},
|
201
176
|
|
202
177
|
async impersonate(ctx) {
|
203
178
|
const { webId } = ctx.params;
|
204
|
-
return await ctx.call('auth.jwt.
|
179
|
+
return await ctx.call('auth.jwt.generateServerSignedToken', {
|
205
180
|
payload: {
|
206
181
|
webId
|
207
182
|
}
|
@@ -209,6 +184,34 @@ const AuthMixin = {
|
|
209
184
|
}
|
210
185
|
},
|
211
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
|
+
},
|
212
215
|
getStrategy() {
|
213
216
|
throw new Error('getStrategy must be implemented by the service');
|
214
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.3",
|
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.3",
|
9
|
+
"@semapps/middlewares": "1.1.3",
|
10
|
+
"@semapps/mime-types": "1.1.3",
|
11
|
+
"@semapps/triplestore": "1.1.3",
|
12
12
|
"bcrypt": "^5.0.1",
|
13
13
|
"express-session": "^1.17.0",
|
14
14
|
"jsonwebtoken": "^8.5.1",
|
@@ -30,5 +30,5 @@
|
|
30
30
|
"engines": {
|
31
31
|
"node": ">=14"
|
32
32
|
},
|
33
|
-
"gitHead": "
|
33
|
+
"gitHead": "cc54d6fb548a7c34d3007d159b31145a5f509898"
|
34
34
|
}
|
package/services/account.js
CHANGED
@@ -186,7 +186,8 @@ module.exports = {
|
|
186
186
|
return account?.username;
|
187
187
|
},
|
188
188
|
async findSettingsByWebId(ctx) {
|
189
|
-
const webId = ctx.
|
189
|
+
const webId = ctx.meta.webId;
|
190
|
+
|
190
191
|
const account = await ctx.call('auth.account.findByWebId', { webId });
|
191
192
|
|
192
193
|
return {
|
package/services/auth.local.js
CHANGED
@@ -73,7 +73,7 @@ const AuthLocalService = {
|
|
73
73
|
|
74
74
|
ctx.emit('auth.registered', { webId, profileData, accountData });
|
75
75
|
|
76
|
-
const token = await ctx.call('auth.jwt.
|
76
|
+
const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId } });
|
77
77
|
|
78
78
|
return { token, webId, newUser: true };
|
79
79
|
} catch (e) {
|
@@ -89,7 +89,7 @@ const AuthLocalService = {
|
|
89
89
|
|
90
90
|
ctx.emit('auth.connected', { webId: accountData.webId, accountData }, { meta: { webId: null, dataset: null } });
|
91
91
|
|
92
|
-
const token = await ctx.call('auth.jwt.
|
92
|
+
const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId: accountData.webId } });
|
93
93
|
|
94
94
|
return { token, webId: accountData.webId, newUser: false };
|
95
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;
|