@seamless-auth/react 0.3.0 → 0.5.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 +164 -0
- package/LICENSE.md +2 -2
- package/README.md +384 -52
- package/dist/AuthProvider.d.ts +20 -18
- package/dist/client/createSeamlessAuthClient.d.ts +99 -55
- package/dist/client/errors.d.ts +15 -0
- package/dist/client/result.d.ts +36 -0
- package/dist/client/webauthnSupport.d.ts +10 -0
- package/dist/components/AuthFallbackOptions.d.ts +1 -1
- package/dist/hooks/useAuthClient.d.ts +1 -1
- package/dist/index.d.ts +12 -10
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/routes.d.ts +21 -0
- package/package.json +7 -4
package/README.md
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
- `useAuthClient()`
|
|
18
18
|
- `usePasskeySupport()`
|
|
19
19
|
- `hasScopedRole()` and `roleGrantsAccess()`
|
|
20
|
-
-
|
|
20
|
+
- `SeamlessAuthError`, the error type carried on a failed result
|
|
21
|
+
- types including `AuthContextType`, `Credential`, `User`, `OAuthProvider`, `StepUpStatus`, the `SeamlessAuthResult` wrapper, and the headless client input/result types
|
|
21
22
|
|
|
22
23
|
## Installation
|
|
23
24
|
|
|
@@ -114,20 +115,21 @@ You are still responsible for your app’s route protection and redirects.
|
|
|
114
115
|
markSignedIn(): void;
|
|
115
116
|
hasRole(role: string): boolean | undefined;
|
|
116
117
|
hasScopedRole(role: string | string[]): boolean | undefined;
|
|
117
|
-
listOAuthProviders(): Promise<OAuthProvidersResult
|
|
118
|
-
startOAuthLogin(input: StartOAuthLoginInput): Promise<StartOAuthLoginResult
|
|
119
|
-
finishOAuthLogin(input: FinishOAuthLoginInput): Promise<
|
|
120
|
-
refreshSession(): Promise<
|
|
121
|
-
refreshStepUpStatus(): Promise<StepUpStatus
|
|
122
|
-
verifyStepUpWithPasskey(): Promise<
|
|
123
|
-
verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
118
|
+
listOAuthProviders(): Promise<SeamlessAuthResult<OAuthProvidersResult>>;
|
|
119
|
+
startOAuthLogin(input: StartOAuthLoginInput): Promise<SeamlessAuthResult<StartOAuthLoginResult>>;
|
|
120
|
+
finishOAuthLogin(input: FinishOAuthLoginInput): Promise<SeamlessAuthResult<MessageResult>>;
|
|
121
|
+
refreshSession(): Promise<SeamlessAuthResult<CurrentUserResult>>;
|
|
122
|
+
refreshStepUpStatus(): Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
123
|
+
verifyStepUpWithPasskey(): Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
124
|
+
verifyStepUpWithPasskeyPrf(input: PasskeyPrfInput): Promise<SeamlessAuthResult<StepUpPrfData>>;
|
|
125
|
+
verifyStepUpWithTotp(code: string): Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
126
|
+
logout(): Promise<SeamlessAuthResult<MessageResult>>;
|
|
127
|
+
logoutAllSessions(): Promise<SeamlessAuthResult<MessageResult>>;
|
|
128
|
+
deleteUser(): Promise<SeamlessAuthResult<MessageResult>>;
|
|
129
|
+
login(identifier: string, passkeyAvailable: boolean): Promise<SeamlessAuthResult<LoginStartResult>>;
|
|
130
|
+
handlePasskeyLogin(): Promise<SeamlessAuthResult<PasskeyLoginData>>;
|
|
131
|
+
updateCredential(credential: Credential): Promise<SeamlessAuthResult<Credential>>;
|
|
132
|
+
deleteCredential(credentialId: string): Promise<SeamlessAuthResult<MessageResult>>;
|
|
131
133
|
}
|
|
132
134
|
```
|
|
133
135
|
|
|
@@ -159,12 +161,12 @@ If you are building a fully custom flow, call `markSignedIn()` after a successfu
|
|
|
159
161
|
const { markSignedIn, refreshSession } = useAuth();
|
|
160
162
|
|
|
161
163
|
async function completeLogin() {
|
|
162
|
-
const
|
|
164
|
+
const { error } = await authClient.login({
|
|
163
165
|
identifier: 'user@example.com',
|
|
164
166
|
passkeyAvailable: true,
|
|
165
167
|
});
|
|
166
168
|
|
|
167
|
-
if (
|
|
169
|
+
if (!error) {
|
|
168
170
|
markSignedIn();
|
|
169
171
|
await refreshSession();
|
|
170
172
|
}
|
|
@@ -200,8 +202,8 @@ function DeleteAccountButton() {
|
|
|
200
202
|
const { refreshStepUpStatus, verifyStepUpWithPasskey } = useAuth();
|
|
201
203
|
|
|
202
204
|
async function handleDeleteAccount() {
|
|
203
|
-
const status = await refreshStepUpStatus();
|
|
204
|
-
const fresh = status?.fresh ? true : (await verifyStepUpWithPasskey()).
|
|
205
|
+
const { data: status } = await refreshStepUpStatus();
|
|
206
|
+
const fresh = status?.fresh ? true : !(await verifyStepUpWithPasskey()).error;
|
|
205
207
|
|
|
206
208
|
if (!fresh) {
|
|
207
209
|
return;
|
|
@@ -214,7 +216,46 @@ function DeleteAccountButton() {
|
|
|
214
216
|
}
|
|
215
217
|
```
|
|
216
218
|
|
|
217
|
-
|
|
219
|
+
Step-up supports WebAuthn/passkeys and TOTP (authenticator apps). `refreshStepUpStatus()` calls `/step-up/status`, `verifyStepUpWithPasskey()` performs the `/step-up/webauthn/start` and `/step-up/webauthn/finish` challenge flow, and `verifyStepUpWithTotp(code)` verifies a 6-digit authenticator code via `/totp/verify-mfa`. The verification helpers return a `SeamlessAuthResult<StepUpStatus>` and refresh the provider's `stepUpStatus` when they succeed.
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const { verifyStepUpWithTotp } = useAuth();
|
|
223
|
+
|
|
224
|
+
const { error } = await verifyStepUpWithTotp('123456'); // 6-digit code from the authenticator app
|
|
225
|
+
if (!error) {
|
|
226
|
+
// step-up is fresh; proceed with the sensitive action
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### TOTP (authenticator apps)
|
|
231
|
+
|
|
232
|
+
TOTP lets users register an authenticator app (Google Authenticator, 1Password, etc.) as a second factor for step-up verification. The SDK exposes headless client methods for enrollment and management; use them from a settings screen. All require an authenticated session.
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { createSeamlessAuthClient } from '@seamless-auth/react';
|
|
236
|
+
import type { TotpStatus, TotpEnrollmentStartResult } from '@seamless-auth/react';
|
|
237
|
+
|
|
238
|
+
const authClient = createSeamlessAuthClient({ apiHost: 'https://your.api' });
|
|
239
|
+
|
|
240
|
+
// 1. Check whether TOTP is already enabled
|
|
241
|
+
const { data: status } = await authClient.getTotpStatus();
|
|
242
|
+
|
|
243
|
+
// 2. Start enrollment: render `otpauthUrl` as a QR code (or show `secret` for manual entry)
|
|
244
|
+
const { data: enrollment } = await authClient.startTotpEnrollment();
|
|
245
|
+
|
|
246
|
+
// 3. Confirm the first code from the user's authenticator app
|
|
247
|
+
const { error } = await authClient.verifyTotpEnrollment('123456');
|
|
248
|
+
if (!error) {
|
|
249
|
+
// TOTP is now enabled
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Disabling requires a current code
|
|
253
|
+
await authClient.disableTotp('123456');
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
These methods follow the standard result convention: check `error`, then read `data`. Enrolling TOTP is a sensitive change; gate it behind a fresh step-up when appropriate.
|
|
257
|
+
|
|
258
|
+
> TOTP is not currently a login second factor. The Seamless Auth API issues a full session on the first factor and does not gate login on TOTP, so TOTP applies to step-up verification, not to the login flow.
|
|
218
259
|
|
|
219
260
|
### WebAuthn PRF
|
|
220
261
|
|
|
@@ -250,18 +291,18 @@ if (prfSupported) {
|
|
|
250
291
|
For local key unwrap flows such as Seamless Secrets, use PRF during step-up and consume the returned bytes in browser memory:
|
|
251
292
|
|
|
252
293
|
```ts
|
|
253
|
-
const
|
|
294
|
+
const { data, error } = await authClient.verifyStepUpWithPasskeyPrf({
|
|
254
295
|
salt: vaultSaltBase64url,
|
|
255
296
|
credentialId,
|
|
256
297
|
});
|
|
257
298
|
|
|
258
|
-
if (
|
|
259
|
-
throw
|
|
299
|
+
if (error) {
|
|
300
|
+
throw error;
|
|
260
301
|
}
|
|
261
302
|
|
|
262
303
|
const vaultUnlockMaterial: { credentialId: string; output: Uint8Array } = {
|
|
263
|
-
credentialId:
|
|
264
|
-
output:
|
|
304
|
+
credentialId: data.credentialId,
|
|
305
|
+
output: data.prf.output,
|
|
265
306
|
};
|
|
266
307
|
```
|
|
267
308
|
|
|
@@ -323,11 +364,14 @@ function OAuthCallback() {
|
|
|
323
364
|
|
|
324
365
|
useEffect(() => {
|
|
325
366
|
const params = new URLSearchParams(window.location.search);
|
|
326
|
-
|
|
367
|
+
// Persist the provider you passed to startOAuthLogin so the callback knows
|
|
368
|
+
// which provider to finish. The built-in AuthRoutes flow stores this in
|
|
369
|
+
// sessionStorage; use whatever your custom start flow saved.
|
|
370
|
+
const providerId = sessionStorage.getItem('seamless:oauth:provider');
|
|
327
371
|
const code = params.get('code');
|
|
328
372
|
const state = params.get('state');
|
|
329
373
|
|
|
330
|
-
if (!code || !state) {
|
|
374
|
+
if (!providerId || !code || !state) {
|
|
331
375
|
return;
|
|
332
376
|
}
|
|
333
377
|
|
|
@@ -374,14 +418,18 @@ const authClient = createSeamlessAuthClient({
|
|
|
374
418
|
apiHost: 'https://your.api',
|
|
375
419
|
});
|
|
376
420
|
|
|
377
|
-
const
|
|
421
|
+
const { data, error } = await authClient.login({
|
|
378
422
|
identifier: 'user@example.com',
|
|
379
423
|
passkeyAvailable: true,
|
|
380
424
|
});
|
|
381
425
|
|
|
382
|
-
if (
|
|
383
|
-
//
|
|
426
|
+
if (error) {
|
|
427
|
+
// error.message, error.status, and error.body carry the server detail
|
|
428
|
+
return;
|
|
384
429
|
}
|
|
430
|
+
|
|
431
|
+
// data is typed as LoginStartResult
|
|
432
|
+
console.log(data.loginMethods);
|
|
385
433
|
```
|
|
386
434
|
|
|
387
435
|
The headless client exposes helpers for:
|
|
@@ -393,20 +441,45 @@ The headless client exposes helpers for:
|
|
|
393
441
|
- magic-link request, verify, and polling
|
|
394
442
|
- OAuth provider listing, start, and callback completion
|
|
395
443
|
- passkey registration
|
|
396
|
-
- step-up status and
|
|
444
|
+
- step-up status, passkey verification, and TOTP verification
|
|
445
|
+
- TOTP enrollment, status, and disable
|
|
397
446
|
- logout and delete-user
|
|
398
447
|
- credential update and deletion
|
|
399
448
|
|
|
400
|
-
|
|
449
|
+
### Result convention
|
|
450
|
+
|
|
451
|
+
Every request method resolves to a `SeamlessAuthResult<T>`:
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
type SeamlessAuthResult<T> =
|
|
455
|
+
| { data: T; error: null }
|
|
456
|
+
| { data: null; error: SeamlessAuthError };
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
Check `error` first, then read `data`. TypeScript enforces this: `data` is not readable until the
|
|
460
|
+
error has been ruled out.
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
const { data, error } = await authClient.getCurrentUser();
|
|
464
|
+
|
|
465
|
+
if (error) {
|
|
466
|
+
console.log(error.message, error.status, error.body);
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
setUser(data.user); // typed as CurrentUserResult
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Nothing throws for an HTTP failure, and transport failures are absorbed too, reported as an error
|
|
474
|
+
with `status` `0`. That means an expected auth outcome such as a wrong OTP, an expired magic link, or
|
|
475
|
+
a disabled provider is a value you can map straight to UI state rather than an exception to catch.
|
|
401
476
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
- `startOAuthLogin(input): Promise<StartOAuthLoginResult>`
|
|
409
|
-
- `finishOAuthLogin(input): Promise<Response>`
|
|
477
|
+
`SeamlessAuthError` carries the server's `message`, the HTTP `status`, and the parsed response
|
|
478
|
+
`body`, so you can branch on a specific failure.
|
|
479
|
+
|
|
480
|
+
The single exception is `isPasskeySupported`-style capability checks:
|
|
481
|
+
`isPasskeyPrfSupported(): Promise<boolean>` is a local check rather than a request, so it returns a
|
|
482
|
+
plain boolean.
|
|
410
483
|
|
|
411
484
|
## React Hooks For Custom UI
|
|
412
485
|
|
|
@@ -421,12 +494,12 @@ function CustomLogin() {
|
|
|
421
494
|
const { passkeySupported, loading } = usePasskeySupport();
|
|
422
495
|
|
|
423
496
|
async function handleEmailLogin() {
|
|
424
|
-
const
|
|
497
|
+
const { error } = await authClient.login({
|
|
425
498
|
identifier: 'user@example.com',
|
|
426
499
|
passkeyAvailable: passkeySupported,
|
|
427
500
|
});
|
|
428
501
|
|
|
429
|
-
if (
|
|
502
|
+
if (!error) {
|
|
430
503
|
await refreshSession();
|
|
431
504
|
}
|
|
432
505
|
}
|
|
@@ -439,20 +512,260 @@ function CustomLogin() {
|
|
|
439
512
|
}
|
|
440
513
|
```
|
|
441
514
|
|
|
515
|
+
### One error style everywhere
|
|
516
|
+
|
|
517
|
+
`useAuth()` helpers and the headless client report failure the same way: both return
|
|
518
|
+
`{ data, error }` and neither throws. Whatever surface you reach for, the handling is identical.
|
|
519
|
+
|
|
520
|
+
```tsx
|
|
521
|
+
const { error } = await updateCredential({ ...credential, friendlyName: 'Work laptop' });
|
|
522
|
+
|
|
523
|
+
if (error) {
|
|
524
|
+
setMessage(error.message);
|
|
525
|
+
}
|
|
526
|
+
```
|
|
527
|
+
|
|
528
|
+
Helpers that also mutate provider state, such as `switchOrganization` and `deleteCredential`, apply
|
|
529
|
+
that state change only when the call succeeds, then hand the result back for you to inspect.
|
|
530
|
+
|
|
531
|
+
## Custom UI Recipes
|
|
532
|
+
|
|
533
|
+
Worked examples for the flows the bundled screens cover, using only public primitives.
|
|
534
|
+
|
|
535
|
+
### Custom registration
|
|
536
|
+
|
|
537
|
+
Registration is two steps: create the account, then verify the emailed code. Call `markSignedIn()`
|
|
538
|
+
once the account is live so returning visits can default to sign-in.
|
|
539
|
+
|
|
540
|
+
```tsx
|
|
541
|
+
import { useAuth, useAuthClient } from '@seamless-auth/react';
|
|
542
|
+
import { useState } from 'react';
|
|
543
|
+
|
|
544
|
+
function CustomRegistration() {
|
|
545
|
+
const { markSignedIn, refreshSession } = useAuth();
|
|
546
|
+
const authClient = useAuthClient();
|
|
547
|
+
const [step, setStep] = useState<'details' | 'verify'>('details');
|
|
548
|
+
const [message, setMessage] = useState('');
|
|
549
|
+
|
|
550
|
+
async function createAccount(email: string) {
|
|
551
|
+
// Registration needs only an email. A phone can be added and verified later.
|
|
552
|
+
const { error } = await authClient.register({ email });
|
|
553
|
+
|
|
554
|
+
if (error) {
|
|
555
|
+
setMessage(error.message);
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// The API emails a verification code as part of registering.
|
|
560
|
+
setStep('verify');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function verifyCode(code: string) {
|
|
564
|
+
const { error } = await authClient.verifyEmailOtp(code);
|
|
565
|
+
|
|
566
|
+
if (error) {
|
|
567
|
+
setMessage(error.message);
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
markSignedIn();
|
|
572
|
+
await refreshSession();
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
return step === 'details' ? (
|
|
576
|
+
<DetailsForm onSubmit={createAccount} error={message} />
|
|
577
|
+
) : (
|
|
578
|
+
<CodeForm
|
|
579
|
+
onSubmit={verifyCode}
|
|
580
|
+
onResend={() => authClient.requestEmailOtp()}
|
|
581
|
+
error={message}
|
|
582
|
+
/>
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
To offer a passkey right after registering, call `registerPasskey()` before `refreshSession()`:
|
|
588
|
+
|
|
589
|
+
```ts
|
|
590
|
+
const { data, error } = await authClient.registerPasskey({
|
|
591
|
+
friendlyName: 'My laptop',
|
|
592
|
+
platform: 'macOS',
|
|
593
|
+
browser: 'Chrome',
|
|
594
|
+
deviceInfo: navigator.userAgent,
|
|
595
|
+
});
|
|
596
|
+
|
|
597
|
+
if (!error) {
|
|
598
|
+
console.log(data.credentialId, data.prfCapable);
|
|
599
|
+
}
|
|
600
|
+
```
|
|
601
|
+
|
|
602
|
+
### OTP and magic-link continuation
|
|
603
|
+
|
|
604
|
+
> **The request helpers take no identifier.** `requestMagicLink()`, `requestLoginEmailOtp()`, and
|
|
605
|
+
> `requestLoginPhoneOtp()` send nothing but the session cookie. They rely on server-side state
|
|
606
|
+
> established by a preceding `login()` call, so calling them without it fails or targets the wrong
|
|
607
|
+
> account. This is not obvious from their signatures. Always call `login()` first, and use the same
|
|
608
|
+
> browser session for the continuation step.
|
|
609
|
+
|
|
610
|
+
```tsx
|
|
611
|
+
function CustomLoginContinuation() {
|
|
612
|
+
const { login, refreshSession } = useAuth();
|
|
613
|
+
const authClient = useAuthClient();
|
|
614
|
+
|
|
615
|
+
async function start(identifier: string) {
|
|
616
|
+
// Required first: this is what the request helpers below depend on.
|
|
617
|
+
const { data, error } = await login(identifier, false);
|
|
618
|
+
|
|
619
|
+
if (error) {
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
// Offer only what the server says this account supports.
|
|
624
|
+
return data.loginMethods ?? ['magic_link', 'email_otp'];
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
async function sendEmailCode() {
|
|
628
|
+
const { error } = await authClient.requestLoginEmailOtp();
|
|
629
|
+
if (error) {
|
|
630
|
+
// surface error.message
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
async function submitEmailCode(code: string) {
|
|
635
|
+
const { error } = await authClient.verifyLoginEmailOtp(code);
|
|
636
|
+
|
|
637
|
+
if (!error) {
|
|
638
|
+
await refreshSession();
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
Magic links complete in whichever tab opens the emailed link, so a custom flow needs two pieces.
|
|
645
|
+
|
|
646
|
+
The waiting screen polls until the link is used:
|
|
647
|
+
|
|
648
|
+
```ts
|
|
649
|
+
const interval = setInterval(async () => {
|
|
650
|
+
const { error } = await authClient.checkMagicLink();
|
|
651
|
+
|
|
652
|
+
if (!error) {
|
|
653
|
+
clearInterval(interval);
|
|
654
|
+
await refreshSession();
|
|
655
|
+
}
|
|
656
|
+
}, 5000);
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
The landing route verifies the token from the query string, then refreshes its own session:
|
|
660
|
+
|
|
661
|
+
```tsx
|
|
662
|
+
function CustomMagicLinkLanding() {
|
|
663
|
+
const { refreshSession } = useAuth(); // plus: import { useEffect } from 'react'
|
|
664
|
+
const authClient = useAuthClient();
|
|
665
|
+
|
|
666
|
+
useEffect(() => {
|
|
667
|
+
const token = new URLSearchParams(window.location.search).get('token');
|
|
668
|
+
if (!token) return;
|
|
669
|
+
|
|
670
|
+
void authClient.verifyMagicLink(token).then(async ({ error }) => {
|
|
671
|
+
if (!error) {
|
|
672
|
+
// Refresh here too. This tab set the cookie, but its provider state
|
|
673
|
+
// was loaded before the cookie existed.
|
|
674
|
+
await refreshSession();
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
}, [authClient, refreshSession]);
|
|
678
|
+
|
|
679
|
+
return <p>Finishing sign-in...</p>;
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
The auth API emails a link pointing at `/verify-magiclink?token=...`, so a custom app must serve that
|
|
684
|
+
path.
|
|
685
|
+
|
|
686
|
+
### Credential management
|
|
687
|
+
|
|
688
|
+
`useAuth()` exposes the signed-in user's passkeys plus helpers to rename and remove them. These
|
|
689
|
+
helpers update provider state on success and report failure through `error`.
|
|
690
|
+
|
|
691
|
+
```tsx
|
|
692
|
+
import { useAuth } from '@seamless-auth/react';
|
|
693
|
+
import type { Credential } from '@seamless-auth/react';
|
|
694
|
+
import { useState } from 'react';
|
|
695
|
+
|
|
696
|
+
function PasskeyList() {
|
|
697
|
+
const { credentials, updateCredential, deleteCredential } = useAuth();
|
|
698
|
+
const [message, setMessage] = useState('');
|
|
699
|
+
|
|
700
|
+
async function rename(credential: Credential, friendlyName: string) {
|
|
701
|
+
const { error } = await updateCredential({ ...credential, friendlyName });
|
|
702
|
+
|
|
703
|
+
if (error) {
|
|
704
|
+
setMessage(error.message);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
async function remove(credentialId: string) {
|
|
709
|
+
const { error } = await deleteCredential(credentialId);
|
|
710
|
+
|
|
711
|
+
if (error) {
|
|
712
|
+
setMessage(error.message);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return (
|
|
717
|
+
<ul>
|
|
718
|
+
{credentials.map(credential => (
|
|
719
|
+
<li key={credential.id}>
|
|
720
|
+
{credential.friendlyName ?? credential.deviceInfo}
|
|
721
|
+
<button onClick={() => void rename(credential, 'Work laptop')}>Rename</button>
|
|
722
|
+
<button onClick={() => void remove(credential.id)}>Remove</button>
|
|
723
|
+
</li>
|
|
724
|
+
))}
|
|
725
|
+
</ul>
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
```
|
|
729
|
+
|
|
730
|
+
Removing a passkey is a sensitive change. Gate it behind a fresh step-up when the account has other
|
|
731
|
+
factors, using `refreshStepUpStatus()` and `verifyStepUpWithPasskey()` from the step-up section.
|
|
732
|
+
|
|
442
733
|
## Built-In Routes
|
|
443
734
|
|
|
444
|
-
`AuthRoutes`
|
|
735
|
+
`AuthRoutes` serves these canonical paths:
|
|
445
736
|
|
|
446
737
|
- `/login`
|
|
447
|
-
- `/
|
|
448
|
-
- `/
|
|
449
|
-
- `/
|
|
450
|
-
- `/verify-
|
|
451
|
-
- `/
|
|
452
|
-
- `/
|
|
738
|
+
- `/passkey-login`
|
|
739
|
+
- `/verify-phone-otp`
|
|
740
|
+
- `/verify-email-otp`
|
|
741
|
+
- `/verify-magic-link`
|
|
742
|
+
- `/oauth/callback`
|
|
743
|
+
- `/register-passkey`
|
|
744
|
+
- `/magic-link-sent`
|
|
453
745
|
|
|
454
746
|
These are optional UI wrappers over the same SDK primitives the package now exports for custom flows.
|
|
455
747
|
|
|
748
|
+
### Renamed routes
|
|
749
|
+
|
|
750
|
+
The earlier mixed-case paths were renamed and are no longer served. Anything linking directly to
|
|
751
|
+
them now falls through to `/login`, so update those links:
|
|
752
|
+
|
|
753
|
+
| Old path | New path |
|
|
754
|
+
| ------------------ | ------------------- |
|
|
755
|
+
| `/passKeyLogin` | `/passkey-login` |
|
|
756
|
+
| `/verifyPhoneOTP` | `/verify-phone-otp` |
|
|
757
|
+
| `/verifyEmailOTP` | `/verify-email-otp` |
|
|
758
|
+
| `/registerPasskey` | `/register-passkey` |
|
|
759
|
+
| `/magiclinks-sent` | `/magic-link-sent` |
|
|
760
|
+
|
|
761
|
+
Two paths are unchanged because they are owned by contracts outside this package:
|
|
762
|
+
|
|
763
|
+
- `/verify-magiclink` is the URL the auth API builds when it emails a magic link, so it has to match
|
|
764
|
+
that value exactly. Renaming it here would send every emailed link to `/login` with the token
|
|
765
|
+
discarded.
|
|
766
|
+
- `/oauth/callback` is registered with OAuth providers as an allowed redirect URI, so renaming it
|
|
767
|
+
would break configured integrations.
|
|
768
|
+
|
|
456
769
|
## Backend Expectations
|
|
457
770
|
|
|
458
771
|
This package assumes a Seamless Auth-compatible backend with the auth adapter mounted at `/auth`.
|
|
@@ -472,11 +785,15 @@ The built-in flows assume compatible endpoints for:
|
|
|
472
785
|
- `/webAuthn/login/finish`
|
|
473
786
|
- `/webAuthn/register/start`
|
|
474
787
|
- `/webAuthn/register/finish`
|
|
475
|
-
-
|
|
476
|
-
-
|
|
788
|
+
- `POST /otp/generate-phone-otp`
|
|
789
|
+
- `POST /otp/generate-email-otp`
|
|
477
790
|
- `/otp/verify-phone-otp`
|
|
478
791
|
- `/otp/verify-email-otp`
|
|
479
|
-
-
|
|
792
|
+
- `POST /otp/generate-login-phone-otp`
|
|
793
|
+
- `POST /otp/generate-login-email-otp`
|
|
794
|
+
- `/otp/verify-login-phone-otp`
|
|
795
|
+
- `/otp/verify-login-email-otp`
|
|
796
|
+
- `POST /magic-link`
|
|
480
797
|
- `/magic-link/check`
|
|
481
798
|
- `/magic-link/verify/:token`
|
|
482
799
|
- `/oauth/providers`
|
|
@@ -485,9 +802,24 @@ The built-in flows assume compatible endpoints for:
|
|
|
485
802
|
- `/step-up/status`
|
|
486
803
|
- `/step-up/webauthn/start`
|
|
487
804
|
- `/step-up/webauthn/finish`
|
|
805
|
+
- `/totp/status`
|
|
806
|
+
- `/totp/enroll/start`
|
|
807
|
+
- `/totp/enroll/verify`
|
|
808
|
+
- `/totp/disable`
|
|
809
|
+
- `/totp/verify-mfa`
|
|
488
810
|
- `/users/me`
|
|
489
811
|
- `/users/credentials`
|
|
490
812
|
- `/users/delete`
|
|
813
|
+
- `/organizations`
|
|
814
|
+
- `/organizations/:organizationId`
|
|
815
|
+
- `/organizations/:organizationId/switch`
|
|
816
|
+
- `/organizations/:organizationId/members`
|
|
817
|
+
- `/organizations/:organizationId/members/:userId`
|
|
818
|
+
|
|
819
|
+
The state-changing OTP and magic-link request routes are `POST` (marked above). They were previously
|
|
820
|
+
`GET`, which made them reachable as simple cross-site requests, so an `<img>` tag could trigger SMS or
|
|
821
|
+
email sends to a signed-in user. Using `@seamless-auth/react` with an older adapter that only serves the
|
|
822
|
+
`GET` forms returns a 404 for those requests. See the changelog for the minimum adapter version.
|
|
491
823
|
|
|
492
824
|
## Notes
|
|
493
825
|
|
package/dist/AuthProvider.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
|
-
import { FinishOAuthLoginInput, OAuthProvidersResult, StartOAuthLoginInput, StartOAuthLoginResult,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { CurrentUserResult, FinishOAuthLoginInput, LoginStartResult, MessageResult, OAuthProvidersResult, OrganizationSwitchResult, PasskeyLoginData, StartOAuthLoginInput, StartOAuthLoginResult, StepUpPrfData, StepUpStatus } from './client/createSeamlessAuthClient';
|
|
2
|
+
import type { SeamlessAuthResult } from './client/result';
|
|
3
|
+
import { PasskeyPrfInput } from './client/webauthnPrf';
|
|
4
|
+
import { Credential, Organization, User } from './types';
|
|
4
5
|
import React, { ReactNode } from 'react';
|
|
5
6
|
export interface AuthContextType {
|
|
6
7
|
user: User | null;
|
|
7
|
-
logout: () => Promise<
|
|
8
|
-
logoutAllSessions: () => Promise<
|
|
9
|
-
deleteUser: () => Promise<
|
|
10
|
-
refreshSession: () => Promise<
|
|
8
|
+
logout: () => Promise<SeamlessAuthResult<MessageResult>>;
|
|
9
|
+
logoutAllSessions: () => Promise<SeamlessAuthResult<MessageResult>>;
|
|
10
|
+
deleteUser: () => Promise<SeamlessAuthResult<MessageResult>>;
|
|
11
|
+
refreshSession: () => Promise<SeamlessAuthResult<CurrentUserResult>>;
|
|
11
12
|
isAuthenticated: boolean;
|
|
12
13
|
hasRole: (role: string) => boolean | undefined;
|
|
13
14
|
hasScopedRole: (role: string | string[]) => boolean | undefined;
|
|
@@ -17,18 +18,19 @@ export interface AuthContextType {
|
|
|
17
18
|
credentials: Credential[];
|
|
18
19
|
organizations: Organization[];
|
|
19
20
|
activeOrganization: Organization | null;
|
|
20
|
-
switchOrganization: (organizationId: string) => Promise<
|
|
21
|
-
listOAuthProviders: () => Promise<OAuthProvidersResult
|
|
22
|
-
startOAuthLogin: (input: StartOAuthLoginInput) => Promise<StartOAuthLoginResult
|
|
23
|
-
finishOAuthLogin: (input: FinishOAuthLoginInput) => Promise<
|
|
21
|
+
switchOrganization: (organizationId: string) => Promise<SeamlessAuthResult<OrganizationSwitchResult>>;
|
|
22
|
+
listOAuthProviders: () => Promise<SeamlessAuthResult<OAuthProvidersResult>>;
|
|
23
|
+
startOAuthLogin: (input: StartOAuthLoginInput) => Promise<SeamlessAuthResult<StartOAuthLoginResult>>;
|
|
24
|
+
finishOAuthLogin: (input: FinishOAuthLoginInput) => Promise<SeamlessAuthResult<MessageResult>>;
|
|
24
25
|
stepUpStatus: StepUpStatus | null;
|
|
25
|
-
updateCredential: (credential: Credential) => Promise<Credential
|
|
26
|
-
deleteCredential: (credentialId: string) => Promise<
|
|
27
|
-
login: (identifier: string, passkeyAvailable: boolean) => Promise<
|
|
28
|
-
handlePasskeyLogin: () => Promise<
|
|
29
|
-
refreshStepUpStatus: () => Promise<StepUpStatus
|
|
30
|
-
verifyStepUpWithPasskey: () => Promise<
|
|
31
|
-
verifyStepUpWithPasskeyPrf: (input: PasskeyPrfInput) => Promise<
|
|
26
|
+
updateCredential: (credential: Credential) => Promise<SeamlessAuthResult<Credential>>;
|
|
27
|
+
deleteCredential: (credentialId: string) => Promise<SeamlessAuthResult<MessageResult>>;
|
|
28
|
+
login: (identifier: string, passkeyAvailable: boolean) => Promise<SeamlessAuthResult<LoginStartResult>>;
|
|
29
|
+
handlePasskeyLogin: () => Promise<SeamlessAuthResult<PasskeyLoginData>>;
|
|
30
|
+
refreshStepUpStatus: () => Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
31
|
+
verifyStepUpWithPasskey: () => Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
32
|
+
verifyStepUpWithPasskeyPrf: (input: PasskeyPrfInput) => Promise<SeamlessAuthResult<StepUpPrfData>>;
|
|
33
|
+
verifyStepUpWithTotp: (code: string) => Promise<SeamlessAuthResult<StepUpStatus>>;
|
|
32
34
|
loading: boolean;
|
|
33
35
|
}
|
|
34
36
|
/**
|