@bitblit/ratchet-warden-server 6.0.146-alpha → 6.0.147-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/package.json +9 -8
- package/src/build/ratchet-warden-server-info.ts +19 -0
- package/src/server/provider/warden-default-send-magic-link-command-validator.ts +21 -0
- package/src/server/provider/warden-default-user-decoration-provider.ts +23 -0
- package/src/server/provider/warden-dynamo-storage-provider-options.ts +8 -0
- package/src/server/provider/warden-dynamo-storage-provider.ts +278 -0
- package/src/server/provider/warden-event-processing-provider.ts +10 -0
- package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-use-code-provider.ts +155 -0
- package/src/server/provider/warden-mailer-and-expiring-code-ratchet-single-user-provider-options.ts +10 -0
- package/src/server/provider/warden-message-sending-provider.ts +14 -0
- package/src/server/provider/warden-no-op-event-processing-provider.ts +12 -0
- package/src/server/provider/warden-s3-single-file-storage-provider-options.ts +6 -0
- package/src/server/provider/warden-s3-single-file-storage-provider.ts +139 -0
- package/src/server/provider/warden-send-magic-link-command-validator.ts +12 -0
- package/src/server/provider/warden-single-use-code-provider.ts +27 -0
- package/src/server/provider/warden-storage-provider.ts +22 -0
- package/src/server/provider/warden-third-party-authentication-provider.ts +18 -0
- package/src/server/provider/warden-twilio-verify-single-use-code-provider-options.ts +5 -0
- package/src/server/provider/warden-twilio-verify-single-use-code-provider.ts +38 -0
- package/src/server/provider/warden-user-decoration-provider.ts +10 -0
- package/src/server/warden-authorizer.ts +130 -0
- package/src/server/warden-entry-builder.ts +56 -0
- package/src/server/warden-service-options.ts +20 -0
- package/src/server/warden-service.spec.ts +102 -0
- package/src/server/warden-service.ts +815 -0
- package/src/server/warden-web-authn-export-token.ts +6 -0
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AuthenticationResponseJSON,
|
|
3
|
+
AuthenticatorTransportFuture,
|
|
4
|
+
generateAuthenticationOptions,
|
|
5
|
+
GenerateAuthenticationOptionsOpts,
|
|
6
|
+
generateRegistrationOptions,
|
|
7
|
+
PublicKeyCredentialCreationOptionsJSON,
|
|
8
|
+
PublicKeyCredentialRequestOptionsJSON,
|
|
9
|
+
RegistrationResponseJSON,
|
|
10
|
+
VerifiedAuthenticationResponse,
|
|
11
|
+
VerifiedRegistrationResponse,
|
|
12
|
+
verifyAuthenticationResponse,
|
|
13
|
+
VerifyAuthenticationResponseOpts,
|
|
14
|
+
verifyRegistrationResponse,
|
|
15
|
+
VerifyRegistrationResponseOpts
|
|
16
|
+
} from "@simplewebauthn/server";
|
|
17
|
+
import { WardenServiceOptions } from "./warden-service-options.js";
|
|
18
|
+
import { WardenContact } from "@bitblit/ratchet-warden-common/common/model/warden-contact";
|
|
19
|
+
import {
|
|
20
|
+
WardenCustomTemplateDescriptor
|
|
21
|
+
} from "@bitblit/ratchet-warden-common/common/command/warden-custom-template-descriptor";
|
|
22
|
+
import { WardenEntry } from "@bitblit/ratchet-warden-common/common/model/warden-entry";
|
|
23
|
+
import { WardenUtils } from "@bitblit/ratchet-warden-common/common/util/warden-utils";
|
|
24
|
+
import { WardenLoginRequest } from "@bitblit/ratchet-warden-common/common/model/warden-login-request";
|
|
25
|
+
import { WardenCommand } from "@bitblit/ratchet-warden-common/common/command/warden-command";
|
|
26
|
+
import { WardenCommandResponse } from "@bitblit/ratchet-warden-common/common/command/warden-command-response";
|
|
27
|
+
import { WardenLoginResults } from "@bitblit/ratchet-warden-common/common/model/warden-login-results";
|
|
28
|
+
import {
|
|
29
|
+
WardenStoreRegistrationResponse
|
|
30
|
+
} from "@bitblit/ratchet-warden-common/common/model/warden-store-registration-response";
|
|
31
|
+
import { WardenUserDecoration } from "@bitblit/ratchet-warden-common/common/model/warden-user-decoration";
|
|
32
|
+
import { WardenJwtToken } from "@bitblit/ratchet-warden-common/common/model/warden-jwt-token";
|
|
33
|
+
import {
|
|
34
|
+
WardenStoreRegistrationResponseType
|
|
35
|
+
} from "@bitblit/ratchet-warden-common/common/model/warden-store-registration-response-type";
|
|
36
|
+
import { WardenWebAuthnEntry } from "@bitblit/ratchet-warden-common/common/model/warden-web-authn-entry";
|
|
37
|
+
|
|
38
|
+
import { RequireRatchet } from "@bitblit/ratchet-common/lang/require-ratchet";
|
|
39
|
+
import { Logger } from "@bitblit/ratchet-common/logger/logger";
|
|
40
|
+
import { ErrorRatchet } from "@bitblit/ratchet-common/lang/error-ratchet";
|
|
41
|
+
import { StringRatchet } from "@bitblit/ratchet-common/lang/string-ratchet";
|
|
42
|
+
import { Base64Ratchet } from "@bitblit/ratchet-common/lang/base64-ratchet";
|
|
43
|
+
import { ExpiredJwtHandling } from "@bitblit/ratchet-common/jwt/expired-jwt-handling";
|
|
44
|
+
|
|
45
|
+
import { WardenDefaultUserDecorationProvider } from "./provider/warden-default-user-decoration-provider.js";
|
|
46
|
+
import { WardenNoOpEventProcessingProvider } from "./provider/warden-no-op-event-processing-provider.js";
|
|
47
|
+
import { WardenSingleUseCodeProvider } from "./provider/warden-single-use-code-provider.js";
|
|
48
|
+
import {
|
|
49
|
+
WardenDefaultSendMagicLinkCommandValidator
|
|
50
|
+
} from "./provider/warden-default-send-magic-link-command-validator.js";
|
|
51
|
+
import { WardenLoginRequestType } from "@bitblit/ratchet-warden-common/common/model/warden-login-request-type";
|
|
52
|
+
import {
|
|
53
|
+
WardenThirdPartyAuthentication
|
|
54
|
+
} from "@bitblit/ratchet-warden-common/common/model/warden-third-party-authentication";
|
|
55
|
+
import { WardenThirdPartyAuthenticationProvider } from "./provider/warden-third-party-authentication-provider.js";
|
|
56
|
+
import { WardenEntryBuilder } from "./warden-entry-builder.js";
|
|
57
|
+
import { WardenAuthorizer } from "./warden-authorizer.ts";
|
|
58
|
+
import { WardenWebAuthnExportToken } from "./warden-web-authn-export-token.ts";
|
|
59
|
+
|
|
60
|
+
export class WardenService {
|
|
61
|
+
private opts: WardenServiceOptions;
|
|
62
|
+
private cacheAuthorizer: WardenAuthorizer;
|
|
63
|
+
|
|
64
|
+
constructor(private inOptions: WardenServiceOptions) {
|
|
65
|
+
RequireRatchet.notNullOrUndefined(inOptions, "options");
|
|
66
|
+
RequireRatchet.notNullOrUndefined(inOptions.relyingPartyName, "options.relyingPartyName");
|
|
67
|
+
RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.allowedOrigins, "options.allowedOrigins");
|
|
68
|
+
RequireRatchet.notNullOrUndefined(inOptions.storageProvider, "options.storageProvider");
|
|
69
|
+
RequireRatchet.notNullOrUndefined(inOptions.jwtRatchet, "options.jwtRatchet");
|
|
70
|
+
RequireRatchet.notNullUndefinedOrEmptyArray(inOptions.singleUseCodeProviders, "options.singleUseCodeProviders");
|
|
71
|
+
|
|
72
|
+
this.opts = Object.assign(
|
|
73
|
+
{
|
|
74
|
+
userTokenDataProvider: new WardenDefaultUserDecorationProvider(),
|
|
75
|
+
eventProcessor: new WardenNoOpEventProcessingProvider(),
|
|
76
|
+
sendMagicLinkCommandValidator: new WardenDefaultSendMagicLinkCommandValidator()
|
|
77
|
+
},
|
|
78
|
+
inOptions
|
|
79
|
+
);
|
|
80
|
+
this.cacheAuthorizer = new WardenAuthorizer(inOptions);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
public get authorizer(): WardenAuthorizer {
|
|
84
|
+
return this.cacheAuthorizer;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
public get options(): WardenServiceOptions {
|
|
88
|
+
return Object.assign({}, this.opts); // Don't allow a reader to change
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Passthru for very common use case
|
|
92
|
+
public findEntryByContact(contact: WardenContact): Promise<WardenEntry> {
|
|
93
|
+
return this.opts.storageProvider.findEntryByContact(contact);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Passthru for very common use case
|
|
97
|
+
public findEntryById(userId: string): Promise<WardenEntry> {
|
|
98
|
+
return this.opts.storageProvider.findEntryById(userId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// A helper function for bridging across GraphQL as an embedded JSON command
|
|
102
|
+
public async processCommandStringToString(cmdString: string, origin: string, loggedInUserId: string): Promise<string> {
|
|
103
|
+
let rval: string = null;
|
|
104
|
+
try {
|
|
105
|
+
const cmd: WardenCommand = JSON.parse(cmdString);
|
|
106
|
+
const resp: WardenCommandResponse = await this.processCommandToResponse(cmd, origin, loggedInUserId);
|
|
107
|
+
if (resp === null) {
|
|
108
|
+
Logger.warn("Response was null for %s %s %s", cmdString, origin, loggedInUserId);
|
|
109
|
+
} else {
|
|
110
|
+
rval = JSON.stringify(resp);
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
// Just cast it directly
|
|
114
|
+
const errString: string = ErrorRatchet.safeStringifyErr(err);
|
|
115
|
+
Logger.error("Failed %s : %j", errString, cmdString, err);
|
|
116
|
+
rval = JSON.stringify({ error: errString } as WardenCommandResponse);
|
|
117
|
+
}
|
|
118
|
+
return rval;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// A helper function for bridging across GraphQL as an embedded JSON command
|
|
122
|
+
public async processCommandToResponse(cmd: WardenCommand, origin: string, loggedInUserId: string): Promise<WardenCommandResponse> {
|
|
123
|
+
let rval: WardenCommandResponse = null;
|
|
124
|
+
if (cmd) {
|
|
125
|
+
Logger.info("Processing command : UserID: %s Origin: %s Command: %j", loggedInUserId, origin, cmd);
|
|
126
|
+
|
|
127
|
+
if (cmd.sendExpiringValidationToken) {
|
|
128
|
+
rval = { sendExpiringValidationToken: await this.sendExpiringValidationToken(cmd.sendExpiringValidationToken, origin) };
|
|
129
|
+
} else if (cmd.generateWebAuthnAuthenticationChallengeForUserId) {
|
|
130
|
+
const tmp: PublicKeyCredentialRequestOptionsJSON = await this.generateWebAuthnAuthenticationChallengeForUserId(
|
|
131
|
+
cmd.generateWebAuthnAuthenticationChallengeForUserId,
|
|
132
|
+
origin
|
|
133
|
+
);
|
|
134
|
+
rval = { generateWebAuthnAuthenticationChallengeForUserId: { dataAsJson: JSON.stringify(tmp) } };
|
|
135
|
+
} else if (cmd.createAccount) {
|
|
136
|
+
const newEntry: WardenEntry = await this.createAccount(
|
|
137
|
+
cmd.createAccount.contact,
|
|
138
|
+
origin,
|
|
139
|
+
cmd.createAccount.sendCode,
|
|
140
|
+
cmd.createAccount.label,
|
|
141
|
+
cmd.createAccount.tags
|
|
142
|
+
);
|
|
143
|
+
rval = {
|
|
144
|
+
createAccount: newEntry.userId
|
|
145
|
+
};
|
|
146
|
+
} else if (cmd.sendMagicLink) {
|
|
147
|
+
if (cmd?.sendMagicLink?.contactLookup && cmd?.sendMagicLink?.contact) {
|
|
148
|
+
throw ErrorRatchet.fErr("You may not specify both contact and contactLookup");
|
|
149
|
+
}
|
|
150
|
+
if (!cmd?.sendMagicLink?.contactLookup && !cmd?.sendMagicLink?.contact) {
|
|
151
|
+
throw ErrorRatchet.fErr("You must not specify either contact and contactLookup");
|
|
152
|
+
}
|
|
153
|
+
if (cmd.sendMagicLink.contactLookup) {
|
|
154
|
+
const entry: WardenEntry = await this.findEntryById(cmd.sendMagicLink.contactLookup.userId);
|
|
155
|
+
if (entry) {
|
|
156
|
+
if (cmd.sendMagicLink.contactLookup.contactType) {
|
|
157
|
+
// Use the one specified, otherwise just first one
|
|
158
|
+
cmd.sendMagicLink.contact = (entry.contactMethods || []).find(
|
|
159
|
+
(cm) => cm.type === cmd.sendMagicLink.contactLookup.contactType
|
|
160
|
+
);
|
|
161
|
+
} else {
|
|
162
|
+
cmd.sendMagicLink.contact = (entry.contactMethods || []).length > 0 ? entry.contactMethods[0] : null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
cmd.sendMagicLink.contactLookup = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!cmd.sendMagicLink.contact) {
|
|
169
|
+
throw ErrorRatchet.fErr("Could not find contract entry either directly or by lookup");
|
|
170
|
+
}
|
|
171
|
+
// Now run all allowance checks on the link
|
|
172
|
+
const loggedInUser: WardenEntry = StringRatchet.trimToNull(loggedInUserId)
|
|
173
|
+
? await this.opts.storageProvider.findEntryById(loggedInUserId)
|
|
174
|
+
: null;
|
|
175
|
+
|
|
176
|
+
await this.opts.sendMagicLinkCommandValidator.allowMagicLinkCommand(cmd.sendMagicLink, origin, loggedInUser);
|
|
177
|
+
|
|
178
|
+
const ttlSeconds: number = cmd?.sendMagicLink?.ttlSeconds || 300;
|
|
179
|
+
|
|
180
|
+
rval = {
|
|
181
|
+
sendMagicLink: await this.sendMagicLink(
|
|
182
|
+
cmd.sendMagicLink.contact,
|
|
183
|
+
cmd.sendMagicLink.overrideDestinationContact,
|
|
184
|
+
this.opts.relyingPartyName,
|
|
185
|
+
cmd.sendMagicLink.landingUrl,
|
|
186
|
+
cmd.sendMagicLink.meta,
|
|
187
|
+
ttlSeconds,
|
|
188
|
+
cmd.sendMagicLink.customTemplate
|
|
189
|
+
)
|
|
190
|
+
};
|
|
191
|
+
} else if (cmd.generateWebAuthnRegistrationChallengeForLoggedInUser) {
|
|
192
|
+
if (!StringRatchet.trimToNull(loggedInUserId)) {
|
|
193
|
+
ErrorRatchet.throwFormattedErr("This requires a logged in user");
|
|
194
|
+
}
|
|
195
|
+
const tmp: PublicKeyCredentialCreationOptionsJSON = await this.generateWebAuthnRegistrationChallengeForLoggedInUser(
|
|
196
|
+
loggedInUserId,
|
|
197
|
+
origin
|
|
198
|
+
);
|
|
199
|
+
rval = { generateWebAuthnRegistrationChallengeForLoggedInUser: { dataAsJson: JSON.stringify(tmp) } };
|
|
200
|
+
} else if (cmd.addContactToLoggedInUser) {
|
|
201
|
+
if (!WardenUtils.validContact(cmd.addContactToLoggedInUser)) {
|
|
202
|
+
ErrorRatchet.throwFormattedErr("Cannot add, invalid contact %j", cmd.addContactToLoggedInUser);
|
|
203
|
+
} else {
|
|
204
|
+
const out: boolean = await this.addContactMethodToUser(loggedInUserId, cmd.addContactToLoggedInUser);
|
|
205
|
+
rval = { addContactToLoggedInUser: out };
|
|
206
|
+
}
|
|
207
|
+
} else if (cmd.addWebAuthnRegistrationToLoggedInUser) {
|
|
208
|
+
if (!StringRatchet.trimToNull(loggedInUserId)) {
|
|
209
|
+
ErrorRatchet.throwFormattedErr("This requires a logged in user");
|
|
210
|
+
}
|
|
211
|
+
const data: RegistrationResponseJSON = JSON.parse(cmd.addWebAuthnRegistrationToLoggedInUser.webAuthn.dataAsJson);
|
|
212
|
+
const out: WardenStoreRegistrationResponse = await this.storeAuthnRegistration(
|
|
213
|
+
loggedInUserId,
|
|
214
|
+
origin,
|
|
215
|
+
cmd.addWebAuthnRegistrationToLoggedInUser.applicationName,
|
|
216
|
+
cmd.addWebAuthnRegistrationToLoggedInUser.deviceLabel,
|
|
217
|
+
data
|
|
218
|
+
);
|
|
219
|
+
if (out.updatedEntry) {
|
|
220
|
+
rval = { addWebAuthnRegistrationToLoggedInUser: WardenUtils.stripWardenEntryToSummary(out.updatedEntry) };
|
|
221
|
+
} else if (out.error) {
|
|
222
|
+
rval = { error: out.error };
|
|
223
|
+
} else {
|
|
224
|
+
rval = { error: "Cannot happen - neither user nor error set" };
|
|
225
|
+
}
|
|
226
|
+
} else if (cmd.removeWebAuthnRegistration) {
|
|
227
|
+
const modified: WardenEntry = await this.removeSingleWebAuthnRegistration(
|
|
228
|
+
cmd.removeWebAuthnRegistration.userId,
|
|
229
|
+
cmd.removeWebAuthnRegistration.credentialId
|
|
230
|
+
);
|
|
231
|
+
rval = {
|
|
232
|
+
removeWebAuthnRegistration: WardenUtils.stripWardenEntryToSummary(modified)
|
|
233
|
+
};
|
|
234
|
+
} else if (cmd.removeWebAuthnRegistrationFromLoggedInUser) {
|
|
235
|
+
const modified: WardenEntry = await this.removeSingleWebAuthnRegistration(
|
|
236
|
+
loggedInUserId,
|
|
237
|
+
cmd.removeWebAuthnRegistrationFromLoggedInUser
|
|
238
|
+
);
|
|
239
|
+
rval = {
|
|
240
|
+
removeWebAuthnRegistrationFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(modified)
|
|
241
|
+
};
|
|
242
|
+
} else if (cmd.removeContactFromLoggedInUser) {
|
|
243
|
+
const output: WardenEntry = await this.removeContactMethodFromUser(loggedInUserId, cmd.removeContactFromLoggedInUser);
|
|
244
|
+
// wardencontact
|
|
245
|
+
rval = {
|
|
246
|
+
removeContactFromLoggedInUser: WardenUtils.stripWardenEntryToSummary(output)
|
|
247
|
+
};
|
|
248
|
+
// return WardenEntrySummary
|
|
249
|
+
} else if (cmd.performLogin) {
|
|
250
|
+
const loginData: WardenLoginRequest = cmd.performLogin;
|
|
251
|
+
const user: WardenEntry = await this.processLogin(loginData, origin);
|
|
252
|
+
if (user) {
|
|
253
|
+
const decoration: WardenUserDecoration<any> = await this.opts.userDecorationProvider.fetchDecoration(user);
|
|
254
|
+
const wardenToken: WardenJwtToken<any> = {
|
|
255
|
+
wardenData: WardenUtils.stripWardenEntryToSummary(user),
|
|
256
|
+
user: decoration.userTokenData,
|
|
257
|
+
proxy: decoration.proxyUserTokenData,
|
|
258
|
+
globalRoleIds: decoration.globalRoleIds,
|
|
259
|
+
teamRoleMappings: decoration.teamRoleMappings
|
|
260
|
+
};
|
|
261
|
+
const jwtToken: string = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
|
|
262
|
+
const output: WardenLoginResults = {
|
|
263
|
+
request: loginData,
|
|
264
|
+
userId: user.userId,
|
|
265
|
+
jwtToken: jwtToken
|
|
266
|
+
};
|
|
267
|
+
rval = { performLogin: output };
|
|
268
|
+
} else {
|
|
269
|
+
rval = { error: "Login failed" };
|
|
270
|
+
}
|
|
271
|
+
} else if (cmd.refreshJwtToken) {
|
|
272
|
+
const parsed: WardenJwtToken<any> = await this.opts.jwtRatchet.decodeToken(cmd.refreshJwtToken, ExpiredJwtHandling.THROW_EXCEPTION);
|
|
273
|
+
const user: WardenEntry = await this.opts.storageProvider.findEntryById(parsed.wardenData.userId);
|
|
274
|
+
const decoration: WardenUserDecoration<any> = await this.opts.userDecorationProvider.fetchDecoration(user);
|
|
275
|
+
const wardenToken: WardenJwtToken<any> = {
|
|
276
|
+
wardenData: WardenUtils.stripWardenEntryToSummary(user),
|
|
277
|
+
user: decoration.userTokenData,
|
|
278
|
+
proxy: decoration.proxyUserTokenData,
|
|
279
|
+
globalRoleIds: decoration.globalRoleIds,
|
|
280
|
+
teamRoleMappings: decoration.teamRoleMappings
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const newToken: string = await this.opts.jwtRatchet.createTokenString(wardenToken, decoration.userTokenExpirationSeconds);
|
|
284
|
+
// CAW : We do not use refresh token because we want any user changes to show up in the new token
|
|
285
|
+
//const newToken: string = await this.opts.jwtRatchet.refreshJWTString(cmd.refreshJwtToken, false, expirationSeconds);
|
|
286
|
+
rval = {
|
|
287
|
+
refreshJwtToken: newToken
|
|
288
|
+
};
|
|
289
|
+
} else if (cmd.exportWebAuthnRegistrationEntryForLoggedInUser) {
|
|
290
|
+
rval = {
|
|
291
|
+
exportWebAuthnRegistrationEntryForLoggedInUser: await this.exportWebAuthnRegistrationEntry(cmd.exportWebAuthnRegistrationEntryForLoggedInUser, loggedInUserId)
|
|
292
|
+
};
|
|
293
|
+
} else if (cmd.importWebAuthnRegistrationEntryForLoggedInUser) {
|
|
294
|
+
rval = {
|
|
295
|
+
importWebAuthnRegistrationEntryForLoggedInUser: await this.importWebAuthnRegistrationEntry(cmd.importWebAuthnRegistrationEntryForLoggedInUser, loggedInUserId)
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
} else {
|
|
299
|
+
rval = { error: "No command sent" };
|
|
300
|
+
}
|
|
301
|
+
return rval;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
public async exportWebAuthnRegistrationEntry(origin: string, userId: string): Promise<string> {
|
|
305
|
+
const ent: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
306
|
+
let rval: string = null;
|
|
307
|
+
if (ent) {
|
|
308
|
+
const webAuth: WardenWebAuthnEntry = ent.webAuthnAuthenticators.find(w => w.origin === origin);
|
|
309
|
+
if (webAuth) {
|
|
310
|
+
const oUrl: URL = new URL(origin);
|
|
311
|
+
const rpId: string = oUrl.hostname;
|
|
312
|
+
Logger.debug('Finding challenge for origin %s / RPID %s', origin, rpId);
|
|
313
|
+
const expectedChallenge: string = await this.opts.storageProvider.fetchCurrentUserChallenge(userId, rpId);
|
|
314
|
+
const token: WardenWebAuthnExportToken = {
|
|
315
|
+
entry: webAuth,
|
|
316
|
+
challenge: expectedChallenge
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const s1: string = JSON.stringify(token);
|
|
320
|
+
rval = Base64Ratchet.encodeStringToBase64String(s1);
|
|
321
|
+
} else {
|
|
322
|
+
Logger.warn("Could not export webauthn - no such origin");
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
Logger.warn("Could not export webauthn - no such user id");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return rval;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
public async importWebAuthnRegistrationEntry(token: string, userId: string): Promise<boolean> {
|
|
332
|
+
const ent: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
333
|
+
let rval: boolean = false;
|
|
334
|
+
if (ent) {
|
|
335
|
+
const s1: string = Base64Ratchet.base64StringToString(token);
|
|
336
|
+
const newEntry: WardenWebAuthnExportToken = JSON.parse(s1);
|
|
337
|
+
const old: WardenWebAuthnEntry = ent.webAuthnAuthenticators.find(w => w.origin === newEntry.entry.origin);
|
|
338
|
+
if (old) {
|
|
339
|
+
Logger.warn("Removing existing entry %j", old);
|
|
340
|
+
ent.webAuthnAuthenticators = ent.webAuthnAuthenticators.filter(w => w.origin !== newEntry.entry.origin);
|
|
341
|
+
}
|
|
342
|
+
ent.webAuthnAuthenticators.push(newEntry.entry);
|
|
343
|
+
await this.opts.storageProvider.saveEntry(ent);
|
|
344
|
+
if (newEntry.challenge) {
|
|
345
|
+
await this.opts.storageProvider.updateUserChallenge(userId, newEntry.entry.origin, newEntry.challenge);
|
|
346
|
+
}
|
|
347
|
+
Logger.info("Wrote ok");
|
|
348
|
+
rval = true;
|
|
349
|
+
} else {
|
|
350
|
+
Logger.warn("Could not export webauthn - no such user id");
|
|
351
|
+
}
|
|
352
|
+
return rval;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public urlIsOnAllowedOrigin(url: string): boolean {
|
|
356
|
+
let rval: boolean = false;
|
|
357
|
+
if (url) {
|
|
358
|
+
const u: URL = new URL(url);
|
|
359
|
+
for (let i = 0; i < this.opts.allowedOrigins.length && !rval; i++) {
|
|
360
|
+
const test: URL = new URL(this.opts.allowedOrigins[i]);
|
|
361
|
+
rval = test.origin === u.origin && test.protocol === u.protocol && test.port === u.port;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
return rval;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
public singleUseCodeProvider(
|
|
368
|
+
contact: WardenContact,
|
|
369
|
+
requireMagicLinkSupport: boolean,
|
|
370
|
+
returnNullIfNoProviders?: boolean
|
|
371
|
+
): WardenSingleUseCodeProvider {
|
|
372
|
+
const rval: WardenSingleUseCodeProvider = this.opts.singleUseCodeProviders.find(
|
|
373
|
+
(s) => s.handlesContactType(contact.type) && (!requireMagicLinkSupport || s.createCodeAndSendMagicLink)
|
|
374
|
+
);
|
|
375
|
+
if (!rval && !returnNullIfNoProviders) {
|
|
376
|
+
throw ErrorRatchet.fErr("Cannot find a single use code provider for contact type : %s", contact.type);
|
|
377
|
+
}
|
|
378
|
+
return rval;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public async sendMagicLink(
|
|
382
|
+
contact: WardenContact,
|
|
383
|
+
overrideDestinationContact: WardenContact,
|
|
384
|
+
relyingPartyName: string,
|
|
385
|
+
landingUrl: string,
|
|
386
|
+
metaIn?: Record<string, string>,
|
|
387
|
+
ttlSeconds?: number,
|
|
388
|
+
customTemplate?: WardenCustomTemplateDescriptor
|
|
389
|
+
): Promise<boolean> {
|
|
390
|
+
let rval: boolean = false;
|
|
391
|
+
RequireRatchet.notNullOrUndefined(contact, "contact");
|
|
392
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(landingUrl, "landingUrl");
|
|
393
|
+
RequireRatchet.true(this.urlIsOnAllowedOrigin(landingUrl), "landingUrl is not on an allowed origin for redirect");
|
|
394
|
+
|
|
395
|
+
if (contact?.type && StringRatchet.trimToNull(contact?.value)) {
|
|
396
|
+
const prov: WardenSingleUseCodeProvider = this.singleUseCodeProvider(contact, true);
|
|
397
|
+
rval = await prov.createCodeAndSendMagicLink(
|
|
398
|
+
contact,
|
|
399
|
+
relyingPartyName,
|
|
400
|
+
landingUrl,
|
|
401
|
+
metaIn,
|
|
402
|
+
ttlSeconds,
|
|
403
|
+
overrideDestinationContact,
|
|
404
|
+
customTemplate
|
|
405
|
+
);
|
|
406
|
+
} else {
|
|
407
|
+
ErrorRatchet.throwFormattedErr("Cannot send - invalid contact %j", contact);
|
|
408
|
+
}
|
|
409
|
+
return rval;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
public async createAccountByThirdParty(thirdParty: WardenThirdPartyAuthentication, origin: string, inLabel?: string): Promise<WardenEntry> {
|
|
413
|
+
let rval: WardenEntry = null;
|
|
414
|
+
RequireRatchet.notNullOrUndefined(thirdParty, "thirdParty");
|
|
415
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(thirdParty.thirdParty, "thirdParty");
|
|
416
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(thirdParty.thirdPartyId, "thirdPartyId");
|
|
417
|
+
|
|
418
|
+
const old: WardenEntry = await this.opts.storageProvider.findEntryByThirdPartyId(thirdParty.thirdParty, thirdParty.thirdPartyId);
|
|
419
|
+
if (old) {
|
|
420
|
+
ErrorRatchet.throwFormattedErr("Cannot create - account already exists for %j", thirdParty);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const label: string = inLabel || thirdParty.thirdParty + " " + thirdParty.thirdPartyId;
|
|
424
|
+
const newUser: WardenEntry = new WardenEntryBuilder(label).withThirdPartyAuthentication([thirdParty]).entry;
|
|
425
|
+
rval = await this.opts.storageProvider.saveEntry(newUser);
|
|
426
|
+
|
|
427
|
+
return rval;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Creates a new account, returns the userId for that account upon success
|
|
431
|
+
public async createAccount(contact: WardenContact, origin: string, sendCode?: boolean, label?: string, tags?: string[]): Promise<WardenEntry> {
|
|
432
|
+
let rval: WardenEntry = null;
|
|
433
|
+
if (WardenUtils.validContact(contact)) {
|
|
434
|
+
const old: WardenEntry = await this.opts.storageProvider.findEntryByContact(contact);
|
|
435
|
+
if (old) {
|
|
436
|
+
ErrorRatchet.throwFormattedErr("Cannot create - account already exists for %j", contact);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Defaults to email if nothing provided, usually full name
|
|
440
|
+
const newUser: WardenEntry = new WardenEntryBuilder(label || contact.value).withContacts([contact]).withTags(tags).entry;
|
|
441
|
+
rval = await this.opts.storageProvider.saveEntry(newUser);
|
|
442
|
+
|
|
443
|
+
if (sendCode) {
|
|
444
|
+
Logger.info("New user %j created and send requested - sending", rval);
|
|
445
|
+
await this.sendExpiringValidationToken(contact, origin);
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
ErrorRatchet.throwFormattedErr("Cannot create - invalid contact (missing or invalid fields)");
|
|
449
|
+
}
|
|
450
|
+
return rval;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
public async saveNewUser(newUser: WardenEntry): Promise<WardenEntry> {
|
|
455
|
+
const next: WardenEntry = await this.opts.storageProvider.saveEntry(newUser);
|
|
456
|
+
if (this?.opts?.eventProcessor) {
|
|
457
|
+
await this.opts.eventProcessor.userCreated(next);
|
|
458
|
+
}
|
|
459
|
+
return next;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// For an existing user, add another contact method
|
|
463
|
+
// A given contact (eg, email address, phone number) can only associated with a single
|
|
464
|
+
// userId at a time
|
|
465
|
+
public async addContactMethodToUser(userId: string, contact: WardenContact): Promise<boolean> {
|
|
466
|
+
let rval: boolean = false;
|
|
467
|
+
if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
|
|
468
|
+
const otherUser: WardenEntry = await this.opts.storageProvider.findEntryByContact(contact);
|
|
469
|
+
if (otherUser && otherUser.userId !== userId) {
|
|
470
|
+
ErrorRatchet.throwFormattedErr("Cannot add contact to this user, another user already has that contact");
|
|
471
|
+
}
|
|
472
|
+
const curUser: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
473
|
+
if (!curUser) {
|
|
474
|
+
ErrorRatchet.throwFormattedErr("Cannot add contact to this user, user does not exist");
|
|
475
|
+
}
|
|
476
|
+
curUser.contactMethods.push(contact);
|
|
477
|
+
await this.opts.storageProvider.saveEntry(curUser);
|
|
478
|
+
rval = true;
|
|
479
|
+
} else {
|
|
480
|
+
ErrorRatchet.throwFormattedErr("Cannot add - invalid config : %s %j", userId, contact);
|
|
481
|
+
}
|
|
482
|
+
return rval;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// For an existing user, remove a contact method
|
|
486
|
+
public async removeContactMethodFromUser(userId: string, contact: WardenContact): Promise<WardenEntry> {
|
|
487
|
+
let rval: WardenEntry = null;
|
|
488
|
+
if (StringRatchet.trimToNull(userId) && WardenUtils.validContact(contact)) {
|
|
489
|
+
const curUser: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
490
|
+
if (!curUser) {
|
|
491
|
+
ErrorRatchet.throwFormattedErr("Cannot remove contact from this user, user does not exist");
|
|
492
|
+
}
|
|
493
|
+
curUser.contactMethods = (curUser.contactMethods || []).filter((s) => s.type !== contact.type || s.value !== contact.value);
|
|
494
|
+
if (curUser.contactMethods.length === 0) {
|
|
495
|
+
ErrorRatchet.throwFormattedErr("Cannot remove the last contact method from a user");
|
|
496
|
+
}
|
|
497
|
+
await this.opts.storageProvider.saveEntry(curUser);
|
|
498
|
+
rval = await this.opts.storageProvider.findEntryById(userId);
|
|
499
|
+
} else {
|
|
500
|
+
ErrorRatchet.throwFormattedErr("Cannot add - invalid config : %s %j", userId, contact);
|
|
501
|
+
}
|
|
502
|
+
return rval;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Used as the first step of adding a new WebAuthn device to an existing (logged in) user
|
|
506
|
+
// Server creates a challenge that the device will sign
|
|
507
|
+
public async generateWebAuthnRegistrationChallengeForLoggedInUser(
|
|
508
|
+
userId: string,
|
|
509
|
+
origin: string
|
|
510
|
+
): Promise<PublicKeyCredentialCreationOptionsJSON> {
|
|
511
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
512
|
+
throw new Error("Invalid origin : " + origin);
|
|
513
|
+
}
|
|
514
|
+
const asUrl: URL = new URL(origin);
|
|
515
|
+
const rpID: string = asUrl.hostname;
|
|
516
|
+
|
|
517
|
+
const entry: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
518
|
+
if (!entry) {
|
|
519
|
+
throw ErrorRatchet.fErr("Cannot generateWebAuthnRegistrationChallengeForLoggedInUser - no user %s / %s", userId, origin);
|
|
520
|
+
}
|
|
521
|
+
if (!entry?.webAuthnAuthenticators?.length) {
|
|
522
|
+
Logger.info("Entry has no webAuthnAuthenticators");
|
|
523
|
+
entry.webAuthnAuthenticators = []; // Just in case
|
|
524
|
+
}
|
|
525
|
+
const options = await generateRegistrationOptions({
|
|
526
|
+
rpName: this.opts.relyingPartyName,
|
|
527
|
+
rpID: rpID,
|
|
528
|
+
userID: StringRatchet.stringToUint8Array(entry.userId),
|
|
529
|
+
userName: entry.userLabel,
|
|
530
|
+
// Don't prompt users for additional information about the authenticator
|
|
531
|
+
// (Recommended for smoother UX)
|
|
532
|
+
attestationType: "none",
|
|
533
|
+
// Prevent users from re-registering existing authenticators
|
|
534
|
+
excludeCredentials: entry.webAuthnAuthenticators.map((authenticator) => ({
|
|
535
|
+
id: authenticator.credentialPublicKeyBase64,
|
|
536
|
+
//type: 'public-key',
|
|
537
|
+
// Optional
|
|
538
|
+
transports: authenticator.transports as unknown as AuthenticatorTransportFuture[]
|
|
539
|
+
}))
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
await this.opts.storageProvider.updateUserChallenge(entry.userId, rpID, options.challenge);
|
|
543
|
+
|
|
544
|
+
return options;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// Given a new device's registration, add it to the specified user account as a valid login method
|
|
548
|
+
public async storeAuthnRegistration(
|
|
549
|
+
userId: string,
|
|
550
|
+
origin: string,
|
|
551
|
+
applicationName: string,
|
|
552
|
+
deviceLabel: string,
|
|
553
|
+
data: RegistrationResponseJSON
|
|
554
|
+
): Promise<WardenStoreRegistrationResponse> {
|
|
555
|
+
Logger.info("Store authn data : %j", data);
|
|
556
|
+
let rval: WardenStoreRegistrationResponse = null;
|
|
557
|
+
try {
|
|
558
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
559
|
+
throw new Error("Invalid origin : " + origin);
|
|
560
|
+
}
|
|
561
|
+
const asUrl: URL = new URL(origin);
|
|
562
|
+
const rpID: string = asUrl.hostname;
|
|
563
|
+
|
|
564
|
+
const user: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
565
|
+
if (!user) {
|
|
566
|
+
throw ErrorRatchet.fErr("Cannot storeAuthnRegistration - no user %s / %s", userId, origin);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// (Pseudocode) Get `options.challenge` that was saved above
|
|
570
|
+
const expectedChallenge: string = await this.opts.storageProvider.fetchCurrentUserChallenge(user.userId, rpID);
|
|
571
|
+
|
|
572
|
+
const vrOpts: VerifyRegistrationResponseOpts = {
|
|
573
|
+
response: data,
|
|
574
|
+
expectedChallenge: expectedChallenge,
|
|
575
|
+
expectedOrigin: origin,
|
|
576
|
+
expectedRPID: rpID
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
Logger.info("Calling verifyRegistrationResponse: %j", vrOpts);
|
|
580
|
+
|
|
581
|
+
const verification: VerifiedRegistrationResponse = await verifyRegistrationResponse(vrOpts);
|
|
582
|
+
Logger.info("verifyRegistrationResponse Result : %j", verification);
|
|
583
|
+
|
|
584
|
+
rval = {
|
|
585
|
+
updatedEntry: null,
|
|
586
|
+
registrationResponseId: data.id,
|
|
587
|
+
result: verification.verified ? WardenStoreRegistrationResponseType.Verified : WardenStoreRegistrationResponseType.Failed
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
if (rval.result === WardenStoreRegistrationResponseType.Verified) {
|
|
591
|
+
Logger.info("Storing registration");
|
|
592
|
+
const newAuth: WardenWebAuthnEntry = {
|
|
593
|
+
origin: origin,
|
|
594
|
+
applicationName: applicationName || "Unknown Application",
|
|
595
|
+
deviceLabel: deviceLabel || "Unknown Device",
|
|
596
|
+
counter: verification.registrationInfo.credential.counter,
|
|
597
|
+
credentialBackedUp: verification.registrationInfo.credentialBackedUp,
|
|
598
|
+
credentialDeviceType: verification.registrationInfo.credentialDeviceType,
|
|
599
|
+
credentialIdBase64: verification.registrationInfo.credential.id,
|
|
600
|
+
credentialPublicKeyBase64: Base64Ratchet.uint8ArrayToBase64UrlString(verification.registrationInfo.credential.publicKey)
|
|
601
|
+
//transports: TBD
|
|
602
|
+
};
|
|
603
|
+
|
|
604
|
+
// (Pseudocode) Save the authenticator info so that we can
|
|
605
|
+
// get it by user ID later
|
|
606
|
+
user.webAuthnAuthenticators = (user.webAuthnAuthenticators || []).filter(
|
|
607
|
+
(wa) => wa.credentialIdBase64 !== newAuth.credentialIdBase64
|
|
608
|
+
);
|
|
609
|
+
user.webAuthnAuthenticators.push(newAuth);
|
|
610
|
+
const storedUser: WardenEntry = await this.opts.storageProvider.saveEntry(user);
|
|
611
|
+
rval.updatedEntry = storedUser;
|
|
612
|
+
Logger.info("Stored auth : %j", storedUser);
|
|
613
|
+
}
|
|
614
|
+
} catch (err) {
|
|
615
|
+
rval = {
|
|
616
|
+
registrationResponseId: data.id,
|
|
617
|
+
result: WardenStoreRegistrationResponseType.Error,
|
|
618
|
+
error: ErrorRatchet.safeStringifyErr(err)
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return rval;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
public async generateWebAuthnAuthenticationChallengeForUserId(
|
|
626
|
+
userId: string,
|
|
627
|
+
origin: string
|
|
628
|
+
): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
629
|
+
const user: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
630
|
+
const rval: PublicKeyCredentialRequestOptionsJSON = await this.generateWebAuthnAuthenticationChallenge(user, origin);
|
|
631
|
+
return rval;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// Part of the login process - for a given user, generate the challenge that the device will have to answer
|
|
635
|
+
public async generateWebAuthnAuthenticationChallenge(user: WardenEntry, origin: string): Promise<PublicKeyCredentialRequestOptionsJSON> {
|
|
636
|
+
// (Pseudocode) Retrieve any of the user's previously-registered authenticators
|
|
637
|
+
const userAuthenticators: WardenWebAuthnEntry[] = user.webAuthnAuthenticators;
|
|
638
|
+
if (!origin || !this.opts.allowedOrigins.includes(origin)) {
|
|
639
|
+
throw new Error("Invalid origin : " + origin);
|
|
640
|
+
}
|
|
641
|
+
const asUrl: URL = new URL(origin);
|
|
642
|
+
const rpID: string = asUrl.hostname;
|
|
643
|
+
|
|
644
|
+
const out: any[] = userAuthenticators.map((authenticator) => {
|
|
645
|
+
const next: any = {
|
|
646
|
+
id: authenticator.credentialIdBase64, // Type is Base64URLString
|
|
647
|
+
//type: 'public-key',
|
|
648
|
+
// Optional
|
|
649
|
+
transports: authenticator.transports
|
|
650
|
+
};
|
|
651
|
+
return next;
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
const opts: GenerateAuthenticationOptionsOpts = {
|
|
655
|
+
// Require users to use a previously-registered authenticator
|
|
656
|
+
rpID: rpID,
|
|
657
|
+
allowCredentials: out,
|
|
658
|
+
userVerification: "preferred"
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
const options: PublicKeyCredentialRequestOptionsJSON = await generateAuthenticationOptions(opts);
|
|
662
|
+
|
|
663
|
+
// (Pseudocode) Remember this challenge for this user
|
|
664
|
+
await this.opts.storageProvider.updateUserChallenge(user.userId, rpID, options.challenge);
|
|
665
|
+
|
|
666
|
+
return options;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Send a single use token to this contact
|
|
670
|
+
public async sendExpiringValidationToken(request: WardenContact, origin: string): Promise<boolean> {
|
|
671
|
+
let rval: boolean = false;
|
|
672
|
+
if (request?.type && StringRatchet.trimToNull(request?.value)) {
|
|
673
|
+
const prov: WardenSingleUseCodeProvider = this.singleUseCodeProvider(request, false);
|
|
674
|
+
rval = await prov.createAndSendNewCode(request, this.opts.relyingPartyName, origin);
|
|
675
|
+
} else {
|
|
676
|
+
ErrorRatchet.throwFormattedErr("Cannot send - invalid request %j", request);
|
|
677
|
+
}
|
|
678
|
+
return rval;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Perform a login using one of several methods
|
|
682
|
+
// Delegates to functions that handle the specific methods
|
|
683
|
+
// Should return a valid WardenEntry if successful, or null if not
|
|
684
|
+
// If createUserIfMissing=true, will create a new user if one does not exist
|
|
685
|
+
public async processLogin(request: WardenLoginRequest, origin: string): Promise<WardenEntry | null> {
|
|
686
|
+
Logger.info("Processing login : %s : %j", origin, request);
|
|
687
|
+
let rval: WardenEntry = null;
|
|
688
|
+
const requestErrors: string[] = WardenUtils.loginRequestErrors(request);
|
|
689
|
+
if (requestErrors.length > 0) {
|
|
690
|
+
throw ErrorRatchet.fErr("Invalid login request : %j", requestErrors);
|
|
691
|
+
}
|
|
692
|
+
if (request.type === WardenLoginRequestType.ThirdParty) {
|
|
693
|
+
// ThirdParty is a bit different since token lookup typically gives us the info we'll use
|
|
694
|
+
// to lookup the user
|
|
695
|
+
const provider: WardenThirdPartyAuthenticationProvider = (this.options.thirdPartyAuthenticationProviders ?? [])
|
|
696
|
+
.find(s => s.handlesThirdParty(request.thirdPartyToken.thirdParty));
|
|
697
|
+
if (provider) {
|
|
698
|
+
const auth: WardenThirdPartyAuthentication = await provider.validateTokenAndReturnThirdPartyUserId(request.thirdPartyToken, origin);
|
|
699
|
+
if (auth) {
|
|
700
|
+
rval = await this.opts.storageProvider.findEntryByThirdPartyId(auth.thirdParty, auth.thirdPartyId);
|
|
701
|
+
if (!rval && request.createUserIfMissing) {
|
|
702
|
+
Logger.info("Found no existing user for %j, creating", auth);
|
|
703
|
+
let label: string = auth.thirdParty + " " + auth.thirdPartyId;
|
|
704
|
+
if (provider.extractUserLabelFromAuthentication) {
|
|
705
|
+
label = await provider.extractUserLabelFromAuthentication(auth);
|
|
706
|
+
}
|
|
707
|
+
rval = await this.createAccountByThirdParty(auth, origin, label);
|
|
708
|
+
Logger.info("Finished create, new id is %s", rval.userId);
|
|
709
|
+
}
|
|
710
|
+
} else {
|
|
711
|
+
Logger.warn("Authentication failed for %j", request.thirdPartyToken, request.createUserIfMissing);
|
|
712
|
+
}
|
|
713
|
+
} else {
|
|
714
|
+
Logger.warn("Could not find any provider to handle third party token %j", request.thirdPartyToken);
|
|
715
|
+
}
|
|
716
|
+
} else {
|
|
717
|
+
let user: WardenEntry = StringRatchet.trimToNull(request?.userId)
|
|
718
|
+
? await this.opts.storageProvider.findEntryById(request?.userId)
|
|
719
|
+
: await this.opts.storageProvider.findEntryByContact(request.contact);
|
|
720
|
+
if (!user) {
|
|
721
|
+
Logger.info("User not found, and createUserIfMissing=%s / %j", request.createUserIfMissing, request.contact);
|
|
722
|
+
if (request.createUserIfMissing && request.contact) {
|
|
723
|
+
user = await this.createAccount(request.contact, origin);
|
|
724
|
+
Logger.info("Finished create, new id is %s", user.userId);
|
|
725
|
+
}
|
|
726
|
+
// If STILL no user...
|
|
727
|
+
if (!user) {
|
|
728
|
+
ErrorRatchet.throwFormattedErr("No user found for %j / %s", request?.contact, request?.userId);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
let loginSuccess: boolean = false;
|
|
733
|
+
if (request.webAuthn) {
|
|
734
|
+
loginSuccess = await this.loginWithWebAuthnRequest(user, origin, request.webAuthn);
|
|
735
|
+
} else if (StringRatchet.trimToNull(request.expiringToken)) {
|
|
736
|
+
const prov: WardenSingleUseCodeProvider = this.singleUseCodeProvider(request.contact, false);
|
|
737
|
+
loginSuccess = await prov.checkCode(request.contact.value, request.expiringToken);
|
|
738
|
+
if (!loginSuccess) {
|
|
739
|
+
ErrorRatchet.throwFormattedErr("Cannot login - token is invalid for this user");
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
rval = loginSuccess ? user : null;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
return rval;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// Perform a login using webAuthn
|
|
749
|
+
public async loginWithWebAuthnRequest(user: WardenEntry, origin: string, data: AuthenticationResponseJSON): Promise<boolean> {
|
|
750
|
+
let rval: boolean = false;
|
|
751
|
+
const asUrl: URL = new URL(origin);
|
|
752
|
+
const rpID: string = asUrl.hostname;
|
|
753
|
+
const expectedChallenge: string = await this.opts.storageProvider.fetchCurrentUserChallenge(user.userId, rpID);
|
|
754
|
+
|
|
755
|
+
// (Pseudocode} Retrieve an authenticator from the DB that
|
|
756
|
+
// should match the `id` in the returned credential
|
|
757
|
+
//const b64id: string = Base64Ratchet.base64UrlStringToString(data.id);
|
|
758
|
+
const auth: WardenWebAuthnEntry = (user.webAuthnAuthenticators || []).find((s) => s.credentialIdBase64 === data.id);
|
|
759
|
+
|
|
760
|
+
if (!auth) {
|
|
761
|
+
const allIds: string[] = (user.webAuthnAuthenticators || []).map((s) => s.credentialIdBase64);
|
|
762
|
+
throw ErrorRatchet.fErr("Could not find authenticator %s (%s) for user %s (avail were : %j)", data.id, data.id, user.userId, allIds);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
const vrOpts: VerifyAuthenticationResponseOpts = {
|
|
766
|
+
response: data,
|
|
767
|
+
expectedChallenge,
|
|
768
|
+
expectedOrigin: origin,
|
|
769
|
+
expectedRPID: rpID,
|
|
770
|
+
credential: {
|
|
771
|
+
counter: auth.counter,
|
|
772
|
+
id: auth.credentialIdBase64,
|
|
773
|
+
publicKey: Base64Ratchet.base64UrlStringToBytes(auth.credentialPublicKeyBase64)
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const verification: VerifiedAuthenticationResponse = await verifyAuthenticationResponse(vrOpts);
|
|
778
|
+
|
|
779
|
+
if (verification.verified) {
|
|
780
|
+
rval = true;
|
|
781
|
+
}
|
|
782
|
+
return rval;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Unregisters a device from a given user account
|
|
786
|
+
public async removeSingleWebAuthnRegistration(userId: string, key: string): Promise<WardenEntry> {
|
|
787
|
+
let ent: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
788
|
+
if (ent) {
|
|
789
|
+
ent.webAuthnAuthenticators = (ent.webAuthnAuthenticators || []).filter((s) => s.credentialIdBase64 !== key);
|
|
790
|
+
ent = await this.opts.storageProvider.saveEntry(ent);
|
|
791
|
+
} else {
|
|
792
|
+
Logger.info("Not removing - no such user as %s", userId);
|
|
793
|
+
}
|
|
794
|
+
return ent;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Admin function - pass thru to the storage layer
|
|
798
|
+
public async removeUser(userId: string): Promise<boolean> {
|
|
799
|
+
let rval: boolean = false;
|
|
800
|
+
if (StringRatchet.trimToNull(userId)) {
|
|
801
|
+
const oldUser: WardenEntry = await this.opts.storageProvider.findEntryById(userId);
|
|
802
|
+
if (oldUser) {
|
|
803
|
+
await this.opts.storageProvider.removeEntry(userId);
|
|
804
|
+
if (this?.opts?.eventProcessor) {
|
|
805
|
+
await this.opts.eventProcessor.userRemoved(oldUser);
|
|
806
|
+
}
|
|
807
|
+
rval = true;
|
|
808
|
+
} else {
|
|
809
|
+
Logger.warn("Cannot remove non-existent user : %s", userId);
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
return rval;
|
|
814
|
+
}
|
|
815
|
+
}
|