@bitblit/ratchet-warden-server 5.1.118 → 5.1.122-alpha

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.
Files changed (36) hide show
  1. package/lib/server/provider/warden-default-user-decoration-provider.js +3 -1
  2. package/lib/server/provider/warden-default-user-decoration-provider.js.map +1 -1
  3. package/lib/server/provider/warden-dynamo-storage-provider-options.d.ts +7 -0
  4. package/lib/server/provider/warden-dynamo-storage-provider-options.js +7 -0
  5. package/lib/server/provider/warden-dynamo-storage-provider-options.js.map +1 -0
  6. package/lib/server/provider/warden-dynamo-storage-provider.d.ts +50 -0
  7. package/lib/server/provider/warden-dynamo-storage-provider.js +214 -0
  8. package/lib/server/provider/warden-dynamo-storage-provider.js.map +1 -0
  9. package/lib/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.d.ts +1 -1
  10. package/lib/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.js +5 -2
  11. package/lib/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.js.map +1 -1
  12. package/lib/server/provider/warden-s3-single-file-storage-provider.d.ts +1 -0
  13. package/lib/server/provider/warden-s3-single-file-storage-provider.js +8 -0
  14. package/lib/server/provider/warden-s3-single-file-storage-provider.js.map +1 -1
  15. package/lib/server/provider/warden-single-use-code-provider.d.ts +1 -1
  16. package/lib/server/provider/warden-storage-provider.d.ts +1 -0
  17. package/lib/server/provider/warden-third-party-authentication-provider.d.ts +7 -0
  18. package/lib/server/provider/warden-third-party-authentication-provider.js +2 -0
  19. package/lib/server/provider/warden-third-party-authentication-provider.js.map +1 -0
  20. package/lib/server/provider/warden-twilio-verify-single-use-code-provider.d.ts +1 -1
  21. package/lib/server/provider/warden-twilio-verify-single-use-code-provider.js +1 -1
  22. package/lib/server/provider/warden-twilio-verify-single-use-code-provider.js.map +1 -1
  23. package/lib/server/warden-authorizer.d.ts +35 -0
  24. package/lib/server/warden-authorizer.js +88 -0
  25. package/lib/server/warden-authorizer.js.map +1 -0
  26. package/lib/server/warden-entry-builder.d.ts +13 -0
  27. package/lib/server/warden-entry-builder.js +42 -0
  28. package/lib/server/warden-entry-builder.js.map +1 -0
  29. package/lib/server/warden-service-options.d.ts +2 -0
  30. package/lib/server/warden-service.d.ts +21 -13
  31. package/lib/server/warden-service.js +239 -140
  32. package/lib/server/warden-service.js.map +1 -1
  33. package/lib/server/warden-web-authn-export-token.d.ts +5 -0
  34. package/lib/server/warden-web-authn-export-token.js +2 -0
  35. package/lib/server/warden-web-authn-export-token.js.map +1 -0
  36. package/package.json +16 -16
@@ -1,30 +1,38 @@
1
- import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
2
- import { WardenUtils } from '@bitblit/ratchet-warden-common/common/util/warden-utils';
3
- import { WardenStoreRegistrationResponseType } from '@bitblit/ratchet-warden-common/common/model/warden-store-registration-response-type';
4
- import { RequireRatchet } from '@bitblit/ratchet-common/lang/require-ratchet';
5
- import { Logger } from '@bitblit/ratchet-common/logger/logger';
6
- import { ErrorRatchet } from '@bitblit/ratchet-common/lang/error-ratchet';
7
- import { StringRatchet } from '@bitblit/ratchet-common/lang/string-ratchet';
8
- import { Base64Ratchet } from '@bitblit/ratchet-common/lang/base64-ratchet';
9
- import { WardenDefaultUserDecorationProvider } from './provider/warden-default-user-decoration-provider.js';
10
- import { WardenNoOpEventProcessingProvider } from './provider/warden-no-op-event-processing-provider.js';
11
- import { WardenDefaultSendMagicLinkCommandValidator } from './provider/warden-default-send-magic-link-command-validator.js';
1
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse } from "@simplewebauthn/server";
2
+ import { WardenUtils } from "@bitblit/ratchet-warden-common/common/util/warden-utils";
3
+ import { WardenStoreRegistrationResponseType } from "@bitblit/ratchet-warden-common/common/model/warden-store-registration-response-type";
4
+ import { RequireRatchet } from "@bitblit/ratchet-common/lang/require-ratchet";
5
+ import { Logger } from "@bitblit/ratchet-common/logger/logger";
6
+ import { ErrorRatchet } from "@bitblit/ratchet-common/lang/error-ratchet";
7
+ import { StringRatchet } from "@bitblit/ratchet-common/lang/string-ratchet";
8
+ import { Base64Ratchet } from "@bitblit/ratchet-common/lang/base64-ratchet";
9
+ import { WardenDefaultUserDecorationProvider } from "./provider/warden-default-user-decoration-provider.js";
10
+ import { WardenNoOpEventProcessingProvider } from "./provider/warden-no-op-event-processing-provider.js";
11
+ import { WardenDefaultSendMagicLinkCommandValidator } from "./provider/warden-default-send-magic-link-command-validator.js";
12
+ import { WardenLoginRequestType } from "@bitblit/ratchet-warden-common/common/model/warden-login-request-type";
13
+ import { WardenEntryBuilder } from "./warden-entry-builder.js";
14
+ import { WardenAuthorizer } from "./warden-authorizer.js";
12
15
  export class WardenService {
13
16
  inOptions;
14
17
  opts;
18
+ cacheAuthorizer;
15
19
  constructor(inOptions) {
16
20
  this.inOptions = inOptions;
17
- RequireRatchet.notNullOrUndefined(inOptions, 'options');
18
- RequireRatchet.notNullOrUndefined(inOptions.relyingPartyName, 'options.relyingPartyName');
19
- RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.allowedOrigins, 'options.allowedOrigins');
20
- RequireRatchet.notNullOrUndefined(inOptions.storageProvider, 'options.storageProvider');
21
- RequireRatchet.notNullOrUndefined(inOptions.jwtRatchet, 'options.jwtRatchet');
22
- RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.singleUseCodeProviders, 'options.singleUseCodeProviders');
21
+ RequireRatchet.notNullOrUndefined(inOptions, "options");
22
+ RequireRatchet.notNullOrUndefined(inOptions.relyingPartyName, "options.relyingPartyName");
23
+ RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.allowedOrigins, "options.allowedOrigins");
24
+ RequireRatchet.notNullOrUndefined(inOptions.storageProvider, "options.storageProvider");
25
+ RequireRatchet.notNullOrUndefined(inOptions.jwtRatchet, "options.jwtRatchet");
26
+ RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.singleUseCodeProviders, "options.singleUseCodeProviders");
23
27
  this.opts = Object.assign({
24
28
  userTokenDataProvider: new WardenDefaultUserDecorationProvider(),
25
29
  eventProcessor: new WardenNoOpEventProcessingProvider(),
26
- sendMagicLinkCommandValidator: new WardenDefaultSendMagicLinkCommandValidator(),
30
+ sendMagicLinkCommandValidator: new WardenDefaultSendMagicLinkCommandValidator()
27
31
  }, inOptions);
