@bitblit/ratchet-warden-server 4.0.84-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.
- package/CHANGELOG.md +19 -0
- package/License.txt +13 -0
- package/README.md +38 -0
- package/dist/cjs/build/ratchet-warden-server-info.js +18 -0
- package/dist/cjs/index.js +18 -0
- package/dist/cjs/server/provider/warden-default-user-decoration-provider.js +15 -0
- package/dist/cjs/server/provider/warden-event-processing-provider.js +2 -0
- package/dist/cjs/server/provider/warden-mailer-message-sending-provider-options.js +2 -0
- package/dist/cjs/server/provider/warden-mailer-message-sending-provider.js +36 -0
- package/dist/cjs/server/provider/warden-message-sending-provider.js +2 -0
- package/dist/cjs/server/provider/warden-no-op-event-processing-provider.js +8 -0
- package/dist/cjs/server/provider/warden-s3-single-file-storage-provider-options.js +2 -0
- package/dist/cjs/server/provider/warden-s3-single-file-storage-provider.js +90 -0
- package/dist/cjs/server/provider/warden-storage-provider.js +2 -0
- package/dist/cjs/server/provider/warden-twilio-text-message-sending-provider-options.js +2 -0
- package/dist/cjs/server/provider/warden-twilio-text-message-sending-provider.js +31 -0
- package/dist/cjs/server/provider/warden-user-decoration-provider.js +2 -0
- package/dist/cjs/server/warden-service-options.js +2 -0
- package/dist/cjs/server/warden-service.js +456 -0
- package/dist/es/build/ratchet-warden-server-info.js +14 -0
- package/dist/es/index.js +15 -0
- package/dist/es/server/provider/warden-default-user-decoration-provider.js +11 -0
- package/dist/es/server/provider/warden-event-processing-provider.js +1 -0
- package/dist/es/server/provider/warden-mailer-message-sending-provider-options.js +1 -0
- package/dist/es/server/provider/warden-mailer-message-sending-provider.js +32 -0
- package/dist/es/server/provider/warden-message-sending-provider.js +1 -0
- package/dist/es/server/provider/warden-no-op-event-processing-provider.js +4 -0
- package/dist/es/server/provider/warden-s3-single-file-storage-provider-options.js +1 -0
- package/dist/es/server/provider/warden-s3-single-file-storage-provider.js +86 -0
- package/dist/es/server/provider/warden-storage-provider.js +1 -0
- package/dist/es/server/provider/warden-twilio-text-message-sending-provider-options.js +1 -0
- package/dist/es/server/provider/warden-twilio-text-message-sending-provider.js +27 -0
- package/dist/es/server/provider/warden-user-decoration-provider.js +1 -0
- package/dist/es/server/warden-service-options.js +1 -0
- package/dist/es/server/warden-service.js +450 -0
- package/dist/tsconfig.cjs.tsbuildinfo +1 -0
- package/dist/tsconfig.es.tsbuildinfo +1 -0
- package/dist/tsconfig.types.tsbuildinfo +1 -0
- package/dist/types/build/ratchet-warden-server-info.d.ts +5 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/server/provider/warden-default-user-decoration-provider.d.ts +9 -0
- package/dist/types/server/provider/warden-event-processing-provider.d.ts +8 -0
- package/dist/types/server/provider/warden-mailer-message-sending-provider-options.d.ts +5 -0
- package/dist/types/server/provider/warden-mailer-message-sending-provider.d.ts +13 -0
- package/dist/types/server/provider/warden-message-sending-provider.d.ts +10 -0
- package/dist/types/server/provider/warden-no-op-event-processing-provider.d.ts +9 -0
- package/dist/types/server/provider/warden-s3-single-file-storage-provider-options.d.ts +4 -0
- package/dist/types/server/provider/warden-s3-single-file-storage-provider.d.ts +29 -0
- package/dist/types/server/provider/warden-storage-provider.d.ts +14 -0
- package/dist/types/server/provider/warden-twilio-text-message-sending-provider-options.d.ts +5 -0
- package/dist/types/server/provider/warden-twilio-text-message-sending-provider.d.ts +10 -0
- package/dist/types/server/provider/warden-user-decoration-provider.d.ts +8 -0
- package/dist/types/server/warden-service-options.d.ts +16 -0
- package/dist/types/server/warden-service.d.ts +27 -0
- package/package.json +71 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WardenContactType } from '@bitblit/ratchet-warden-common';
|
|
2
|
+
import { Logger, TwilioRatchet } from '@bitblit/ratchet-common';
|
|
3
|
+
export class WardenTwilioTextMessageSendingProvider {
|
|
4
|
+
constructor(optsPromise) {
|
|
5
|
+
this.optsPromise = optsPromise;
|
|
6
|
+
}
|
|
7
|
+
async formatMessage(contact, messageType, context) {
|
|
8
|
+
Logger.info('Creating text');
|
|
9
|
+
const msg = context['code'] +
|
|
10
|
+
' is your ' +
|
|
11
|
+
context['relyingPartyName'] +
|
|
12
|
+
' authentication code.\n@' +
|
|
13
|
+
context['relyingPartyName'] +
|
|
14
|
+
' #' +
|
|
15
|
+
context['code'];
|
|
16
|
+
return msg;
|
|
17
|
+
}
|
|
18
|
+
handlesContactType(type) {
|
|
19
|
+
return type === WardenContactType.TextCapablePhoneNumber;
|
|
20
|
+
}
|
|
21
|
+
async sendMessage(contact, message) {
|
|
22
|
+
const opts = await this.optsPromise;
|
|
23
|
+
const rval = await TwilioRatchet.sendMessageDirect(opts.accountSID, opts.authToken, opts.outBoundNumber, [contact.value], message);
|
|
24
|
+
Logger.debug('sendMessage was : %j', rval);
|
|
25
|
+
return !!rval && rval.length > 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from '@simplewebauthn/server';
|
|
2
|
+
import { WardenCustomerMessageType, WardenStoreRegistrationResponseType, WardenUtils, } from '@bitblit/ratchet-warden-common';
|
|
3
|
+
import { ExpiringCodeRatchet } from '@bitblit/ratchet-aws';
|
|
4
|
+
import { Base64Ratchet, ErrorRatchet, ExpiredJwtHandling, Logger, RequireRatchet, StringRatchet } from '@bitblit/ratchet-common';
|
|
5
|
+
import { WardenDefaultUserDecorationProvider } from './provider/warden-default-user-decoration-provider';
|
|
6
|
+
import { WardenNoOpEventProcessingProvider } from './provider/warden-no-op-event-processing-provider';
|
|
7
|
+
export class WardenService {
|
|
8
|
+
constructor(inOptions) {
|
|
9
|
+
this.inOptions = inOptions;
|
|
10
|
+
RequireRatchet.notNullOrUndefined(inOptions, 'options');
|
|
11
|
+
RequireRatchet.notNullOrUndefined(inOptions.relyingPartyName, 'options.relyingPartyName');
|
|
12
|
+
RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.allowedOrigins, 'options.allowedOrigins');
|
|
13
|
+
RequireRatchet.notNullOrUndefined(inOptions.storageProvider, 'options.storageProvider');
|
|
14
|
+
RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.messageSendingProviders, 'options.messageSendingProviders');
|
|
15
|
+
RequireRatchet.notNullOrUndefined(inOptions.expiringCodeProvider, 'options.expiringCodeProvider');
|
|
16
|
+
RequireRatchet.notNullOrUndefined(inOptions.jwtRatchet, 'options.jwtRatchet');
|
|
17
|
+
this.opts = Object.assign({ userTokenDataProvider: new WardenDefaultUserDecorationProvider(), eventProcessor: new WardenNoOpEventProcessingProvider() }, inOptions);
|
|
18
|
+
this.expiringCodeRatchet = new ExpiringCodeRatchet(this.opts.expiringCodeProvider);
|
|
19
|
+
}
|
|
20
|
+
get options() {
|
|
21
|
+
return Object.assign({}, this.opts);
|
|
22
|
+
}
|
|
23
|
+
findEntryByContact(contact) {
|
|
24
|
+
return this.opts.storageProvider.findEntryByContact(contact);
|
|
25
|
+
}
|
|
26
|
+
async processCommandStringToString(cmdString, origin, loggedInUserId) {
|
|
27
|
+
let rval = null;
|
|
28
|
+
try {
|
|
29
|
+
const cmd = JSON.parse(cmdString);
|
|
30
|
+
const resp = await this.processCommandToResponse(cmd, origin, loggedInUserId);
|
|
31
|
+
if (resp === null) {
|
|
32
|
+
Logger.warn('Response was null for %s %s %s', cmdString, origin, loggedInUserId);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
rval = JSON.stringify(resp);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
catch (err) {
|
|
39
|
+
const errString = ErrorRatchet.safeStringifyErr(err);
|
|
40
|
+
Logger.error('Failed %s : %j', errString, cmdString, err);
|
|
41
|
+
rval = JSON.stringify({ error: errString });
|
|
42
|
+
}
|
|
43
|
+
return rval;
|
|
44
|
+
}
|
|
45
|
+
async processCommandToResponse(cmd, origin, loggedInUserId) {
|
|
46
|
+
let rval = null;
|
|
47
|
+
if (cmd) {
|
|
48
|
+
Logger.info('Processing command : UserID: %s Origin: %s Command: %j', loggedInUserId, origin, cmd);
|
|
49
|
+
if (cmd.sendExpiringValidationToken) {
|
|
50
|
+
rval = { sendExpiringValidationToken: await this.sendExpiringValidationToken(cmd.sendExpiringValidationToken) };
|
|
51
|
+
}
|
|
52
|
+
else if (cmd.generateWebAuthnAuthenticationChallengeForUserId) {
|
|
53
|
+
const tmp = await this.generateWebAuthnAuthenticationChallengeForUserId(cmd.generateWebAuthnAuthenticationChallengeForUserId, origin);
|
|
54
|
+
rval = { generateWebAuthnAuthenticationChallengeForUserId: { dataAsJson: JSON.stringify(tmp) } };
|
|
55
|
+
}
|
|
56
|
+
else if (cmd.createAccount) {
|
|
57
|
+
rval = {
|
|
58
|
+
createAccount: await this.createAccount(cmd.createAccount.contact, cmd.createAccount.sendCode, cmd.createAccount.label, cmd.createAccount.tags),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
else if (cmd.generateWebAuthnRegistrationChallengeForLoggedInUser) {
|
|
62
|
+
if (!StringRatchet.trimToNull(loggedInUserId)) {
|
|
63
|
+
ErrorRatchet.throwFormattedErr('This requires a logged in user');
|
|
64
|
+
}
|
|
65
|
+
const tmp = await this.generateWebAuthnRegistrationChallengeForLoggedInUser(loggedInUserId, origin);
|
|
66
|
+
rval = { generateWebAuthnRegistrationChallengeForLoggedInUser: { dataAsJson: JSON.stringify(tmp) } };
|
|
67
|
+
}
|
|
68
|
+
else if (cmd.addContactToLoggedInUser) {
|
|
69
|
+
if (!WardenUtils.validContact(cmd.addContactToLoggedInUser)) {
|
|
70
|
+
ErrorRatchet.throwFormattedErr('Cannot add, invalid contact %j', cmd.addContactToLoggedInUser);
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
const out = await this.addContactMethodToUser(loggedInUserId, cmd.addContactToLoggedInUser);
|
|
74
|
+
rval = { addContactToLoggedInUser: out };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (cmd.addWebAuthnRegistrationToLoggedInUser) {
|
|
78
|
+
if (!StringRatchet.trimToNull(loggedInUserId)) {
|
|
79
|
+
ErrorRatchet.throwFormattedErr('This requires a logged in user');
|
|
80
|
+
}
|
|
81
|
+
const data = JSON.parse(cmd.addWebAuthnRegistrationToLoggedInUser.dataAsJson);
|
|
82
|
+
const out = await this.storeAuthnRegistration(loggedInUserId, origin, data);
|
|
83
|
+
if (out.updatedEntry) {
|
|
84
|
+
rval = { addWebAuthnRegistrationToLoggedInUser: WardenUtils.stripWardenEntryToSummary(out.updatedEntry) };
|
|
85
|
+
}
|
|
86
|
+
else if (out.error) {
|
|
87
|
+
rval = { error: out.error };
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
rval = { error: 'Cannot happen - neither user nor error set' };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
else if (cmd.removeWebAuthnRegistration) {
|
|
94
|
+
const modified = await this.removeSingleWebAuthnRegistration(cmd.removeWebAuthnRegistration.userId, cmd.removeWebAuthnRegistration.credentialId);
|
|
95
|
+
rval = {
|
|
96
|
+
removeWebAuthnRegistration: WardenUtils.stripWardenEntryToSummary(modified),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
else if (cmd.removeWebAuthnRegistrationFromLoggedInUser) {
|
|
100
|
+
const modified = await this.removeSingleWebAuthnRegistration(loggedInUserId, cmd.removeWebAuthnRegistrationFromLoggedInUser);
|
|
101
|
+
rval = {
|
|
102
|
+
removeWebAuthnRegistrationFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(modified),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
else if (cmd.removeContactFromLoggedInUser) {
|
|
106
|
+
const output = await this.removeContactMethodFromUser(loggedInUserId, cmd.removeContactFromLoggedInUser);
|
|
107
|
+
rval = {
|
|
108
|
+
removeContactFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(output),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
else if (cmd.performLogin) {
|
|
112
|
+
const loginData = cmd.performLogin;
|
|
113
|
+
const loginOk = await this.processLogin(loginData, origin);
|
|
114
|
+
Logger.info('Performing login - login auth check was : %s', loginOk);
|
|
115
|
+
if (loginOk) {
|
|
116
|
+
const user = StringRatchet.trimToNull(loginData.userId)
|
|
117
|
+
? await this.opts.storageProvider.findEntryById(loginData.userId)
|
|
118
|
+
: await this.opts.storageProvider.findEntryByContact(loginData.contact);
|
|
119
|
+
const decoration = await this.opts.userDecorationProvider.fetchDecoration(user);
|
|
120
|
+
const wardenToken = {
|
|
121
|
+
loginData: WardenUtils.stripWardenEntryToSummary(user),
|
|
122
|
+
user: decoration.userTokenData,
|
|
123
|
+
roles: WardenUtils.teamRolesToRoles(decoration.userTeamRoles),
|
|
124
|
+
proxy: null,
|
|
125
|
+
};
|
|
126
|
+
const jwtToken = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
|
|
127
|
+
const output = {
|
|
128
|
+
request: loginData,
|
|
129
|
+
userId: user.userId,
|
|
130
|
+
jwtToken: jwtToken,
|
|
131
|
+
};
|
|
132
|
+
rval = { performLogin: output };
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
rval = { error: 'Login failed' };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (cmd.refreshJwtToken) {
|
|
139
|
+
const parsed = await this.opts.jwtRatchet.decodeToken(cmd.refreshJwtToken, ExpiredJwtHandling.THROW_EXCEPTION);
|
|
140
|
+
const user = await this.opts.storageProvider.findEntryById(parsed.loginData.userId);
|
|
141
|
+
const decoration = await this.opts.userDecorationProvider.fetchDecoration(user);
|
|
142
|
+
const wardenToken = {
|
|
143
|
+
loginData: WardenUtils.stripWardenEntryToSummary(user),
|
|
144
|
+
user: decoration.userTokenData,
|
|
145
|
+
roles: WardenUtils.teamRolesToRoles(decoration.userTeamRoles),
|
|
146
|
+
proxy: null,
|
|
147
|
+
};
|
|
148
|
+
const newToken = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
|
|
149
|
+
rval = {
|
|
150
|
+
refreshJwtToken: newToken,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
rval = { error: 'No command sent' };
|
|
156
|
+
}
|
|
157
|
+
return rval;
|
|
158
|
+
}
|
|
159
|
+
async createAccount(contact, sendCode, label, tags) {
|
|
160
|
+
let rval = null;
|
|
161
|
+
if (WardenUtils.validContact(contact)) {
|
|
162
|
+
const old = await this.opts.storageProvider.findEntryByContact(contact);
|
|
163
|
+
if (!!old) {
|
|
164
|
+
ErrorRatchet.throwFormattedErr('Cannot create - account already exists for %j', contact);
|
|
165
|
+
}
|
|
166
|
+
const prov = this.senderForContact(contact);
|
|
167
|
+
if (!prov) {
|
|
168
|
+
ErrorRatchet.throwFormattedErr('Cannot create - no sending provider for type %s', contact.type);
|
|
169
|
+
}
|
|
170
|
+
const guid = StringRatchet.createType4Guid();
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
const newUser = {
|
|
173
|
+
userId: guid,
|
|
174
|
+
userLabel: label || 'User ' + guid,
|
|
175
|
+
contactMethods: [contact],
|
|
176
|
+
tags: tags || [],
|
|
177
|
+
webAuthnAuthenticators: [],
|
|
178
|
+
createdEpochMS: now,
|
|
179
|
+
updatedEpochMS: now,
|
|
180
|
+
};
|
|
181
|
+
const next = await this.opts.storageProvider.saveEntry(newUser);
|
|
182
|
+
rval = next.userId;
|
|
183
|
+
if (this?.opts?.eventProcessor) {
|
|
184
|
+
await this.opts.eventProcessor.userCreated(next);
|
|
185
|
+
}
|
|
186
|
+
if (sendCode) {
|
|
187
|
+
Logger.info('New user %j created and send requested - sending', next);
|
|
188
|
+
await this.sendExpiringValidationToken(contact);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
ErrorRatchet.throwFormattedErr('Cannot create - invalid contact (missing or invalid fields)');
|
|
193
|
+
}
|
|
194
|
+
return rval;
|
|
195
|
+
}
|
|
196
|
+
async addContactMethodToUser(userId, contact) {
|
|
197
|
+
let rval = false;
|
|
198
|
+
if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
|
|
199
|
+
const otherUser = await this.opts.storageProvider.findEntryByContact(contact);
|
|
200
|
+
if (otherUser && otherUser.userId !== userId) {
|
|
201
|
+
ErrorRatchet.throwFormattedErr('Cannot add contact to this user, another user already has that contact');
|
|
202
|
+
}
|
|
203
|
+
const curUser = await this.opts.storageProvider.findEntryById(userId);
|
|
204
|
+
if (!curUser) {
|
|
205
|
+
ErrorRatchet.throwFormattedErr('Cannot add contact to this user, user does not exist');
|
|
206
|
+
}
|
|
207
|
+
curUser.contactMethods.push(contact);
|
|
208
|
+
await this.opts.storageProvider.saveEntry(curUser);
|
|
209
|
+
rval = true;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
ErrorRatchet.throwFormattedErr('Cannot add - invalid config : %s %j', userId, contact);
|
|
213
|
+
}
|
|
214
|
+
return rval;
|
|
215
|
+
}
|
|
216
|
+
async removeContactMethodFromUser(userId, contact) {
|
|
217
|
+
let rval = null;
|
|
218
|
+
if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
|
|
219
|
+
const curUser = await this.opts.storageProvider.findEntryById(userId);
|
|
220
|
+
if (!curUser) {
|
|
221
|
+
ErrorRatchet.throwFormattedErr('Cannot remove contact from this user, user does not exist');
|
|
222
|
+
}
|
|
223
|
+
curUser.contactMethods = (curUser.contactMethods || []).filter((s) => s.type !== contact.type || s.value !== contact.value);
|
|
224
|
+
if (curUser.contactMethods.length === 0) {
|
|
225
|
+
ErrorRatchet.throwFormattedErr('Cannot remove the last contact method from a user');
|
|
226
|
+
}
|
|
227
|
+
await this.opts.storageProvider.saveEntry(curUser);
|
|
228
|
+
rval = await this.opts.storageProvider.findEntryById(userId);
|
|
229
|
+
}
|
|
230
|
+
else {
|
|
231
|
+
ErrorRatchet.throwFormattedErr('Cannot add - invalid config : %s %j', userId, contact);
|
|
232
|
+
}
|
|
233
|
+
return rval;
|
|
234
|
+
}
|
|
235
|
+
async generateWebAuthnRegistrationChallengeForLoggedInUser(userId, origin) {
|
|
236
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
237
|
+
throw new Error('Invalid origin : ' + origin);
|
|
238
|
+
}
|
|
239
|
+
const asUrl = new URL(origin);
|
|
240
|
+
const rpID = asUrl.hostname;
|
|
241
|
+
const entry = await this.opts.storageProvider.findEntryById(userId);
|
|
242
|
+
const options = generateRegistrationOptions({
|
|
243
|
+
rpName: this.opts.relyingPartyName,
|
|
244
|
+
rpID: rpID,
|
|
245
|
+
userID: entry.userId,
|
|
246
|
+
userName: entry.userLabel,
|
|
247
|
+
attestationType: 'none',
|
|
248
|
+
excludeCredentials: entry.webAuthnAuthenticators.map((authenticator) => ({
|
|
249
|
+
id: Base64Ratchet.base64StringToBuffer(authenticator.credentialPublicKeyBase64),
|
|
250
|
+
type: 'public-key',
|
|
251
|
+
transports: authenticator.transports,
|
|
252
|
+
})),
|
|
253
|
+
});
|
|
254
|
+
await this.opts.storageProvider.updateUserChallenge(entry.userId, rpID, options.challenge);
|
|
255
|
+
return options;
|
|
256
|
+
}
|
|
257
|
+
async storeAuthnRegistration(userId, origin, data) {
|
|
258
|
+
Logger.info('Store authn data : %j', data);
|
|
259
|
+
let rval = null;
|
|
260
|
+
try {
|
|
261
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
262
|
+
throw new Error('Invalid origin : ' + origin);
|
|
263
|
+
}
|
|
264
|
+
const asUrl = new URL(origin);
|
|
265
|
+
const rpID = asUrl.hostname;
|
|
266
|
+
const user = await this.opts.storageProvider.findEntryById(userId);
|
|
267
|
+
const expectedChallenge = await this.opts.storageProvider.fetchCurrentUserChallenge(user.userId, rpID);
|
|
268
|
+
const vrOpts = {
|
|
269
|
+
response: data,
|
|
270
|
+
expectedChallenge: expectedChallenge,
|
|
271
|
+
expectedOrigin: origin,
|
|
272
|
+
expectedRPID: rpID,
|
|
273
|
+
};
|
|
274
|
+
Logger.info('Calling verifyRegistrationResponse: %j', vrOpts);
|
|
275
|
+
const verification = await verifyRegistrationResponse(vrOpts);
|
|
276
|
+
Logger.info('verifyRegistrationResponse Result : %j', verification);
|
|
277
|
+
rval = {
|
|
278
|
+
updatedEntry: null,
|
|
279
|
+
registrationResponseId: data.id,
|
|
280
|
+
result: verification.verified ? WardenStoreRegistrationResponseType.Verified : WardenStoreRegistrationResponseType.Failed,
|
|
281
|
+
};
|
|
282
|
+
if (rval.result === WardenStoreRegistrationResponseType.Verified) {
|
|
283
|
+
Logger.info('Storing registration');
|
|
284
|
+
const newAuth = {
|
|
285
|
+
counter: verification.registrationInfo.counter,
|
|
286
|
+
credentialBackedUp: verification.registrationInfo.credentialBackedUp,
|
|
287
|
+
credentialDeviceType: verification.registrationInfo.credentialDeviceType,
|
|
288
|
+
credentialIdBase64: data.id,
|
|
289
|
+
credentialPublicKeyBase64: Base64Ratchet.generateBase64VersionOfBuffer(Buffer.from(verification.registrationInfo.credentialPublicKey)),
|
|
290
|
+
};
|
|
291
|
+
user.webAuthnAuthenticators = (user.webAuthnAuthenticators || []).filter((wa) => wa.credentialIdBase64 !== newAuth.credentialIdBase64);
|
|
292
|
+
user.webAuthnAuthenticators.push(newAuth);
|
|
293
|
+
const storedUser = await this.opts.storageProvider.saveEntry(user);
|
|
294
|
+
rval.updatedEntry = storedUser;
|
|
295
|
+
Logger.info('Stored auth : %j', storedUser);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
catch (err) {
|
|
299
|
+
rval = {
|
|
300
|
+
registrationResponseId: data.id,
|
|
301
|
+
result: WardenStoreRegistrationResponseType.Error,
|
|
302
|
+
error: ErrorRatchet.safeStringifyErr(err),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return rval;
|
|
306
|
+
}
|
|
307
|
+
async generateWebAuthnAuthenticationChallengeForUserId(userId, origin) {
|
|
308
|
+
const user = await this.opts.storageProvider.findEntryById(userId);
|
|
309
|
+
const rval = await this.generateWebAuthnAuthenticationChallenge(user, origin);
|
|
310
|
+
return rval;
|
|
311
|
+
}
|
|
312
|
+
async generateWebAuthnAuthenticationChallenge(user, origin) {
|
|
313
|
+
const userAuthenticators = user.webAuthnAuthenticators;
|
|
314
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
315
|
+
throw new Error('Invalid origin : ' + origin);
|
|
316
|
+
}
|
|
317
|
+
const asUrl = new URL(origin);
|
|
318
|
+
const rpID = asUrl.hostname;
|
|
319
|
+
const out = userAuthenticators.map((authenticator) => {
|
|
320
|
+
const next = {
|
|
321
|
+
id: Buffer.from(authenticator.credentialIdBase64, 'base64'),
|
|
322
|
+
type: 'public-key',
|
|
323
|
+
transports: authenticator.transports,
|
|
324
|
+
};
|
|
325
|
+
return next;
|
|
326
|
+
});
|
|
327
|
+
const options = generateAuthenticationOptions({
|
|
328
|
+
allowCredentials: out,
|
|
329
|
+
userVerification: 'preferred',
|
|
330
|
+
});
|
|
331
|
+
await this.opts.storageProvider.updateUserChallenge(user.userId, rpID, options.challenge);
|
|
332
|
+
return options;
|
|
333
|
+
}
|
|
334
|
+
senderForContact(contact) {
|
|
335
|
+
let rval = null;
|
|
336
|
+
if (contact?.type) {
|
|
337
|
+
rval = (this.opts.messageSendingProviders || []).find((p) => p.handlesContactType(contact.type));
|
|
338
|
+
}
|
|
339
|
+
return rval;
|
|
340
|
+
}
|
|
341
|
+
async sendExpiringValidationToken(request) {
|
|
342
|
+
let rval = false;
|
|
343
|
+
if (request?.type && StringRatchet.trimToNull(request?.value)) {
|
|
344
|
+
const prov = this.senderForContact(request);
|
|
345
|
+
if (prov) {
|
|
346
|
+
const token = await this.expiringCodeRatchet.createNewCode({
|
|
347
|
+
context: request.value,
|
|
348
|
+
length: 6,
|
|
349
|
+
alphabet: '0123456789',
|
|
350
|
+
timeToLiveSeconds: 300,
|
|
351
|
+
tags: ['Login'],
|
|
352
|
+
});
|
|
353
|
+
const msg = await prov.formatMessage(request, WardenCustomerMessageType.ExpiringCode, {
|
|
354
|
+
code: token.code,
|
|
355
|
+
relyingPartyName: this.opts.relyingPartyName,
|
|
356
|
+
});
|
|
357
|
+
rval = await prov.sendMessage(request, msg);
|
|
358
|
+
}
|
|
359
|
+
else {
|
|
360
|
+
ErrorRatchet.throwFormattedErr('No provider found for contact type %s', request.type);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
ErrorRatchet.throwFormattedErr('Cannot send - invalid request %j', request);
|
|
365
|
+
}
|
|
366
|
+
return rval;
|
|
367
|
+
}
|
|
368
|
+
async processLogin(request, origin) {
|
|
369
|
+
Logger.info('Processing login : %s : %j', origin, request);
|
|
370
|
+
let rval = false;
|
|
371
|
+
RequireRatchet.notNullOrUndefined(request, 'request');
|
|
372
|
+
RequireRatchet.true(!!StringRatchet.trimToNull(request?.userId) || WardenUtils.validContact(request?.contact), 'Invalid contact and no userId');
|
|
373
|
+
RequireRatchet.true(!!request?.webAuthn || !!StringRatchet.trimToNull(request?.expiringToken), 'You must provide one of webAuthn or expiringToken');
|
|
374
|
+
RequireRatchet.true(!request?.webAuthn || !StringRatchet.trimToNull(request?.expiringToken), 'WebAuthn and ExpiringToken may not BOTH be set');
|
|
375
|
+
const user = StringRatchet.trimToNull(request?.userId)
|
|
376
|
+
? await this.opts.storageProvider.findEntryById(request?.userId)
|
|
377
|
+
: await this.opts.storageProvider.findEntryByContact(request.contact);
|
|
378
|
+
if (!user) {
|
|
379
|
+
ErrorRatchet.throwFormattedErr('No user found for %j / %s', request?.contact, request?.userId);
|
|
380
|
+
}
|
|
381
|
+
if (request.webAuthn) {
|
|
382
|
+
rval = await this.loginWithWebAuthnRequest(user, origin, request.webAuthn);
|
|
383
|
+
}
|
|
384
|
+
else if (StringRatchet.trimToNull(request.expiringToken)) {
|
|
385
|
+
const lookup = await this.expiringCodeRatchet.checkCode(StringRatchet.trimToEmpty(request.expiringToken), StringRatchet.trimToEmpty(request.contact.value), true);
|
|
386
|
+
if (lookup) {
|
|
387
|
+
rval = true;
|
|
388
|
+
}
|
|
389
|
+
else {
|
|
390
|
+
ErrorRatchet.throwFormattedErr('Cannot login - token is invalid for this user');
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
return rval;
|
|
394
|
+
}
|
|
395
|
+
async loginWithWebAuthnRequest(user, origin, data) {
|
|
396
|
+
let rval = false;
|
|
397
|
+
const asUrl = new URL(origin);
|
|
398
|
+
const rpID = asUrl.hostname;
|
|
399
|
+
const expectedChallenge = await this.opts.storageProvider.fetchCurrentUserChallenge(user.userId, rpID);
|
|
400
|
+
const auth = (user.webAuthnAuthenticators || []).find((s) => s.credentialIdBase64 === data.id);
|
|
401
|
+
if (!auth) {
|
|
402
|
+
throw new Error(`Could not find authenticator ${data.id} for user ${user.userId}`);
|
|
403
|
+
}
|
|
404
|
+
const authenticator = {
|
|
405
|
+
counter: auth.counter,
|
|
406
|
+
credentialID: Base64Ratchet.base64StringToBuffer(auth.credentialIdBase64),
|
|
407
|
+
credentialPublicKey: Base64Ratchet.base64StringToBuffer(auth.credentialPublicKeyBase64),
|
|
408
|
+
};
|
|
409
|
+
const vrOpts = {
|
|
410
|
+
response: data,
|
|
411
|
+
expectedChallenge,
|
|
412
|
+
expectedOrigin: origin,
|
|
413
|
+
expectedRPID: rpID,
|
|
414
|
+
authenticator,
|
|
415
|
+
};
|
|
416
|
+
const verification = await verifyAuthenticationResponse(vrOpts);
|
|
417
|
+
if (verification.verified) {
|
|
418
|
+
rval = true;
|
|
419
|
+
}
|
|
420
|
+
return rval;
|
|
421
|
+
}
|
|
422
|
+
async removeSingleWebAuthnRegistration(userId, key) {
|
|
423
|
+
let ent = await this.opts.storageProvider.findEntryById(userId);
|
|
424
|
+
if (ent) {
|
|
425
|
+
ent.webAuthnAuthenticators = (ent.webAuthnAuthenticators || []).filter((s) => s.credentialIdBase64 !== key);
|
|
426
|
+
ent = await this.opts.storageProvider.saveEntry(ent);
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
Logger.info('Not removing - no such user as %s', userId);
|
|
430
|
+
}
|
|
431
|
+
return ent;
|
|
432
|
+
}
|
|
433
|
+
async removeUser(userId) {
|
|
434
|
+
let rval = false;
|
|
435
|
+
if (StringRatchet.trimToNull(userId)) {
|
|
436
|
+
const oldUser = await this.opts.storageProvider.findEntryById(userId);
|
|
437
|
+
if (oldUser) {
|
|
438
|
+
await this.opts.storageProvider.removeEntry(userId);
|
|
439
|
+
if (this?.opts?.eventProcessor) {
|
|
440
|
+
await this.opts.eventProcessor.userRemoved(oldUser);
|
|
441
|
+
}
|
|
442
|
+
rval = true;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
Logger.warn('Cannot remove non-existent user : %s', userId);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return rval;
|
|
449
|
+
}
|
|
450
|
+
}
|