@ovixa/auth-client 0.1.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/CHANGELOG.md +23 -0
- package/README.md +113 -0
- package/dist/{chunk-IIOWUPWH.js → chunk-5ZWKDQQM.js} +417 -3
- package/dist/chunk-5ZWKDQQM.js.map +1 -0
- package/dist/{chunk-MU444WPK.js → chunk-WON3EB4B.js} +2 -2
- package/dist/index.d.ts +414 -2
- package/dist/index.js +15 -3
- package/dist/middleware/astro.js +2 -2
- package/dist/middleware/express.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-IIOWUPWH.js.map +0 -1
- /package/dist/{chunk-MU444WPK.js.map → chunk-WON3EB4B.js.map} +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
7
|
|
|
8
|
+
## [0.2.0] - 2026-01-29
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **WebAuthn/Passkey authentication support** - Passwordless authentication using FIDO2
|
|
13
|
+
- `auth.webauthn.getRegistrationOptions(realmId)` - Get options to register a new passkey
|
|
14
|
+
- `auth.webauthn.verifyRegistration(realmId, registration)` - Complete passkey registration
|
|
15
|
+
- `auth.webauthn.getAuthenticationOptions(realmId, email?)` - Get options to authenticate with passkey
|
|
16
|
+
- `auth.webauthn.authenticate(realmId, authentication)` - Authenticate and receive tokens
|
|
17
|
+
- `auth.webauthn.getCredentials(realmId)` - List user's registered passkeys
|
|
18
|
+
- `auth.webauthn.deleteCredential(realmId, credentialId)` - Remove a passkey
|
|
19
|
+
- `auth.webauthn.updateCredentialName(realmId, credentialId, name)` - Rename a passkey
|
|
20
|
+
|
|
21
|
+
## [0.1.3] - 2026-01-29
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
|
|
25
|
+
- `auth.exchangeOAuthCode(code)` - Exchange OAuth authorization code for tokens, completing the OAuth 2.0 Authorization Code flow
|
|
26
|
+
|
|
27
|
+
### Fixed
|
|
28
|
+
|
|
29
|
+
- OAuth flow now works correctly with the auth server's secure authorization code pattern
|
|
30
|
+
|
|
8
31
|
## [0.1.2] - 2026-01-28
|
|
9
32
|
|
|
10
33
|
### Added
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@ Client SDK for the Ovixa Auth service. Provides authentication, token verificati
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- Email/password authentication (signup, login, password reset)
|
|
8
|
+
- **Passkey (WebAuthn) authentication** - phishing-resistant passwordless sign-in
|
|
8
9
|
- OAuth integration (Google, GitHub)
|
|
9
10
|
- JWT verification with JWKS caching
|
|
10
11
|
- Automatic token refresh
|
|
@@ -141,6 +142,15 @@ await auth.forgotPassword({
|
|
|
141
142
|
});
|
|
142
143
|
```
|
|
143
144
|
|
|
145
|
+
### Email Branding
|
|
146
|
+
|
|
147
|
+
Verification and password reset emails automatically display your realm's `display_name` as the brand name. To customize the branding in emails:
|
|
148
|
+
|
|
149
|
+
1. Set `display_name` when creating your realm
|
|
150
|
+
2. Emails will show your brand (e.g., "Linkdrop") instead of "Ovixa"
|
|
151
|
+
|
|
152
|
+
If no `display_name` is set, emails default to "Ovixa".
|
|
153
|
+
|
|
144
154
|
#### `resetPassword(options)`
|
|
145
155
|
|
|
146
156
|
Set a new password using a reset token.
|
|
@@ -234,6 +244,109 @@ try {
|
|
|
234
244
|
}
|
|
235
245
|
```
|
|
236
246
|
|
|
247
|
+
### Passkeys (WebAuthn)
|
|
248
|
+
|
|
249
|
+
Passkeys provide phishing-resistant, passwordless authentication using FIDO2/WebAuthn.
|
|
250
|
+
|
|
251
|
+
#### `webauthn.getRegistrationOptions(options)`
|
|
252
|
+
|
|
253
|
+
Get options for registering a new passkey. **Requires authentication.**
|
|
254
|
+
|
|
255
|
+
```typescript
|
|
256
|
+
const options = await auth.webauthn.getRegistrationOptions({
|
|
257
|
+
accessToken: 'user-access-token',
|
|
258
|
+
});
|
|
259
|
+
// Returns PublicKeyCredentialCreationOptions for navigator.credentials.create()
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
#### `webauthn.verifyRegistration(options)`
|
|
263
|
+
|
|
264
|
+
Verify and store a new passkey registration. **Requires authentication.**
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
const result = await auth.webauthn.verifyRegistration({
|
|
268
|
+
accessToken: 'user-access-token',
|
|
269
|
+
registration: credentialResponse, // From navigator.credentials.create()
|
|
270
|
+
deviceName: 'My MacBook', // Optional friendly name
|
|
271
|
+
});
|
|
272
|
+
// Returns: { success: true, credential_id: string, device_name?: string }
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
#### `webauthn.getAuthenticationOptions(options)`
|
|
276
|
+
|
|
277
|
+
Get options for signing in with a passkey. Does not require authentication.
|
|
278
|
+
|
|
279
|
+
```typescript
|
|
280
|
+
const options = await auth.webauthn.getAuthenticationOptions({
|
|
281
|
+
email: 'user@example.com', // Optional - provides allowCredentials hint
|
|
282
|
+
});
|
|
283
|
+
// Returns PublicKeyCredentialRequestOptions for navigator.credentials.get()
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
#### `webauthn.authenticate(options)`
|
|
287
|
+
|
|
288
|
+
Verify passkey authentication and receive tokens.
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
const tokens = await auth.webauthn.authenticate({
|
|
292
|
+
authentication: credentialResponse, // From navigator.credentials.get()
|
|
293
|
+
});
|
|
294
|
+
// Returns: { access_token, refresh_token, token_type, expires_in }
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
#### Complete Passkey Flow Example
|
|
298
|
+
|
|
299
|
+
```typescript
|
|
300
|
+
import { OvixaAuth } from '@ovixa/auth-client';
|
|
301
|
+
|
|
302
|
+
const auth = new OvixaAuth({
|
|
303
|
+
authUrl: 'https://auth.ovixa.io',
|
|
304
|
+
realmId: 'your-realm-id',
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Register a passkey (user must be logged in)
|
|
308
|
+
async function registerPasskey(accessToken: string) {
|
|
309
|
+
// 1. Get registration options from server
|
|
310
|
+
const options = await auth.webauthn.getRegistrationOptions({ accessToken });
|
|
311
|
+
|
|
312
|
+
// 2. Create credential using browser API
|
|
313
|
+
const credential = await navigator.credentials.create({
|
|
314
|
+
publicKey: options,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
if (!credential) throw new Error('Registration cancelled');
|
|
318
|
+
|
|
319
|
+
// 3. Verify with server
|
|
320
|
+
const result = await auth.webauthn.verifyRegistration({
|
|
321
|
+
accessToken,
|
|
322
|
+
registration: credential as PublicKeyCredential,
|
|
323
|
+
deviceName: 'My Device',
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
return result;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Sign in with passkey
|
|
330
|
+
async function signInWithPasskey(email?: string) {
|
|
331
|
+
// 1. Get authentication options
|
|
332
|
+
const options = await auth.webauthn.getAuthenticationOptions({ email });
|
|
333
|
+
|
|
334
|
+
// 2. Get credential using browser API
|
|
335
|
+
const credential = await navigator.credentials.get({
|
|
336
|
+
publicKey: options,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
if (!credential) throw new Error('Authentication cancelled');
|
|
340
|
+
|
|
341
|
+
// 3. Verify and get tokens
|
|
342
|
+
const tokens = await auth.webauthn.authenticate({
|
|
343
|
+
authentication: credential as PublicKeyCredential,
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return tokens;
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
237
350
|
### OAuth
|
|
238
351
|
|
|
239
352
|
#### `getOAuthUrl(options)`
|
|
@@ -415,7 +415,8 @@ var OvixaAuth = class {
|
|
|
415
415
|
* Generate an OAuth authorization URL.
|
|
416
416
|
*
|
|
417
417
|
* Redirect the user to this URL to start the OAuth flow. After authentication,
|
|
418
|
-
* the user will be redirected back to your `redirectUri` with
|
|
418
|
+
* the user will be redirected back to your `redirectUri` with an authorization code.
|
|
419
|
+
* Use `exchangeOAuthCode()` to exchange the code for tokens.
|
|
419
420
|
*
|
|
420
421
|
* @param options - OAuth URL options
|
|
421
422
|
* @returns The full OAuth authorization URL
|
|
@@ -429,6 +430,10 @@ var OvixaAuth = class {
|
|
|
429
430
|
*
|
|
430
431
|
* // Redirect user to start OAuth flow
|
|
431
432
|
* window.location.href = googleAuthUrl;
|
|
433
|
+
*
|
|
434
|
+
* // In your callback handler:
|
|
435
|
+
* // const code = new URLSearchParams(window.location.search).get('code');
|
|
436
|
+
* // const tokens = await auth.exchangeOAuthCode(code);
|
|
432
437
|
* ```
|
|
433
438
|
*/
|
|
434
439
|
getOAuthUrl(options) {
|
|
@@ -437,6 +442,329 @@ var OvixaAuth = class {
|
|
|
437
442
|
url.searchParams.set("redirect_uri", options.redirectUri);
|
|
438
443
|
return url.toString();
|
|
439
444
|
}
|
|
445
|
+
// ============================================================
|
|
446
|
+
// WebAuthn / Passkey Methods
|
|
447
|
+
// ============================================================
|
|
448
|
+
/**
|
|
449
|
+
* Get registration options for creating a new passkey.
|
|
450
|
+
*
|
|
451
|
+
* The user must be logged in to register a passkey. Use the returned options
|
|
452
|
+
* with navigator.credentials.create() to create the credential.
|
|
453
|
+
*
|
|
454
|
+
* @param options - Registration options with access token
|
|
455
|
+
* @returns Registration options to pass to navigator.credentials.create()
|
|
456
|
+
* @throws {OvixaAuthError} If request fails
|
|
457
|
+
*
|
|
458
|
+
* @example
|
|
459
|
+
* ```typescript
|
|
460
|
+
* // User must be logged in
|
|
461
|
+
* const options = await auth.getPasskeyRegistrationOptions({
|
|
462
|
+
* accessToken: session.accessToken,
|
|
463
|
+
* });
|
|
464
|
+
*
|
|
465
|
+
* // Create the credential using the browser API
|
|
466
|
+
* const credential = await navigator.credentials.create({
|
|
467
|
+
* publicKey: {
|
|
468
|
+
* ...options,
|
|
469
|
+
* challenge: base64UrlToArrayBuffer(options.challenge),
|
|
470
|
+
* user: {
|
|
471
|
+
* ...options.user,
|
|
472
|
+
* id: new TextEncoder().encode(options.user.id),
|
|
473
|
+
* },
|
|
474
|
+
* excludeCredentials: options.excludeCredentials.map(c => ({
|
|
475
|
+
* ...c,
|
|
476
|
+
* id: base64UrlToArrayBuffer(c.id),
|
|
477
|
+
* })),
|
|
478
|
+
* },
|
|
479
|
+
* });
|
|
480
|
+
*
|
|
481
|
+
* // Verify the registration with the server
|
|
482
|
+
* await auth.verifyPasskeyRegistration({
|
|
483
|
+
* accessToken: session.accessToken,
|
|
484
|
+
* registration: formatRegistrationResponse(credential),
|
|
485
|
+
* deviceName: 'My MacBook',
|
|
486
|
+
* });
|
|
487
|
+
* ```
|
|
488
|
+
*/
|
|
489
|
+
async getPasskeyRegistrationOptions(options) {
|
|
490
|
+
const url = `${this.config.authUrl}/webauthn/register/options`;
|
|
491
|
+
try {
|
|
492
|
+
const response = await fetch(url, {
|
|
493
|
+
method: "POST",
|
|
494
|
+
headers: {
|
|
495
|
+
"Content-Type": "application/json",
|
|
496
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
497
|
+
},
|
|
498
|
+
body: JSON.stringify({
|
|
499
|
+
realm_id: this.config.realmId
|
|
500
|
+
})
|
|
501
|
+
});
|
|
502
|
+
if (!response.ok) {
|
|
503
|
+
await this.handleErrorResponse(response);
|
|
504
|
+
}
|
|
505
|
+
return await response.json();
|
|
506
|
+
} catch (error) {
|
|
507
|
+
if (error instanceof OvixaAuthError) {
|
|
508
|
+
throw error;
|
|
509
|
+
}
|
|
510
|
+
if (error instanceof Error) {
|
|
511
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
512
|
+
}
|
|
513
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Verify a passkey registration and store the credential.
|
|
518
|
+
*
|
|
519
|
+
* Call this after navigator.credentials.create() succeeds to complete the
|
|
520
|
+
* registration on the server.
|
|
521
|
+
*
|
|
522
|
+
* @param options - Verification options with registration response
|
|
523
|
+
* @returns Registration response with credential ID
|
|
524
|
+
* @throws {OvixaAuthError} If verification fails
|
|
525
|
+
*
|
|
526
|
+
* @example
|
|
527
|
+
* ```typescript
|
|
528
|
+
* const result = await auth.verifyPasskeyRegistration({
|
|
529
|
+
* accessToken: session.accessToken,
|
|
530
|
+
* registration: {
|
|
531
|
+
* id: credential.id,
|
|
532
|
+
* rawId: arrayBufferToBase64Url(credential.rawId),
|
|
533
|
+
* type: 'public-key',
|
|
534
|
+
* response: {
|
|
535
|
+
* clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON),
|
|
536
|
+
* attestationObject: arrayBufferToBase64Url(credential.response.attestationObject),
|
|
537
|
+
* transports: credential.response.getTransports?.(),
|
|
538
|
+
* },
|
|
539
|
+
* },
|
|
540
|
+
* deviceName: 'My MacBook',
|
|
541
|
+
* });
|
|
542
|
+
*
|
|
543
|
+
* console.log('Passkey registered:', result.credential_id);
|
|
544
|
+
* ```
|
|
545
|
+
*/
|
|
546
|
+
async verifyPasskeyRegistration(options) {
|
|
547
|
+
const url = `${this.config.authUrl}/webauthn/register/verify`;
|
|
548
|
+
const body = {
|
|
549
|
+
realm_id: this.config.realmId,
|
|
550
|
+
registration: options.registration
|
|
551
|
+
};
|
|
552
|
+
if (options.deviceName) {
|
|
553
|
+
body.device_name = options.deviceName;
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const response = await fetch(url, {
|
|
557
|
+
method: "POST",
|
|
558
|
+
headers: {
|
|
559
|
+
"Content-Type": "application/json",
|
|
560
|
+
Authorization: `Bearer ${options.accessToken}`
|
|
561
|
+
},
|
|
562
|
+
body: JSON.stringify(body)
|
|
563
|
+
});
|
|
564
|
+
if (!response.ok) {
|
|
565
|
+
await this.handleErrorResponse(response);
|
|
566
|
+
}
|
|
567
|
+
return await response.json();
|
|
568
|
+
} catch (error) {
|
|
569
|
+
if (error instanceof OvixaAuthError) {
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
if (error instanceof Error) {
|
|
573
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
574
|
+
}
|
|
575
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Get authentication options for signing in with a passkey.
|
|
580
|
+
*
|
|
581
|
+
* Use the returned options with navigator.credentials.get() to authenticate.
|
|
582
|
+
*
|
|
583
|
+
* @param options - Optional email for allowCredentials hint
|
|
584
|
+
* @returns Authentication options to pass to navigator.credentials.get()
|
|
585
|
+
* @throws {OvixaAuthError} If request fails
|
|
586
|
+
*
|
|
587
|
+
* @example
|
|
588
|
+
* ```typescript
|
|
589
|
+
* // Option 1: Discoverable credentials (no email needed)
|
|
590
|
+
* const options = await auth.getPasskeyAuthenticationOptions();
|
|
591
|
+
*
|
|
592
|
+
* // Option 2: Provide email for allowCredentials hint
|
|
593
|
+
* const options = await auth.getPasskeyAuthenticationOptions({
|
|
594
|
+
* email: 'user@example.com',
|
|
595
|
+
* });
|
|
596
|
+
*
|
|
597
|
+
* // Authenticate using the browser API
|
|
598
|
+
* const credential = await navigator.credentials.get({
|
|
599
|
+
* publicKey: {
|
|
600
|
+
* ...options,
|
|
601
|
+
* challenge: base64UrlToArrayBuffer(options.challenge),
|
|
602
|
+
* allowCredentials: options.allowCredentials.map(c => ({
|
|
603
|
+
* ...c,
|
|
604
|
+
* id: base64UrlToArrayBuffer(c.id),
|
|
605
|
+
* })),
|
|
606
|
+
* },
|
|
607
|
+
* });
|
|
608
|
+
*
|
|
609
|
+
* // Verify and get tokens
|
|
610
|
+
* const tokens = await auth.verifyPasskeyAuthentication({
|
|
611
|
+
* authentication: formatAuthenticationResponse(credential),
|
|
612
|
+
* });
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
async getPasskeyAuthenticationOptions(options) {
|
|
616
|
+
const url = `${this.config.authUrl}/webauthn/authenticate/options`;
|
|
617
|
+
const body = {
|
|
618
|
+
realm_id: this.config.realmId
|
|
619
|
+
};
|
|
620
|
+
if (options?.email) {
|
|
621
|
+
body.email = options.email;
|
|
622
|
+
}
|
|
623
|
+
try {
|
|
624
|
+
const response = await fetch(url, {
|
|
625
|
+
method: "POST",
|
|
626
|
+
headers: {
|
|
627
|
+
"Content-Type": "application/json"
|
|
628
|
+
},
|
|
629
|
+
body: JSON.stringify(body)
|
|
630
|
+
});
|
|
631
|
+
if (!response.ok) {
|
|
632
|
+
await this.handleErrorResponse(response);
|
|
633
|
+
}
|
|
634
|
+
return await response.json();
|
|
635
|
+
} catch (error) {
|
|
636
|
+
if (error instanceof OvixaAuthError) {
|
|
637
|
+
throw error;
|
|
638
|
+
}
|
|
639
|
+
if (error instanceof Error) {
|
|
640
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
641
|
+
}
|
|
642
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Verify a passkey authentication and get tokens.
|
|
647
|
+
*
|
|
648
|
+
* Call this after navigator.credentials.get() succeeds to complete the
|
|
649
|
+
* authentication and receive access/refresh tokens.
|
|
650
|
+
*
|
|
651
|
+
* @param options - Verification options with authentication response
|
|
652
|
+
* @returns Token response with access and refresh tokens
|
|
653
|
+
* @throws {OvixaAuthError} If verification fails
|
|
654
|
+
*
|
|
655
|
+
* @example
|
|
656
|
+
* ```typescript
|
|
657
|
+
* const tokens = await auth.verifyPasskeyAuthentication({
|
|
658
|
+
* authentication: {
|
|
659
|
+
* id: credential.id,
|
|
660
|
+
* rawId: arrayBufferToBase64Url(credential.rawId),
|
|
661
|
+
* type: 'public-key',
|
|
662
|
+
* response: {
|
|
663
|
+
* clientDataJSON: arrayBufferToBase64Url(credential.response.clientDataJSON),
|
|
664
|
+
* authenticatorData: arrayBufferToBase64Url(credential.response.authenticatorData),
|
|
665
|
+
* signature: arrayBufferToBase64Url(credential.response.signature),
|
|
666
|
+
* userHandle: credential.response.userHandle
|
|
667
|
+
* ? arrayBufferToBase64Url(credential.response.userHandle)
|
|
668
|
+
* : null,
|
|
669
|
+
* },
|
|
670
|
+
* },
|
|
671
|
+
* });
|
|
672
|
+
*
|
|
673
|
+
* // Use the tokens
|
|
674
|
+
* console.log('Logged in!', tokens.access_token);
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
async verifyPasskeyAuthentication(options) {
|
|
678
|
+
const url = `${this.config.authUrl}/webauthn/authenticate/verify`;
|
|
679
|
+
const body = {
|
|
680
|
+
realm_id: this.config.realmId,
|
|
681
|
+
authentication: options.authentication
|
|
682
|
+
};
|
|
683
|
+
try {
|
|
684
|
+
const response = await fetch(url, {
|
|
685
|
+
method: "POST",
|
|
686
|
+
headers: {
|
|
687
|
+
"Content-Type": "application/json"
|
|
688
|
+
},
|
|
689
|
+
body: JSON.stringify(body)
|
|
690
|
+
});
|
|
691
|
+
if (!response.ok) {
|
|
692
|
+
await this.handleErrorResponse(response);
|
|
693
|
+
}
|
|
694
|
+
return await response.json();
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (error instanceof OvixaAuthError) {
|
|
697
|
+
throw error;
|
|
698
|
+
}
|
|
699
|
+
if (error instanceof Error) {
|
|
700
|
+
throw new OvixaAuthError(`Network error: ${error.message}`, "NETWORK_ERROR");
|
|
701
|
+
}
|
|
702
|
+
throw new OvixaAuthError("Request failed", "REQUEST_FAILED");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Handle error response from the server.
|
|
707
|
+
* @throws {OvixaAuthError} Always throws with appropriate error details
|
|
708
|
+
*/
|
|
709
|
+
async handleErrorResponse(response) {
|
|
710
|
+
const errorBody = await response.json().catch(() => ({}));
|
|
711
|
+
const errorData = errorBody;
|
|
712
|
+
let errorCode;
|
|
713
|
+
let errorMessage;
|
|
714
|
+
if (typeof errorData.error === "object" && errorData.error) {
|
|
715
|
+
errorCode = errorData.error.code || this.mapHttpStatusToErrorCode(response.status);
|
|
716
|
+
errorMessage = errorData.error.message || "Request failed";
|
|
717
|
+
} else {
|
|
718
|
+
errorCode = this.mapHttpStatusToErrorCode(response.status);
|
|
719
|
+
errorMessage = typeof errorData.error === "string" ? errorData.error : "Request failed";
|
|
720
|
+
}
|
|
721
|
+
throw new OvixaAuthError(errorMessage, errorCode, response.status);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Exchange an OAuth authorization code for tokens.
|
|
725
|
+
*
|
|
726
|
+
* After the OAuth flow completes, the user is redirected back to your app with
|
|
727
|
+
* an authorization code in the URL query params. Use this method to exchange
|
|
728
|
+
* that code for access and refresh tokens.
|
|
729
|
+
*
|
|
730
|
+
* Note: Authorization codes expire after 60 seconds.
|
|
731
|
+
*
|
|
732
|
+
* @param code - The authorization code from the OAuth callback URL
|
|
733
|
+
* @returns Token response with access and refresh tokens
|
|
734
|
+
* @throws {OvixaAuthError} If the exchange fails
|
|
735
|
+
*
|
|
736
|
+
* @example
|
|
737
|
+
* ```typescript
|
|
738
|
+
* // In your OAuth callback handler
|
|
739
|
+
* const code = new URLSearchParams(window.location.search).get('code');
|
|
740
|
+
*
|
|
741
|
+
* if (code) {
|
|
742
|
+
* try {
|
|
743
|
+
* const tokens = await auth.exchangeOAuthCode(code);
|
|
744
|
+
* console.log('OAuth successful!', tokens.access_token);
|
|
745
|
+
*
|
|
746
|
+
* // Check if this is a new user (first-time OAuth login)
|
|
747
|
+
* if (tokens.is_new_user) {
|
|
748
|
+
* // Show onboarding flow
|
|
749
|
+
* }
|
|
750
|
+
* } catch (error) {
|
|
751
|
+
* if (error instanceof OvixaAuthError) {
|
|
752
|
+
* if (error.code === 'INVALID_CODE') {
|
|
753
|
+
* console.error('Invalid or expired authorization code');
|
|
754
|
+
* }
|
|
755
|
+
* }
|
|
756
|
+
* }
|
|
757
|
+
* }
|
|
758
|
+
* ```
|
|
759
|
+
*/
|
|
760
|
+
async exchangeOAuthCode(code) {
|
|
761
|
+
const url = `${this.config.authUrl}/oauth/token`;
|
|
762
|
+
const body = {
|
|
763
|
+
code,
|
|
764
|
+
realm_id: this.config.realmId
|
|
765
|
+
};
|
|
766
|
+
return this.makeRequest(url, body);
|
|
767
|
+
}
|
|
440
768
|
/**
|
|
441
769
|
* Transform a token response into an AuthResult with user and session data.
|
|
442
770
|
*
|
|
@@ -723,9 +1051,95 @@ var OvixaAuthAdmin = class {
|
|
|
723
1051
|
}
|
|
724
1052
|
}
|
|
725
1053
|
};
|
|
1054
|
+
function arrayBufferToBase64Url(buffer) {
|
|
1055
|
+
const bytes = new Uint8Array(buffer);
|
|
1056
|
+
let binary = "";
|
|
1057
|
+
for (const byte of bytes) {
|
|
1058
|
+
binary += String.fromCharCode(byte);
|
|
1059
|
+
}
|
|
1060
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1061
|
+
}
|
|
1062
|
+
function base64UrlToArrayBuffer(base64url) {
|
|
1063
|
+
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
|
|
1064
|
+
const padded = base64 + "=".repeat((4 - base64.length % 4) % 4);
|
|
1065
|
+
const binary = atob(padded);
|
|
1066
|
+
const bytes = new Uint8Array(binary.length);
|
|
1067
|
+
for (let i = 0; i < binary.length; i++) {
|
|
1068
|
+
bytes[i] = binary.charCodeAt(i);
|
|
1069
|
+
}
|
|
1070
|
+
return bytes.buffer;
|
|
1071
|
+
}
|
|
1072
|
+
function prepareRegistrationOptions(options) {
|
|
1073
|
+
return {
|
|
1074
|
+
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
1075
|
+
rp: options.rp,
|
|
1076
|
+
user: {
|
|
1077
|
+
id: new TextEncoder().encode(options.user.id),
|
|
1078
|
+
name: options.user.name,
|
|
1079
|
+
displayName: options.user.displayName
|
|
1080
|
+
},
|
|
1081
|
+
pubKeyCredParams: options.pubKeyCredParams,
|
|
1082
|
+
excludeCredentials: options.excludeCredentials.map((cred) => ({
|
|
1083
|
+
id: base64UrlToArrayBuffer(cred.id),
|
|
1084
|
+
type: cred.type,
|
|
1085
|
+
transports: cred.transports
|
|
1086
|
+
})),
|
|
1087
|
+
authenticatorSelection: options.authenticatorSelection,
|
|
1088
|
+
timeout: options.timeout,
|
|
1089
|
+
attestation: options.attestation
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
function prepareAuthenticationOptions(options) {
|
|
1093
|
+
return {
|
|
1094
|
+
challenge: base64UrlToArrayBuffer(options.challenge),
|
|
1095
|
+
rpId: options.rpId,
|
|
1096
|
+
allowCredentials: options.allowCredentials.map((cred) => ({
|
|
1097
|
+
id: base64UrlToArrayBuffer(cred.id),
|
|
1098
|
+
type: cred.type,
|
|
1099
|
+
transports: cred.transports
|
|
1100
|
+
})),
|
|
1101
|
+
userVerification: options.userVerification,
|
|
1102
|
+
timeout: options.timeout
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
function formatRegistrationResponse(credential) {
|
|
1106
|
+
const response = credential.response;
|
|
1107
|
+
return {
|
|
1108
|
+
id: credential.id,
|
|
1109
|
+
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
1110
|
+
type: "public-key",
|
|
1111
|
+
response: {
|
|
1112
|
+
clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
|
|
1113
|
+
attestationObject: arrayBufferToBase64Url(response.attestationObject),
|
|
1114
|
+
transports: response.getTransports?.()
|
|
1115
|
+
},
|
|
1116
|
+
authenticatorAttachment: credential.authenticatorAttachment
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
function formatAuthenticationResponse(credential) {
|
|
1120
|
+
const response = credential.response;
|
|
1121
|
+
return {
|
|
1122
|
+
id: credential.id,
|
|
1123
|
+
rawId: arrayBufferToBase64Url(credential.rawId),
|
|
1124
|
+
type: "public-key",
|
|
1125
|
+
response: {
|
|
1126
|
+
clientDataJSON: arrayBufferToBase64Url(response.clientDataJSON),
|
|
1127
|
+
authenticatorData: arrayBufferToBase64Url(response.authenticatorData),
|
|
1128
|
+
signature: arrayBufferToBase64Url(response.signature),
|
|
1129
|
+
userHandle: response.userHandle ? arrayBufferToBase64Url(response.userHandle) : null
|
|
1130
|
+
},
|
|
1131
|
+
authenticatorAttachment: credential.authenticatorAttachment
|
|
1132
|
+
};
|
|
1133
|
+
}
|
|
726
1134
|
|
|
727
1135
|
export {
|
|
728
1136
|
OvixaAuthError,
|
|
729
|
-
OvixaAuth
|
|
1137
|
+
OvixaAuth,
|
|
1138
|
+
arrayBufferToBase64Url,
|
|
1139
|
+
base64UrlToArrayBuffer,
|
|
1140
|
+
prepareRegistrationOptions,
|
|
1141
|
+
prepareAuthenticationOptions,
|
|
1142
|
+
formatRegistrationResponse,
|
|
1143
|
+
formatAuthenticationResponse
|
|
730
1144
|
};
|
|
731
|
-
//# sourceMappingURL=chunk-
|
|
1145
|
+
//# sourceMappingURL=chunk-5ZWKDQQM.js.map
|