32
+ this.cacheAuthorizer = new WardenAuthorizer(inOptions);
33
+ }
34
+ get authorizer() {
35
+ return this.cacheAuthorizer;
28
36
  }
29
37
  get options() {
30
38
  return Object.assign({}, this.opts);
@@ -41,7 +49,7 @@ export class WardenService {
41
49
  const cmd = JSON.parse(cmdString);
42
50
  const resp = await this.processCommandToResponse(cmd, origin, loggedInUserId);
43
51
  if (resp === null) {
44
- Logger.warn('Response was null for %s %s %s', cmdString, origin, loggedInUserId);
52
+ Logger.warn("Response was null for %s %s %s", cmdString, origin, loggedInUserId);
45
53
  }
46
54
  else {
47
55
  rval = JSON.stringify(resp);
@@ -49,7 +57,7 @@ export class WardenService {
49
57
  }
50
58
  catch (err) {
51
59
  const errString = ErrorRatchet.safeStringifyErr(err);
52
- Logger.error('Failed %s : %j', errString, cmdString, err);
60
+ Logger.error("Failed %s : %j", errString, cmdString, err);
53
61
  rval = JSON.stringify({ error: errString });
54
62
  }
55
63
  return rval;
@@ -57,25 +65,26 @@ export class WardenService {
57
65
  async processCommandToResponse(cmd, origin, loggedInUserId) {
58
66
  let rval = null;
59
67
  if (cmd) {
60
- Logger.info('Processing command : UserID: %s Origin: %s Command: %j', loggedInUserId, origin, cmd);
68
+ Logger.info("Processing command : UserID: %s Origin: %s Command: %j", loggedInUserId, origin, cmd);
61
69
  if (cmd.sendExpiringValidationToken) {
62
- rval = { sendExpiringValidationToken: await this.sendExpiringValidationToken(cmd.sendExpiringValidationToken) };
70
+ rval = { sendExpiringValidationToken: await this.sendExpiringValidationToken(cmd.sendExpiringValidationToken, origin) };
63
71
  }
64
72
  else if (cmd.generateWebAuthnAuthenticationChallengeForUserId) {
65
73
  const tmp = await this.generateWebAuthnAuthenticationChallengeForUserId(cmd.generateWebAuthnAuthenticationChallengeForUserId, origin);
66
74
  rval = { generateWebAuthnAuthenticationChallengeForUserId: { dataAsJson: JSON.stringify(tmp) } };
67
75
  }
68
76
  else if (cmd.createAccount) {
77
+ const newEntry = await this.createAccount(cmd.createAccount.contact, origin, cmd.createAccount.sendCode, cmd.createAccount.label, cmd.createAccount.tags);
69
78
  rval = {
70
- createAccount: await this.createAccount(cmd.createAccount.contact, cmd.createAccount.sendCode, cmd.createAccount.label, cmd.createAccount.tags),
79
+ createAccount: newEntry.userId
71
80
  };
72
81
  }
73
82
  else if (cmd.sendMagicLink) {
74
83
  if (cmd?.sendMagicLink?.contactLookup && cmd?.sendMagicLink?.contact) {
75
- throw ErrorRatchet.fErr('You may not specify both contact and contactLookup');
84
+ throw ErrorRatchet.fErr("You may not specify both contact and contactLookup");
76
85
  }
77
86
  if (!cmd?.sendMagicLink?.contactLookup && !cmd?.sendMagicLink?.contact) {
78
- throw ErrorRatchet.fErr('You must not specify either contact and contactLookup');
87
+ throw ErrorRatchet.fErr("You must not specify either contact and contactLookup");
79
88
  }
80
89
  if (cmd.sendMagicLink.contactLookup) {
81
90
  const entry = await this.findEntryById(cmd.sendMagicLink.contactLookup.userId);
@@ -90,7 +99,7 @@ export class WardenService {
90
99
  cmd.sendMagicLink.contactLookup = null;
91
100
  }
92
101
  if (!cmd.sendMagicLink.contact) {
93
- throw ErrorRatchet.fErr('Could not find contract entry either directly or by lookup');
102
+ throw ErrorRatchet.fErr("Could not find contract entry either directly or by lookup");
94
103
  }
95
104
  const loggedInUser = StringRatchet.trimToNull(loggedInUserId)
96
105
  ? await this.opts.storageProvider.findEntryById(loggedInUserId)
@@ -98,19 +107,19 @@ export class WardenService {
98
107
  await this.opts.sendMagicLinkCommandValidator.allowMagicLinkCommand(cmd.sendMagicLink, origin, loggedInUser);
99
108
  const ttlSeconds = cmd?.sendMagicLink?.ttlSeconds || 300;
100
109
  rval = {
101
- sendMagicLink: await this.sendMagicLink(cmd.sendMagicLink.contact, cmd.sendMagicLink.overrideDestinationContact, this.opts.relyingPartyName, cmd.sendMagicLink.landingUrl, cmd.sendMagicLink.meta, ttlSeconds, cmd.sendMagicLink.customTemplate),
110
+ sendMagicLink: await this.sendMagicLink(cmd.sendMagicLink.contact, cmd.sendMagicLink.overrideDestinationContact, this.opts.relyingPartyName, cmd.sendMagicLink.landingUrl, cmd.sendMagicLink.meta, ttlSeconds, cmd.sendMagicLink.customTemplate)
102
111
  };
103
112
  }
104
113
  else if (cmd.generateWebAuthnRegistrationChallengeForLoggedInUser) {
105
114
  if (!StringRatchet.trimToNull(loggedInUserId)) {
106
- ErrorRatchet.throwFormattedErr('This requires a logged in user');
115
+ ErrorRatchet.throwFormattedErr("This requires a logged in user");
107
116
  }
108
117
  const tmp = await this.generateWebAuthnRegistrationChallengeForLoggedInUser(loggedInUserId, origin);
109
118
  rval = { generateWebAuthnRegistrationChallengeForLoggedInUser: { dataAsJson: JSON.stringify(tmp) } };
110
119
  }
111
120
  else if (cmd.addContactToLoggedInUser) {
112
121
  if (!WardenUtils.validContact(cmd.addContactToLoggedInUser)) {
113
- ErrorRatchet.throwFormattedErr('Cannot add, invalid contact %j', cmd.addContactToLoggedInUser);
122
+ ErrorRatchet.throwFormattedErr("Cannot add, invalid contact %j", cmd.addContactToLoggedInUser);
114
123
  }
115
124
  else {
116
125
  const out = await this.addContactMethodToUser(loggedInUserId, cmd.addContactToLoggedInUser);
@@ -119,7 +128,7 @@ export class WardenService {
119
128
  }
120
129
  else if (cmd.addWebAuthnRegistrationToLoggedInUser) {
121
130
  if (!StringRatchet.trimToNull(loggedInUserId)) {
122
- ErrorRatchet.throwFormattedErr('This requires a logged in user');
131
+ ErrorRatchet.throwFormattedErr("This requires a logged in user");
123
132
  }
124
133
  const data = JSON.parse(cmd.addWebAuthnRegistrationToLoggedInUser.webAuthn.dataAsJson);
125
134
  const out = await this.storeAuthnRegistration(loggedInUserId, origin, cmd.addWebAuthnRegistrationToLoggedInUser.applicationName, cmd.addWebAuthnRegistrationToLoggedInUser.deviceLabel, data);
@@ -130,72 +139,130 @@ export class WardenService {
130
139
  rval = { error: out.error };
131
140
  }
132
141
  else {
133
- rval = { error: 'Cannot happen - neither user nor error set' };
142
+ rval = { error: "Cannot happen - neither user nor error set" };
134
143
  }
135
144
  }
136
145
  else if (cmd.removeWebAuthnRegistration) {
137
146
  const modified = await this.removeSingleWebAuthnRegistration(cmd.removeWebAuthnRegistration.userId, cmd.removeWebAuthnRegistration.credentialId);
138
147
  rval = {
139
- removeWebAuthnRegistration: WardenUtils.stripWardenEntryToSummary(modified),
148
+ removeWebAuthnRegistration: WardenUtils.stripWardenEntryToSummary(modified)
140
149
  };
141
150
  }
142
151
  else if (cmd.removeWebAuthnRegistrationFromLoggedInUser) {
143
152
  const modified = await this.removeSingleWebAuthnRegistration(loggedInUserId, cmd.removeWebAuthnRegistrationFromLoggedInUser);
144
153
  rval = {
145
- removeWebAuthnRegistrationFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(modified),
154
+ removeWebAuthnRegistrationFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(modified)
146
155
  };
147
156
  }
148
157
  else if (cmd.removeContactFromLoggedInUser) {
149
158
  const output = await this.removeContactMethodFromUser(loggedInUserId, cmd.removeContactFromLoggedInUser);
150
159
  rval = {
151
- removeContactFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(output),
160
+ removeContactFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(output)
152
161
  };
153
162
  }
154
163
  else if (cmd.performLogin) {
155
164
  const loginData = cmd.performLogin;
156
- const loginOk = await this.processLogin(loginData, origin);
157
- Logger.info('Performing login - login auth check was : %s', loginOk);
158
- if (loginOk) {
159
- const user = StringRatchet.trimToNull(loginData.userId)
160
- ? await this.opts.storageProvider.findEntryById(loginData.userId)
161
- : await this.opts.storageProvider.findEntryByContact(loginData.contact);
165
+ const user = await this.processLogin(loginData, origin);
166
+ if (user) {
162
167
  const decoration = await this.opts.userDecorationProvider.fetchDecoration(user);
163
168
  const wardenToken = {
164
- loginData: WardenUtils.stripWardenEntryToSummary(user),
169
+ wardenData: WardenUtils.stripWardenEntryToSummary(user),
165
170
  user: decoration.userTokenData,
166
- roles: WardenUtils.teamRolesToRoles(decoration.userTeamRoles),
167
- proxy: null,
171
+ proxy: decoration.proxyUserTokenData,
172
+ globalRoleIds: decoration.globalRoleIds,
173
+ teamRoleMappings: decoration.teamRoleMappings
168
174
  };
169
175
  const jwtToken = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
170
176
  const output = {
171
177
  request: loginData,
172
178
  userId: user.userId,
173
- jwtToken: jwtToken,
179
+ jwtToken: jwtToken
174
180
  };
175
181
  rval = { performLogin: output };
176
182
  }
177
183
  else {
178
- rval = { error: 'Login failed' };
184
+ rval = { error: "Login failed" };
179
185
  }
180
186
  }
181
187
  else if (cmd.refreshJwtToken) {
182
188
  const parsed = await this.opts.jwtRatchet.decodeToken(cmd.refreshJwtToken, 1);
183
- const user = await this.opts.storageProvider.findEntryById(parsed.loginData.userId);
189
+ const user = await this.opts.storageProvider.findEntryById(parsed.wardenData.userId);
184
190
  const decoration = await this.opts.userDecorationProvider.fetchDecoration(user);
185
191
  const wardenToken = {
186
- loginData: WardenUtils.stripWardenEntryToSummary(user),
192
+ wardenData: WardenUtils.stripWardenEntryToSummary(user),
187
193
  user: decoration.userTokenData,
188
- roles: WardenUtils.teamRolesToRoles(decoration.userTeamRoles),
189
- proxy: null,
194
+ proxy: decoration.proxyUserTokenData,
195
+ globalRoleIds: decoration.globalRoleIds,
196
+ teamRoleMappings: decoration.teamRoleMappings
190
197
  };
191
198
  const newToken = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
192
199
  rval = {
193
- refreshJwtToken: newToken,
200
+ refreshJwtToken: newToken
201
+ };
202
+ }
203
+ else if (cmd.exportWebAuthnRegistrationEntryForLoggedInUser) {
204
+ rval = {
205
+ exportWebAuthnRegistrationEntryForLoggedInUser: await this.exportWebAuthnRegistrationEntry(cmd.exportWebAuthnRegistrationEntryForLoggedInUser, loggedInUserId)
206
+ };
207
+ }
208
+ else if (cmd.importWebAuthnRegistrationEntryForLoggedInUser) {
209
+ rval = {
210
+ importWebAuthnRegistrationEntryForLoggedInUser: await this.importWebAuthnRegistrationEntry(cmd.importWebAuthnRegistrationEntryForLoggedInUser, loggedInUserId)
211
+ };
212
+ }
213
+ }
214
+ else {
215
+ rval = { error: "No command sent" };
216
+ }
217
+ return rval;
218
+ }
219
+ async exportWebAuthnRegistrationEntry(origin, userId) {
220
+ const ent = await this.opts.storageProvider.findEntryById(userId);
221
+ let rval = null;
222
+ if (ent) {
223
+ const webAuth = ent.webAuthnAuthenticators.find(w => w.origin === origin);
224
+ if (webAuth) {
225
+ const oUrl = new URL(origin);
226
+ const rpId = oUrl.hostname;
227
+ Logger.debug('Finding challenge for origin %s / RPID %s', origin, rpId);
228
+ const expectedChallenge = await this.opts.storageProvider.fetchCurrentUserChallenge(userId, rpId);
229
+ const token = {
230
+ entry: webAuth,
231
+ challenge: expectedChallenge
194
232
  };
233
+ const s1 = JSON.stringify(token);
234
+ rval = Base64Ratchet.encodeStringToBase64String(s1);
235
+ }
236
+ else {
237
+ Logger.warn("Could not export webauthn - no such origin");
238
+ }
239
+ }
240
+ else {
241
+ Logger.warn("Could not export webauthn - no such user id");
242
+ }
243
+ return rval;
244
+ }
245
+ async importWebAuthnRegistrationEntry(token, userId) {
246
+ const ent = await this.opts.storageProvider.findEntryById(userId);
247
+ let rval = false;
248
+ if (ent) {
249
+ const s1 = Base64Ratchet.base64StringToString(token);
250
+ const newEntry = JSON.parse(s1);
251
+ const old = ent.webAuthnAuthenticators.find(w => w.origin === newEntry.entry.origin);
252
+ if (old) {
253
+ Logger.warn("Removing existing entry %j", old);
254
+ ent.webAuthnAuthenticators = ent.webAuthnAuthenticators.filter(w => w.origin !== newEntry.entry.origin);
255
+ }
256
+ ent.webAuthnAuthenticators.push(newEntry.entry);
257
+ await this.opts.storageProvider.saveEntry(ent);
258
+ if (newEntry.challenge) {
259
+ await this.opts.storageProvider.updateUserChallenge(userId, newEntry.entry.origin, newEntry.challenge);
195
260
  }
261
+ Logger.info("Wrote ok");
262
+ rval = true;
196
263
  }
197
264
  else {
198
- rval = { error: 'No command sent' };
265
+ Logger.warn("Could not export webauthn - no such user id");
199
266
  }
200
267
  return rval;
201
268
  }
@@ -213,74 +280,81 @@ export class WardenService {
213
280
  singleUseCodeProvider(contact, requireMagicLinkSupport, returnNullIfNoProviders) {
214
281
  const rval = this.opts.singleUseCodeProviders.find((s) => s.handlesContactType(contact.type) && (!requireMagicLinkSupport || s.createCodeAndSendMagicLink));
215
282
  if (!rval && !returnNullIfNoProviders) {
216
- throw ErrorRatchet.fErr('Cannot find a single use code provider for contact type : %s', contact.type);
283
+ throw ErrorRatchet.fErr("Cannot find a single use code provider for contact type : %s", contact.type);
217
284
  }
218
285
  return rval;
219
286
  }
220
287
  async sendMagicLink(contact, overrideDestinationContact, relyingPartyName, landingUrl, metaIn, ttlSeconds, customTemplate) {
221
288
  let rval = false;
222
- RequireRatchet.notNullOrUndefined(contact, 'contact');
223
- RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(landingUrl, 'landingUrl');
224
- RequireRatchet.true(this.urlIsOnAllowedOrigin(landingUrl), 'landingUrl is not on an allowed origin for redirect');
289
+ RequireRatchet.notNullOrUndefined(contact, "contact");
290
+ RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(landingUrl, "landingUrl");
291
+ RequireRatchet.true(this.urlIsOnAllowedOrigin(landingUrl), "landingUrl is not on an allowed origin for redirect");
225
292
  if (contact?.type && StringRatchet.trimToNull(contact?.value)) {
226
293
  const prov = this.singleUseCodeProvider(contact, true);
227
294
  rval = await prov.createCodeAndSendMagicLink(contact, relyingPartyName, landingUrl, metaIn, ttlSeconds, overrideDestinationContact, customTemplate);
228
295
  }
229
296
  else {
230
- ErrorRatchet.throwFormattedErr('Cannot send - invalid contact %j', contact);
297
+ ErrorRatchet.throwFormattedErr("Cannot send - invalid contact %j", contact);
231
298
  }
232
299
  return rval;
233
300
  }
234
- async createAccount(contact, sendCode, label, tags) {
301
+ async createAccountByThirdParty(thirdParty, origin, inLabel) {
302
+ let rval = null;
303
+ RequireRatchet.notNullOrUndefined(thirdParty, "thirdParty");
304
+ RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(thirdParty.thirdParty, "thirdParty");
305
+ RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(thirdParty.thirdPartyId, "thirdPartyId");
306
+ const old = await this.opts.storageProvider.findEntryByThirdPartyId(thirdParty.thirdParty, thirdParty.thirdPartyId);
307
+ if (old) {
308
+ ErrorRatchet.throwFormattedErr("Cannot create - account already exists for %j", thirdParty);
309
+ }
310
+ const label = inLabel || thirdParty.thirdParty + " " + thirdParty.thirdPartyId;
311
+ const newUser = new WardenEntryBuilder(label).withThirdPartyAuthentication([thirdParty]).entry;
312
+ rval = await this.opts.storageProvider.saveEntry(newUser);
313
+ return rval;
314
+ }
315
+ async createAccount(contact, origin, sendCode, label, tags) {
235
316
  let rval = null;
236
317
  if (WardenUtils.validContact(contact)) {
237
318
  const old = await this.opts.storageProvider.findEntryByContact(contact);
238
319
  if (old) {
239
- ErrorRatchet.throwFormattedErr('Cannot create - account already exists for %j', contact);
240
- }
241
- const guid = StringRatchet.createType4Guid();
242
- const now = Date.now();
243
- const newUser = {
244
- userId: guid,
245
- userLabel: label || 'User ' + guid,
246
- contactMethods: [contact],
247
- tags: tags || [],
248
- webAuthnAuthenticators: [],
249
- createdEpochMS: now,
250
- updatedEpochMS: now,
251
- };
252
- const next = await this.opts.storageProvider.saveEntry(newUser);
253
- rval = next.userId;
254
- if (this?.opts?.eventProcessor) {
255
- await this.opts.eventProcessor.userCreated(next);
320
+ ErrorRatchet.throwFormattedErr("Cannot create - account already exists for %j", contact);
256
321
  }
322
+ const newUser = new WardenEntryBuilder(label || contact.value).withContacts([contact]).withTags(tags).entry;
323
+ rval = await this.opts.storageProvider.saveEntry(newUser);
257
324
  if (sendCode) {
258
- Logger.info('New user %j created and send requested - sending', next);
259
- await this.sendExpiringValidationToken(contact);
325
+ Logger.info("New user %j created and send requested - sending", rval);
326
+ await this.sendExpiringValidationToken(contact, origin);
260
327
  }
261
328
  }
262
329
  else {
263
- ErrorRatchet.throwFormattedErr('Cannot create - invalid contact (missing or invalid fields)');
330
+ ErrorRatchet.throwFormattedErr("Cannot create - invalid contact (missing or invalid fields)");
264
331
  }
265
332
  return rval;
266
333
  }
334
+ async saveNewUser(newUser) {
335
+ const next = await this.opts.storageProvider.saveEntry(newUser);
336
+ if (this?.opts?.eventProcessor) {
337
+ await this.opts.eventProcessor.userCreated(next);
338
+ }
339
+ return next;
340
+ }
267
341
  async addContactMethodToUser(userId, contact) {
268
342
  let rval = false;
269
343
  if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
270
344
  const otherUser = await this.opts.storageProvider.findEntryByContact(contact);
271
345
  if (otherUser && otherUser.userId !== userId) {
272
- ErrorRatchet.throwFormattedErr('Cannot add contact to this user, another user already has that contact');
346
+ ErrorRatchet.throwFormattedErr("Cannot add contact to this user, another user already has that contact");
273
347
  }
274
348
  const curUser = await this.opts.storageProvider.findEntryById(userId);
275
349
  if (!curUser) {
276
- ErrorRatchet.throwFormattedErr('Cannot add contact to this user, user does not exist');
350
+ ErrorRatchet.throwFormattedErr("Cannot add contact to this user, user does not exist");
277
351
  }
278
352
  curUser.contactMethods.push(contact);
279
353
  await this.opts.storageProvider.saveEntry(curUser);
280
354
  rval = true;
281
355
  }
282
356
  else {
283
- ErrorRatchet.throwFormattedErr('Cannot add - invalid config : %s %j', userId, contact);
357
+ ErrorRatchet.throwFormattedErr("Cannot add - invalid config : %s %j", userId, contact);
284
358
  }
285
359
  return rval;
286
360
  }
@@ -289,32 +363,32 @@ export class WardenService {
289
363
  if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
290
364
  const curUser = await this.opts.storageProvider.findEntryById(userId);
291
365
  if (!curUser) {
292
- ErrorRatchet.throwFormattedErr('Cannot remove contact from this user, user does not exist');
366
+ ErrorRatchet.throwFormattedErr("Cannot remove contact from this user, user does not exist");
293
367
  }
294
368
  curUser.contactMethods = (curUser.contactMethods || []).filter((s) => s.type !== contact.type || s.value !== contact.value);
295
369
  if (curUser.contactMethods.length === 0) {
296
- ErrorRatchet.throwFormattedErr('Cannot remove the last contact method from a user');
370
+ ErrorRatchet.throwFormattedErr("Cannot remove the last contact method from a user");
297
371
  }
298
372
  await this.opts.storageProvider.saveEntry(curUser);
299
373
  rval = await this.opts.storageProvider.findEntryById(userId);
300
374
  }
301
375
  else {
302
- ErrorRatchet.throwFormattedErr('Cannot add - invalid config : %s %j', userId, contact);
376
+ ErrorRatchet.throwFormattedErr("Cannot add - invalid config : %s %j", userId, contact);
303
377
  }
304
378
  return rval;
305
379
  }
306
380
  async generateWebAuthnRegistrationChallengeForLoggedInUser(userId, origin) {
307
381
  if (!origin || !this.opts.allowedOrigins.includes(origin)) {
308
- throw new Error('Invalid origin : ' + origin);
382
+ throw new Error("Invalid origin : " + origin);
309
383
  }
310
384
  const asUrl = new URL(origin);
311
385
  const rpID = asUrl.hostname;
312
386
  const entry = await this.opts.storageProvider.findEntryById(userId);
313
387
  if (!entry) {
314
- throw ErrorRatchet.fErr('Cannot generateWebAuthnRegistrationChallengeForLoggedInUser - no user %s / %s', userId, origin);
388
+ throw ErrorRatchet.fErr("Cannot generateWebAuthnRegistrationChallengeForLoggedInUser - no user %s / %s", userId, origin);
315
389
  }
316
390
  if (!entry?.webAuthnAuthenticators?.length) {
317
- Logger.info('Entry has no webAuthnAuthenticators');
391
+ Logger.info("Entry has no webAuthnAuthenticators");
318
392
  entry.webAuthnAuthenticators = [];
319
393
  }
320
394
  const options = await generateRegistrationOptions({
@@ -322,67 +396,67 @@ export class WardenService {
322
396
  rpID: rpID,
323
397
  userID: StringRatchet.stringToUint8Array(entry.userId),
324
398
  userName: entry.userLabel,
325
- attestationType: 'none',
399
+ attestationType: "none",
326
400
  excludeCredentials: entry.webAuthnAuthenticators.map((authenticator) => ({
327
401
  id: authenticator.credentialPublicKeyBase64,
328
- transports: authenticator.transports,
329
- })),
402
+ transports: authenticator.transports
403
+ }))
330
404
  });
331
405
  await this.opts.storageProvider.updateUserChallenge(entry.userId, rpID, options.challenge);
332
406
  return options;
333
407
  }
334
408
  async storeAuthnRegistration(userId, origin, applicationName, deviceLabel, data) {
335
- Logger.info('Store authn data : %j', data);
409
+ Logger.info("Store authn data : %j", data);
336
410
  let rval = null;
337
411
  try {
338
412
  if (!origin || !this.opts.allowedOrigins.includes(origin)) {
339
- throw new Error('Invalid origin : ' + origin);
413
+ throw new Error("Invalid origin : " + origin);
340
414
  }
341
415
  const asUrl = new URL(origin);
342
416
  const rpID = asUrl.hostname;
343
417
  const user = await this.opts.storageProvider.findEntryById(userId);
344
418
  if (!user) {
345
- throw ErrorRatchet.fErr('Cannot storeAuthnRegistration - no user %s / %s', userId, origin);
419
+ throw ErrorRatchet.fErr("Cannot storeAuthnRegistration - no user %s / %s", userId, origin);
346
420
  }
347
421
  const expectedChallenge = await this.opts.storageProvider.fetchCurrentUserChallenge(user.userId, rpID);
348
422
  const vrOpts = {
349
423
  response: data,
350
424
  expectedChallenge: expectedChallenge,
351
425
  expectedOrigin: origin,
352
- expectedRPID: rpID,
426
+ expectedRPID: rpID
353
427
  };
354
- Logger.info('Calling verifyRegistrationResponse: %j', vrOpts);
428
+ Logger.info("Calling verifyRegistrationResponse: %j", vrOpts);
355
429
  const verification = await verifyRegistrationResponse(vrOpts);
356
- Logger.info('verifyRegistrationResponse Result : %j', verification);
430
+ Logger.info("verifyRegistrationResponse Result : %j", verification);
357
431
  rval = {
358
432
  updatedEntry: null,
359
433
  registrationResponseId: data.id,
360
- result: verification.verified ? WardenStoreRegistrationResponseType.Verified : WardenStoreRegistrationResponseType.Failed,
434
+ result: verification.verified ? WardenStoreRegistrationResponseType.Verified : WardenStoreRegistrationResponseType.Failed
361
435
  };
362
436
  if (rval.result === WardenStoreRegistrationResponseType.Verified) {
363
- Logger.info('Storing registration');
437
+ Logger.info("Storing registration");
364
438
  const newAuth = {
365
439
  origin: origin,
366
- applicationName: applicationName || 'Unknown Application',
367
- deviceLabel: deviceLabel || 'Unknown Device',
440
+ applicationName: applicationName || "Unknown Application",
441
+ deviceLabel: deviceLabel || "Unknown Device",
368
442
  counter: verification.registrationInfo.credential.counter,
369
443
  credentialBackedUp: verification.registrationInfo.credentialBackedUp,
370
444
  credentialDeviceType: verification.registrationInfo.credentialDeviceType,
371
445
  credentialIdBase64: verification.registrationInfo.credential.id,
372
- credentialPublicKeyBase64: Base64Ratchet.uint8ArrayToBase64UrlString(verification.registrationInfo.credential.publicKey),
446
+ credentialPublicKeyBase64: Base64Ratchet.uint8ArrayToBase64UrlString(verification.registrationInfo.credential.publicKey)
373
447
  };
374
448
  user.webAuthnAuthenticators = (user.webAuthnAuthenticators || []).filter((wa) => wa.credentialIdBase64 !== newAuth.credentialIdBase64);
375
449
  user.webAuthnAuthenticators.push(newAuth);
376
450
  const storedUser = await this.opts.storageProvider.saveEntry(user);
377
451
  rval.updatedEntry = storedUser;
378
- Logger.info('Stored auth : %j', storedUser);
452
+ Logger.info("Stored auth : %j", storedUser);
379
453
  }
380
454
  }
381
455
  catch (err) {
382
456
  rval = {
383
457
  registrationResponseId: data.id,
384
458
  result: WardenStoreRegistrationResponseType.Error,
385
- error: ErrorRatchet.safeStringifyErr(err),
459
+ error: ErrorRatchet.safeStringifyErr(err)
386
460
  };
387
461
  }
388
462
  return rval;
@@ -395,70 +469,95 @@ export class WardenService {
395
469
  async generateWebAuthnAuthenticationChallenge(user, origin) {
396
470
  const userAuthenticators = user.webAuthnAuthenticators;
397
471
  if (!origin || !this.opts.allowedOrigins.includes(origin)) {
398
- throw new Error('Invalid origin : ' + origin);
472
+ throw new Error("Invalid origin : " + origin);
399
473
  }
400
474
  const asUrl = new URL(origin);
401
475
  const rpID = asUrl.hostname;
402
476
  const out = userAuthenticators.map((authenticator) => {
403
477
  const next = {
404
478
  id: authenticator.credentialIdBase64,
405
- transports: authenticator.transports,
479
+ transports: authenticator.transports
406
480
  };
407
481
  return next;
408
482
  });
409
483
  const opts = {
410
484
  rpID: rpID,
411
485
  allowCredentials: out,
412
- userVerification: 'preferred',
486
+ userVerification: "preferred"
413
487
  };
414
488
  const options = await generateAuthenticationOptions(opts);
415
489
  await this.opts.storageProvider.updateUserChallenge(user.userId, rpID, options.challenge);
416
490
  return options;
417
491
  }
418
- async sendExpiringValidationToken(request) {
492
+ async sendExpiringValidationToken(request, origin) {
419
493
  let rval = false;
420
494
  if (request?.type && StringRatchet.trimToNull(request?.value)) {
421
495
  const prov = this.singleUseCodeProvider(request, false);
422
- rval = await prov.createAndSendNewCode(request, this.opts.relyingPartyName);
496
+ rval = await prov.createAndSendNewCode(request, this.opts.relyingPartyName, origin);
423
497
  }
424
498
  else {
425
- ErrorRatchet.throwFormattedErr('Cannot send - invalid request %j', request);
499
+ ErrorRatchet.throwFormattedErr("Cannot send - invalid request %j", request);
426
500
  }
427
501
  return rval;
428
502
  }
429
503
  async processLogin(request, origin) {
430
- Logger.info('Processing login : %s : %j', origin, request);
431
- let rval = false;
432
- RequireRatchet.notNullOrUndefined(request, 'request');
433
- RequireRatchet.true(!!StringRatchet.trimToNull(request?.userId) || WardenUtils.validContact(request?.contact), 'Invalid contact and no userId');
434
- RequireRatchet.true(!!request?.webAuthn || !!StringRatchet.trimToNull(request?.expiringToken), 'You must provide one of webAuthn or expiringToken');
435
- RequireRatchet.true(!request?.webAuthn || !StringRatchet.trimToNull(request?.expiringToken), 'WebAuthn and ExpiringToken may not BOTH be set');
436
- let user = StringRatchet.trimToNull(request?.userId)
437
- ? await this.opts.storageProvider.findEntryById(request?.userId)
438
- : await this.opts.storageProvider.findEntryByContact(request.contact);
439
- if (!user) {
440
- Logger.info('User not found, and createUserIfMissing=%s / %j', request.createUserIfMissing, request.contact);
441
- if (request.createUserIfMissing && request.contact) {
442
- const newVal = await this.createAccount(request.contact);
443
- Logger.info('Finished create, new id is %s', newVal);
444
- user = await this.opts.storageProvider.findEntryById(newVal);
504
+ Logger.info("Processing login : %s : %j", origin, request);
505
+ let rval = null;
506
+ const requestErrors = WardenUtils.loginRequestErrors(request);
507
+ if (requestErrors.length > 0) {
508
+ throw ErrorRatchet.fErr("Invalid login request : %j", requestErrors);
509
+ }
510
+ if (request.type === WardenLoginRequestType.ThirdParty) {
511
+ const provider = (this.options.thirdPartyAuthenticationProviders ?? [])
512
+ .find(s => s.handlesThirdParty(request.thirdPartyToken.thirdParty));
513
+ if (provider) {
514
+ const auth = await provider.validateTokenAndReturnThirdPartyUserId(request.thirdPartyToken, origin);
515
+ if (auth) {
516
+ rval = await this.opts.storageProvider.findEntryByThirdPartyId(auth.thirdParty, auth.thirdPartyId);
517
+ if (!rval && request.createUserIfMissing) {
518
+ Logger.info("Found no existing user for %j, creating", auth);
519
+ let label = auth.thirdParty + " " + auth.thirdPartyId;
520
+ if (provider.extractUserLabelFromAuthentication) {
521
+ label = await provider.extractUserLabelFromAuthentication(auth);
522
+ }
523
+ rval = await this.createAccountByThirdParty(auth, origin, label);
524
+ Logger.info("Finished create, new id is %s", rval.userId);
525
+ }
526
+ }
527
+ else {
528
+ Logger.warn("Authentication failed for %j", request.thirdPartyToken, request.createUserIfMissing);
529
+ }
445
530
  }
446
- if (!user) {
447
- ErrorRatchet.throwFormattedErr('No user found for %j / %s', request?.contact, request?.userId);
531
+ else {
532
+ Logger.warn("Could not find any provider to handle third party token %j", request.thirdPartyToken);
448
533
  }
449
534
  }
450
- if (request.webAuthn) {
451
- rval = await this.loginWithWebAuthnRequest(user, origin, request.webAuthn);
452
- }
453
- else if (StringRatchet.trimToNull(request.expiringToken)) {
454
- const prov = this.singleUseCodeProvider(request.contact, false);
455
- const lookup = await prov.checkCode(request.contact.value, request.expiringToken);
456
- if (lookup) {
457
- rval = true;
535
+ else {
536
+ let user = StringRatchet.trimToNull(request?.userId)
537
+ ? await this.opts.storageProvider.findEntryById(request?.userId)
538
+ : await this.opts.storageProvider.findEntryByContact(request.contact);
539
+ if (!user) {
540
+ Logger.info("User not found, and createUserIfMissing=%s / %j", request.createUserIfMissing, request.contact);
541
+ if (request.createUserIfMissing && request.contact) {
542
+ user = await this.createAccount(request.contact, origin);
543
+ Logger.info("Finished create, new id is %s", user.userId);
544
+ }
545
+ if (!user) {
546
+ ErrorRatchet.throwFormattedErr("No user found for %j / %s", request?.contact, request?.userId);
547
+ }
458
548
  }
459
- else {
460
- ErrorRatchet.throwFormattedErr('Cannot login - token is invalid for this user');
549
+ let loginSuccess = false;
550
+ if (request.webAuthn) {
551
+ loginSuccess = await this.loginWithWebAuthnRequest(user, origin, request.webAuthn);
461
552
  }
553
+ else if (StringRatchet.trimToNull(request.expiringToken)) {
554
+ const prov = this.singleUseCodeProvider(request.contact, false);
555
+ loginSuccess = await prov.checkCode(request.contact.value, request.expiringToken);
556
+ if (!loginSuccess) {
557
+ ErrorRatchet.throwFormattedErr("Cannot login - token is invalid for this user");
558
+ }
559
+ }
560
+ rval = loginSuccess ? user : null;
462
561
  }
463
562
  return rval;
464
563
  }
@@ -470,7 +569,7 @@ export class WardenService {
470
569
  const auth = (user.webAuthnAuthenticators || []).find((s) => s.credentialIdBase64 === data.id);
471
570
  if (!auth) {
472
571
  const allIds = (user.webAuthnAuthenticators || []).map((s) => s.credentialIdBase64);
473
- throw ErrorRatchet.fErr('Could not find authenticator %s (%s) for user %s (avail were : %j)', data.id, data.id, user.userId, allIds);
572
+ throw ErrorRatchet.fErr("Could not find authenticator %s (%s) for user %s (avail were : %j)", data.id, data.id, user.userId, allIds);
474
573
  }
475
574
  const vrOpts = {
476
575
  response: data,
@@ -480,8 +579,8 @@ export class WardenService {
480
579
  credential: {
481
580
  counter: auth.counter,
482
581
  id: auth.credentialIdBase64,
483
- publicKey: Base64Ratchet.base64UrlStringToBytes(auth.credentialPublicKeyBase64),
484
- },
582
+ publicKey: Base64Ratchet.base64UrlStringToBytes(auth.credentialPublicKeyBase64)
583
+ }
485
584
  };
486
585
  const verification = await verifyAuthenticationResponse(vrOpts);
487
586
  if (verification.verified) {
@@ -496,7 +595,7 @@ export class WardenService {
496
595
  ent = await this.opts.storageProvider.saveEntry(ent);
497
596
  }
498
597
  else {
499
- Logger.info('Not removing - no such user as %s', userId);
598
+ Logger.info("Not removing - no such user as %s", userId);
500
599
  }
501
600
  return ent;
502
601
  }
@@ -512,7 +611,7 @@ export class WardenService {
512
611
  rval = true;
513
612
  }
514
613
  else {
515
- Logger.warn('Cannot remove non-existent user : %s', userId);
614
+ Logger.warn("Cannot remove non-existent user : %s", userId);
516
615
  }
517
616
  }
518
617
  return rval;