@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 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
- /** @type {import('moleculer').ServiceSchema} */
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.verifyToken', { token });
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 (method === 'Bearer') {
119
- const payload = await ctx.call('auth.jwt.verifyToken', { token });
120
- if (payload) {
121
- ctx.meta.tokenPayload = payload;
122
- ctx.meta.webId = payload.webId;
123
- return Promise.resolve(payload);
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
- req.$ctx.meta.webId = 'anon';
135
- req.$ctx.meta.authorization = { capability };
136
- if (capability) {
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.generateToken', {
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
  },
@@ -60,7 +60,7 @@ const AuthSSOMixin = {
60
60
  );
61
61
  }
62
62
 
63
- const token = await ctx.call('auth.jwt.generateToken', { payload: { webId } });
63
+ const token = await ctx.call('auth.jwt.generateServerSignedToken', { payload: { webId } });
64
64
 
65
65
  return { token, newUser };
66
66
  }
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@semapps/auth",
3
- "version": "1.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.1",
9
- "@semapps/middlewares": "1.1.1",
10
- "@semapps/mime-types": "1.1.1",
11
- "@semapps/triplestore": "1.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": "5782cd74389aea502368e292be5b827ddf3d123d"
33
+ "gitHead": "cc54d6fb548a7c34d3007d159b31145a5f509898"
34
34
  }
@@ -186,7 +186,8 @@ module.exports = {
186
186
  return account?.username;
187
187
  },
188
188
  async findSettingsByWebId(ctx) {
189
- const webId = ctx.params.webId || ctx.meta.webId;
189
+ const webId = ctx.meta.webId;
190
+
190
191
  const account = await ctx.call('auth.account.findByWebId', { webId });
191
192
 
192
193
  return {
@@ -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.generateToken', { payload: { webId } });
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.generateToken', { payload: { webId: accountData.webId } });
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 generateToken(ctx) {
73
+ async generateServerSignedToken(ctx) {
67
74
  const { payload } = ctx.params;
68
75
  return jwt.sign(payload, this.privateKey, { algorithm: 'RS256' });
69
76
  },
70
- async verifyToken(ctx) {
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;
@@ -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;