ngx-webauthn 0.0.2
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 +412 -0
- package/fesm2022/ngx-webauthn.mjs +1058 -0
- package/fesm2022/ngx-webauthn.mjs.map +1 -0
- package/index.d.ts +702 -0
- package/package.json +23 -0
|
@@ -0,0 +1,1058 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, Injectable } from '@angular/core';
|
|
3
|
+
import { throwError, from } from 'rxjs';
|
|
4
|
+
import { map, catchError } from 'rxjs/operators';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* High-level configuration interfaces for WebAuthn operations
|
|
8
|
+
*
|
|
9
|
+
* These interfaces provide a convenient, preset-driven API while still allowing
|
|
10
|
+
* full customization through override properties. They derive from standard WebAuthn
|
|
11
|
+
* types where possible to reduce duplication and ensure compatibility.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Type guard to check if input is a RegisterConfig
|
|
15
|
+
*/
|
|
16
|
+
function isRegisterConfig(input) {
|
|
17
|
+
return typeof input === 'object' && input !== null && 'username' in input;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Type guard to check if input is an AuthenticateConfig
|
|
21
|
+
*/
|
|
22
|
+
function isAuthenticateConfig(input) {
|
|
23
|
+
return (typeof input === 'object' &&
|
|
24
|
+
input !== null &&
|
|
25
|
+
('username' in input || 'preset' in input) &&
|
|
26
|
+
!('rp' in input) && // WebAuthn options have 'rp'
|
|
27
|
+
!('user' in input)); // WebAuthn options have 'user'
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Type guard to check if input is WebAuthn creation options
|
|
31
|
+
*/
|
|
32
|
+
function isCreationOptions(input) {
|
|
33
|
+
return (typeof input === 'object' &&
|
|
34
|
+
input !== null &&
|
|
35
|
+
'rp' in input &&
|
|
36
|
+
'user' in input);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Type guard to check if input is WebAuthn request options
|
|
40
|
+
*/
|
|
41
|
+
function isRequestOptions(input) {
|
|
42
|
+
return (typeof input === 'object' &&
|
|
43
|
+
input !== null &&
|
|
44
|
+
!('username' in input) &&
|
|
45
|
+
!('rp' in input) &&
|
|
46
|
+
!('user' in input));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* WebAuthn Preset Configurations
|
|
51
|
+
*
|
|
52
|
+
* This file contains predefined configurations for common WebAuthn use cases.
|
|
53
|
+
* These presets provide sensible defaults while remaining fully customizable.
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* A shared configuration for common, strong, and widely-supported
|
|
57
|
+
* public key credential algorithms.
|
|
58
|
+
*/
|
|
59
|
+
const COMMON_PUB_KEY_CRED_PARAMS = {
|
|
60
|
+
pubKeyCredParams: [
|
|
61
|
+
{ type: 'public-key', alg: -7 }, // ES256 (ECDSA w/ SHA-256)
|
|
62
|
+
{ type: 'public-key', alg: -257 }, // RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256)
|
|
63
|
+
],
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Preset for modern, passwordless, cross-device credentials.
|
|
67
|
+
*
|
|
68
|
+
* Best for: Passkey-based authentication where users can sync credentials
|
|
69
|
+
* across devices and use them for passwordless login.
|
|
70
|
+
*
|
|
71
|
+
* Features:
|
|
72
|
+
* - Requires resident keys (discoverable credentials)
|
|
73
|
+
* - Prefers user verification but doesn't require it
|
|
74
|
+
* - Works with both platform and cross-platform authenticators
|
|
75
|
+
* - Supports credential syncing across devices
|
|
76
|
+
*/
|
|
77
|
+
const PASSKEY_PRESET = {
|
|
78
|
+
...COMMON_PUB_KEY_CRED_PARAMS,
|
|
79
|
+
authenticatorSelection: {
|
|
80
|
+
residentKey: 'required',
|
|
81
|
+
userVerification: 'preferred',
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Preset for using a security key as a second factor after a password.
|
|
86
|
+
*
|
|
87
|
+
* Best for: Traditional 2FA scenarios where users already have a password
|
|
88
|
+
* and want to add hardware security key as a second factor.
|
|
89
|
+
*
|
|
90
|
+
* Features:
|
|
91
|
+
* - Discourages resident keys (server-side credential storage)
|
|
92
|
+
* - Prefers user verification
|
|
93
|
+
* - Favors cross-platform authenticators (USB/NFC security keys)
|
|
94
|
+
* - Credentials typically not synced between devices
|
|
95
|
+
*/
|
|
96
|
+
const SECOND_FACTOR_PRESET = {
|
|
97
|
+
...COMMON_PUB_KEY_CRED_PARAMS,
|
|
98
|
+
authenticatorSelection: {
|
|
99
|
+
residentKey: 'discouraged',
|
|
100
|
+
userVerification: 'preferred',
|
|
101
|
+
authenticatorAttachment: 'cross-platform',
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Preset for high-security, non-synced, single-device credentials.
|
|
106
|
+
*
|
|
107
|
+
* Best for: High-security scenarios where credentials must stay on a single
|
|
108
|
+
* device and user verification is mandatory.
|
|
109
|
+
*
|
|
110
|
+
* Features:
|
|
111
|
+
* - Requires platform authenticators (built-in biometrics/PIN)
|
|
112
|
+
* - Requires resident keys for discoverability
|
|
113
|
+
* - Requires user verification (biometric/PIN)
|
|
114
|
+
* - Credentials bound to specific device (no syncing)
|
|
115
|
+
*/
|
|
116
|
+
const DEVICE_BOUND_PRESET = {
|
|
117
|
+
...COMMON_PUB_KEY_CRED_PARAMS,
|
|
118
|
+
authenticatorSelection: {
|
|
119
|
+
authenticatorAttachment: 'platform',
|
|
120
|
+
residentKey: 'required',
|
|
121
|
+
userVerification: 'required',
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
/**
|
|
125
|
+
* Map of preset names to their configurations
|
|
126
|
+
* Used internally for preset resolution
|
|
127
|
+
*/
|
|
128
|
+
const PRESET_MAP = {
|
|
129
|
+
passkey: PASSKEY_PRESET,
|
|
130
|
+
secondFactor: SECOND_FACTOR_PRESET,
|
|
131
|
+
deviceBound: DEVICE_BOUND_PRESET,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Utility functions for resolving and merging WebAuthn presets
|
|
136
|
+
*
|
|
137
|
+
* These utilities handle the logic of taking a RegisterConfig or AuthenticateConfig,
|
|
138
|
+
* resolving any preset, and merging it with user overrides to produce final
|
|
139
|
+
* WebAuthn options ready for the browser API.
|
|
140
|
+
*/
|
|
141
|
+
/**
|
|
142
|
+
* Deep merge utility that properly handles nested objects
|
|
143
|
+
* Later properties override earlier ones
|
|
144
|
+
*/
|
|
145
|
+
function deepMerge(target, ...sources) {
|
|
146
|
+
if (!sources.length)
|
|
147
|
+
return target;
|
|
148
|
+
const source = sources.shift();
|
|
149
|
+
if (isObject(target) && isObject(source)) {
|
|
150
|
+
for (const key in source) {
|
|
151
|
+
if (isObject(source[key])) {
|
|
152
|
+
if (!target[key])
|
|
153
|
+
Object.assign(target, { [key]: {} });
|
|
154
|
+
deepMerge(target[key], source[key]);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
Object.assign(target, { [key]: source[key] });
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return deepMerge(target, ...sources);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Check if a value is a plain object
|
|
165
|
+
*/
|
|
166
|
+
function isObject(item) {
|
|
167
|
+
return item && typeof item === 'object' && !Array.isArray(item);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Generate a secure random challenge as Uint8Array
|
|
171
|
+
*/
|
|
172
|
+
function generateChallenge$1() {
|
|
173
|
+
return crypto.getRandomValues(new Uint8Array(32));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Generate a user ID from username
|
|
177
|
+
* Uses TextEncoder to convert username to Uint8Array for consistency
|
|
178
|
+
*/
|
|
179
|
+
function generateUserId$1(username) {
|
|
180
|
+
return new TextEncoder().encode(username);
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Convert challenge to appropriate format
|
|
184
|
+
* Handles both string (base64url) and Uint8Array inputs
|
|
185
|
+
*/
|
|
186
|
+
function processChallenge(challenge) {
|
|
187
|
+
if (!challenge) {
|
|
188
|
+
return generateChallenge$1();
|
|
189
|
+
}
|
|
190
|
+
if (typeof challenge === 'string') {
|
|
191
|
+
// Assume base64url string, convert to Uint8Array
|
|
192
|
+
return Uint8Array.from(atob(challenge.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
|
|
193
|
+
}
|
|
194
|
+
return challenge;
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Convert user ID to appropriate format
|
|
198
|
+
* Handles both string (base64url) and Uint8Array inputs
|
|
199
|
+
*/
|
|
200
|
+
function processUserId(userId, username) {
|
|
201
|
+
if (userId) {
|
|
202
|
+
if (typeof userId === 'string') {
|
|
203
|
+
// Assume base64url string, convert to Uint8Array
|
|
204
|
+
return Uint8Array.from(atob(userId.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0));
|
|
205
|
+
}
|
|
206
|
+
return userId;
|
|
207
|
+
}
|
|
208
|
+
if (username) {
|
|
209
|
+
return generateUserId$1(username);
|
|
210
|
+
}
|
|
211
|
+
// Fallback to random ID
|
|
212
|
+
return crypto.getRandomValues(new Uint8Array(16));
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Convert string credential IDs to PublicKeyCredentialDescriptor format
|
|
216
|
+
*/
|
|
217
|
+
function processCredentialDescriptors(credentials) {
|
|
218
|
+
if (!credentials || credentials.length === 0) {
|
|
219
|
+
return undefined;
|
|
220
|
+
}
|
|
221
|
+
// If already in descriptor format, return as-is
|
|
222
|
+
if (typeof credentials[0] === 'object' && 'type' in credentials[0]) {
|
|
223
|
+
return credentials;
|
|
224
|
+
}
|
|
225
|
+
// Convert string IDs to descriptors
|
|
226
|
+
return credentials.map((id) => ({
|
|
227
|
+
type: 'public-key',
|
|
228
|
+
id: Uint8Array.from(atob(id.replace(/-/g, '+').replace(/_/g, '/')), (c) => c.charCodeAt(0)),
|
|
229
|
+
}));
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Resolve a preset configuration by name
|
|
233
|
+
*/
|
|
234
|
+
function resolvePreset(presetName) {
|
|
235
|
+
const preset = PRESET_MAP[presetName];
|
|
236
|
+
if (!preset) {
|
|
237
|
+
throw new Error(`Unknown preset: ${presetName}`);
|
|
238
|
+
}
|
|
239
|
+
return preset;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Build complete PublicKeyCredentialCreationOptions from RegisterConfig
|
|
243
|
+
* Now uses WebAuthnConfig for better defaults and relying party information
|
|
244
|
+
*/
|
|
245
|
+
function buildCreationOptionsFromConfig(config, webAuthnConfig) {
|
|
246
|
+
// Start with base configuration from WebAuthnConfig
|
|
247
|
+
let options = {
|
|
248
|
+
timeout: webAuthnConfig.defaultTimeout || 60000,
|
|
249
|
+
attestation: webAuthnConfig.defaultAttestation || 'none',
|
|
250
|
+
};
|
|
251
|
+
// Apply preset if specified
|
|
252
|
+
if (config.preset) {
|
|
253
|
+
const preset = resolvePreset(config.preset);
|
|
254
|
+
options = deepMerge(options, {
|
|
255
|
+
authenticatorSelection: preset.authenticatorSelection,
|
|
256
|
+
pubKeyCredParams: [...preset.pubKeyCredParams], // Convert readonly to mutable
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Apply default authenticator selection from config
|
|
261
|
+
if (webAuthnConfig.defaultAuthenticatorSelection) {
|
|
262
|
+
options.authenticatorSelection =
|
|
263
|
+
webAuthnConfig.defaultAuthenticatorSelection;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
// Apply user overrides
|
|
267
|
+
if (config.timeout !== undefined) {
|
|
268
|
+
options.timeout = config.timeout;
|
|
269
|
+
}
|
|
270
|
+
if (config.attestation !== undefined) {
|
|
271
|
+
options.attestation = config.attestation;
|
|
272
|
+
}
|
|
273
|
+
if (config.authenticatorSelection !== undefined) {
|
|
274
|
+
options.authenticatorSelection = deepMerge(options.authenticatorSelection || {}, config.authenticatorSelection);
|
|
275
|
+
}
|
|
276
|
+
if (config.pubKeyCredParams !== undefined) {
|
|
277
|
+
options.pubKeyCredParams = config.pubKeyCredParams;
|
|
278
|
+
}
|
|
279
|
+
if (config.extensions !== undefined) {
|
|
280
|
+
options.extensions = config.extensions;
|
|
281
|
+
}
|
|
282
|
+
// Handle required fields
|
|
283
|
+
const challenge = processChallenge(config.challenge);
|
|
284
|
+
const userId = processUserId(config.userId, config.username);
|
|
285
|
+
// Use relying party from config, with user override capability
|
|
286
|
+
const relyingParty = config.rp || webAuthnConfig.relyingParty;
|
|
287
|
+
// Build final options
|
|
288
|
+
const finalOptions = {
|
|
289
|
+
...options,
|
|
290
|
+
rp: relyingParty,
|
|
291
|
+
user: {
|
|
292
|
+
id: userId,
|
|
293
|
+
name: config.username,
|
|
294
|
+
displayName: config.displayName || config.username,
|
|
295
|
+
},
|
|
296
|
+
challenge,
|
|
297
|
+
pubKeyCredParams: options.pubKeyCredParams ||
|
|
298
|
+
webAuthnConfig.defaultAlgorithms || [
|
|
299
|
+
{ type: 'public-key', alg: -7 }, // ES256
|
|
300
|
+
{ type: 'public-key', alg: -257 }, // RS256
|
|
301
|
+
],
|
|
302
|
+
excludeCredentials: processCredentialDescriptors(config.excludeCredentials),
|
|
303
|
+
};
|
|
304
|
+
return finalOptions;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Build complete PublicKeyCredentialRequestOptions from AuthenticateConfig
|
|
308
|
+
* Now uses WebAuthnConfig for better defaults
|
|
309
|
+
*/
|
|
310
|
+
function buildRequestOptionsFromConfig(config, webAuthnConfig) {
|
|
311
|
+
// Start with base configuration from WebAuthnConfig
|
|
312
|
+
const options = {
|
|
313
|
+
timeout: webAuthnConfig.defaultTimeout || 60000,
|
|
314
|
+
userVerification: webAuthnConfig.enforceUserVerification
|
|
315
|
+
? 'required'
|
|
316
|
+
: 'preferred',
|
|
317
|
+
};
|
|
318
|
+
// Apply preset if specified
|
|
319
|
+
if (config.preset) {
|
|
320
|
+
const preset = resolvePreset(config.preset);
|
|
321
|
+
// Extract relevant parts for authentication (userVerification from authenticatorSelection)
|
|
322
|
+
if (preset.authenticatorSelection?.userVerification) {
|
|
323
|
+
options.userVerification = preset.authenticatorSelection.userVerification;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// Apply user overrides
|
|
327
|
+
if (config.timeout !== undefined) {
|
|
328
|
+
options.timeout = config.timeout;
|
|
329
|
+
}
|
|
330
|
+
if (config.userVerification !== undefined) {
|
|
331
|
+
options.userVerification = config.userVerification;
|
|
332
|
+
}
|
|
333
|
+
if (config.extensions !== undefined) {
|
|
334
|
+
options.extensions = config.extensions;
|
|
335
|
+
}
|
|
336
|
+
// Handle required fields
|
|
337
|
+
const challenge = processChallenge(config.challenge);
|
|
338
|
+
// Build final options
|
|
339
|
+
const finalOptions = {
|
|
340
|
+
...options,
|
|
341
|
+
challenge,
|
|
342
|
+
allowCredentials: processCredentialDescriptors(config.allowCredentials),
|
|
343
|
+
};
|
|
344
|
+
return finalOptions;
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Validate that a RegisterConfig has all required fields
|
|
348
|
+
*/
|
|
349
|
+
function validateRegisterConfig(config) {
|
|
350
|
+
if (!config.username || typeof config.username !== 'string') {
|
|
351
|
+
throw new Error('RegisterConfig must have a valid username');
|
|
352
|
+
}
|
|
353
|
+
if (config.preset && !PRESET_MAP[config.preset]) {
|
|
354
|
+
throw new Error(`Invalid preset: ${config.preset}. Valid presets are: ${Object.keys(PRESET_MAP).join(', ')}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Validate that an AuthenticateConfig has all required fields
|
|
359
|
+
*/
|
|
360
|
+
function validateAuthenticateConfig(config) {
|
|
361
|
+
if (config.preset && !PRESET_MAP[config.preset]) {
|
|
362
|
+
throw new Error(`Invalid preset: ${config.preset}. Valid presets are: ${Object.keys(PRESET_MAP).join(', ')}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Enhanced WebAuthn Error Classes
|
|
368
|
+
* Provides specific, actionable error types for better developer experience
|
|
369
|
+
*/
|
|
370
|
+
var WebAuthnErrorType;
|
|
371
|
+
(function (WebAuthnErrorType) {
|
|
372
|
+
WebAuthnErrorType["NOT_SUPPORTED"] = "NOT_SUPPORTED";
|
|
373
|
+
WebAuthnErrorType["USER_CANCELLED"] = "USER_CANCELLED";
|
|
374
|
+
WebAuthnErrorType["AUTHENTICATOR_ERROR"] = "AUTHENTICATOR_ERROR";
|
|
375
|
+
WebAuthnErrorType["INVALID_OPTIONS"] = "INVALID_OPTIONS";
|
|
376
|
+
WebAuthnErrorType["UNSUPPORTED_OPERATION"] = "UNSUPPORTED_OPERATION";
|
|
377
|
+
WebAuthnErrorType["NETWORK_ERROR"] = "NETWORK_ERROR";
|
|
378
|
+
WebAuthnErrorType["SECURITY_ERROR"] = "SECURITY_ERROR";
|
|
379
|
+
WebAuthnErrorType["TIMEOUT_ERROR"] = "TIMEOUT_ERROR";
|
|
380
|
+
WebAuthnErrorType["UNKNOWN"] = "UNKNOWN";
|
|
381
|
+
})(WebAuthnErrorType || (WebAuthnErrorType = {}));
|
|
382
|
+
/**
|
|
383
|
+
* Base WebAuthn error class with additional context
|
|
384
|
+
*/
|
|
385
|
+
class WebAuthnError extends Error {
|
|
386
|
+
type;
|
|
387
|
+
originalError;
|
|
388
|
+
constructor(type, message, originalError) {
|
|
389
|
+
super(message);
|
|
390
|
+
this.type = type;
|
|
391
|
+
this.originalError = originalError;
|
|
392
|
+
this.name = 'WebAuthnError';
|
|
393
|
+
}
|
|
394
|
+
static fromDOMException(error) {
|
|
395
|
+
const type = WebAuthnError.mapDOMExceptionToType(error.name);
|
|
396
|
+
return new WebAuthnError(type, error.message, error);
|
|
397
|
+
}
|
|
398
|
+
static mapDOMExceptionToType(name) {
|
|
399
|
+
switch (name) {
|
|
400
|
+
case 'NotSupportedError':
|
|
401
|
+
return WebAuthnErrorType.NOT_SUPPORTED;
|
|
402
|
+
case 'NotAllowedError':
|
|
403
|
+
return WebAuthnErrorType.USER_CANCELLED;
|
|
404
|
+
case 'InvalidStateError':
|
|
405
|
+
return WebAuthnErrorType.AUTHENTICATOR_ERROR;
|
|
406
|
+
case 'SecurityError':
|
|
407
|
+
return WebAuthnErrorType.SECURITY_ERROR;
|
|
408
|
+
case 'TimeoutError':
|
|
409
|
+
return WebAuthnErrorType.TIMEOUT_ERROR;
|
|
410
|
+
case 'NetworkError':
|
|
411
|
+
return WebAuthnErrorType.NETWORK_ERROR;
|
|
412
|
+
case 'EncodingError':
|
|
413
|
+
return WebAuthnErrorType.INVALID_OPTIONS;
|
|
414
|
+
default:
|
|
415
|
+
return WebAuthnErrorType.UNKNOWN;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Error thrown when user cancels the WebAuthn operation
|
|
421
|
+
*/
|
|
422
|
+
class UserCancelledError extends WebAuthnError {
|
|
423
|
+
constructor(originalError) {
|
|
424
|
+
super(WebAuthnErrorType.USER_CANCELLED, 'User cancelled the WebAuthn operation', originalError);
|
|
425
|
+
this.name = 'UserCancelledError';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
/**
|
|
429
|
+
* Error thrown when there's an issue with the authenticator
|
|
430
|
+
*/
|
|
431
|
+
class AuthenticatorError extends WebAuthnError {
|
|
432
|
+
constructor(message, originalError) {
|
|
433
|
+
super(WebAuthnErrorType.AUTHENTICATOR_ERROR, `Authenticator error: ${message}`, originalError);
|
|
434
|
+
this.name = 'AuthenticatorError';
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Error thrown when the provided options are invalid
|
|
439
|
+
*/
|
|
440
|
+
class InvalidOptionsError extends WebAuthnError {
|
|
441
|
+
constructor(message, originalError) {
|
|
442
|
+
super(WebAuthnErrorType.INVALID_OPTIONS, `Invalid options: ${message}`, originalError);
|
|
443
|
+
this.name = 'InvalidOptionsError';
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* Error thrown when the requested operation is not supported
|
|
448
|
+
*/
|
|
449
|
+
class UnsupportedOperationError extends WebAuthnError {
|
|
450
|
+
constructor(message, originalError) {
|
|
451
|
+
super(WebAuthnErrorType.UNSUPPORTED_OPERATION, `Unsupported operation: ${message}`, originalError);
|
|
452
|
+
this.name = 'UnsupportedOperationError';
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Error thrown when there's a network-related issue
|
|
457
|
+
*/
|
|
458
|
+
class NetworkError extends WebAuthnError {
|
|
459
|
+
constructor(message, originalError) {
|
|
460
|
+
super(WebAuthnErrorType.NETWORK_ERROR, `Network error: ${message}`, originalError);
|
|
461
|
+
this.name = 'NetworkError';
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
/**
|
|
465
|
+
* Error thrown when there's a security-related issue
|
|
466
|
+
*/
|
|
467
|
+
class SecurityError extends WebAuthnError {
|
|
468
|
+
constructor(message, originalError) {
|
|
469
|
+
super(WebAuthnErrorType.SECURITY_ERROR, `Security error: ${message}`, originalError);
|
|
470
|
+
this.name = 'SecurityError';
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/**
|
|
474
|
+
* Error thrown when the operation times out
|
|
475
|
+
*/
|
|
476
|
+
class TimeoutError extends WebAuthnError {
|
|
477
|
+
constructor(message, originalError) {
|
|
478
|
+
super(WebAuthnErrorType.TIMEOUT_ERROR, `Timeout error: ${message}`, originalError);
|
|
479
|
+
this.name = 'TimeoutError';
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* WebAuthn Configuration
|
|
485
|
+
* Provides configuration interfaces and injection token for WebAuthn service
|
|
486
|
+
*/
|
|
487
|
+
/**
|
|
488
|
+
* Default configuration for WebAuthn service
|
|
489
|
+
* Note: relyingParty must be provided by the application
|
|
490
|
+
*/
|
|
491
|
+
const DEFAULT_WEBAUTHN_CONFIG = {
|
|
492
|
+
defaultTimeout: 60000,
|
|
493
|
+
defaultAlgorithms: [
|
|
494
|
+
{ type: 'public-key', alg: -7 }, // ES256 (ECDSA w/ SHA-256)
|
|
495
|
+
{ type: 'public-key', alg: -257 }, // RS256 (RSASSA-PKCS1-v1_5 w/ SHA-256)
|
|
496
|
+
],
|
|
497
|
+
enforceUserVerification: false,
|
|
498
|
+
defaultAttestation: 'none',
|
|
499
|
+
defaultAuthenticatorSelection: {
|
|
500
|
+
userVerification: 'preferred',
|
|
501
|
+
},
|
|
502
|
+
};
|
|
503
|
+
/**
|
|
504
|
+
* Injection token for WebAuthn configuration
|
|
505
|
+
*/
|
|
506
|
+
const WEBAUTHN_CONFIG = new InjectionToken('WEBAUTHN_CONFIG');
|
|
507
|
+
/**
|
|
508
|
+
* Creates a complete WebAuthn configuration with required relying party information
|
|
509
|
+
* @param relyingParty Required relying party configuration
|
|
510
|
+
* @param overrides Optional configuration overrides
|
|
511
|
+
*/
|
|
512
|
+
function createWebAuthnConfig(relyingParty, overrides) {
|
|
513
|
+
return {
|
|
514
|
+
relyingParty,
|
|
515
|
+
...DEFAULT_WEBAUTHN_CONFIG,
|
|
516
|
+
...overrides,
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* WebAuthn Utility Functions
|
|
522
|
+
* Centralized utilities for ArrayBuffer conversions and WebAuthn-specific operations
|
|
523
|
+
* This is the single source of truth for data transformation functions
|
|
524
|
+
*/
|
|
525
|
+
/**
|
|
526
|
+
* Converts a string to ArrayBuffer
|
|
527
|
+
*/
|
|
528
|
+
function stringToArrayBuffer(str) {
|
|
529
|
+
const encoder = new TextEncoder();
|
|
530
|
+
return encoder.encode(str);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Converts ArrayBuffer to string
|
|
534
|
+
*/
|
|
535
|
+
function arrayBufferToString(buffer) {
|
|
536
|
+
const decoder = new TextDecoder();
|
|
537
|
+
return decoder.decode(buffer);
|
|
538
|
+
}
|
|
539
|
+
/**
|
|
540
|
+
* Converts ArrayBuffer to base64 string
|
|
541
|
+
*/
|
|
542
|
+
function arrayBufferToBase64(buffer) {
|
|
543
|
+
const bytes = new Uint8Array(buffer);
|
|
544
|
+
let binary = '';
|
|
545
|
+
for (let i = 0; i < bytes.byteLength; i++) {
|
|
546
|
+
binary += String.fromCharCode(bytes[i]);
|
|
547
|
+
}
|
|
548
|
+
return btoa(binary);
|
|
549
|
+
}
|
|
550
|
+
/**
|
|
551
|
+
* Converts base64 string to ArrayBuffer
|
|
552
|
+
*/
|
|
553
|
+
function base64ToArrayBuffer(base64) {
|
|
554
|
+
const binary = atob(base64);
|
|
555
|
+
const bytes = new Uint8Array(binary.length);
|
|
556
|
+
for (let i = 0; i < binary.length; i++) {
|
|
557
|
+
bytes[i] = binary.charCodeAt(i);
|
|
558
|
+
}
|
|
559
|
+
return bytes.buffer;
|
|
560
|
+
}
|
|
561
|
+
/**
|
|
562
|
+
* Converts base64url string to ArrayBuffer
|
|
563
|
+
*/
|
|
564
|
+
function base64urlToArrayBuffer(base64url) {
|
|
565
|
+
// Convert base64url to base64
|
|
566
|
+
const base64 = base64url.replace(/-/g, '+').replace(/_/g, '/');
|
|
567
|
+
// Add padding if necessary
|
|
568
|
+
const padded = base64 + '==='.slice(0, (4 - (base64.length % 4)) % 4);
|
|
569
|
+
return base64ToArrayBuffer(padded);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Converts ArrayBuffer to base64url string
|
|
573
|
+
* This is the canonical implementation used throughout the library
|
|
574
|
+
*/
|
|
575
|
+
function arrayBufferToBase64url(buffer) {
|
|
576
|
+
const base64 = arrayBufferToBase64(buffer);
|
|
577
|
+
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Generates a random challenge as ArrayBuffer
|
|
581
|
+
*/
|
|
582
|
+
function generateChallenge(length = 32) {
|
|
583
|
+
const array = new Uint8Array(length);
|
|
584
|
+
crypto.getRandomValues(array);
|
|
585
|
+
return array.buffer;
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Generates a random user ID as ArrayBuffer
|
|
589
|
+
*/
|
|
590
|
+
function generateUserId(length = 32) {
|
|
591
|
+
const array = new Uint8Array(length);
|
|
592
|
+
crypto.getRandomValues(array);
|
|
593
|
+
return array.buffer;
|
|
594
|
+
}
|
|
595
|
+
/**
|
|
596
|
+
* Converts credential ID string to ArrayBuffer for WebAuthn API
|
|
597
|
+
*/
|
|
598
|
+
function credentialIdToArrayBuffer(credentialId) {
|
|
599
|
+
return base64urlToArrayBuffer(credentialId);
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Converts ArrayBuffer credential ID to string
|
|
603
|
+
*/
|
|
604
|
+
function arrayBufferToCredentialId(buffer) {
|
|
605
|
+
return arrayBufferToBase64url(buffer);
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Checks if the current environment supports WebAuthn
|
|
609
|
+
*/
|
|
610
|
+
function isWebAuthnSupported() {
|
|
611
|
+
return !!(typeof window !== 'undefined' &&
|
|
612
|
+
window.PublicKeyCredential &&
|
|
613
|
+
typeof navigator !== 'undefined' &&
|
|
614
|
+
navigator.credentials &&
|
|
615
|
+
typeof navigator.credentials.create === 'function' &&
|
|
616
|
+
typeof navigator.credentials.get === 'function');
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Checks if platform authenticator is available
|
|
620
|
+
*/
|
|
621
|
+
async function isPlatformAuthenticatorAvailable() {
|
|
622
|
+
if (!isWebAuthnSupported()) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
try {
|
|
626
|
+
return await PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable();
|
|
627
|
+
}
|
|
628
|
+
catch {
|
|
629
|
+
return false;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* Gets supported authenticator transports for this platform
|
|
634
|
+
* Enhanced detection with better browser compatibility
|
|
635
|
+
*/
|
|
636
|
+
function getSupportedTransports() {
|
|
637
|
+
const transports = ['usb', 'internal'];
|
|
638
|
+
// Add NFC support for Android devices
|
|
639
|
+
if (typeof navigator !== 'undefined' &&
|
|
640
|
+
/Android/i.test(navigator.userAgent)) {
|
|
641
|
+
transports.push('nfc');
|
|
642
|
+
}
|
|
643
|
+
// Add BLE support for modern browsers with Web Bluetooth API
|
|
644
|
+
if (typeof navigator !== 'undefined' && navigator.bluetooth) {
|
|
645
|
+
transports.push('ble');
|
|
646
|
+
}
|
|
647
|
+
return transports;
|
|
648
|
+
}
|
|
649
|
+
/**
|
|
650
|
+
* Validates that required WebAuthn options are present
|
|
651
|
+
*/
|
|
652
|
+
function validateRegistrationOptions(options) {
|
|
653
|
+
if (!options.user) {
|
|
654
|
+
throw new Error('User information is required for registration');
|
|
655
|
+
}
|
|
656
|
+
if (!options.user.id) {
|
|
657
|
+
throw new Error('User ID is required for registration');
|
|
658
|
+
}
|
|
659
|
+
if (!options.user.name) {
|
|
660
|
+
throw new Error('User name is required for registration');
|
|
661
|
+
}
|
|
662
|
+
if (!options.user.displayName) {
|
|
663
|
+
throw new Error('User display name is required for registration');
|
|
664
|
+
}
|
|
665
|
+
if (!options.rp) {
|
|
666
|
+
throw new Error('Relying party information is required for registration');
|
|
667
|
+
}
|
|
668
|
+
if (!options.rp.name) {
|
|
669
|
+
throw new Error('Relying party name is required for registration');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Creates default public key credential parameters
|
|
674
|
+
*/
|
|
675
|
+
function getDefaultPubKeyCredParams() {
|
|
676
|
+
return [
|
|
677
|
+
{
|
|
678
|
+
type: 'public-key',
|
|
679
|
+
alg: -7, // ES256
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
type: 'public-key',
|
|
683
|
+
alg: -257, // RS256
|
|
684
|
+
},
|
|
685
|
+
];
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Enhanced type guard to detect JSON-formatted WebAuthn options
|
|
689
|
+
* More robust than simple challenge type checking
|
|
690
|
+
*/
|
|
691
|
+
function isJSONOptions(options) {
|
|
692
|
+
if (!options || typeof options !== 'object') {
|
|
693
|
+
return false;
|
|
694
|
+
}
|
|
695
|
+
// Check multiple indicators that this is JSON format (base64url strings)
|
|
696
|
+
return (typeof options.challenge === 'string' ||
|
|
697
|
+
(options.user && typeof options.user.id === 'string') ||
|
|
698
|
+
(options.allowCredentials?.length > 0 &&
|
|
699
|
+
typeof options.allowCredentials[0]?.id === 'string') ||
|
|
700
|
+
(options.excludeCredentials?.length > 0 &&
|
|
701
|
+
typeof options.excludeCredentials[0]?.id === 'string'));
|
|
702
|
+
}
|
|
703
|
+
/**
|
|
704
|
+
* Type guard to check if input is a PublicKeyCredential
|
|
705
|
+
*/
|
|
706
|
+
function isPublicKeyCredential(credential) {
|
|
707
|
+
return credential !== null && credential.type === 'public-key';
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Enhanced WebAuthn Service
|
|
712
|
+
*
|
|
713
|
+
* Provides a clean, high-level API for WebAuthn operations with:
|
|
714
|
+
* - Modern inject() pattern instead of constructor DI
|
|
715
|
+
* - Flexible options (JSON base64url strings OR native ArrayBuffers)
|
|
716
|
+
* - Enhanced error handling with specific error types
|
|
717
|
+
* - Native browser parsing functions for optimal performance
|
|
718
|
+
* - Clean, developer-friendly response objects
|
|
719
|
+
*/
|
|
720
|
+
/**
|
|
721
|
+
* Enhanced Angular service for WebAuthn operations
|
|
722
|
+
* Provides a clean abstraction over the WebAuthn API with RxJS observables
|
|
723
|
+
* and enhanced error handling
|
|
724
|
+
*/
|
|
725
|
+
class WebAuthnService {
|
|
726
|
+
config = inject(WEBAUTHN_CONFIG);
|
|
727
|
+
/**
|
|
728
|
+
* Checks if WebAuthn is supported in the current browser
|
|
729
|
+
*/
|
|
730
|
+
isSupported() {
|
|
731
|
+
return isWebAuthnSupported();
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Gets comprehensive WebAuthn support information
|
|
735
|
+
*/
|
|
736
|
+
getSupport() {
|
|
737
|
+
if (!this.isSupported()) {
|
|
738
|
+
return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
|
|
739
|
+
}
|
|
740
|
+
return from(PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable()).pipe(map((isPlatformAvailable) => ({
|
|
741
|
+
isSupported: true,
|
|
742
|
+
isPlatformAuthenticatorAvailable: isPlatformAvailable,
|
|
743
|
+
supportedTransports: getSupportedTransports(),
|
|
744
|
+
})), catchError((error) => throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, 'Failed to check WebAuthn support', error))));
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Registers a new WebAuthn credential with flexible configuration support
|
|
748
|
+
*
|
|
749
|
+
* @param input Either a high-level RegisterConfig with presets, or direct WebAuthn creation options
|
|
750
|
+
* @returns Observable of RegistrationResponse with clean, developer-friendly format
|
|
751
|
+
*
|
|
752
|
+
* @example
|
|
753
|
+
* ```typescript
|
|
754
|
+
* // Simple preset usage
|
|
755
|
+
* this.webAuthnService.register({ username: 'john.doe', preset: 'passkey' });
|
|
756
|
+
*
|
|
757
|
+
* // Preset with overrides
|
|
758
|
+
* this.webAuthnService.register({
|
|
759
|
+
* username: 'john.doe',
|
|
760
|
+
* preset: 'passkey',
|
|
761
|
+
* authenticatorSelection: { userVerification: 'required' }
|
|
762
|
+
* });
|
|
763
|
+
*
|
|
764
|
+
* // Direct WebAuthn options (native)
|
|
765
|
+
* const nativeOptions: PublicKeyCredentialCreationOptions = {
|
|
766
|
+
* challenge: new Uint8Array([...]),
|
|
767
|
+
* rp: { name: "My App" },
|
|
768
|
+
* user: { id: new Uint8Array([...]), name: "user@example.com", displayName: "User" },
|
|
769
|
+
* pubKeyCredParams: [{ type: "public-key", alg: -7 }]
|
|
770
|
+
* };
|
|
771
|
+
* this.webAuthnService.register(nativeOptions);
|
|
772
|
+
*
|
|
773
|
+
* // Direct WebAuthn options (JSON)
|
|
774
|
+
* const jsonOptions: PublicKeyCredentialCreationOptionsJSON = {
|
|
775
|
+
* challenge: "Y2hhbGxlbmdl",
|
|
776
|
+
* rp: { name: "My App" },
|
|
777
|
+
* user: { id: "dXNlcklk", name: "user@example.com", displayName: "User" },
|
|
778
|
+
* pubKeyCredParams: [{ type: "public-key", alg: -7 }]
|
|
779
|
+
* };
|
|
780
|
+
* this.webAuthnService.register(jsonOptions);
|
|
781
|
+
* ```
|
|
782
|
+
*/
|
|
783
|
+
register(input) {
|
|
784
|
+
if (!this.isSupported()) {
|
|
785
|
+
return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
|
|
786
|
+
}
|
|
787
|
+
try {
|
|
788
|
+
let creationOptions;
|
|
789
|
+
if (isRegisterConfig(input)) {
|
|
790
|
+
// High-level config path: validate, resolve preset, build options
|
|
791
|
+
validateRegisterConfig(input);
|
|
792
|
+
creationOptions = buildCreationOptionsFromConfig(input, this.config);
|
|
793
|
+
}
|
|
794
|
+
else {
|
|
795
|
+
// Direct options path: use provided options
|
|
796
|
+
creationOptions = input;
|
|
797
|
+
}
|
|
798
|
+
const parsedOptions = this.parseRegistrationOptions(creationOptions);
|
|
799
|
+
return from(navigator.credentials.create({ publicKey: parsedOptions })).pipe(map((credential) => this.processRegistrationResult(credential)), catchError((error) => this.handleWebAuthnError(error)));
|
|
800
|
+
}
|
|
801
|
+
catch (error) {
|
|
802
|
+
return throwError(() => new InvalidOptionsError('Failed to process registration input', error));
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
/**
|
|
806
|
+
* Authenticates using an existing WebAuthn credential with flexible configuration support
|
|
807
|
+
*
|
|
808
|
+
* @param input Either a high-level AuthenticateConfig with presets, or direct WebAuthn request options
|
|
809
|
+
* @returns Observable of AuthenticationResponse with clean, developer-friendly format
|
|
810
|
+
*
|
|
811
|
+
* @example
|
|
812
|
+
* ```typescript
|
|
813
|
+
* // Simple preset usage
|
|
814
|
+
* this.webAuthnService.authenticate({ preset: 'passkey' });
|
|
815
|
+
*
|
|
816
|
+
* // Config with credential filtering
|
|
817
|
+
* this.webAuthnService.authenticate({
|
|
818
|
+
* username: 'john.doe',
|
|
819
|
+
* preset: 'secondFactor',
|
|
820
|
+
* allowCredentials: ['credential-id-1', 'credential-id-2']
|
|
821
|
+
* });
|
|
822
|
+
*
|
|
823
|
+
* // Direct WebAuthn options (JSON)
|
|
824
|
+
* const jsonOptions: PublicKeyCredentialRequestOptionsJSON = {
|
|
825
|
+
* challenge: "Y2hhbGxlbmdl",
|
|
826
|
+
* allowCredentials: [{
|
|
827
|
+
* type: "public-key",
|
|
828
|
+
* id: "Y3JlZElk"
|
|
829
|
+
* }]
|
|
830
|
+
* };
|
|
831
|
+
* this.webAuthnService.authenticate(jsonOptions);
|
|
832
|
+
*
|
|
833
|
+
* // Direct WebAuthn options (native)
|
|
834
|
+
* const nativeOptions: PublicKeyCredentialRequestOptions = {
|
|
835
|
+
* challenge: new Uint8Array([...]),
|
|
836
|
+
* allowCredentials: [{
|
|
837
|
+
* type: "public-key",
|
|
838
|
+
* id: new Uint8Array([...])
|
|
839
|
+
* }]
|
|
840
|
+
* };
|
|
841
|
+
* this.webAuthnService.authenticate(nativeOptions);
|
|
842
|
+
* ```
|
|
843
|
+
*/
|
|
844
|
+
authenticate(input) {
|
|
845
|
+
if (!this.isSupported()) {
|
|
846
|
+
return throwError(() => new UnsupportedOperationError('WebAuthn is not supported in this browser'));
|
|
847
|
+
}
|
|
848
|
+
try {
|
|
849
|
+
let requestOptions;
|
|
850
|
+
if (isAuthenticateConfig(input)) {
|
|
851
|
+
// High-level config path: validate, resolve preset, build options
|
|
852
|
+
validateAuthenticateConfig(input);
|
|
853
|
+
requestOptions = buildRequestOptionsFromConfig(input, this.config);
|
|
854
|
+
}
|
|
855
|
+
else {
|
|
856
|
+
// Direct options path: use provided options
|
|
857
|
+
requestOptions = input;
|
|
858
|
+
}
|
|
859
|
+
const parsedOptions = this.parseAuthenticationOptions(requestOptions);
|
|
860
|
+
return from(navigator.credentials.get({ publicKey: parsedOptions })).pipe(map((credential) => this.processAuthenticationResult(credential)), catchError((error) => this.handleWebAuthnError(error)));
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
return throwError(() => new InvalidOptionsError('Failed to process authentication input', error));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Parses registration options, handling both JSON and native formats
|
|
868
|
+
*/
|
|
869
|
+
parseRegistrationOptions(options) {
|
|
870
|
+
if (isJSONOptions(options)) {
|
|
871
|
+
// Use native browser function for JSON options
|
|
872
|
+
return PublicKeyCredential.parseCreationOptionsFromJSON(options);
|
|
873
|
+
}
|
|
874
|
+
else {
|
|
875
|
+
// Options are already in native format
|
|
876
|
+
return options;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* Parses authentication options, handling both JSON and native formats
|
|
881
|
+
*/
|
|
882
|
+
parseAuthenticationOptions(options) {
|
|
883
|
+
if (isJSONOptions(options)) {
|
|
884
|
+
// Use native browser function for JSON options
|
|
885
|
+
return PublicKeyCredential.parseRequestOptionsFromJSON(options);
|
|
886
|
+
}
|
|
887
|
+
else {
|
|
888
|
+
// Options are already in native format
|
|
889
|
+
return options;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
893
|
+
* Processes the raw credential result into a clean RegistrationResponse
|
|
894
|
+
*/
|
|
895
|
+
processRegistrationResult(credential) {
|
|
896
|
+
if (!isPublicKeyCredential(credential)) {
|
|
897
|
+
throw new AuthenticatorError('No credential returned from authenticator');
|
|
898
|
+
}
|
|
899
|
+
const response = credential.response;
|
|
900
|
+
// Extract data using the response methods
|
|
901
|
+
const credentialId = arrayBufferToBase64url(credential.rawId);
|
|
902
|
+
const transports = (response.getTransports?.() ||
|
|
903
|
+
[]);
|
|
904
|
+
let publicKey;
|
|
905
|
+
try {
|
|
906
|
+
const publicKeyBuffer = response.getPublicKey?.();
|
|
907
|
+
if (publicKeyBuffer) {
|
|
908
|
+
publicKey = arrayBufferToBase64url(publicKeyBuffer);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
// Public key extraction failed - this is okay, not all algorithms are supported
|
|
913
|
+
}
|
|
914
|
+
// Create the raw response for backward compatibility
|
|
915
|
+
const rawResponse = {
|
|
916
|
+
credentialId,
|
|
917
|
+
publicKey: publicKey ||
|
|
918
|
+
arrayBufferToBase64url(response.getPublicKey?.() || new ArrayBuffer(0)),
|
|
919
|
+
attestationObject: arrayBufferToBase64url(response.attestationObject),
|
|
920
|
+
clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
|
|
921
|
+
transports: transports,
|
|
922
|
+
};
|
|
923
|
+
return {
|
|
924
|
+
success: true,
|
|
925
|
+
credentialId,
|
|
926
|
+
publicKey,
|
|
927
|
+
transports,
|
|
928
|
+
rawResponse,
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* Processes the raw credential result into a clean AuthenticationResponse
|
|
933
|
+
*/
|
|
934
|
+
processAuthenticationResult(credential) {
|
|
935
|
+
if (!isPublicKeyCredential(credential)) {
|
|
936
|
+
throw new AuthenticatorError('No credential returned from authenticator');
|
|
937
|
+
}
|
|
938
|
+
const response = credential.response;
|
|
939
|
+
const credentialId = arrayBufferToBase64url(credential.rawId);
|
|
940
|
+
let userHandle;
|
|
941
|
+
if (response.userHandle) {
|
|
942
|
+
userHandle = arrayBufferToBase64url(response.userHandle);
|
|
943
|
+
}
|
|
944
|
+
// Create the raw response for backward compatibility
|
|
945
|
+
const rawResponse = {
|
|
946
|
+
credentialId,
|
|
947
|
+
authenticatorData: arrayBufferToBase64url(response.authenticatorData),
|
|
948
|
+
clientDataJSON: arrayBufferToBase64url(response.clientDataJSON),
|
|
949
|
+
signature: arrayBufferToBase64url(response.signature),
|
|
950
|
+
userHandle,
|
|
951
|
+
};
|
|
952
|
+
return {
|
|
953
|
+
success: true,
|
|
954
|
+
credentialId,
|
|
955
|
+
userHandle,
|
|
956
|
+
rawResponse,
|
|
957
|
+
};
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* Enhanced error handling that maps DOMExceptions to specific error types
|
|
961
|
+
*/
|
|
962
|
+
handleWebAuthnError(error) {
|
|
963
|
+
// Handle DOMExceptions from WebAuthn API
|
|
964
|
+
if (error instanceof DOMException) {
|
|
965
|
+
switch (error.name) {
|
|
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
|
+
}
|
|
981
|
+
}
|
|
982
|
+
// Handle JSON parsing errors
|
|
983
|
+
if (error instanceof TypeError &&
|
|
984
|
+
(error.message.includes('parseCreationOptionsFromJSON') ||
|
|
985
|
+
error.message.includes('parseRequestOptionsFromJSON'))) {
|
|
986
|
+
return throwError(() => new InvalidOptionsError('Invalid JSON options format', error));
|
|
987
|
+
}
|
|
988
|
+
// Handle other errors
|
|
989
|
+
return throwError(() => new WebAuthnError(WebAuthnErrorType.UNKNOWN, `Unexpected error: ${error.message}`, error));
|
|
990
|
+
}
|
|
991
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
992
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, providedIn: 'root' });
|
|
993
|
+
}
|
|
994
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.0.3", ngImport: i0, type: WebAuthnService, decorators: [{
|
|
995
|
+
type: Injectable,
|
|
996
|
+
args: [{
|
|
997
|
+
providedIn: 'root',
|
|
998
|
+
}]
|
|
999
|
+
}] });
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* WebAuthn Providers
|
|
1003
|
+
* Modern Angular standalone provider function for WebAuthn service
|
|
1004
|
+
*/
|
|
1005
|
+
/**
|
|
1006
|
+
* Provides WebAuthn service with required relying party configuration
|
|
1007
|
+
*
|
|
1008
|
+
* @param relyingParty Required relying party configuration
|
|
1009
|
+
* @param config Optional configuration overrides
|
|
1010
|
+
* @returns Array of providers for WebAuthn functionality
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* ```typescript
|
|
1014
|
+
* // main.ts
|
|
1015
|
+
* bootstrapApplication(AppComponent, {
|
|
1016
|
+
* providers: [
|
|
1017
|
+
* provideWebAuthn(
|
|
1018
|
+
* { name: 'My App', id: 'myapp.com' },
|
|
1019
|
+
* { defaultTimeout: 30000 }
|
|
1020
|
+
* )
|
|
1021
|
+
* ]
|
|
1022
|
+
* });
|
|
1023
|
+
* ```
|
|
1024
|
+
*/
|
|
1025
|
+
function provideWebAuthn(relyingParty, config = {}) {
|
|
1026
|
+
return [
|
|
1027
|
+
{
|
|
1028
|
+
provide: WEBAUTHN_CONFIG,
|
|
1029
|
+
useValue: createWebAuthnConfig(relyingParty, config),
|
|
1030
|
+
},
|
|
1031
|
+
WebAuthnService,
|
|
1032
|
+
];
|
|
1033
|
+
}
|
|
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
|
+
|
|
1051
|
+
// Core service
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* Generated bundle index. Do not edit.
|
|
1055
|
+
*/
|
|
1056
|
+
|
|
1057
|
+
export { AuthenticatorError, DEFAULT_WEBAUTHN_CONFIG, DEVICE_BOUND_PRESET, InvalidOptionsError, NetworkError, PASSKEY_PRESET, PRESET_MAP, SECOND_FACTOR_PRESET, SecurityError, TimeoutError, UnsupportedOperationError, UserCancelledError, WEBAUTHN_CONFIG, WebAuthnError, WebAuthnErrorType, WebAuthnService, arrayBufferToBase64, arrayBufferToBase64url, arrayBufferToCredentialId, arrayBufferToString, base64ToArrayBuffer, base64urlToArrayBuffer, createWebAuthnConfig, credentialIdToArrayBuffer, generateChallenge, generateUserId, getDefaultPubKeyCredParams, getSupportedTransports, isJSONOptions, isPlatformAuthenticatorAvailable, isPublicKeyCredential, isWebAuthnSupported, provideWebAuthn, provideWebAuthnLegacy, stringToArrayBuffer, validateRegistrationOptions };
|
|
1058
|
+
//# sourceMappingURL=ngx-webauthn.mjs.map
|