ngx-webauthn 0.0.2 → 0.2.0
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/README.md +922 -412
- package/fesm2022/ngx-webauthn.mjs +1117 -190
- package/fesm2022/ngx-webauthn.mjs.map +1 -1
- package/index.d.ts +841 -165
- package/package.json +2 -1
|
@@ -1,23 +1,56 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { InjectionToken, inject, Injectable } from '@angular/core';
|
|
3
|
+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
|
|
3
4
|
import { throwError, from } from 'rxjs';
|
|
4
|
-
import { map, catchError } from 'rxjs/operators';
|
|
5
|
+
import { map, catchError, timeout, switchMap } from 'rxjs/operators';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
+
* Type guard utilities for WebAuthn operations
|
|
8
9
|
*
|
|
9
|
-
* These
|
|
10
|
-
*
|
|
11
|
-
* types where possible to reduce duplication and ensure compatibility.
|
|
10
|
+
* These functions help determine the type of input provided to WebAuthn operations,
|
|
11
|
+
* enabling proper handling of different input formats (high-level configs vs native WebAuthn options).
|
|
12
12
|
*/
|
|
13
13
|
/**
|
|
14
|
-
* Type guard to
|
|
14
|
+
* Type guard to determine if input is a high-level RegisterConfig object.
|
|
15
|
+
* Distinguishes between RegisterConfig and direct WebAuthn creation options.
|
|
16
|
+
*
|
|
17
|
+
* @param input The input to check
|
|
18
|
+
* @returns True if the input is a RegisterConfig, false otherwise
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* if (isRegisterConfig(input)) {
|
|
23
|
+
* // Handle high-level config with preset support
|
|
24
|
+
* const options = buildCreationOptionsFromConfig(input, config);
|
|
25
|
+
* } else {
|
|
26
|
+
* // Handle direct WebAuthn options
|
|
27
|
+
* const options = parseRegistrationOptions(input);
|
|
28
|
+
* }
|
|
29
|
+
* ```
|
|
15
30
|
*/
|
|
16
31
|
function isRegisterConfig(input) {
|
|
17
32
|
return typeof input === 'object' && input !== null && 'username' in input;
|
|
18
33
|
}
|
|
19
34
|
/**
|
|
20
|
-
* Type guard to
|
|
35
|
+
* Type guard to determine if input is a high-level AuthenticateConfig object.
|
|
36
|
+
* Distinguishes between AuthenticateConfig and direct WebAuthn request options.
|
|
37
|
+
*
|
|
38
|
+
* Uses the presence of 'username' or 'preset' fields and absence of WebAuthn-specific
|
|
39
|
+
* fields ('rp', 'user') to identify AuthenticateConfig objects.
|
|
40
|
+
*
|
|
41
|
+
* @param input The input to check
|
|
42
|
+
* @returns True if the input is an AuthenticateConfig, false otherwise
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```typescript
|
|
46
|
+
* if (isAuthenticateConfig(input)) {
|
|
47
|
+
* // Handle high-level config with preset support
|
|
48
|
+
* const options = buildRequestOptionsFromConfig(input, config);
|
|
49
|
+
* } else {
|
|
50
|
+
* // Handle direct WebAuthn options
|
|
51
|
+
* const options = parseAuthenticationOptions(input);
|
|
52
|
+
* }
|
|
53
|
+
* ```
|
|
21
54
|
*/
|
|
22
55
|
function isAuthenticateConfig(input) {
|
|
23
56
|
return (typeof input === 'object' &&
|
|
@@ -27,7 +60,20 @@ function isAuthenticateConfig(input) {
|
|
|
27
60
|
!('user' in input)); // WebAuthn options have 'user'
|
|
28
61
|
}
|
|
29
62
|
/**
|
|
30
|
-
* Type guard to check if input
|
|
63
|
+
* Type guard to check if input contains WebAuthn creation options.
|
|
64
|
+
* Identifies objects that have the structure of PublicKeyCredentialCreationOptions
|
|
65
|
+
* by checking for required fields like 'rp' and 'user'.
|
|
66
|
+
*
|
|
67
|
+
* @param input The input to check
|
|
68
|
+
* @returns True if the input has creation options structure, false otherwise
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* ```typescript
|
|
72
|
+
* if (isCreationOptions(input)) {
|
|
73
|
+
* // Input is already in WebAuthn format
|
|
74
|
+
* return navigator.credentials.create({ publicKey: input });
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
31
77
|
*/
|
|
32
78
|
function isCreationOptions(input) {
|
|
33
79
|
return (typeof input === 'object' &&
|
|
@@ -36,7 +82,20 @@ function isCreationOptions(input) {
|
|
|
36
82
|
'user' in input);
|
|
37
83
|
}
|
|
38
84
|
/**
|
|
39
|
-
* Type guard to check if input
|
|
85
|
+
* Type guard to check if input contains WebAuthn request options.
|
|
86
|
+
* Identifies objects that have the structure of PublicKeyCredentialRequestOptions
|
|
87
|
+
* by checking for the required 'challenge' field.
|
|
88
|
+
*
|
|
89
|
+
* @param input The input to check
|
|
90
|
+
* @returns True if the input has request options structure, false otherwise
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```typescript
|
|
94
|
+
* if (isRequestOptions(input)) {
|
|
95
|
+
* // Input is already in WebAuthn format
|
|
96
|
+
* return navigator.credentials.get({ publicKey: input });
|
|
97
|
+
* }
|
|
98
|
+
* ```
|
|
40
99
|
*/
|
|
41
100
|
function isRequestOptions(input) {
|
|
42
101
|
return (typeof input === 'object' &&
|
|
@@ -82,7 +141,7 @@ const PASSKEY_PRESET = {
|
|
|
82
141
|
},
|
|
83
142
|
};
|
|
84
143
|
/**
|
|
85
|
-
* Preset for using
|
|
144
|
+
* Preset for using an external security key as a second factor after a password.
|
|
86
145
|
*
|
|
87
146
|
* Best for: Traditional 2FA scenarios where users already have a password
|
|
88
147
|
* and want to add hardware security key as a second factor.
|
|
@@ -93,7 +152,7 @@ const PASSKEY_PRESET = {
|
|
|
93
152
|
* - Favors cross-platform authenticators (USB/NFC security keys)
|
|
94
153
|
* - Credentials typically not synced between devices
|
|
95
154
|
*/
|
|
96
|
-
const
|
|
155
|
+
const EXTERNAL_SECURITY_KEY_PRESET = {
|
|
97
156
|
...COMMON_PUB_KEY_CRED_PARAMS,
|
|
98
157
|
authenticatorSelection: {
|
|
99
158
|
residentKey: 'discouraged',
|
|
@@ -102,10 +161,10 @@ const SECOND_FACTOR_PRESET = {
|
|
|
102
161
|
},
|
|
103
162
|
};
|
|
104
163
|
/**
|
|
105
|
-
* Preset for high-security, non-synced,
|
|
164
|
+
* Preset for high-security, non-synced, platform authenticator credentials.
|
|
106
165
|
*
|
|
107
166
|
* Best for: High-security scenarios where credentials must stay on a single
|
|
108
|
-
* device and user verification is mandatory.
|
|
167
|
+
* device and user verification is mandatory using built-in platform authenticators.
|
|
109
168
|
*
|
|
110
169
|
* Features:
|
|
111
170
|
* - Requires platform authenticators (built-in biometrics/PIN)
|
|
@@ -113,7 +172,7 @@ const SECOND_FACTOR_PRESET = {
|
|
|
113
172
|
* - Requires user verification (biometric/PIN)
|
|
114
173
|
* - Credentials bound to specific device (no syncing)
|
|
115
174
|
*/
|
|
116
|
-
const
|
|
175
|
+
const PLATFORM_AUTHENTICATOR_PRESET = {
|
|
117
176
|
...COMMON_PUB_KEY_CRED_PARAMS,
|
|
118
177
|
authenticatorSelection: {
|
|
119
178
|
authenticatorAttachment: 'platform',
|
|
@@ -127,8 +186,8 @@ const DEVICE_BOUND_PRESET = {
|
|
|
127
186
|
*/
|
|
128
187
|
const PRESET_MAP = {
|
|
129
188
|
passkey: PASSKEY_PRESET,
|
|
130
|
-
|
|
131
|
-
|
|
189
|
+
externalSecurityKey: EXTERNAL_SECURITY_KEY_PRESET,
|
|
190
|
+
platformAuthenticator: PLATFORM_AUTHENTICATOR_PRESET,
|
|
132
191
|
};
|
|
133
192
|
|
|
134
193
|
/**
|
|
@@ -139,8 +198,21 @@ const PRESET_MAP = {
|
|
|
139
198
|
* WebAuthn options ready for the browser API.
|
|
140
199
|
*/
|
|
141
200
|
/**
|
|
142
|
-
* Deep merge utility that properly handles nested objects
|
|
143
|
-
* Later properties override earlier ones
|
|
201
|
+
* Deep merge utility that properly handles nested objects.
|
|
202
|
+
* Later properties override earlier ones, with recursive merging for nested objects.
|
|
203
|
+
*
|
|
204
|
+
* @param target The target object to merge into
|
|
205
|
+
* @param sources Source objects to merge from (processed left to right)
|
|
206
|
+
* @returns The merged target object
|
|
207
|
+
* @example
|
|
208
|
+
* ```typescript
|
|
209
|
+
* const result = deepMerge(
|
|
210
|
+
* { a: 1, b: { x: 1 } },
|
|
211
|
+
* { b: { y: 2 } },
|
|
212
|
+
* { c: 3 }
|
|
213
|
+
* );
|
|
214
|
+
* // Result: { a: 1, b: { x: 1, y: 2 }, c: 3 }
|
|
215
|
+
* ```
|
|
144
216
|
*/
|
|
145
217
|
function deepMerge(target, ...sources) {
|
|
146
218
|
if (!sources.length)
|
|
@@ -161,27 +233,40 @@ function deepMerge(target, ...sources) {
|
|
|
161
233
|
return deepMerge(target, ...sources);
|
|
162
234
|
}
|
|
163
235
|
/**
|
|
164
|
-
*
|
|
236
|
+
* Type guard to check if a value is a plain object (not array, null, or primitive).
|
|
237
|
+
*
|
|
238
|
+
* @param item The value to check
|
|
239
|
+
* @returns True if the item is a plain object, false otherwise
|
|
165
240
|
*/
|
|
166
241
|
function isObject(item) {
|
|
167
242
|
return item && typeof item === 'object' && !Array.isArray(item);
|
|
168
243
|
}
|
|
169
244
|
/**
|
|
170
|
-
*
|
|
245
|
+
* Generates a cryptographically secure random challenge for WebAuthn operations.
|
|
246
|
+
* Uses the Web Crypto API for secure random number generation.
|
|
247
|
+
*
|
|
248
|
+
* @returns A 32-byte Uint8Array containing the random challenge
|
|
171
249
|
*/
|
|
172
250
|
function generateChallenge$1() {
|
|
173
251
|
return crypto.getRandomValues(new Uint8Array(32));
|
|
174
252
|
}
|
|
175
253
|
/**
|
|
176
|
-
*
|
|
177
|
-
*
|
|
254
|
+
* Generates a unique user ID based on the username.
|
|
255
|
+
* Creates a consistent, URL-safe identifier for the user.
|
|
256
|
+
*
|
|
257
|
+
* @param username The username to generate an ID from
|
|
258
|
+
* @returns A Uint8Array containing the encoded user ID
|
|
178
259
|
*/
|
|
179
260
|
function generateUserId$1(username) {
|
|
180
261
|
return new TextEncoder().encode(username);
|
|
181
262
|
}
|
|
182
263
|
/**
|
|
183
|
-
*
|
|
184
|
-
* Handles
|
|
264
|
+
* Processes and normalizes challenge values from various input formats.
|
|
265
|
+
* Handles string (base64url), Uint8Array, or generates a new challenge if none provided.
|
|
266
|
+
*
|
|
267
|
+
* @param challenge Optional challenge as string or Uint8Array
|
|
268
|
+
* @returns Normalized Uint8Array challenge ready for WebAuthn API
|
|
269
|
+
* @throws {Error} When provided string challenge is not valid base64url
|
|
185
270
|
*/
|
|
186
271
|
function processChallenge(challenge) {
|
|
187
272
|
if (!challenge) {
|
|
@@ -194,8 +279,14 @@ function processChallenge(challenge) {
|
|
|
194
279
|
return challenge;
|
|
195
280
|
}
|
|
196
281
|
/**
|
|
197
|
-
*
|
|
198
|
-
* Handles
|
|
282
|
+
* Processes and normalizes user ID values from various input formats.
|
|
283
|
+
* Handles string (base64url), Uint8Array, or generates from username if none provided.
|
|
284
|
+
*
|
|
285
|
+
* @param userId Optional user ID as string or Uint8Array
|
|
286
|
+
* @param username Username to generate ID from if userId not provided
|
|
287
|
+
* @returns Normalized Uint8Array user ID ready for WebAuthn API
|
|
288
|
+
* @throws {Error} When no userId provided and no username available for generation
|
|
289
|
+
* @throws {Error} When provided string userId is not valid base64url
|
|
199
290
|
*/
|
|
200
291
|
function processUserId(userId, username) {
|
|
201
292
|
if (userId) {
|
|
@@ -212,7 +303,12 @@ function processUserId(userId, username) {
|
|
|
212
303
|
return crypto.getRandomValues(new Uint8Array(16));
|
|
213
304
|
}
|
|
214
305
|
/**
|
|
215
|
-
*
|
|
306
|
+
* Processes and normalizes credential descriptors from various input formats.
|
|
307
|
+
* Converts string credential IDs to proper PublicKeyCredentialDescriptor objects.
|
|
308
|
+
*
|
|
309
|
+
* @param credentials Optional array of credential IDs (strings) or full descriptors
|
|
310
|
+
* @returns Array of normalized PublicKeyCredentialDescriptor objects, or undefined if none provided
|
|
311
|
+
* @throws {Error} When a credential ID string is not valid base64url
|
|
216
312
|
*/
|
|
217
313
|
function processCredentialDescriptors(credentials) {
|
|
218
314
|
if (!credentials || credentials.length === 0) {
|
|
@@ -229,7 +325,18 @@ function processCredentialDescriptors(credentials) {
|
|
|
229
325
|
}));
|
|
230
326
|
}
|
|
231
327
|
/**
|
|
232
|
-
*
|
|
328
|
+
* Resolves a preset configuration by name.
|
|
329
|
+
* Returns the complete preset configuration object for the specified preset.
|
|
330
|
+
*
|
|
331
|
+
* @param presetName The name of the preset to resolve
|
|
332
|
+
* @returns The preset configuration object
|
|
333
|
+
* @throws {Error} When the preset name is not found in PRESET_MAP
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* ```typescript
|
|
337
|
+
* const preset = resolvePreset('passkey');
|
|
338
|
+
* console.log(preset.authenticatorSelection.userVerification); // 'preferred'
|
|
339
|
+
* ```
|
|
233
340
|
*/
|
|
234
341
|
function resolvePreset(presetName) {
|
|
235
342
|
const preset = PRESET_MAP[presetName];
|
|
@@ -239,53 +346,85 @@ function resolvePreset(presetName) {
|
|
|
239
346
|
return preset;
|
|
240
347
|
}
|
|
241
348
|
/**
|
|
242
|
-
*
|
|
243
|
-
*
|
|
349
|
+
* Creates base creation options from WebAuthn service configuration.
|
|
350
|
+
* Establishes default timeout and attestation settings that can be overridden later.
|
|
351
|
+
*
|
|
352
|
+
* @param webAuthnConfig The global WebAuthn service configuration
|
|
353
|
+
* @returns Partial creation options with base settings applied
|
|
244
354
|
*/
|
|
245
|
-
function
|
|
246
|
-
|
|
247
|
-
let options = {
|
|
355
|
+
function createBaseCreationOptions(webAuthnConfig) {
|
|
356
|
+
return {
|
|
248
357
|
timeout: webAuthnConfig.defaultTimeout || 60000,
|
|
249
358
|
attestation: webAuthnConfig.defaultAttestation || 'none',
|
|
250
359
|
};
|
|
251
|
-
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Applies preset configuration to base creation options.
|
|
363
|
+
* Merges preset-specific authenticator selection and public key parameters into the options.
|
|
364
|
+
*
|
|
365
|
+
* @param config The register configuration containing preset information
|
|
366
|
+
* @param baseOptions The base options to apply preset configuration to
|
|
367
|
+
* @param webAuthnConfig The global WebAuthn service configuration
|
|
368
|
+
* @returns Options with preset configuration applied
|
|
369
|
+
*/
|
|
370
|
+
function applyPresetConfiguration(config, baseOptions, webAuthnConfig) {
|
|
252
371
|
if (config.preset) {
|
|
253
372
|
const preset = resolvePreset(config.preset);
|
|
254
|
-
|
|
373
|
+
return deepMerge(baseOptions, {
|
|
255
374
|
authenticatorSelection: preset.authenticatorSelection,
|
|
256
375
|
pubKeyCredParams: [...preset.pubKeyCredParams], // Convert readonly to mutable
|
|
257
376
|
});
|
|
258
377
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
}
|
|
378
|
+
// Apply default authenticator selection from config when no preset
|
|
379
|
+
if (webAuthnConfig.defaultAuthenticatorSelection) {
|
|
380
|
+
return {
|
|
381
|
+
...baseOptions,
|
|
382
|
+
authenticatorSelection: webAuthnConfig.defaultAuthenticatorSelection,
|
|
383
|
+
};
|
|
265
384
|
}
|
|
266
|
-
|
|
385
|
+
return baseOptions;
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Applies user-specified overrides to creation options.
|
|
389
|
+
* Allows users to override any preset or default settings with their own values.
|
|
390
|
+
*
|
|
391
|
+
* @param config The register configuration containing user overrides
|
|
392
|
+
* @param options The options to apply user overrides to
|
|
393
|
+
* @returns Options with user overrides applied
|
|
394
|
+
*/
|
|
395
|
+
function applyUserOverrides(config, options) {
|
|
396
|
+
const result = { ...options };
|
|
267
397
|
if (config.timeout !== undefined) {
|
|
268
|
-
|
|
398
|
+
result.timeout = config.timeout;
|
|
269
399
|
}
|
|
270
400
|
if (config.attestation !== undefined) {
|
|
271
|
-
|
|
401
|
+
result.attestation = config.attestation;
|
|
272
402
|
}
|
|
273
403
|
if (config.authenticatorSelection !== undefined) {
|
|
274
|
-
|
|
404
|
+
result.authenticatorSelection = deepMerge(result.authenticatorSelection || {}, config.authenticatorSelection);
|
|
275
405
|
}
|
|
276
406
|
if (config.pubKeyCredParams !== undefined) {
|
|
277
|
-
|
|
407
|
+
result.pubKeyCredParams = config.pubKeyCredParams;
|
|
278
408
|
}
|
|
279
409
|
if (config.extensions !== undefined) {
|
|
280
|
-
|
|
410
|
+
result.extensions = config.extensions;
|
|
281
411
|
}
|
|
282
|
-
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Assembles final creation options with all required WebAuthn fields.
|
|
416
|
+
* Processes user information, challenge, and applies final service configuration.
|
|
417
|
+
*
|
|
418
|
+
* @param options The partially built options from previous steps
|
|
419
|
+
* @param config The register configuration containing user and RP information
|
|
420
|
+
* @param webAuthnConfig The global WebAuthn service configuration
|
|
421
|
+
* @returns Complete PublicKeyCredentialCreationOptions ready for WebAuthn API
|
|
422
|
+
*/
|
|
423
|
+
function assembleFinalCreationOptions(options, config, webAuthnConfig) {
|
|
283
424
|
const challenge = processChallenge(config.challenge);
|
|
284
425
|
const userId = processUserId(config.userId, config.username);
|
|
285
|
-
// Use relying party from config, with user override capability
|
|
286
426
|
const relyingParty = config.rp || webAuthnConfig.relyingParty;
|
|
287
|
-
|
|
288
|
-
const finalOptions = {
|
|
427
|
+
return {
|
|
289
428
|
...options,
|
|
290
429
|
rp: relyingParty,
|
|
291
430
|
user: {
|
|
@@ -301,11 +440,63 @@ function buildCreationOptionsFromConfig(config, webAuthnConfig) {
|
|
|
301
440
|
],
|
|
302
441
|
excludeCredentials: processCredentialDescriptors(config.excludeCredentials),
|
|
303
442
|
};
|
|
304
|
-
return finalOptions;
|
|
305
443
|
}
|
|
306
444
|
/**
|
|
307
|
-
*
|
|
308
|
-
*
|
|
445
|
+
* Builds complete WebAuthn creation options from a high-level register configuration.
|
|
446
|
+
*
|
|
447
|
+
* This function orchestrates the creation option building process by:
|
|
448
|
+
* 1. Creating base options from service configuration
|
|
449
|
+
* 2. Applying preset-specific settings if specified
|
|
450
|
+
* 3. Applying user overrides for customization
|
|
451
|
+
* 4. Assembling final options with all required fields
|
|
452
|
+
*
|
|
453
|
+
* @param config The high-level register configuration with preset support
|
|
454
|
+
* @param webAuthnConfig The global WebAuthn service configuration
|
|
455
|
+
* @returns Complete PublicKeyCredentialCreationOptions ready for navigator.credentials.create()
|
|
456
|
+
*
|
|
457
|
+
* @example
|
|
458
|
+
* ```typescript
|
|
459
|
+
* const config: RegisterConfig = {
|
|
460
|
+
* preset: 'passkey',
|
|
461
|
+
* user: {
|
|
462
|
+
* id: 'user123',
|
|
463
|
+
* name: 'user@example.com',
|
|
464
|
+
* displayName: 'John Doe'
|
|
465
|
+
* },
|
|
466
|
+
* challenge: 'custom-challenge'
|
|
467
|
+
* };
|
|
468
|
+
*
|
|
469
|
+
* const options = buildCreationOptionsFromConfig(config, webAuthnConfig);
|
|
470
|
+
* // Returns complete creation options ready for WebAuthn API
|
|
471
|
+
* ```
|
|
472
|
+
*/
|
|
473
|
+
function buildCreationOptionsFromConfig(config, webAuthnConfig) {
|
|
474
|
+
const baseOptions = createBaseCreationOptions(webAuthnConfig);
|
|
475
|
+
const presetOptions = applyPresetConfiguration(config, baseOptions, webAuthnConfig);
|
|
476
|
+
const finalOptions = applyUserOverrides(config, presetOptions);
|
|
477
|
+
return assembleFinalCreationOptions(finalOptions, config, webAuthnConfig);
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Builds complete WebAuthn request options from a high-level authenticate configuration.
|
|
481
|
+
*
|
|
482
|
+
* Handles preset resolution, user overrides, and proper field processing to create
|
|
483
|
+
* request options suitable for navigator.credentials.get().
|
|
484
|
+
*
|
|
485
|
+
* @param config The high-level authenticate configuration with preset support
|
|
486
|
+
* @param webAuthnConfig The global WebAuthn service configuration
|
|
487
|
+
* @returns Complete PublicKeyCredentialRequestOptions ready for navigator.credentials.get()
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* const config: AuthenticateConfig = {
|
|
492
|
+
* preset: 'passkey',
|
|
493
|
+
* challenge: 'auth-challenge',
|
|
494
|
+
* allowCredentials: ['cred-id-1', 'cred-id-2']
|
|
495
|
+
* };
|
|
496
|
+
*
|
|
497
|
+
* const options = buildRequestOptionsFromConfig(config, webAuthnConfig);
|
|
498
|
+
* // Returns complete request options ready for WebAuthn API
|
|
499
|
+
* ```
|
|
309
500
|
*/
|
|
310
501
|
function buildRequestOptionsFromConfig(config, webAuthnConfig) {
|
|
311
502
|
// Start with base configuration from WebAuthnConfig
|
|
@@ -344,7 +535,21 @@ function buildRequestOptionsFromConfig(config, webAuthnConfig) {
|
|
|
344
535
|
return finalOptions;
|
|
345
536
|
}
|
|
346
537
|
/**
|
|
347
|
-
*
|
|
538
|
+
* Validates a register configuration for completeness and correctness.
|
|
539
|
+
* Ensures all required fields are present and properly formatted.
|
|
540
|
+
*
|
|
541
|
+
* @param config The register configuration to validate
|
|
542
|
+
* @throws {Error} When required fields are missing or invalid
|
|
543
|
+
*
|
|
544
|
+
* @example
|
|
545
|
+
* ```typescript
|
|
546
|
+
* try {
|
|
547
|
+
* validateRegisterConfig(config);
|
|
548
|
+
* // Config is valid, proceed with registration
|
|
549
|
+
* } catch (error) {
|
|
550
|
+
* console.error('Invalid register config:', error.message);
|
|
551
|
+
* }
|
|
552
|
+
* ```
|
|
348
553
|
*/
|
|
349
554
|
function validateRegisterConfig(config) {
|
|
350
555
|
if (!config.username || typeof config.username !== 'string') {
|
|
@@ -355,7 +560,21 @@ function validateRegisterConfig(config) {
|
|
|
355
560
|
}
|
|
356
561
|
}
|
|
357
562
|
/**
|
|
358
|
-
*
|
|
563
|
+
* Validates an authenticate configuration for completeness and correctness.
|
|
564
|
+
* Ensures all required fields are present and properly formatted.
|
|
565
|
+
*
|
|
566
|
+
* @param config The authenticate configuration to validate
|
|
567
|
+
* @throws {Error} When required fields are missing or invalid
|
|
568
|
+
*
|
|
569
|
+
* @example
|
|
570
|
+
* ```typescript
|
|
571
|
+
* try {
|
|
572
|
+
* validateAuthenticateConfig(config);
|
|
573
|
+
* // Config is valid, proceed with authentication
|
|
574
|
+
* } catch (error) {
|
|
575
|
+
* console.error('Invalid authenticate config:', error.message);
|
|
576
|
+
* }
|
|
577
|
+
* ```
|
|
359
578
|
*/
|
|
360
579
|
function validateAuthenticateConfig(config) {
|
|
361
580
|
if (config.preset && !PRESET_MAP[config.preset]) {
|
|
@@ -367,6 +586,10 @@ function validateAuthenticateConfig(config) {
|
|
|
367
586
|
* Enhanced WebAuthn Error Classes
|
|
368
587
|
* Provides specific, actionable error types for better developer experience
|
|
369
588
|
*/
|
|
589
|
+
/**
|
|
590
|
+
* Enumeration of WebAuthn error types for categorizing different failure scenarios.
|
|
591
|
+
* Provides semantic error classification for better error handling and user experience.
|
|
592
|
+
*/
|
|
370
593
|
var WebAuthnErrorType;
|
|
371
594
|
(function (WebAuthnErrorType) {
|
|
372
595
|
WebAuthnErrorType["NOT_SUPPORTED"] = "NOT_SUPPORTED";
|
|
@@ -377,24 +600,61 @@ var WebAuthnErrorType;
|
|
|
377
600
|
WebAuthnErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
378
601
|
WebAuthnErrorType["SECURITY_ERROR"] = "SECURITY_ERROR";
|
|
379
602
|
WebAuthnErrorType["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
|
|
603
|
+
WebAuthnErrorType["REMOTE_ENDPOINT_ERROR"] = "REMOTE_ENDPOINT_ERROR";
|
|
604
|
+
WebAuthnErrorType["INVALID_REMOTE_OPTIONS"] = "INVALID_REMOTE_OPTIONS";
|
|
380
605
|
WebAuthnErrorType["UNKNOWN"] = "UNKNOWN";
|
|
381
606
|
})(WebAuthnErrorType || (WebAuthnErrorType = {}));
|
|
382
607
|
/**
|
|
383
|
-
* Base WebAuthn error class
|
|
608
|
+
* Base WebAuthn error class that provides enhanced error information.
|
|
609
|
+
* All WebAuthn-specific errors extend from this class for consistent error handling.
|
|
610
|
+
*
|
|
611
|
+
* @example
|
|
612
|
+
* ```typescript
|
|
613
|
+
* try {
|
|
614
|
+
* await webAuthnService.register(config);
|
|
615
|
+
* } catch (error) {
|
|
616
|
+
* if (error instanceof WebAuthnError) {
|
|
617
|
+
* console.log('Error type:', error.type);
|
|
618
|
+
* console.log('Original error:', error.originalError);
|
|
619
|
+
* }
|
|
620
|
+
* }
|
|
621
|
+
* ```
|
|
384
622
|
*/
|
|
385
623
|
class WebAuthnError extends Error {
|
|
386
624
|
type;
|
|
387
625
|
originalError;
|
|
626
|
+
/**
|
|
627
|
+
* Creates a new WebAuthnError instance.
|
|
628
|
+
*
|
|
629
|
+
* @param type The semantic error type
|
|
630
|
+
* @param message Human-readable error message
|
|
631
|
+
* @param originalError The original error that caused this WebAuthn error (optional)
|
|
632
|
+
*/
|
|
388
633
|
constructor(type, message, originalError) {
|
|
389
634
|
super(message);
|
|
390
635
|
this.type = type;
|
|
391
636
|
this.originalError = originalError;
|
|
392
637
|
this.name = 'WebAuthnError';
|
|
393
638
|
}
|
|
639
|
+
/**
|
|
640
|
+
* Factory method to create WebAuthnError from DOMException.
|
|
641
|
+
* Maps browser DOMException types to semantic WebAuthn error types.
|
|
642
|
+
*
|
|
643
|
+
* @param error The DOMException thrown by WebAuthn API
|
|
644
|
+
* @returns Appropriate WebAuthnError subclass based on the DOMException type
|
|
645
|
+
*/
|
|
394
646
|
static fromDOMException(error) {
|
|
395
647
|
const type = WebAuthnError.mapDOMExceptionToType(error.name);
|
|
396
648
|
return new WebAuthnError(type, error.message, error);
|
|
397
649
|
}
|
|
650
|
+
/**
|
|
651
|
+
* Maps DOMException names to WebAuthn error types.
|
|
652
|
+
* Provides semantic classification of browser-level errors.
|
|
653
|
+
*
|
|
654
|
+
* @param name The DOMException name
|
|
655
|
+
* @returns Corresponding WebAuthnErrorType
|
|
656
|
+
* @private
|
|
657
|
+
*/
|
|
398
658
|
static mapDOMExceptionToType(name) {
|
|
399
659
|
switch (name) {
|
|
400
660
|
case 'NotSupportedError':
|
|
@@ -417,72 +677,268 @@ class WebAuthnError extends Error {
|
|
|
417
677
|
}
|
|
418
678
|
}
|
|
419
679
|
/**
|
|
420
|
-
* Error thrown when user cancels
|
|
680
|
+
* Error thrown when the user cancels a WebAuthn operation.
|
|
681
|
+
* This is the most common error and typically requires no action from the developer.
|
|
682
|
+
*
|
|
683
|
+
* @example
|
|
684
|
+
* ```typescript
|
|
685
|
+
* try {
|
|
686
|
+
* await webAuthnService.register(config);
|
|
687
|
+
* } catch (error) {
|
|
688
|
+
* if (error instanceof UserCancelledError) {
|
|
689
|
+
* // User chose not to proceed - this is normal behavior
|
|
690
|
+
* console.log('User cancelled the operation');
|
|
691
|
+
* }
|
|
692
|
+
* }
|
|
693
|
+
* ```
|
|
421
694
|
*/
|
|
422
695
|
class UserCancelledError extends WebAuthnError {
|
|
696
|
+
/**
|
|
697
|
+
* Creates a new UserCancelledError.
|
|
698
|
+
*
|
|
699
|
+
* @param originalError The original DOMException that triggered this error (optional)
|
|
700
|
+
*/
|
|
423
701
|
constructor(originalError) {
|
|
424
702
|
super(WebAuthnErrorType.USER_CANCELLED, 'User cancelled the WebAuthn operation', originalError);
|
|
425
703
|
this.name = 'UserCancelledError';
|
|
426
704
|
}
|
|
427
705
|
}
|
|
428
706
|
/**
|
|
429
|
-
* Error thrown when there's an issue with the authenticator
|
|
707
|
+
* Error thrown when there's an issue with the authenticator device.
|
|
708
|
+
* This could indicate hardware problems, invalid state, or other device-specific issues.
|
|
709
|
+
*
|
|
710
|
+
* @example
|
|
711
|
+
* ```typescript
|
|
712
|
+
* try {
|
|
713
|
+
* await webAuthnService.authenticate(config);
|
|
714
|
+
* } catch (error) {
|
|
715
|
+
* if (error instanceof AuthenticatorError) {
|
|
716
|
+
* // Show user-friendly message about trying again or using different authenticator
|
|
717
|
+
* console.log('Authenticator issue:', error.message);
|
|
718
|
+
* }
|
|
719
|
+
* }
|
|
720
|
+
* ```
|
|
430
721
|
*/
|
|
431
722
|
class AuthenticatorError extends WebAuthnError {
|
|
723
|
+
/**
|
|
724
|
+
* Creates a new AuthenticatorError.
|
|
725
|
+
*
|
|
726
|
+
* @param message Descriptive error message
|
|
727
|
+
* @param originalError The original error that caused this authenticator error (optional)
|
|
728
|
+
*/
|
|
432
729
|
constructor(message, originalError) {
|
|
433
730
|
super(WebAuthnErrorType.AUTHENTICATOR_ERROR, `Authenticator error: ${message}`, originalError);
|
|
434
731
|
this.name = 'AuthenticatorError';
|
|
435
732
|
}
|
|
436
733
|
}
|
|
437
734
|
/**
|
|
438
|
-
* Error thrown when the provided options are invalid
|
|
735
|
+
* Error thrown when the provided WebAuthn options are invalid or malformed.
|
|
736
|
+
* This typically indicates a programming error in option construction.
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```typescript
|
|
740
|
+
* try {
|
|
741
|
+
* await webAuthnService.register(invalidConfig);
|
|
742
|
+
* } catch (error) {
|
|
743
|
+
* if (error instanceof InvalidOptionsError) {
|
|
744
|
+
* // Check your configuration and options
|
|
745
|
+
* console.error('Invalid options provided:', error.message);
|
|
746
|
+
* }
|
|
747
|
+
* }
|
|
748
|
+
* ```
|
|
439
749
|
*/
|
|
440
750
|
class InvalidOptionsError extends WebAuthnError {
|
|
751
|
+
/**
|
|
752
|
+
* Creates a new InvalidOptionsError.
|
|
753
|
+
*
|
|
754
|
+
* @param message Descriptive error message explaining what's invalid
|
|
755
|
+
* @param originalError The original error that revealed the invalid options (optional)
|
|
756
|
+
*/
|
|
441
757
|
constructor(message, originalError) {
|
|
442
758
|
super(WebAuthnErrorType.INVALID_OPTIONS, `Invalid options: ${message}`, originalError);
|
|
443
759
|
this.name = 'InvalidOptionsError';
|
|
444
760
|
}
|
|
445
761
|
}
|
|
446
762
|
/**
|
|
447
|
-
* Error thrown when
|
|
763
|
+
* Error thrown when a WebAuthn operation is not supported in the current environment.
|
|
764
|
+
* This could be due to browser limitations or missing hardware capabilities.
|
|
765
|
+
*
|
|
766
|
+
* @example
|
|
767
|
+
* ```typescript
|
|
768
|
+
* if (!webAuthnService.isSupported()) {
|
|
769
|
+
* // Handle unsupported environment
|
|
770
|
+
* }
|
|
771
|
+
*
|
|
772
|
+
* try {
|
|
773
|
+
* await webAuthnService.register(config);
|
|
774
|
+
* } catch (error) {
|
|
775
|
+
* if (error instanceof UnsupportedOperationError) {
|
|
776
|
+
* // Show fallback authentication method
|
|
777
|
+
* console.log('WebAuthn not supported, using fallback');
|
|
778
|
+
* }
|
|
779
|
+
* }
|
|
780
|
+
* ```
|
|
448
781
|
*/
|
|
449
782
|
class UnsupportedOperationError extends WebAuthnError {
|
|
783
|
+
/**
|
|
784
|
+
* Creates a new UnsupportedOperationError.
|
|
785
|
+
*
|
|
786
|
+
* @param message Descriptive error message explaining what's not supported
|
|
787
|
+
* @param originalError The original error that indicated lack of support (optional)
|
|
788
|
+
*/
|
|
450
789
|
constructor(message, originalError) {
|
|
451
790
|
super(WebAuthnErrorType.UNSUPPORTED_OPERATION, `Unsupported operation: ${message}`, originalError);
|
|
452
791
|
this.name = 'UnsupportedOperationError';
|
|
453
792
|
}
|
|
454
793
|
}
|
|
455
794
|
/**
|
|
456
|
-
* Error thrown when there's a network-related issue
|
|
795
|
+
* Error thrown when there's a network-related issue during WebAuthn operations.
|
|
796
|
+
* This is rare but can occur in certain network conditions.
|
|
797
|
+
*
|
|
798
|
+
* @example
|
|
799
|
+
* ```typescript
|
|
800
|
+
* try {
|
|
801
|
+
* await webAuthnService.authenticate(config);
|
|
802
|
+
* } catch (error) {
|
|
803
|
+
* if (error instanceof NetworkError) {
|
|
804
|
+
* // Suggest user check connection and retry
|
|
805
|
+
* console.log('Network issue during authentication:', error.message);
|
|
806
|
+
* }
|
|
807
|
+
* }
|
|
808
|
+
* ```
|
|
457
809
|
*/
|
|
458
810
|
class NetworkError extends WebAuthnError {
|
|
811
|
+
/**
|
|
812
|
+
* Creates a new NetworkError.
|
|
813
|
+
*
|
|
814
|
+
* @param message Descriptive error message about the network issue
|
|
815
|
+
* @param originalError The original network-related error (optional)
|
|
816
|
+
*/
|
|
459
817
|
constructor(message, originalError) {
|
|
460
818
|
super(WebAuthnErrorType.NETWORK_ERROR, `Network error: ${message}`, originalError);
|
|
461
819
|
this.name = 'NetworkError';
|
|
462
820
|
}
|
|
463
821
|
}
|
|
464
822
|
/**
|
|
465
|
-
* Error thrown when
|
|
823
|
+
* Error thrown when a security violation occurs during WebAuthn operations.
|
|
824
|
+
* This typically indicates issues with origin validation or other security checks.
|
|
825
|
+
*
|
|
826
|
+
* @example
|
|
827
|
+
* ```typescript
|
|
828
|
+
* try {
|
|
829
|
+
* await webAuthnService.register(config);
|
|
830
|
+
* } catch (error) {
|
|
831
|
+
* if (error instanceof SecurityError) {
|
|
832
|
+
* // Security issue - check origin, HTTPS, etc.
|
|
833
|
+
* console.error('Security violation:', error.message);
|
|
834
|
+
* }
|
|
835
|
+
* }
|
|
836
|
+
* ```
|
|
466
837
|
*/
|
|
467
838
|
class SecurityError extends WebAuthnError {
|
|
839
|
+
/**
|
|
840
|
+
* Creates a new SecurityError.
|
|
841
|
+
*
|
|
842
|
+
* @param message Descriptive error message about the security issue
|
|
843
|
+
* @param originalError The original security-related error (optional)
|
|
844
|
+
*/
|
|
468
845
|
constructor(message, originalError) {
|
|
469
846
|
super(WebAuthnErrorType.SECURITY_ERROR, `Security error: ${message}`, originalError);
|
|
470
847
|
this.name = 'SecurityError';
|
|
471
848
|
}
|
|
472
849
|
}
|
|
473
850
|
/**
|
|
474
|
-
* Error thrown when
|
|
851
|
+
* Error thrown when a WebAuthn operation times out.
|
|
852
|
+
* This can happen when the user takes too long to interact with their authenticator.
|
|
853
|
+
*
|
|
854
|
+
* @example
|
|
855
|
+
* ```typescript
|
|
856
|
+
* try {
|
|
857
|
+
* await webAuthnService.register(config);
|
|
858
|
+
* } catch (error) {
|
|
859
|
+
* if (error instanceof TimeoutError) {
|
|
860
|
+
* // Suggest user try again and respond more quickly
|
|
861
|
+
* console.log('Operation timed out - please try again');
|
|
862
|
+
* }
|
|
863
|
+
* }
|
|
864
|
+
* ```
|
|
475
865
|
*/
|
|
476
866
|
class TimeoutError extends WebAuthnError {
|
|
867
|
+
/**
|
|
868
|
+
* Creates a new TimeoutError.
|
|
869
|
+
*
|
|
870
|
+
* @param message Descriptive error message about the timeout
|
|
871
|
+
* @param originalError The original timeout-related error (optional)
|
|
872
|
+
*/
|
|
477
873
|
constructor(message, originalError) {
|
|
478
874
|
super(WebAuthnErrorType.TIMEOUT_ERROR, `Timeout error: ${message}`, originalError);
|
|
479
875
|
this.name = 'TimeoutError';
|
|
480
876
|
}
|
|
481
877
|
}
|
|
878
|
+
/**
|
|
879
|
+
* Error thrown when remote endpoint request fails.
|
|
880
|
+
* Includes network errors, HTTP errors, and timeout errors.
|
|
881
|
+
*
|
|
882
|
+
* @example
|
|
883
|
+
* ```typescript
|
|
884
|
+
* try {
|
|
885
|
+
* await webAuthnService.registerRemote(request);
|
|
886
|
+
* } catch (error) {
|
|
887
|
+
* if (error instanceof RemoteEndpointError) {
|
|
888
|
+
* console.log('Endpoint:', error.context.url);
|
|
889
|
+
* console.log('Status:', error.context.status);
|
|
890
|
+
* }
|
|
891
|
+
* }
|
|
892
|
+
* ```
|
|
893
|
+
*/
|
|
894
|
+
class RemoteEndpointError extends WebAuthnError {
|
|
895
|
+
context;
|
|
896
|
+
/**
|
|
897
|
+
* Creates a new RemoteEndpointError.
|
|
898
|
+
*
|
|
899
|
+
* @param message Descriptive error message about the remote endpoint failure
|
|
900
|
+
* @param context Contextual information about the failed request
|
|
901
|
+
* @param originalError The original error that caused this remote endpoint error (optional)
|
|
902
|
+
*/
|
|
903
|
+
constructor(message, context, originalError) {
|
|
904
|
+
super(WebAuthnErrorType.REMOTE_ENDPOINT_ERROR, `Remote endpoint error: ${message}`, originalError);
|
|
905
|
+
this.context = context;
|
|
906
|
+
this.name = 'RemoteEndpointError';
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Error thrown when remote server returns invalid WebAuthn options.
|
|
911
|
+
* This indicates the server response doesn't match expected WebAuthn option format.
|
|
912
|
+
*
|
|
913
|
+
* @example
|
|
914
|
+
* ```typescript
|
|
915
|
+
* try {
|
|
916
|
+
* await webAuthnService.registerRemote(request);
|
|
917
|
+
* } catch (error) {
|
|
918
|
+
* if (error instanceof InvalidRemoteOptionsError) {
|
|
919
|
+
* console.log('Invalid server response:', error.message);
|
|
920
|
+
* }
|
|
921
|
+
* }
|
|
922
|
+
* ```
|
|
923
|
+
*/
|
|
924
|
+
class InvalidRemoteOptionsError extends WebAuthnError {
|
|
925
|
+
/**
|
|
926
|
+
* Creates a new InvalidRemoteOptionsError.
|
|
927
|
+
*
|
|
928
|
+
* @param message Descriptive error message about the invalid remote options
|
|
929
|
+
* @param originalError The original error that revealed the invalid options (optional)
|
|
930
|
+
*/
|
|
931
|
+
constructor(message, originalError) {
|
|
932
|
+
super(WebAuthnErrorType.INVALID_REMOTE_OPTIONS, `Invalid remote options: ${message}`, originalError);
|
|
933
|
+
this.name = 'InvalidRemoteOptionsError';
|
|
934
|
+
}
|
|
935
|
+
}
|
|
482
936
|
|
|
483
937
|
/**
|
|
484
|
-
* WebAuthn
|
|
485
|
-
*
|
|
938
|
+
* Service-level configuration for WebAuthn operations
|
|
939
|
+
*
|
|
940
|
+
* These interfaces define the global configuration that affects the entire WebAuthn service,
|
|
941
|
+
* including relying party information and default settings.
|
|
486
942
|
*/
|
|
487
943
|
/**
|
|
488
944
|
* Default configuration for WebAuthn service
|
|
@@ -707,6 +1163,202 @@ function isPublicKeyCredential(credential) {
|
|
|
707
1163
|
return credential !== null && credential.type === 'public-key';
|
|
708
1164
|
}
|
|
709
1165
|
|
|
1166
|
+
/**
|
|
1167
|
+
* Utility functions for remote WebAuthn option validation
|
|
1168
|
+
*
|
|
1169
|
+
* These utilities provide comprehensive validation of server responses
|
|
1170
|
+
* to ensure they contain valid WebAuthn options before processing.
|
|
1171
|
+
* All validation is done without external dependencies for minimal
|
|
1172
|
+
* bundle size impact.
|
|
1173
|
+
*/
|
|
1174
|
+
/**
|
|
1175
|
+
* Validates that server response contains valid WebAuthn creation options.
|
|
1176
|
+
* Performs essential validation without external dependencies.
|
|
1177
|
+
*
|
|
1178
|
+
* @param options Response from registration endpoint
|
|
1179
|
+
* @throws {InvalidRemoteOptionsError} When options are invalid or incomplete
|
|
1180
|
+
*
|
|
1181
|
+
* @example
|
|
1182
|
+
* ```typescript
|
|
1183
|
+
* try {
|
|
1184
|
+
* validateRemoteCreationOptions(serverResponse);
|
|
1185
|
+
* // Options are valid, proceed with registration
|
|
1186
|
+
* } catch (error) {
|
|
1187
|
+
* console.error('Invalid server response:', error.message);
|
|
1188
|
+
* }
|
|
1189
|
+
* ```
|
|
1190
|
+
*/
|
|
1191
|
+
function validateRemoteCreationOptions(options) {
|
|
1192
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1193
|
+
throw new InvalidRemoteOptionsError('Response must be an object');
|
|
1194
|
+
}
|
|
1195
|
+
const opts = options;
|
|
1196
|
+
// Validate relying party (required)
|
|
1197
|
+
if (!opts.rp || typeof opts.rp !== 'object') {
|
|
1198
|
+
throw new InvalidRemoteOptionsError('Missing or invalid rp (relying party) field');
|
|
1199
|
+
}
|
|
1200
|
+
if (typeof opts.rp.name !== 'string' || opts.rp.name.trim() === '') {
|
|
1201
|
+
throw new InvalidRemoteOptionsError('rp.name must be a non-empty string');
|
|
1202
|
+
}
|
|
1203
|
+
if (opts.rp.id !== undefined && typeof opts.rp.id !== 'string') {
|
|
1204
|
+
throw new InvalidRemoteOptionsError('rp.id must be a string when provided');
|
|
1205
|
+
}
|
|
1206
|
+
// Validate user (required)
|
|
1207
|
+
if (!opts.user || typeof opts.user !== 'object') {
|
|
1208
|
+
throw new InvalidRemoteOptionsError('Missing or invalid user field');
|
|
1209
|
+
}
|
|
1210
|
+
if (typeof opts.user.id !== 'string' || opts.user.id.trim() === '') {
|
|
1211
|
+
throw new InvalidRemoteOptionsError('user.id must be a non-empty base64url string');
|
|
1212
|
+
}
|
|
1213
|
+
if (typeof opts.user.name !== 'string' || opts.user.name.trim() === '') {
|
|
1214
|
+
throw new InvalidRemoteOptionsError('user.name must be a non-empty string');
|
|
1215
|
+
}
|
|
1216
|
+
if (typeof opts.user.displayName !== 'string' ||
|
|
1217
|
+
opts.user.displayName.trim() === '') {
|
|
1218
|
+
throw new InvalidRemoteOptionsError('user.displayName must be a non-empty string');
|
|
1219
|
+
}
|
|
1220
|
+
// Validate challenge (required)
|
|
1221
|
+
if (!opts.challenge ||
|
|
1222
|
+
typeof opts.challenge !== 'string' ||
|
|
1223
|
+
opts.challenge.trim() === '') {
|
|
1224
|
+
throw new InvalidRemoteOptionsError('Missing or invalid challenge field - must be a non-empty base64url string');
|
|
1225
|
+
}
|
|
1226
|
+
// Validate pubKeyCredParams (required)
|
|
1227
|
+
if (!Array.isArray(opts.pubKeyCredParams)) {
|
|
1228
|
+
throw new InvalidRemoteOptionsError('pubKeyCredParams must be an array');
|
|
1229
|
+
}
|
|
1230
|
+
if (opts.pubKeyCredParams.length === 0) {
|
|
1231
|
+
throw new InvalidRemoteOptionsError('pubKeyCredParams cannot be empty');
|
|
1232
|
+
}
|
|
1233
|
+
// Validate each pubKeyCredParams entry
|
|
1234
|
+
for (let i = 0; i < opts.pubKeyCredParams.length; i++) {
|
|
1235
|
+
const param = opts.pubKeyCredParams[i];
|
|
1236
|
+
if (!param || typeof param !== 'object') {
|
|
1237
|
+
throw new InvalidRemoteOptionsError(`pubKeyCredParams[${i}] must be an object`);
|
|
1238
|
+
}
|
|
1239
|
+
if (param.type !== 'public-key') {
|
|
1240
|
+
throw new InvalidRemoteOptionsError(`pubKeyCredParams[${i}].type must be "public-key"`);
|
|
1241
|
+
}
|
|
1242
|
+
if (typeof param.alg !== 'number') {
|
|
1243
|
+
throw new InvalidRemoteOptionsError(`pubKeyCredParams[${i}].alg must be a number`);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
// Validate optional fields if present
|
|
1247
|
+
if (opts.timeout !== undefined &&
|
|
1248
|
+
(typeof opts.timeout !== 'number' || opts.timeout <= 0)) {
|
|
1249
|
+
throw new InvalidRemoteOptionsError('timeout must be a positive number when provided');
|
|
1250
|
+
}
|
|
1251
|
+
if (opts.attestation !== undefined &&
|
|
1252
|
+
!['none', 'indirect', 'direct', 'enterprise'].includes(opts.attestation)) {
|
|
1253
|
+
throw new InvalidRemoteOptionsError('attestation must be one of: none, indirect, direct, enterprise');
|
|
1254
|
+
}
|
|
1255
|
+
// Validate authenticatorSelection if present
|
|
1256
|
+
if (opts.authenticatorSelection !== undefined) {
|
|
1257
|
+
if (typeof opts.authenticatorSelection !== 'object') {
|
|
1258
|
+
throw new InvalidRemoteOptionsError('authenticatorSelection must be an object when provided');
|
|
1259
|
+
}
|
|
1260
|
+
const authSel = opts.authenticatorSelection;
|
|
1261
|
+
if (authSel.authenticatorAttachment !== undefined &&
|
|
1262
|
+
!['platform', 'cross-platform'].includes(authSel.authenticatorAttachment)) {
|
|
1263
|
+
throw new InvalidRemoteOptionsError('authenticatorAttachment must be "platform" or "cross-platform"');
|
|
1264
|
+
}
|
|
1265
|
+
if (authSel.userVerification !== undefined &&
|
|
1266
|
+
!['required', 'preferred', 'discouraged'].includes(authSel.userVerification)) {
|
|
1267
|
+
throw new InvalidRemoteOptionsError('userVerification must be "required", "preferred", or "discouraged"');
|
|
1268
|
+
}
|
|
1269
|
+
if (authSel.residentKey !== undefined &&
|
|
1270
|
+
!['discouraged', 'preferred', 'required'].includes(authSel.residentKey)) {
|
|
1271
|
+
throw new InvalidRemoteOptionsError('residentKey must be "discouraged", "preferred", or "required"');
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
// Validate excludeCredentials if present
|
|
1275
|
+
if (opts.excludeCredentials !== undefined) {
|
|
1276
|
+
if (!Array.isArray(opts.excludeCredentials)) {
|
|
1277
|
+
throw new InvalidRemoteOptionsError('excludeCredentials must be an array when provided');
|
|
1278
|
+
}
|
|
1279
|
+
for (let i = 0; i < opts.excludeCredentials.length; i++) {
|
|
1280
|
+
const cred = opts.excludeCredentials[i];
|
|
1281
|
+
if (!cred || typeof cred !== 'object') {
|
|
1282
|
+
throw new InvalidRemoteOptionsError(`excludeCredentials[${i}] must be an object`);
|
|
1283
|
+
}
|
|
1284
|
+
if (cred.type !== 'public-key') {
|
|
1285
|
+
throw new InvalidRemoteOptionsError(`excludeCredentials[${i}].type must be "public-key"`);
|
|
1286
|
+
}
|
|
1287
|
+
if (typeof cred.id !== 'string' || cred.id.trim() === '') {
|
|
1288
|
+
throw new InvalidRemoteOptionsError(`excludeCredentials[${i}].id must be a non-empty base64url string`);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
/**
|
|
1294
|
+
* Validates that server response contains valid WebAuthn request options.
|
|
1295
|
+
* Performs essential validation without external dependencies.
|
|
1296
|
+
*
|
|
1297
|
+
* @param options Response from authentication endpoint
|
|
1298
|
+
* @throws {InvalidRemoteOptionsError} When options are invalid or incomplete
|
|
1299
|
+
*
|
|
1300
|
+
* @example
|
|
1301
|
+
* ```typescript
|
|
1302
|
+
* try {
|
|
1303
|
+
* validateRemoteRequestOptions(serverResponse);
|
|
1304
|
+
* // Options are valid, proceed with authentication
|
|
1305
|
+
* } catch (error) {
|
|
1306
|
+
* console.error('Invalid server response:', error.message);
|
|
1307
|
+
* }
|
|
1308
|
+
* ```
|
|
1309
|
+
*/
|
|
1310
|
+
function validateRemoteRequestOptions(options) {
|
|
1311
|
+
if (!options || typeof options !== 'object' || Array.isArray(options)) {
|
|
1312
|
+
throw new InvalidRemoteOptionsError('Response must be an object');
|
|
1313
|
+
}
|
|
1314
|
+
const opts = options;
|
|
1315
|
+
// Validate challenge (required)
|
|
1316
|
+
if (!opts.challenge ||
|
|
1317
|
+
typeof opts.challenge !== 'string' ||
|
|
1318
|
+
opts.challenge.trim() === '') {
|
|
1319
|
+
throw new InvalidRemoteOptionsError('Missing or invalid challenge field - must be a non-empty base64url string');
|
|
1320
|
+
}
|
|
1321
|
+
// Validate optional fields if present
|
|
1322
|
+
if (opts.timeout !== undefined &&
|
|
1323
|
+
(typeof opts.timeout !== 'number' || opts.timeout <= 0)) {
|
|
1324
|
+
throw new InvalidRemoteOptionsError('timeout must be a positive number when provided');
|
|
1325
|
+
}
|
|
1326
|
+
if (opts.userVerification !== undefined &&
|
|
1327
|
+
!['required', 'preferred', 'discouraged'].includes(opts.userVerification)) {
|
|
1328
|
+
throw new InvalidRemoteOptionsError('userVerification must be "required", "preferred", or "discouraged"');
|
|
1329
|
+
}
|
|
1330
|
+
// Validate allowCredentials if present
|
|
1331
|
+
if (opts.allowCredentials !== undefined) {
|
|
1332
|
+
if (!Array.isArray(opts.allowCredentials)) {
|
|
1333
|
+
throw new InvalidRemoteOptionsError('allowCredentials must be an array when provided');
|
|
1334
|
+
}
|
|
1335
|
+
for (let i = 0; i < opts.allowCredentials.length; i++) {
|
|
1336
|
+
const cred = opts.allowCredentials[i];
|
|
1337
|
+
if (!cred || typeof cred !== 'object') {
|
|
1338
|
+
throw new InvalidRemoteOptionsError(`allowCredentials[${i}] must be an object`);
|
|
1339
|
+
}
|
|
1340
|
+
if (cred.type !== 'public-key') {
|
|
1341
|
+
throw new InvalidRemoteOptionsError(`allowCredentials[${i}].type must be "public-key"`);
|
|
1342
|
+
}
|
|
1343
|
+
if (typeof cred.id !== 'string' || cred.id.trim() === '') {
|
|
1344
|
+
throw new InvalidRemoteOptionsError(`allowCredentials[${i}].id must be a non-empty base64url string`);
|
|
1345
|
+
}
|
|
1346
|
+
// Validate transports if present
|
|
1347
|
+
if (cred.transports !== undefined) {
|
|
1348
|
+
if (!Array.isArray(cred.transports)) {
|
|
1349
|
+
throw new InvalidRemoteOptionsError(`allowCredentials[${i}].transports must be an array when provided`);
|
|
1350
|
+
}
|
|
1351
|
+
const validTransports = ['usb', 'nfc', 'ble', 'internal'];
|
|
1352
|
+
for (let j = 0; j < cred.transports.length; j++) {
|
|
1353
|
+
if (!validTransports.includes(cred.transports[j])) {
|
|
1354
|
+
throw new InvalidRemoteOptionsError(`allowCredentials[${i}].transports[${j}] must be one of: ${validTransports.join(', ')}`);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
710
1362
|
/**
|
|
711
1363
|
* Enhanced WebAuthn Service
|
|
712
1364
|
*
|
|
@@ -724,14 +1376,35 @@ function isPublicKeyCredential(credential) {
|
|
|
724
1376
|
*/
|
|
725
1377
|
class WebAuthnService {
|
|
726
1378
|
config = inject(WEBAUTHN_CONFIG);
|
|
1379
|
+
http = inject(HttpClient);
|
|
727
1380
|
/**
|
|
728
|
-
* Checks if WebAuthn is supported in the current browser
|
|
1381
|
+
* Checks if WebAuthn is supported in the current browser environment.
|
|
1382
|
+
*
|
|
1383
|
+
* @returns True if WebAuthn is supported, false otherwise
|
|
1384
|
+
* @example
|
|
1385
|
+
* ```typescript
|
|
1386
|
+
* if (this.webAuthnService.isSupported()) {
|
|
1387
|
+
* // Proceed with WebAuthn operations
|
|
1388
|
+
* } else {
|
|
1389
|
+
* // Show fallback authentication method
|
|
1390
|
+
* }
|
|
1391
|
+
* ```
|
|
729
1392
|
*/
|
|
730
1393
|
isSupported() {
|
|
731
1394
|
return isWebAuthnSupported();
|
|
732
1395
|
}
|
|
733
1396
|
/**
|
|
734
|
-
* Gets
|
|
1397
|
+
* Gets detailed WebAuthn support information for the current browser.
|
|
1398
|
+
* Provides information about available authenticator types and capabilities.
|
|
1399
|
+
*
|
|
1400
|
+
* @returns Observable containing WebAuthn support details
|
|
1401
|
+
* @example
|
|
1402
|
+
* ```typescript
|
|
1403
|
+
* this.webAuthnService.getSupport().subscribe(support => {
|
|
1404
|
+
* console.log('Platform authenticator:', support.platformAuthenticator);
|
|
1405
|
+
* console.log('Cross-platform authenticator:', support.crossPlatformAuthenticator);
|
|
1406
|
+
* });
|
|
1407
|
+
* ```
|
|
735
1408
|
*/
|
|
736
1409
|
getSupport() {
|
|
737
1410
|
if (!this.isSupported()) {
|
|
@@ -744,40 +1417,57 @@ class WebAuthnService {
|
|
|
744
1417
|
})), catchError((error) => throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, 'Failed to check WebAuthn support', error))));
|
|
745
1418
|
}
|
|
746
1419
|
/**
|
|
747
|
-
* Registers a new WebAuthn credential
|
|
1420
|
+
* Registers a new WebAuthn credential for a user.
|
|
748
1421
|
*
|
|
749
|
-
*
|
|
750
|
-
*
|
|
1422
|
+
* Supports two input formats:
|
|
1423
|
+
* 1. High-level RegisterConfig with preset support and automatic option building
|
|
1424
|
+
* 2. Direct PublicKeyCredentialCreationOptions for full control
|
|
751
1425
|
*
|
|
752
|
-
* @
|
|
753
|
-
*
|
|
754
|
-
* // Simple preset usage
|
|
755
|
-
* this.webAuthnService.register({ username: 'john.doe', preset: 'passkey' });
|
|
1426
|
+
* @param input Either a RegisterConfig object or raw WebAuthn creation options
|
|
1427
|
+
* @returns Observable containing the registration response with credential details
|
|
756
1428
|
*
|
|
757
|
-
*
|
|
758
|
-
*
|
|
759
|
-
*
|
|
760
|
-
*
|
|
761
|
-
*
|
|
762
|
-
* }
|
|
1429
|
+
* @throws {UnsupportedOperationError} When WebAuthn is not supported
|
|
1430
|
+
* @throws {InvalidOptionsError} When provided options are invalid
|
|
1431
|
+
* @throws {UserCancelledError} When user cancels the registration
|
|
1432
|
+
* @throws {AuthenticatorError} When authenticator encounters an error
|
|
1433
|
+
* @throws {TimeoutError} When the operation times out
|
|
1434
|
+
* @throws {SecurityError} When a security violation occurs
|
|
763
1435
|
*
|
|
764
|
-
*
|
|
765
|
-
*
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* user: {
|
|
769
|
-
*
|
|
1436
|
+
* @example Using high-level config:
|
|
1437
|
+
* ```typescript
|
|
1438
|
+
* const config: RegisterConfig = {
|
|
1439
|
+
* preset: 'passkey',
|
|
1440
|
+
* user: {
|
|
1441
|
+
* id: 'user123',
|
|
1442
|
+
* name: 'user@example.com',
|
|
1443
|
+
* displayName: 'John Doe'
|
|
1444
|
+
* },
|
|
1445
|
+
* challenge: 'random-challenge'
|
|
770
1446
|
* };
|
|
771
|
-
* this.webAuthnService.register(nativeOptions);
|
|
772
1447
|
*
|
|
773
|
-
*
|
|
774
|
-
*
|
|
775
|
-
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
1448
|
+
* this.webAuthnService.register(config).subscribe({
|
|
1449
|
+
* next: (response) => {
|
|
1450
|
+
* console.log('Registration successful:', response.credential.id);
|
|
1451
|
+
* },
|
|
1452
|
+
* error: (error) => {
|
|
1453
|
+
* if (error instanceof UserCancelledError) {
|
|
1454
|
+
* console.log('User cancelled registration');
|
|
1455
|
+
* }
|
|
1456
|
+
* }
|
|
1457
|
+
* });
|
|
1458
|
+
* ```
|
|
1459
|
+
*
|
|
1460
|
+
* @example Using direct options:
|
|
1461
|
+
* ```typescript
|
|
1462
|
+
* const options: PublicKeyCredentialCreationOptions = {
|
|
1463
|
+
* rp: { name: "Example Corp" },
|
|
1464
|
+
* user: { id: new Uint8Array([1,2,3]), name: "user@example.com", displayName: "User" },
|
|
1465
|
+
* challenge: new Uint8Array([4,5,6]),
|
|
778
1466
|
* pubKeyCredParams: [{ type: "public-key", alg: -7 }]
|
|
779
1467
|
* };
|
|
780
|
-
* this.webAuthnService.register(
|
|
1468
|
+
* this.webAuthnService.register(options).subscribe(response => {
|
|
1469
|
+
* // Handle response
|
|
1470
|
+
* });
|
|
781
1471
|
* ```
|
|
782
1472
|
*/
|
|
783
1473
|
register(input) {
|
|
@@ -803,42 +1493,51 @@ class WebAuthnService {
|
|
|
803
1493
|
}
|
|
804
1494
|
}
|
|
805
1495
|
/**
|
|
806
|
-
* Authenticates using an existing WebAuthn credential
|
|
1496
|
+
* Authenticates a user using an existing WebAuthn credential.
|
|
807
1497
|
*
|
|
808
|
-
*
|
|
809
|
-
*
|
|
1498
|
+
* Supports two input formats:
|
|
1499
|
+
* 1. High-level AuthenticateConfig with preset support and automatic option building
|
|
1500
|
+
* 2. Direct PublicKeyCredentialRequestOptions for full control
|
|
810
1501
|
*
|
|
811
|
-
* @
|
|
812
|
-
*
|
|
813
|
-
* // Simple preset usage
|
|
814
|
-
* this.webAuthnService.authenticate({ preset: 'passkey' });
|
|
1502
|
+
* @param input Either an AuthenticateConfig object or raw WebAuthn request options
|
|
1503
|
+
* @returns Observable containing the authentication response with assertion details
|
|
815
1504
|
*
|
|
816
|
-
*
|
|
817
|
-
*
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
* }
|
|
1505
|
+
* @throws {UnsupportedOperationError} When WebAuthn is not supported
|
|
1506
|
+
* @throws {InvalidOptionsError} When provided options are invalid
|
|
1507
|
+
* @throws {UserCancelledError} When user cancels the authentication
|
|
1508
|
+
* @throws {AuthenticatorError} When authenticator encounters an error
|
|
1509
|
+
* @throws {TimeoutError} When the operation times out
|
|
1510
|
+
* @throws {SecurityError} When a security violation occurs
|
|
822
1511
|
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
827
|
-
*
|
|
828
|
-
*
|
|
829
|
-
* }]
|
|
1512
|
+
* @example Using high-level config:
|
|
1513
|
+
* ```typescript
|
|
1514
|
+
* const config: AuthenticateConfig = {
|
|
1515
|
+
* preset: 'passkey',
|
|
1516
|
+
* challenge: 'auth-challenge',
|
|
1517
|
+
* allowCredentials: ['credential-id-1', 'credential-id-2']
|
|
830
1518
|
* };
|
|
831
|
-
* this.webAuthnService.authenticate(jsonOptions);
|
|
832
1519
|
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
1520
|
+
* this.webAuthnService.authenticate(config).subscribe({
|
|
1521
|
+
* next: (response) => {
|
|
1522
|
+
* console.log('Authentication successful:', response.credential.id);
|
|
1523
|
+
* },
|
|
1524
|
+
* error: (error) => {
|
|
1525
|
+
* if (error instanceof UserCancelledError) {
|
|
1526
|
+
* console.log('User cancelled authentication');
|
|
1527
|
+
* }
|
|
1528
|
+
* }
|
|
1529
|
+
* });
|
|
1530
|
+
* ```
|
|
1531
|
+
*
|
|
1532
|
+
* @example Using direct options:
|
|
1533
|
+
* ```typescript
|
|
1534
|
+
* const options: PublicKeyCredentialRequestOptions = {
|
|
1535
|
+
* challenge: new Uint8Array([1,2,3]),
|
|
1536
|
+
* allowCredentials: [{ type: 'public-key', id: new Uint8Array([4,5,6]) }]
|
|
840
1537
|
* };
|
|
841
|
-
* this.webAuthnService.authenticate(
|
|
1538
|
+
* this.webAuthnService.authenticate(options).subscribe(response => {
|
|
1539
|
+
* // Handle response
|
|
1540
|
+
* });
|
|
842
1541
|
* ```
|
|
843
1542
|
*/
|
|
844
1543
|
authenticate(input) {
|
|
@@ -848,12 +1547,10 @@ class WebAuthnService {
|
|
|
848
1547
|
try {
|
|
849
1548
|
let requestOptions;
|
|
850
1549
|
if (isAuthenticateConfig(input)) {
|
|
851
|
-
// High-level config path: validate, resolve preset, build options
|
|
852
1550
|
validateAuthenticateConfig(input);
|
|
853
1551
|
requestOptions = buildRequestOptionsFromConfig(input, this.config);
|
|
854
1552
|
}
|
|
855
1553
|
else {
|
|
856
|
-
// Direct options path: use provided options
|
|
857
1554
|
requestOptions = input;
|
|
858
1555
|
}
|
|
859
1556
|
const parsedOptions = this.parseAuthenticationOptions(requestOptions);
|
|
@@ -864,72 +1561,273 @@ class WebAuthnService {
|
|
|
864
1561
|
}
|
|
865
1562
|
}
|
|
866
1563
|
/**
|
|
867
|
-
*
|
|
1564
|
+
* Registers a new WebAuthn credential using options fetched from a remote server.
|
|
1565
|
+
*
|
|
1566
|
+
* Sends request data via POST to the configured registration endpoint,
|
|
1567
|
+
* receives PublicKeyCredentialCreationOptionsJSON, then proceeds with
|
|
1568
|
+
* standard WebAuthn registration flow.
|
|
1569
|
+
*
|
|
1570
|
+
* @param request Data to send to server (can be any object your server expects)
|
|
1571
|
+
* @template T Type constraint for the request payload
|
|
1572
|
+
* @returns Observable containing the registration response
|
|
1573
|
+
*
|
|
1574
|
+
* @throws {InvalidOptionsError} When remote registration endpoint is not configured
|
|
1575
|
+
* @throws {RemoteEndpointError} When server request fails
|
|
1576
|
+
* @throws {InvalidRemoteOptionsError} When server returns invalid options
|
|
1577
|
+
*
|
|
1578
|
+
* @example
|
|
1579
|
+
* ```typescript
|
|
1580
|
+
* // Simple request
|
|
1581
|
+
* this.webAuthnService.registerRemote({
|
|
1582
|
+
* username: 'john.doe@example.com'
|
|
1583
|
+
* }).subscribe(response => console.log('Success:', response));
|
|
1584
|
+
*
|
|
1585
|
+
* // Typed request with additional context
|
|
1586
|
+
* interface ServerPayload {
|
|
1587
|
+
* tenantId: string;
|
|
1588
|
+
* department: string;
|
|
1589
|
+
* }
|
|
1590
|
+
*
|
|
1591
|
+
* this.webAuthnService.registerRemote<ServerPayload>({
|
|
1592
|
+
* tenantId: 'acme-corp',
|
|
1593
|
+
* department: 'engineering'
|
|
1594
|
+
* }).subscribe(response => console.log('Success:', response));
|
|
1595
|
+
* ```
|
|
1596
|
+
*/
|
|
1597
|
+
registerRemote(request) {
|
|
1598
|
+
this.validateRemoteRegistrationConfig();
|
|
1599
|
+
const endpoint = this.config.remoteEndpoints.registration;
|
|
1600
|
+
const timeoutMs = this.config.remoteEndpoints?.requestOptions?.timeout || 10000;
|
|
1601
|
+
return this.http
|
|
1602
|
+
.post(endpoint, request)
|
|
1603
|
+
.pipe(timeout(timeoutMs), map((options) => {
|
|
1604
|
+
validateRemoteCreationOptions(options);
|
|
1605
|
+
return options;
|
|
1606
|
+
}), switchMap((options) => this.register(options)), catchError((error) => this.handleRemoteError(error, endpoint, 'registration')));
|
|
1607
|
+
}
|
|
1608
|
+
/**
|
|
1609
|
+
* Authenticates using WebAuthn with options fetched from a remote server.
|
|
1610
|
+
*
|
|
1611
|
+
* Sends request data via POST to the configured authentication endpoint,
|
|
1612
|
+
* receives PublicKeyCredentialRequestOptionsJSON, then proceeds with
|
|
1613
|
+
* standard WebAuthn authentication flow.
|
|
1614
|
+
*
|
|
1615
|
+
* @param request Optional data to send to server (defaults to empty object)
|
|
1616
|
+
* @template T Type constraint for the request payload
|
|
1617
|
+
* @returns Observable containing the authentication response
|
|
1618
|
+
*
|
|
1619
|
+
* @throws {InvalidOptionsError} When remote authentication endpoint is not configured
|
|
1620
|
+
* @throws {RemoteEndpointError} When server request fails
|
|
1621
|
+
* @throws {InvalidRemoteOptionsError} When server returns invalid options
|
|
1622
|
+
*
|
|
1623
|
+
* @example
|
|
1624
|
+
* ```typescript
|
|
1625
|
+
* // Simple request
|
|
1626
|
+
* this.webAuthnService.authenticateRemote({
|
|
1627
|
+
* username: 'john.doe@example.com'
|
|
1628
|
+
* }).subscribe(response => console.log('Success:', response));
|
|
1629
|
+
*
|
|
1630
|
+
* // Request with no payload (server uses session/context)
|
|
1631
|
+
* this.webAuthnService.authenticateRemote()
|
|
1632
|
+
* .subscribe(response => console.log('Success:', response));
|
|
1633
|
+
* ```
|
|
1634
|
+
*/
|
|
1635
|
+
authenticateRemote(request = {}) {
|
|
1636
|
+
this.validateRemoteAuthenticationConfig();
|
|
1637
|
+
const endpoint = this.config.remoteEndpoints.authentication;
|
|
1638
|
+
const timeoutMs = this.config.remoteEndpoints?.requestOptions?.timeout || 10000;
|
|
1639
|
+
return this.http
|
|
1640
|
+
.post(endpoint, request)
|
|
1641
|
+
.pipe(timeout(timeoutMs), map((options) => {
|
|
1642
|
+
validateRemoteRequestOptions(options);
|
|
1643
|
+
return options;
|
|
1644
|
+
}), switchMap((options) => this.authenticate(options)), catchError((error) => this.handleRemoteError(error, endpoint, 'authentication')));
|
|
1645
|
+
}
|
|
1646
|
+
/**
|
|
1647
|
+
* Validates that remote registration endpoint is configured
|
|
1648
|
+
* @private
|
|
1649
|
+
*/
|
|
1650
|
+
validateRemoteRegistrationConfig() {
|
|
1651
|
+
if (!this.config.remoteEndpoints?.registration) {
|
|
1652
|
+
throw new InvalidOptionsError('Remote registration endpoint not configured. Add remoteEndpoints.registration to your WebAuthn config.');
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
/**
|
|
1656
|
+
* Validates that remote authentication endpoint is configured
|
|
1657
|
+
* @private
|
|
1658
|
+
*/
|
|
1659
|
+
validateRemoteAuthenticationConfig() {
|
|
1660
|
+
if (!this.config.remoteEndpoints?.authentication) {
|
|
1661
|
+
throw new InvalidOptionsError('Remote authentication endpoint not configured. Add remoteEndpoints.authentication to your WebAuthn config.');
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
/**
|
|
1665
|
+
* Handles errors from remote HTTP requests
|
|
1666
|
+
* @private
|
|
1667
|
+
*/
|
|
1668
|
+
handleRemoteError(error, url, operation) {
|
|
1669
|
+
if (error instanceof HttpErrorResponse) {
|
|
1670
|
+
const context = {
|
|
1671
|
+
url,
|
|
1672
|
+
method: 'POST',
|
|
1673
|
+
operation,
|
|
1674
|
+
status: error.status,
|
|
1675
|
+
statusText: error.statusText,
|
|
1676
|
+
};
|
|
1677
|
+
return throwError(() => new RemoteEndpointError(`${operation} request failed`, context, error));
|
|
1678
|
+
}
|
|
1679
|
+
// Preserve InvalidRemoteOptionsError instances (validation errors)
|
|
1680
|
+
if (error instanceof InvalidRemoteOptionsError) {
|
|
1681
|
+
return throwError(() => error);
|
|
1682
|
+
}
|
|
1683
|
+
return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unexpected error during remote ${operation}`, error));
|
|
1684
|
+
}
|
|
1685
|
+
/**
|
|
1686
|
+
* Parses and normalizes registration options from either native or JSON format.
|
|
1687
|
+
* Converts base64url-encoded strings to Uint8Array where necessary.
|
|
1688
|
+
*
|
|
1689
|
+
* @param options Registration options in either native or JSON format
|
|
1690
|
+
* @returns Normalized PublicKeyCredentialCreationOptions for browser API
|
|
1691
|
+
* @private
|
|
868
1692
|
*/
|
|
869
1693
|
parseRegistrationOptions(options) {
|
|
870
1694
|
if (isJSONOptions(options)) {
|
|
871
|
-
// Use native browser function for JSON options
|
|
872
1695
|
return PublicKeyCredential.parseCreationOptionsFromJSON(options);
|
|
873
1696
|
}
|
|
874
1697
|
else {
|
|
875
|
-
// Options are already in native format
|
|
876
1698
|
return options;
|
|
877
1699
|
}
|
|
878
1700
|
}
|
|
879
1701
|
/**
|
|
880
|
-
* Parses authentication options
|
|
1702
|
+
* Parses and normalizes authentication options from either native or JSON format.
|
|
1703
|
+
* Converts base64url-encoded strings to Uint8Array where necessary.
|
|
1704
|
+
*
|
|
1705
|
+
* @param options Authentication options in either native or JSON format
|
|
1706
|
+
* @returns Normalized PublicKeyCredentialRequestOptions for browser API
|
|
1707
|
+
* @private
|
|
881
1708
|
*/
|
|
882
1709
|
parseAuthenticationOptions(options) {
|
|
883
1710
|
if (isJSONOptions(options)) {
|
|
884
|
-
// Use native browser function for JSON options
|
|
885
1711
|
return PublicKeyCredential.parseRequestOptionsFromJSON(options);
|
|
886
1712
|
}
|
|
887
1713
|
else {
|
|
888
|
-
// Options are already in native format
|
|
889
1714
|
return options;
|
|
890
1715
|
}
|
|
891
1716
|
}
|
|
892
1717
|
/**
|
|
893
|
-
*
|
|
1718
|
+
* Validates that the credential is a valid PublicKeyCredential.
|
|
1719
|
+
*
|
|
1720
|
+
* @param credential Raw credential from navigator.credentials.create()
|
|
1721
|
+
* @returns Validated PublicKeyCredential
|
|
1722
|
+
* @throws {AuthenticatorError} When credential is invalid or null
|
|
1723
|
+
* @private
|
|
894
1724
|
*/
|
|
895
|
-
|
|
1725
|
+
validateRegistrationCredential(credential) {
|
|
896
1726
|
if (!isPublicKeyCredential(credential)) {
|
|
897
1727
|
throw new AuthenticatorError('No credential returned from authenticator');
|
|
898
1728
|
}
|
|
1729
|
+
return credential;
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Extracts basic credential information (ID and transports).
|
|
1733
|
+
*
|
|
1734
|
+
* @param credential Validated PublicKeyCredential
|
|
1735
|
+
* @returns Object containing credential ID and supported transports
|
|
1736
|
+
* @private
|
|
1737
|
+
*/
|
|
1738
|
+
extractCredentialInfo(credential) {
|
|
899
1739
|
const response = credential.response;
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
1740
|
+
return {
|
|
1741
|
+
credentialId: arrayBufferToBase64url(credential.rawId),
|
|
1742
|
+
transports: (response.getTransports?.() ||
|
|
1743
|
+
[]),
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
/**
|
|
1747
|
+
* Safely extracts the public key with proper error handling.
|
|
1748
|
+
*
|
|
1749
|
+
* @param response AuthenticatorAttestationResponse
|
|
1750
|
+
* @returns Public key as base64url string, or undefined if extraction fails
|
|
1751
|
+
* @private
|
|
1752
|
+
*/
|
|
1753
|
+
extractPublicKey(response) {
|
|
905
1754
|
try {
|
|
906
1755
|
const publicKeyBuffer = response.getPublicKey?.();
|
|
907
1756
|
if (publicKeyBuffer) {
|
|
908
|
-
|
|
1757
|
+
return arrayBufferToBase64url(publicKeyBuffer);
|
|
909
1758
|
}
|
|
910
1759
|
}
|
|
911
1760
|
catch {
|
|
912
1761
|
// Public key extraction failed - this is okay, not all algorithms are supported
|
|
1762
|
+
// Some authenticators or algorithms don't provide extractable public keys
|
|
913
1763
|
}
|
|
914
|
-
|
|
915
|
-
|
|
1764
|
+
return undefined;
|
|
1765
|
+
}
|
|
1766
|
+
/**
|
|
1767
|
+
* Creates the complete raw WebAuthn response for advanced use cases.
|
|
1768
|
+
* Provides access to all WebAuthn data including attestation objects and metadata.
|
|
1769
|
+
*
|
|
1770
|
+
* @param credential Validated PublicKeyCredential
|
|
1771
|
+
* @param credentialId Already extracted credential ID
|
|
1772
|
+
* @param publicKey Extracted public key (may be undefined)
|
|
1773
|
+
* @returns Complete WebAuthnRegistrationResult with all WebAuthn data
|
|
1774
|
+
* @private
|
|
1775
|
+
*/
|
|
1776
|
+
createRawRegistrationResponse(credential, credentialId, publicKey) {
|
|
1777
|
+
const response = credential.response;
|
|
1778
|
+
return {
|
|
916
1779
|
credentialId,
|
|
917
|
-
publicKey: publicKey ||
|
|
918
|
-
arrayBufferToBase64url(response.getPublicKey?.() || new ArrayBuffer(0)),
|
|
1780
|
+
publicKey: publicKey || arrayBufferToBase64url(new ArrayBuffer(0)), // Clean fallback
|
|
919
1781
|
attestationObject: arrayBufferToBase64url(response.attestationObject),
|
|
920
1782
|
clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
|
|
921
|
-
transports:
|
|
1783
|
+
transports: (response.getTransports?.() ||
|
|
1784
|
+
[]),
|
|
922
1785
|
};
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Assembles the final registration response.
|
|
1789
|
+
* Combines all extracted data into the final response format.
|
|
1790
|
+
*
|
|
1791
|
+
* @param credentialInfo Basic credential information
|
|
1792
|
+
* @param publicKey Extracted public key
|
|
1793
|
+
* @param rawResponse Raw WebAuthn response
|
|
1794
|
+
* @returns Complete RegistrationResponse
|
|
1795
|
+
* @private
|
|
1796
|
+
*/
|
|
1797
|
+
assembleRegistrationResponse(credentialInfo, publicKey, rawResponse) {
|
|
923
1798
|
return {
|
|
924
1799
|
success: true,
|
|
925
|
-
credentialId,
|
|
1800
|
+
credentialId: credentialInfo.credentialId,
|
|
926
1801
|
publicKey,
|
|
927
|
-
transports,
|
|
1802
|
+
transports: credentialInfo.transports,
|
|
928
1803
|
rawResponse,
|
|
929
1804
|
};
|
|
930
1805
|
}
|
|
931
1806
|
/**
|
|
932
|
-
* Processes the
|
|
1807
|
+
* Processes the result of a WebAuthn registration operation.
|
|
1808
|
+
* Converts the browser credential response into a structured RegistrationResponse.
|
|
1809
|
+
*
|
|
1810
|
+
* @param credential The credential returned by navigator.credentials.create()
|
|
1811
|
+
* @returns Structured registration response with parsed credential data
|
|
1812
|
+
* @throws {AuthenticatorError} When credential creation fails or returns null
|
|
1813
|
+
* @private
|
|
1814
|
+
*/
|
|
1815
|
+
processRegistrationResult(credential) {
|
|
1816
|
+
const validCredential = this.validateRegistrationCredential(credential);
|
|
1817
|
+
const credentialInfo = this.extractCredentialInfo(validCredential);
|
|
1818
|
+
const response = validCredential.response;
|
|
1819
|
+
const publicKey = this.extractPublicKey(response);
|
|
1820
|
+
const rawResponse = this.createRawRegistrationResponse(validCredential, credentialInfo.credentialId, publicKey);
|
|
1821
|
+
return this.assembleRegistrationResponse(credentialInfo, publicKey, rawResponse);
|
|
1822
|
+
}
|
|
1823
|
+
/**
|
|
1824
|
+
* Processes the result of a WebAuthn authentication operation.
|
|
1825
|
+
* Converts the browser credential response into a structured AuthenticationResponse.
|
|
1826
|
+
*
|
|
1827
|
+
* @param credential The credential returned by navigator.credentials.get()
|
|
1828
|
+
* @returns Structured authentication response with parsed assertion data
|
|
1829
|
+
* @throws {AuthenticatorError} When authentication fails or returns null
|
|
1830
|
+
* @private
|
|
933
1831
|
*/
|
|
934
1832
|
processAuthenticationResult(credential) {
|
|
935
1833
|
if (!isPublicKeyCredential(credential)) {
|
|
@@ -941,7 +1839,7 @@ class WebAuthnService {
|
|
|
941
1839
|
if (response.userHandle) {
|
|
942
1840
|
userHandle = arrayBufferToBase64url(response.userHandle);
|
|
943
1841
|
}
|
|
944
|
-
// Create the raw response for
|
|
1842
|
+
// Create the complete raw response for advanced use cases
|
|
945
1843
|
const rawResponse = {
|
|
946
1844
|
credentialId,
|
|
947
1845
|
authenticatorData: arrayBufferToBase64url(response.authenticatorData),
|
|
@@ -957,36 +1855,81 @@ class WebAuthnService {
|
|
|
957
1855
|
};
|
|
958
1856
|
}
|
|
959
1857
|
/**
|
|
960
|
-
*
|
|
1858
|
+
* Central error handling dispatcher for WebAuthn operations.
|
|
1859
|
+
* Routes different error types to specialized handlers for proper error classification.
|
|
1860
|
+
*
|
|
1861
|
+
* @param error The error to handle (can be DOMException, TypeError, or any other error)
|
|
1862
|
+
* @returns Observable that throws an appropriate WebAuthnError subclass
|
|
1863
|
+
* @private
|
|
961
1864
|
*/
|
|
962
1865
|
handleWebAuthnError(error) {
|
|
963
|
-
// Handle DOMExceptions from WebAuthn API
|
|
964
1866
|
if (error instanceof DOMException) {
|
|
965
|
-
|
|
966
|
-
case 'NotAllowedError':
|
|
967
|
-
return throwError(() => new UserCancelledError(error));
|
|
968
|
-
case 'InvalidStateError':
|
|
969
|
-
return throwError(() => new AuthenticatorError('Invalid authenticator state', error));
|
|
970
|
-
case 'NotSupportedError':
|
|
971
|
-
return throwError(() => new UnsupportedOperationError('Operation not supported', error));
|
|
972
|
-
case 'SecurityError':
|
|
973
|
-
return throwError(() => new SecurityError('Security error occurred', error));
|
|
974
|
-
case 'TimeoutError':
|
|
975
|
-
return throwError(() => new TimeoutError('Operation timed out', error));
|
|
976
|
-
case 'EncodingError':
|
|
977
|
-
return throwError(() => new InvalidOptionsError('Encoding error in options', error));
|
|
978
|
-
default:
|
|
979
|
-
return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unknown WebAuthn error: ${error.message}`, error));
|
|
980
|
-
}
|
|
1867
|
+
return this.handleDOMException(error);
|
|
981
1868
|
}
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
(error.message.includes('parseCreationOptionsFromJSON') ||
|
|
985
|
-
error.message.includes('parseRequestOptionsFromJSON'))) {
|
|
986
|
-
return throwError(() => new InvalidOptionsError('Invalid JSON options format', error));
|
|
1869
|
+
if (this.isJSONParsingError(error)) {
|
|
1870
|
+
return this.handleJSONParsingError(error);
|
|
987
1871
|
}
|
|
988
|
-
|
|
989
|
-
|
|
1872
|
+
return this.handleUnknownError(error);
|
|
1873
|
+
}
|
|
1874
|
+
/**
|
|
1875
|
+
* Handles DOMExceptions from the WebAuthn API using a mapping approach.
|
|
1876
|
+
* Maps specific DOMException names to appropriate WebAuthnError subclasses.
|
|
1877
|
+
*
|
|
1878
|
+
* @param error The DOMException thrown by the WebAuthn API
|
|
1879
|
+
* @returns Observable that throws an appropriate WebAuthnError subclass
|
|
1880
|
+
* @private
|
|
1881
|
+
*/
|
|
1882
|
+
handleDOMException(error) {
|
|
1883
|
+
const errorMap = {
|
|
1884
|
+
NotAllowedError: () => new UserCancelledError(error),
|
|
1885
|
+
InvalidStateError: () => new AuthenticatorError('Invalid authenticator state', error),
|
|
1886
|
+
NotSupportedError: () => new UnsupportedOperationError('Operation not supported', error),
|
|
1887
|
+
SecurityError: () => new SecurityError('Security error occurred', error),
|
|
1888
|
+
TimeoutError: () => new TimeoutError('Operation timed out', error),
|
|
1889
|
+
EncodingError: () => new InvalidOptionsError('Encoding error in options', error),
|
|
1890
|
+
};
|
|
1891
|
+
const errorFactory = errorMap[error.name];
|
|
1892
|
+
const webAuthnError = errorFactory
|
|
1893
|
+
? errorFactory()
|
|
1894
|
+
: new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unknown WebAuthn error: ${error.message}`, error);
|
|
1895
|
+
return throwError(() => webAuthnError);
|
|
1896
|
+
}
|
|
1897
|
+
/**
|
|
1898
|
+
* Determines if an error is related to JSON parsing issues.
|
|
1899
|
+
* Specifically checks for TypeError messages indicating JSON parsing failures.
|
|
1900
|
+
*
|
|
1901
|
+
* @param error The error to check
|
|
1902
|
+
* @returns True if the error is a JSON parsing error, false otherwise
|
|
1903
|
+
* @private
|
|
1904
|
+
*/
|
|
1905
|
+
isJSONParsingError(error) {
|
|
1906
|
+
return (error instanceof TypeError &&
|
|
1907
|
+
(error.message.includes('parseCreationOptionsFromJSON') ||
|
|
1908
|
+
error.message.includes('parseRequestOptionsFromJSON')));
|
|
1909
|
+
}
|
|
1910
|
+
/**
|
|
1911
|
+
* Handles JSON parsing errors specifically.
|
|
1912
|
+
* These errors occur when invalid JSON format options are provided.
|
|
1913
|
+
*
|
|
1914
|
+
* @param error The TypeError from JSON parsing
|
|
1915
|
+
* @returns Observable that throws an InvalidOptionsError
|
|
1916
|
+
* @private
|
|
1917
|
+
*/
|
|
1918
|
+
handleJSONParsingError(error) {
|
|
1919
|
+
// At this point we know it's a TypeError from isJSONParsingError check
|
|
1920
|
+
return throwError(() => new InvalidOptionsError('Invalid JSON options format', error));
|
|
1921
|
+
}
|
|
1922
|
+
/**
|
|
1923
|
+
* Handles any unexpected errors that don't fall into other categories.
|
|
1924
|
+
* Provides a fallback for errors that aren't DOMExceptions or JSON parsing errors.
|
|
1925
|
+
*
|
|
1926
|
+
* @param error The unexpected error
|
|
1927
|
+
* @returns Observable that throws a generic WebAuthnError
|
|
1928
|
+
* @private
|
|
1929
|
+
*/
|
|
1930
|
+
handleUnknownError(error) {
|
|
1931
|
+
const message = error instanceof Error ? error.message : 'Unknown error occurred';
|
|
1932
|
+
return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unexpected error: ${message}`, error instanceof Error ? error : new Error(String(error))));
|
|
990
1933
|
}
|
|
991
1934
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
992
1935
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, providedIn: 'root' });
|
|
@@ -1031,22 +1974,6 @@ function provideWebAuthn(relyingParty, config = {}) {
|
|
|
1031
1974
|
WebAuthnService,
|
|
1032
1975
|
];
|
|
1033
1976
|
}
|
|
1034
|
-
/**
|
|
1035
|
-
* @deprecated Use provideWebAuthn(relyingParty, config) instead.
|
|
1036
|
-
* This version is kept for backward compatibility but requires relying party information.
|
|
1037
|
-
*/
|
|
1038
|
-
function provideWebAuthnLegacy(config) {
|
|
1039
|
-
if (!config.relyingParty) {
|
|
1040
|
-
throw new Error('WebAuthn configuration must include relying party information. Use provideWebAuthn(relyingParty, config) instead.');
|
|
1041
|
-
}
|
|
1042
|
-
return [
|
|
1043
|
-
{
|
|
1044
|
-
provide: WEBAUTHN_CONFIG,
|
|
1045
|
-
useValue: config,
|
|
1046
|
-
},
|
|
1047
|
-
WebAuthnService,
|
|
1048
|
-
];
|
|
1049
|
-
}
|
|
1050
1977
|
|
|
1051
1978
|
// Core service
|
|
1052
1979
|
|
|
@@ -1054,5 +1981,5 @@ function provideWebAuthnLegacy(config) {
|
|
|
1054
1981
|
* Generated bundle index. Do not edit.
|
|
1055
1982
|
*/
|
|
1056
1983
|
|
|
1057
|
-
export { AuthenticatorError, DEFAULT_WEBAUTHN_CONFIG,
|
|
1984
|
+
export { AuthenticatorError, DEFAULT_WEBAUTHN_CONFIG, EXTERNAL_SECURITY_KEY_PRESET, InvalidOptionsError, InvalidRemoteOptionsError, NetworkError, PASSKEY_PRESET, PLATFORM_AUTHENTICATOR_PRESET, PRESET_MAP, RemoteEndpointError, SecurityError, TimeoutError, UnsupportedOperationError, UserCancelledError, WEBAUTHN_CONFIG, WebAuthnError, WebAuthnErrorType, WebAuthnService, arrayBufferToBase64, arrayBufferToBase64url, arrayBufferToCredentialId, arrayBufferToString, base64ToArrayBuffer, base64urlToArrayBuffer, createWebAuthnConfig, credentialIdToArrayBuffer, generateChallenge, generateUserId, getDefaultPubKeyCredParams, getSupportedTransports, isAuthenticateConfig, isCreationOptions, isJSONOptions, isPlatformAuthenticatorAvailable, isPublicKeyCredential, isRegisterConfig, isRequestOptions, isWebAuthnSupported, provideWebAuthn, stringToArrayBuffer, validateRegistrationOptions, validateRemoteCreationOptions, validateRemoteRequestOptions };
|
|
1058
1985
|
//# sourceMappingURL=ngx-webauthn.mjs.map
|