@authon/react 0.3.0 → 0.3.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.ko.md +44 -177
- package/README.md +141 -710
- package/dist/index.cjs +1 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,786 +2,217 @@
|
|
|
2
2
|
|
|
3
3
|
# @authon/react
|
|
4
4
|
|
|
5
|
-
React
|
|
5
|
+
> Drop-in React authentication with hooks and components — Auth0 alternative, open-source auth
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```bash
|
|
10
|
-
npm install @authon/react @authon/js
|
|
11
|
-
# or
|
|
12
|
-
pnpm add @authon/react @authon/js
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
Requires `react >= 18.0.0`.
|
|
16
|
-
|
|
17
|
-
## Setup
|
|
18
|
-
|
|
19
|
-
Wrap your app with `<AuthonProvider>` at the root level:
|
|
20
|
-
|
|
21
|
-
```tsx
|
|
22
|
-
// src/main.tsx
|
|
23
|
-
import { AuthonProvider } from '@authon/react';
|
|
24
|
-
|
|
25
|
-
function App() {
|
|
26
|
-
return (
|
|
27
|
-
<AuthonProvider publishableKey="pk_live_...">
|
|
28
|
-
<Router />
|
|
29
|
-
</AuthonProvider>
|
|
30
|
-
);
|
|
31
|
-
}
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
Get your publishable key from the [Authon Dashboard](https://authon.dev/dashboard).
|
|
35
|
-
|
|
36
|
-
---
|
|
7
|
+
[](https://www.npmjs.com/package/@authon/react)
|
|
8
|
+
[](../../LICENSE)
|
|
37
9
|
|
|
38
|
-
##
|
|
10
|
+
## Prerequisites
|
|
39
11
|
|
|
40
|
-
|
|
12
|
+
Before installing the SDK, create an Authon project and get your API keys:
|
|
41
13
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
import { AuthonProvider } from '@authon/react';
|
|
46
|
-
|
|
47
|
-
<AuthonProvider
|
|
48
|
-
publishableKey="pk_live_..."
|
|
49
|
-
config={{
|
|
50
|
-
apiUrl: 'https://api.authon.dev',
|
|
51
|
-
theme: 'auto',
|
|
52
|
-
locale: 'en',
|
|
53
|
-
appearance: {
|
|
54
|
-
primaryColorStart: '#7c3aed',
|
|
55
|
-
primaryColorEnd: '#4f46e5',
|
|
56
|
-
borderRadius: 12,
|
|
57
|
-
},
|
|
58
|
-
}}
|
|
59
|
-
>
|
|
60
|
-
{children}
|
|
61
|
-
</AuthonProvider>
|
|
62
|
-
```
|
|
14
|
+
1. **Create a project** at [Authon Dashboard](https://authon.dev/dashboard/overview)
|
|
15
|
+
- Click "Create Project" and enter your app name
|
|
16
|
+
- Select the authentication methods you want (Email/Password, OAuth providers, etc.)
|
|
63
17
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| `config` | `AuthonConfig` (optional) | Additional client configuration |
|
|
18
|
+
2. **Get your API keys** from Project Settings → API Keys
|
|
19
|
+
- **Publishable Key** (`pk_live_...`) — use in your frontend code
|
|
20
|
+
- **Test Key** (`pk_test_...`) — for development, enables Dev Teleport
|
|
68
21
|
|
|
69
|
-
|
|
22
|
+
3. **Configure OAuth providers** (optional) in Project Settings → OAuth
|
|
23
|
+
- Add Google, Apple, GitHub, etc. with their respective Client ID and Secret
|
|
24
|
+
- Set the redirect URL to `https://api.authon.dev/v1/auth/oauth/redirect`
|
|
70
25
|
|
|
71
|
-
|
|
26
|
+
> **Test vs Live keys:** Use `pk_test_...` during development. Switch to `pk_live_...` before deploying to production. Test keys use a sandbox environment with no rate limits.
|
|
72
27
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
```tsx
|
|
76
|
-
import { SignIn } from '@authon/react';
|
|
77
|
-
|
|
78
|
-
// Popup mode (default) — opens the modal immediately on mount
|
|
79
|
-
<SignIn mode="popup" />
|
|
28
|
+
## Install
|
|
80
29
|
|
|
81
|
-
|
|
82
|
-
|
|
30
|
+
```bash
|
|
31
|
+
npm install @authon/react
|
|
83
32
|
```
|
|
84
33
|
|
|
85
|
-
|
|
86
|
-
|------|------|---------|-------------|
|
|
87
|
-
| `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
|
|
88
|
-
| `redirectUrl` | `string` (optional) | — | URL to redirect after sign-in |
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
### `<SignUp>`
|
|
93
|
-
|
|
94
|
-
Opens the sign-up modal or renders an embedded sign-up form.
|
|
34
|
+
## Quick Start
|
|
95
35
|
|
|
96
36
|
```tsx
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
// Embedded mode
|
|
103
|
-
<SignUp mode="embedded" />
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
| Prop | Type | Default | Description |
|
|
107
|
-
|------|------|---------|-------------|
|
|
108
|
-
| `mode` | `'popup' \| 'embedded'` | `'popup'` | Display mode |
|
|
109
|
-
|
|
110
|
-
---
|
|
37
|
+
// src/main.tsx — complete working file
|
|
38
|
+
import React from 'react';
|
|
39
|
+
import ReactDOM from 'react-dom/client';
|
|
40
|
+
import { AuthonProvider, useAuthon, useUser, SignedIn, SignedOut, UserButton } from '@authon/react';
|
|
111
41
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
```tsx
|
|
117
|
-
import { UserButton } from '@authon/react';
|
|
42
|
+
function App() {
|
|
43
|
+
const { openSignIn, signOut } = useAuthon();
|
|
44
|
+
const { user } = useUser();
|
|
118
45
|
|
|
119
|
-
function Navbar() {
|
|
120
46
|
return (
|
|
121
|
-
<
|
|
122
|
-
<
|
|
123
|
-
|
|
47
|
+
<div>
|
|
48
|
+
<SignedOut>
|
|
49
|
+
<button onClick={() => openSignIn()}>Sign In</button>
|
|
50
|
+
</SignedOut>
|
|
51
|
+
<SignedIn>
|
|
52
|
+
<p>Welcome, {user?.email}</p>
|
|
53
|
+
<UserButton />
|
|
54
|
+
<button onClick={() => signOut()}>Sign Out</button>
|
|
55
|
+
</SignedIn>
|
|
56
|
+
</div>
|
|
124
57
|
);
|
|
125
58
|
}
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
No props required.
|
|
129
|
-
|
|
130
|
-
---
|
|
131
|
-
|
|
132
|
-
### `<SignedIn>`
|
|
133
|
-
|
|
134
|
-
Renders `children` only when a user is signed in. Renders nothing while auth is loading.
|
|
135
|
-
|
|
136
|
-
```tsx
|
|
137
|
-
import { SignedIn } from '@authon/react';
|
|
138
|
-
|
|
139
|
-
<SignedIn>
|
|
140
|
-
<Dashboard />
|
|
141
|
-
</SignedIn>
|
|
142
|
-
```
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
### `<SignedOut>`
|
|
147
|
-
|
|
148
|
-
Renders `children` only when no user is signed in. Renders nothing while auth is loading.
|
|
149
|
-
|
|
150
|
-
```tsx
|
|
151
|
-
import { SignedOut } from '@authon/react';
|
|
152
59
|
|
|
153
|
-
|
|
154
|
-
<
|
|
155
|
-
|
|
60
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
61
|
+
<AuthonProvider
|
|
62
|
+
publishableKey="pk_live_YOUR_PUBLISHABLE_KEY"
|
|
63
|
+
>
|
|
64
|
+
<App />
|
|
65
|
+
</AuthonProvider>
|
|
66
|
+
);
|
|
156
67
|
```
|
|
157
68
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
### `<Protect>`
|
|
69
|
+
## Common Tasks
|
|
161
70
|
|
|
162
|
-
|
|
71
|
+
### Add Google OAuth Login
|
|
163
72
|
|
|
164
73
|
```tsx
|
|
165
|
-
import {
|
|
166
|
-
|
|
167
|
-
// Require sign-in only
|
|
168
|
-
<Protect fallback={<p>Please sign in to continue.</p>}>
|
|
169
|
-
<PrivatePage />
|
|
170
|
-
</Protect>
|
|
171
|
-
|
|
172
|
-
// Require a specific role
|
|
173
|
-
<Protect
|
|
174
|
-
fallback={<p>Admin access required.</p>}
|
|
175
|
-
condition={(user) => user.publicMetadata?.role === 'admin'}
|
|
176
|
-
>
|
|
177
|
-
<AdminPanel />
|
|
178
|
-
</Protect>
|
|
179
|
-
```
|
|
180
|
-
|
|
181
|
-
| Prop | Type | Description |
|
|
182
|
-
|------|------|-------------|
|
|
183
|
-
| `children` | `ReactNode` | Content to render when access is granted |
|
|
184
|
-
| `fallback` | `ReactNode` (optional) | Content to render when access is denied |
|
|
185
|
-
| `condition` | `(user: AuthonUser) => boolean` (optional) | Additional check beyond sign-in status |
|
|
186
|
-
|
|
187
|
-
---
|
|
188
|
-
|
|
189
|
-
### `<SocialButton>`
|
|
190
|
-
|
|
191
|
-
A single OAuth provider button with built-in styles, icons, and loading state.
|
|
192
|
-
|
|
193
|
-
```tsx
|
|
194
|
-
import { SocialButton } from '@authon/react';
|
|
74
|
+
import { useAuthon } from '@authon/react';
|
|
195
75
|
|
|
196
|
-
function
|
|
76
|
+
function GoogleLoginButton() {
|
|
197
77
|
const { client } = useAuthon();
|
|
198
|
-
|
|
199
78
|
return (
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
onClick={async (provider) => {
|
|
204
|
-
await client!.signInWithOAuth(provider);
|
|
205
|
-
}}
|
|
206
|
-
/>
|
|
207
|
-
<SocialButton
|
|
208
|
-
provider="github"
|
|
209
|
-
onClick={async (provider) => {
|
|
210
|
-
await client!.signInWithOAuth(provider);
|
|
211
|
-
}}
|
|
212
|
-
compact
|
|
213
|
-
size={48}
|
|
214
|
-
/>
|
|
215
|
-
</div>
|
|
79
|
+
<button onClick={() => client?.signInWithOAuth('google')}>
|
|
80
|
+
Sign in with Google
|
|
81
|
+
</button>
|
|
216
82
|
);
|
|
217
83
|
}
|
|
218
84
|
```
|
|
219
85
|
|
|
220
|
-
|
|
221
|
-
|------|------|---------|-------------|
|
|
222
|
-
| `provider` | `OAuthProviderType` | — | OAuth provider identifier |
|
|
223
|
-
| `onClick` | `(provider: OAuthProviderType) => void \| Promise<void>` | — | Click handler |
|
|
224
|
-
| `loading` | `boolean` | `false` | Show spinner |
|
|
225
|
-
| `disabled` | `boolean` | `false` | Disable button |
|
|
226
|
-
| `label` | `string` (optional) | `"Continue with {Provider}"` | Override button label |
|
|
227
|
-
| `compact` | `boolean` | `false` | Icon-only square button |
|
|
228
|
-
| `className` | `string` (optional) | — | Custom class |
|
|
229
|
-
| `style` | `CSSProperties` (optional) | — | Custom inline style |
|
|
230
|
-
| `iconSize` | `number` (optional) | `20` (`24` in compact) | Icon size in px |
|
|
231
|
-
| `borderRadius` | `number` | `10` | Border radius in px |
|
|
232
|
-
| `height` | `number` | `48` | Button height in px (full mode) |
|
|
233
|
-
| `size` | `number` | `48` | Button size in px (compact mode) |
|
|
234
|
-
|
|
235
|
-
Supported `OAuthProviderType` values: `'google' | 'apple' | 'kakao' | 'naver' | 'facebook' | 'github' | 'discord' | 'x' | 'line' | 'microsoft'`
|
|
236
|
-
|
|
237
|
-
---
|
|
238
|
-
|
|
239
|
-
### `<SocialButtons>`
|
|
240
|
-
|
|
241
|
-
Automatically fetches your project's enabled OAuth providers and renders a list of `<SocialButton>` components. Handles OAuth sign-in flow internally.
|
|
242
|
-
|
|
243
|
-
```tsx
|
|
244
|
-
import { SocialButtons } from '@authon/react';
|
|
245
|
-
|
|
246
|
-
// Vertical list (default)
|
|
247
|
-
<SocialButtons
|
|
248
|
-
onSuccess={() => console.log('Signed in!')}
|
|
249
|
-
onError={(error) => console.error(error)}
|
|
250
|
-
/>
|
|
251
|
-
|
|
252
|
-
// Compact icon row
|
|
253
|
-
<SocialButtons
|
|
254
|
-
compact
|
|
255
|
-
gap={12}
|
|
256
|
-
labels={{ google: 'Sign in with Google', kakao: '카카오로 로그인' }}
|
|
257
|
-
buttonProps={{ borderRadius: 8 }}
|
|
258
|
-
/>
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
| Prop | Type | Default | Description |
|
|
262
|
-
|------|------|---------|-------------|
|
|
263
|
-
| `onSuccess` | `() => void` (optional) | — | Called after successful OAuth sign-in |
|
|
264
|
-
| `onError` | `(error: Error) => void` (optional) | — | Called on OAuth error |
|
|
265
|
-
| `className` | `string` (optional) | — | Container class |
|
|
266
|
-
| `style` | `CSSProperties` (optional) | — | Container style |
|
|
267
|
-
| `gap` | `number` (optional) | `10` (`12` compact) | Gap between buttons in px |
|
|
268
|
-
| `compact` | `boolean` | `false` | Render icon-only buttons in a row |
|
|
269
|
-
| `labels` | `Partial<Record<OAuthProviderType, string>>` (optional) | — | Custom label per provider |
|
|
270
|
-
| `buttonProps` | `Partial<SocialButtonProps>` (optional) | — | Props forwarded to each `<SocialButton>` |
|
|
271
|
-
|
|
272
|
-
---
|
|
273
|
-
|
|
274
|
-
## Hooks
|
|
275
|
-
|
|
276
|
-
### `useAuthon()`
|
|
277
|
-
|
|
278
|
-
Returns the full auth context. Throws if used outside `<AuthonProvider>`.
|
|
86
|
+
### Protect a Route
|
|
279
87
|
|
|
280
88
|
```tsx
|
|
281
|
-
import {
|
|
282
|
-
|
|
283
|
-
function ProfileButton() {
|
|
284
|
-
const { isSignedIn, isLoading, user, signOut, openSignIn, openSignUp, getToken, client } = useAuthon();
|
|
285
|
-
|
|
286
|
-
if (isLoading) return <span>Loading...</span>;
|
|
287
|
-
|
|
288
|
-
if (!isSignedIn) {
|
|
289
|
-
return <button onClick={() => openSignIn()}>Sign In</button>;
|
|
290
|
-
}
|
|
89
|
+
import { Protect } from '@authon/react';
|
|
291
90
|
|
|
91
|
+
function Dashboard() {
|
|
292
92
|
return (
|
|
293
|
-
<
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
</div>
|
|
93
|
+
<Protect fallback={<p>Please sign in to view this page.</p>}>
|
|
94
|
+
<h1>Dashboard</h1>
|
|
95
|
+
</Protect>
|
|
297
96
|
);
|
|
298
97
|
}
|
|
299
|
-
```
|
|
300
98
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
getToken: () => string | null;
|
|
312
|
-
client: Authon | null;
|
|
99
|
+
// With role-based condition
|
|
100
|
+
function AdminPanel() {
|
|
101
|
+
return (
|
|
102
|
+
<Protect
|
|
103
|
+
condition={(user) => user.publicMetadata?.role === 'admin'}
|
|
104
|
+
fallback={<p>Admin access required.</p>}
|
|
105
|
+
>
|
|
106
|
+
<h1>Admin Panel</h1>
|
|
107
|
+
</Protect>
|
|
108
|
+
);
|
|
313
109
|
}
|
|
314
110
|
```
|
|
315
111
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
### `useUser()`
|
|
319
|
-
|
|
320
|
-
Shorthand hook that returns only the current user and loading state.
|
|
112
|
+
### Get Current User
|
|
321
113
|
|
|
322
114
|
```tsx
|
|
323
115
|
import { useUser } from '@authon/react';
|
|
324
116
|
|
|
325
|
-
function
|
|
117
|
+
function Profile() {
|
|
326
118
|
const { user, isLoading } = useUser();
|
|
327
|
-
|
|
328
119
|
if (isLoading) return <p>Loading...</p>;
|
|
329
|
-
if (!user) return
|
|
330
|
-
|
|
331
|
-
return <h2>Welcome back, {user.displayName ?? user.email}!</h2>;
|
|
332
|
-
}
|
|
333
|
-
```
|
|
334
|
-
|
|
335
|
-
**Return type:**
|
|
336
|
-
|
|
337
|
-
```ts
|
|
338
|
-
{
|
|
339
|
-
user: AuthonUser | null;
|
|
340
|
-
isLoading: boolean;
|
|
341
|
-
}
|
|
342
|
-
```
|
|
343
|
-
|
|
344
|
-
---
|
|
345
|
-
|
|
346
|
-
### `useAuthonMfa()`
|
|
347
|
-
|
|
348
|
-
Manages TOTP-based multi-factor authentication (Google Authenticator, Authy, etc.).
|
|
349
|
-
|
|
350
|
-
```tsx
|
|
351
|
-
import { useAuthonMfa } from '@authon/react';
|
|
352
|
-
import { useState } from 'react';
|
|
353
|
-
|
|
354
|
-
function MfaSetupPage() {
|
|
355
|
-
const { setupMfa, verifyMfaSetup, disableMfa, getMfaStatus, regenerateBackupCodes, isLoading, error } = useAuthonMfa();
|
|
356
|
-
const [qrSvg, setQrSvg] = useState('');
|
|
357
|
-
const [backupCodes, setBackupCodes] = useState<string[]>([]);
|
|
358
|
-
const [code, setCode] = useState('');
|
|
359
|
-
|
|
360
|
-
const handleSetup = async () => {
|
|
361
|
-
const result = await setupMfa();
|
|
362
|
-
if (result) {
|
|
363
|
-
setQrSvg(result.qrCodeSvg);
|
|
364
|
-
setBackupCodes(result.backupCodes);
|
|
365
|
-
}
|
|
366
|
-
};
|
|
367
|
-
|
|
368
|
-
const handleVerify = async () => {
|
|
369
|
-
const success = await verifyMfaSetup(code);
|
|
370
|
-
if (success) alert('MFA enabled successfully!');
|
|
371
|
-
};
|
|
372
|
-
|
|
373
|
-
const handleCheckStatus = async () => {
|
|
374
|
-
const status = await getMfaStatus();
|
|
375
|
-
console.log('MFA enabled:', status?.enabled, 'Backup codes left:', status?.backupCodesRemaining);
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
return (
|
|
379
|
-
<div>
|
|
380
|
-
<button onClick={handleSetup} disabled={isLoading}>
|
|
381
|
-
Enable MFA
|
|
382
|
-
</button>
|
|
383
|
-
|
|
384
|
-
{qrSvg && (
|
|
385
|
-
<>
|
|
386
|
-
<div dangerouslySetInnerHTML={{ __html: qrSvg }} />
|
|
387
|
-
<p>Backup codes: {backupCodes.join(', ')}</p>
|
|
388
|
-
<input
|
|
389
|
-
value={code}
|
|
390
|
-
onChange={(e) => setCode(e.target.value)}
|
|
391
|
-
placeholder="Enter 6-digit code"
|
|
392
|
-
/>
|
|
393
|
-
<button onClick={handleVerify} disabled={isLoading}>
|
|
394
|
-
Verify & Enable
|
|
395
|
-
</button>
|
|
396
|
-
</>
|
|
397
|
-
)}
|
|
398
|
-
|
|
399
|
-
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
400
|
-
</div>
|
|
401
|
-
);
|
|
402
|
-
}
|
|
403
|
-
```
|
|
404
|
-
|
|
405
|
-
**MFA sign-in flow (verifying TOTP after password):**
|
|
406
|
-
|
|
407
|
-
```tsx
|
|
408
|
-
import { useAuthon, useAuthonMfa } from '@authon/react';
|
|
409
|
-
import { AuthonMfaRequiredError } from '@authon/js';
|
|
410
|
-
|
|
411
|
-
function LoginForm() {
|
|
412
|
-
const { client } = useAuthon();
|
|
413
|
-
const { verifyMfa, isLoading } = useAuthonMfa();
|
|
414
|
-
const [mfaToken, setMfaToken] = useState('');
|
|
415
|
-
const [mfaStep, setMfaStep] = useState(false);
|
|
416
|
-
|
|
417
|
-
const handleSignIn = async (email: string, password: string) => {
|
|
418
|
-
try {
|
|
419
|
-
await client!.signInWithEmail(email, password);
|
|
420
|
-
// signed in — no MFA required
|
|
421
|
-
} catch (err) {
|
|
422
|
-
if (err instanceof AuthonMfaRequiredError) {
|
|
423
|
-
setMfaToken(err.mfaToken);
|
|
424
|
-
setMfaStep(true);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
};
|
|
428
|
-
|
|
429
|
-
const handleMfa = async (code: string) => {
|
|
430
|
-
const success = await verifyMfa(mfaToken, code);
|
|
431
|
-
if (success) console.log('Signed in with MFA!');
|
|
432
|
-
};
|
|
433
|
-
|
|
434
|
-
// ...
|
|
435
|
-
}
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**Return type:**
|
|
439
|
-
|
|
440
|
-
```ts
|
|
441
|
-
interface UseAuthonMfaReturn {
|
|
442
|
-
setupMfa: () => Promise<(MfaSetupResponse & { qrCodeSvg: string }) | null>;
|
|
443
|
-
verifyMfaSetup: (code: string) => Promise<boolean>;
|
|
444
|
-
verifyMfa: (mfaToken: string, code: string) => Promise<boolean>;
|
|
445
|
-
disableMfa: (code: string) => Promise<boolean>;
|
|
446
|
-
getMfaStatus: () => Promise<MfaStatus | null>;
|
|
447
|
-
regenerateBackupCodes: (code: string) => Promise<string[] | null>;
|
|
448
|
-
isLoading: boolean;
|
|
449
|
-
error: Error | null;
|
|
450
|
-
}
|
|
451
|
-
```
|
|
452
|
-
|
|
453
|
-
---
|
|
454
|
-
|
|
455
|
-
### `useAuthonPasskeys()`
|
|
456
|
-
|
|
457
|
-
Manages WebAuthn passkey registration and authentication.
|
|
458
|
-
|
|
459
|
-
```tsx
|
|
460
|
-
import { useAuthonPasskeys } from '@authon/react';
|
|
461
|
-
|
|
462
|
-
function PasskeySettings() {
|
|
463
|
-
const {
|
|
464
|
-
registerPasskey,
|
|
465
|
-
authenticateWithPasskey,
|
|
466
|
-
listPasskeys,
|
|
467
|
-
renamePasskey,
|
|
468
|
-
revokePasskey,
|
|
469
|
-
isLoading,
|
|
470
|
-
error,
|
|
471
|
-
} = useAuthonPasskeys();
|
|
472
|
-
|
|
473
|
-
const handleRegister = async () => {
|
|
474
|
-
const passkey = await registerPasskey('My MacBook');
|
|
475
|
-
if (passkey) {
|
|
476
|
-
console.log('Passkey registered:', passkey.id);
|
|
477
|
-
}
|
|
478
|
-
};
|
|
479
|
-
|
|
480
|
-
const handleList = async () => {
|
|
481
|
-
const passkeys = await listPasskeys();
|
|
482
|
-
console.log('Registered passkeys:', passkeys);
|
|
483
|
-
};
|
|
484
|
-
|
|
485
|
-
const handleRevoke = async (id: string) => {
|
|
486
|
-
const success = await revokePasskey(id);
|
|
487
|
-
if (success) console.log('Passkey revoked');
|
|
488
|
-
};
|
|
489
|
-
|
|
120
|
+
if (!user) return <p>Not signed in</p>;
|
|
490
121
|
return (
|
|
491
122
|
<div>
|
|
492
|
-
<
|
|
493
|
-
|
|
494
|
-
</button>
|
|
495
|
-
<button onClick={handleList} disabled={isLoading}>
|
|
496
|
-
List Passkeys
|
|
497
|
-
</button>
|
|
498
|
-
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
123
|
+
<p>Email: {user.email}</p>
|
|
124
|
+
<p>Name: {user.displayName}</p>
|
|
499
125
|
</div>
|
|
500
126
|
);
|
|
501
127
|
}
|
|
502
128
|
```
|
|
503
129
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
```ts
|
|
507
|
-
interface UseAuthonPasskeysReturn {
|
|
508
|
-
registerPasskey: (name?: string) => Promise<PasskeyCredential | null>;
|
|
509
|
-
authenticateWithPasskey: (email?: string) => Promise<boolean>;
|
|
510
|
-
listPasskeys: () => Promise<PasskeyCredential[] | null>;
|
|
511
|
-
renamePasskey: (id: string, name: string) => Promise<PasskeyCredential | null>;
|
|
512
|
-
revokePasskey: (id: string) => Promise<boolean>;
|
|
513
|
-
isLoading: boolean;
|
|
514
|
-
error: Error | null;
|
|
515
|
-
}
|
|
516
|
-
```
|
|
517
|
-
|
|
518
|
-
---
|
|
519
|
-
|
|
520
|
-
### `useAuthonPasswordless()`
|
|
521
|
-
|
|
522
|
-
Handles magic link and email OTP (one-time password) authentication flows.
|
|
130
|
+
### Add Email/Password Auth
|
|
523
131
|
|
|
524
132
|
```tsx
|
|
525
|
-
import {
|
|
133
|
+
import { useAuthon } from '@authon/react';
|
|
526
134
|
import { useState } from 'react';
|
|
527
135
|
|
|
528
|
-
function
|
|
529
|
-
const {
|
|
136
|
+
function EmailSignIn() {
|
|
137
|
+
const { client } = useAuthon();
|
|
530
138
|
const [email, setEmail] = useState('');
|
|
531
|
-
const [
|
|
532
|
-
const [code, setCode] = useState('');
|
|
139
|
+
const [password, setPassword] = useState('');
|
|
533
140
|
|
|
534
|
-
const
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const handleVerifyOtp = async () => {
|
|
540
|
-
const success = await verifyPasswordless({ email, code });
|
|
541
|
-
if (success) console.log('Signed in!');
|
|
542
|
-
};
|
|
543
|
-
|
|
544
|
-
const handleMagicLink = async () => {
|
|
545
|
-
const success = await sendMagicLink(email);
|
|
546
|
-
if (success) alert('Check your email for a sign-in link!');
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
// Verify magic link token from URL
|
|
550
|
-
const handleTokenVerify = async (token: string) => {
|
|
551
|
-
const success = await verifyPasswordless({ token });
|
|
552
|
-
if (success) console.log('Signed in via magic link!');
|
|
141
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
await client?.signInWithEmail(email, password);
|
|
553
144
|
};
|
|
554
145
|
|
|
555
146
|
return (
|
|
556
|
-
<
|
|
557
|
-
<input value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
558
|
-
<
|
|
559
|
-
<button
|
|
560
|
-
|
|
561
|
-
{otpSent && (
|
|
562
|
-
<>
|
|
563
|
-
<input value={code} onChange={(e) => setCode(e.target.value)} placeholder="Enter code" />
|
|
564
|
-
<button onClick={handleVerifyOtp} disabled={isLoading}>Verify</button>
|
|
565
|
-
</>
|
|
566
|
-
)}
|
|
567
|
-
|
|
568
|
-
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
569
|
-
</div>
|
|
147
|
+
<form onSubmit={handleSubmit}>
|
|
148
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
149
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
|
150
|
+
<button type="submit">Sign In</button>
|
|
151
|
+
</form>
|
|
570
152
|
);
|
|
571
153
|
}
|
|
572
154
|
```
|
|
573
155
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
```ts
|
|
577
|
-
interface UseAuthonPasswordlessReturn {
|
|
578
|
-
sendMagicLink: (email: string) => Promise<boolean>;
|
|
579
|
-
sendEmailOtp: (email: string) => Promise<boolean>;
|
|
580
|
-
verifyPasswordless: (opts: { token?: string; email?: string; code?: string }) => Promise<boolean>;
|
|
581
|
-
isLoading: boolean;
|
|
582
|
-
error: Error | null;
|
|
583
|
-
}
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
---
|
|
587
|
-
|
|
588
|
-
### `useAuthonWeb3()`
|
|
589
|
-
|
|
590
|
-
Handles Web3 wallet authentication (Sign-In with Ethereum / Solana).
|
|
156
|
+
### Handle Sign Out
|
|
591
157
|
|
|
592
158
|
```tsx
|
|
593
|
-
import {
|
|
594
|
-
|
|
595
|
-
function Web3Login() {
|
|
596
|
-
const { getNonce, verify, listWallets, linkWallet, unlinkWallet, isLoading, error } = useAuthonWeb3();
|
|
597
|
-
|
|
598
|
-
const handleSignIn = async () => {
|
|
599
|
-
const address = '0xYourAddress';
|
|
600
|
-
|
|
601
|
-
// 1. Get a nonce to sign
|
|
602
|
-
const nonceResponse = await getNonce(address, 'evm', 'metamask', 1);
|
|
603
|
-
if (!nonceResponse) return;
|
|
604
|
-
|
|
605
|
-
// 2. Sign the message with your wallet (e.g. MetaMask)
|
|
606
|
-
const signature = await window.ethereum.request({
|
|
607
|
-
method: 'personal_sign',
|
|
608
|
-
params: [nonceResponse.message, address],
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
// 3. Verify the signature to sign in
|
|
612
|
-
const success = await verify(nonceResponse.message, signature, address, 'evm', 'metamask');
|
|
613
|
-
if (success) console.log('Signed in with wallet!');
|
|
614
|
-
};
|
|
615
|
-
|
|
616
|
-
const handleLinkWallet = async () => {
|
|
617
|
-
const address = '0xYourAddress';
|
|
618
|
-
const nonceResponse = await getNonce(address, 'evm', 'metamask');
|
|
619
|
-
if (!nonceResponse) return;
|
|
620
|
-
|
|
621
|
-
const signature = await window.ethereum.request({
|
|
622
|
-
method: 'personal_sign',
|
|
623
|
-
params: [nonceResponse.message, address],
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
const wallet = await linkWallet({
|
|
627
|
-
address,
|
|
628
|
-
chain: 'evm',
|
|
629
|
-
walletType: 'metamask',
|
|
630
|
-
message: nonceResponse.message,
|
|
631
|
-
signature,
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
if (wallet) console.log('Wallet linked:', wallet.id);
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
const handleListWallets = async () => {
|
|
638
|
-
const wallets = await listWallets();
|
|
639
|
-
console.log('Linked wallets:', wallets);
|
|
640
|
-
};
|
|
641
|
-
|
|
642
|
-
return (
|
|
643
|
-
<div>
|
|
644
|
-
<button onClick={handleSignIn} disabled={isLoading}>Sign In with MetaMask</button>
|
|
645
|
-
<button onClick={handleLinkWallet} disabled={isLoading}>Link Wallet</button>
|
|
646
|
-
<button onClick={handleListWallets} disabled={isLoading}>List Wallets</button>
|
|
647
|
-
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
648
|
-
</div>
|
|
649
|
-
);
|
|
650
|
-
}
|
|
651
|
-
```
|
|
652
|
-
|
|
653
|
-
**Return type:**
|
|
654
|
-
|
|
655
|
-
```ts
|
|
656
|
-
interface UseAuthonWeb3Return {
|
|
657
|
-
getNonce: (
|
|
658
|
-
address: string,
|
|
659
|
-
chain: Web3Chain,
|
|
660
|
-
walletType: Web3WalletType,
|
|
661
|
-
chainId?: number,
|
|
662
|
-
) => Promise<Web3NonceResponse | null>;
|
|
663
|
-
verify: (
|
|
664
|
-
message: string,
|
|
665
|
-
signature: string,
|
|
666
|
-
address: string,
|
|
667
|
-
chain: Web3Chain,
|
|
668
|
-
walletType: Web3WalletType,
|
|
669
|
-
) => Promise<boolean>;
|
|
670
|
-
listWallets: () => Promise<Web3Wallet[] | null>;
|
|
671
|
-
linkWallet: (params: LinkWalletParams) => Promise<Web3Wallet | null>;
|
|
672
|
-
unlinkWallet: (walletId: string) => Promise<boolean>;
|
|
673
|
-
isLoading: boolean;
|
|
674
|
-
error: Error | null;
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
interface LinkWalletParams {
|
|
678
|
-
address: string;
|
|
679
|
-
chain: Web3Chain;
|
|
680
|
-
walletType: Web3WalletType;
|
|
681
|
-
chainId?: number;
|
|
682
|
-
message: string;
|
|
683
|
-
signature: string;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
type Web3Chain = 'evm' | 'solana';
|
|
687
|
-
type Web3WalletType = 'metamask' | 'pexus' | 'walletconnect' | 'coinbase' | 'phantom' | 'trust' | 'other';
|
|
688
|
-
```
|
|
689
|
-
|
|
690
|
-
---
|
|
691
|
-
|
|
692
|
-
### `useAuthonSessions()`
|
|
693
|
-
|
|
694
|
-
Lists and revokes active user sessions.
|
|
695
|
-
|
|
696
|
-
```tsx
|
|
697
|
-
import { useAuthonSessions } from '@authon/react';
|
|
698
|
-
import { useEffect, useState } from 'react';
|
|
699
|
-
import type { SessionInfo } from '@authon/shared';
|
|
700
|
-
|
|
701
|
-
function SessionManager() {
|
|
702
|
-
const { listSessions, revokeSession, isLoading, error } = useAuthonSessions();
|
|
703
|
-
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
|
704
|
-
|
|
705
|
-
useEffect(() => {
|
|
706
|
-
listSessions().then((s) => {
|
|
707
|
-
if (s) setSessions(s);
|
|
708
|
-
});
|
|
709
|
-
}, []);
|
|
710
|
-
|
|
711
|
-
const handleRevoke = async (sessionId: string) => {
|
|
712
|
-
const success = await revokeSession(sessionId);
|
|
713
|
-
if (success) {
|
|
714
|
-
setSessions((prev) => prev.filter((s) => s.id !== sessionId));
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
|
|
718
|
-
if (isLoading) return <p>Loading sessions...</p>;
|
|
719
|
-
|
|
720
|
-
return (
|
|
721
|
-
<ul>
|
|
722
|
-
{sessions.map((session) => (
|
|
723
|
-
<li key={session.id}>
|
|
724
|
-
<span>{session.userAgent ?? 'Unknown device'}</span>
|
|
725
|
-
<span>{session.ipAddress}</span>
|
|
726
|
-
<button onClick={() => handleRevoke(session.id)}>Revoke</button>
|
|
727
|
-
</li>
|
|
728
|
-
))}
|
|
729
|
-
{error && <p style={{ color: 'red' }}>{error.message}</p>}
|
|
730
|
-
</ul>
|
|
731
|
-
);
|
|
732
|
-
}
|
|
733
|
-
```
|
|
734
|
-
|
|
735
|
-
**Return type:**
|
|
736
|
-
|
|
737
|
-
```ts
|
|
738
|
-
interface UseAuthonSessionsReturn {
|
|
739
|
-
listSessions: () => Promise<SessionInfo[] | null>;
|
|
740
|
-
revokeSession: (sessionId: string) => Promise<boolean>;
|
|
741
|
-
isLoading: boolean;
|
|
742
|
-
error: Error | null;
|
|
743
|
-
}
|
|
744
|
-
```
|
|
745
|
-
|
|
746
|
-
---
|
|
747
|
-
|
|
748
|
-
## TypeScript Types
|
|
749
|
-
|
|
750
|
-
Key types exported from `@authon/react` and `@authon/shared`:
|
|
751
|
-
|
|
752
|
-
```ts
|
|
753
|
-
import type {
|
|
754
|
-
AuthonContextValue,
|
|
755
|
-
SocialButtonProps,
|
|
756
|
-
SocialButtonsProps,
|
|
757
|
-
UseAuthonMfaReturn,
|
|
758
|
-
UseAuthonPasskeysReturn,
|
|
759
|
-
UseAuthonPasswordlessReturn,
|
|
760
|
-
UseAuthonWeb3Return,
|
|
761
|
-
LinkWalletParams,
|
|
762
|
-
UseAuthonSessionsReturn,
|
|
763
|
-
} from '@authon/react';
|
|
764
|
-
|
|
765
|
-
import type {
|
|
766
|
-
AuthonUser,
|
|
767
|
-
SessionInfo,
|
|
768
|
-
PasskeyCredential,
|
|
769
|
-
Web3Wallet,
|
|
770
|
-
Web3NonceResponse,
|
|
771
|
-
MfaSetupResponse,
|
|
772
|
-
MfaStatus,
|
|
773
|
-
OAuthProviderType,
|
|
774
|
-
Web3Chain,
|
|
775
|
-
Web3WalletType,
|
|
776
|
-
} from '@authon/shared';
|
|
777
|
-
```
|
|
778
|
-
|
|
779
|
-
---
|
|
780
|
-
|
|
781
|
-
## Documentation
|
|
159
|
+
import { useAuthon } from '@authon/react';
|
|
782
160
|
|
|
783
|
-
|
|
161
|
+
function SignOutButton() {
|
|
162
|
+
const { signOut } = useAuthon();
|
|
163
|
+
return <button onClick={() => signOut()}>Sign Out</button>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Environment Variables
|
|
168
|
+
|
|
169
|
+
| Variable | Required | Description |
|
|
170
|
+
|----------|----------|-------------|
|
|
171
|
+
| `AUTHON_PUBLISHABLE_KEY` | Yes | Project publishable key (`pk_live_...` or `pk_test_...`) |
|
|
172
|
+
| `AUTHON_API_URL` | No | Optional — defaults to `api.authon.dev` |
|
|
173
|
+
|
|
174
|
+
## API Reference
|
|
175
|
+
|
|
176
|
+
### Components
|
|
177
|
+
|
|
178
|
+
| Component | Description |
|
|
179
|
+
|-----------|-------------|
|
|
180
|
+
| `<AuthonProvider>` | Context provider. Props: `publishableKey`, `config?` |
|
|
181
|
+
| `<SignIn>` | Pre-built sign-in form (`mode="popup"` or `"embedded"`) |
|
|
182
|
+
| `<SignUp>` | Pre-built sign-up form |
|
|
183
|
+
| `<UserButton>` | Avatar dropdown with user info and sign-out |
|
|
184
|
+
| `<UserProfile>` | Full user profile management |
|
|
185
|
+
| `<SignedIn>` | Renders children only when signed in |
|
|
186
|
+
| `<SignedOut>` | Renders children only when signed out |
|
|
187
|
+
| `<Protect>` | Conditional render with `fallback` and `condition` |
|
|
188
|
+
| `<SocialButton>` | Single OAuth provider button |
|
|
189
|
+
| `<SocialButtons>` | All enabled OAuth provider buttons |
|
|
190
|
+
|
|
191
|
+
### Hooks
|
|
192
|
+
|
|
193
|
+
| Hook | Returns |
|
|
194
|
+
|------|---------|
|
|
195
|
+
| `useAuthon()` | `{ isSignedIn, isLoading, user, signOut, openSignIn, openSignUp, getToken, client }` |
|
|
196
|
+
| `useUser()` | `{ user, isLoading }` |
|
|
197
|
+
| `useAuthonMfa()` | MFA setup, verification, and management |
|
|
198
|
+
| `useAuthonPasskeys()` | Passkey registration and authentication |
|
|
199
|
+
| `useAuthonPasswordless()` | Magic link and email OTP |
|
|
200
|
+
| `useAuthonWeb3()` | Web3 wallet sign-in and management |
|
|
201
|
+
| `useAuthonSessions()` | List and revoke active sessions |
|
|
202
|
+
| `useOrganization()` | Active organization management |
|
|
203
|
+
| `useOrganizationList()` | List and switch organizations |
|
|
204
|
+
|
|
205
|
+
## Comparison
|
|
206
|
+
|
|
207
|
+
| Feature | Authon | Clerk | Auth.js |
|
|
208
|
+
|---------|--------|-------|---------|
|
|
209
|
+
| Pricing | Free | $25/mo+ | Free |
|
|
210
|
+
| OAuth providers | 10+ | 20+ | 80+ |
|
|
211
|
+
| ShadowDOM modal | Yes | No | No |
|
|
212
|
+
| MFA/Passkeys | Yes | Yes | Plugin |
|
|
213
|
+
| Web3 auth | Yes | No | No |
|
|
214
|
+
| Organizations | Yes | Yes | No |
|
|
784
215
|
|
|
785
216
|
## License
|
|
786
217
|
|
|
787
|
-
|
|
218
|
+
MIT
|