@authon/react 0.3.0 → 0.3.1
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 +45 -177
- package/README.md +143 -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 +1 -1
package/README.md
CHANGED
|
@@ -2,786 +2,219 @@
|
|
|
2
2
|
|
|
3
3
|
# @authon/react
|
|
4
4
|
|
|
5
|
-
React
|
|
5
|
+
> Drop-in React authentication with hooks and components — self-hosted Clerk alternative, 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_...` or `pk_test_...`) — safe to use in client-side code
|
|
20
|
+
- **Secret Key** (`sk_live_...` or `sk_test_...`) — server-side only, never expose to clients
|
|
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
|
+
config={{ apiUrl: 'https://your-authon-server.com' }}
|
|
64
|
+
>
|
|
65
|
+
<App />
|
|
66
|
+
</AuthonProvider>
|
|
67
|
+
);
|
|
156
68
|
```
|
|
157
69
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
### `<Protect>`
|
|
70
|
+
## Common Tasks
|
|
161
71
|
|
|
162
|
-
|
|
72
|
+
### Add Google OAuth Login
|
|
163
73
|
|
|
164
74
|
```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';
|
|
75
|
+
import { useAuthon } from '@authon/react';
|
|
195
76
|
|
|
196
|
-
function
|
|
77
|
+
function GoogleLoginButton() {
|
|
197
78
|
const { client } = useAuthon();
|
|
198
|
-
|
|
199
79
|
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>
|
|
80
|
+
<button onClick={() => client?.signInWithOAuth('google')}>
|
|
81
|
+
Sign in with Google
|
|
82
|
+
</button>
|
|
216
83
|
);
|
|
217
84
|
}
|
|
218
85
|
```
|
|
219
86
|
|
|
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>`.
|
|
87
|
+
### Protect a Route
|
|
279
88
|
|
|
280
89
|
```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
|
-
}
|
|
90
|
+
import { Protect } from '@authon/react';
|
|
291
91
|
|
|
92
|
+
function Dashboard() {
|
|
292
93
|
return (
|
|
293
|
-
<
|
|
294
|
-
<
|
|
295
|
-
|
|
296
|
-
</div>
|
|
94
|
+
<Protect fallback={<p>Please sign in to view this page.</p>}>
|
|
95
|
+
<h1>Dashboard</h1>
|
|
96
|
+
</Protect>
|
|
297
97
|
);
|
|
298
98
|
}
|
|
299
|
-
```
|
|
300
99
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
getToken: () => string | null;
|
|
312
|
-
client: Authon | null;
|
|
100
|
+
// With role-based condition
|
|
101
|
+
function AdminPanel() {
|
|
102
|
+
return (
|
|
103
|
+
<Protect
|
|
104
|
+
condition={(user) => user.publicMetadata?.role === 'admin'}
|
|
105
|
+
fallback={<p>Admin access required.</p>}
|
|
106
|
+
>
|
|
107
|
+
<h1>Admin Panel</h1>
|
|
108
|
+
</Protect>
|
|
109
|
+
);
|
|
313
110
|
}
|
|
314
111
|
```
|
|
315
112
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
### `useUser()`
|
|
319
|
-
|
|
320
|
-
Shorthand hook that returns only the current user and loading state.
|
|
113
|
+
### Get Current User
|
|
321
114
|
|
|
322
115
|
```tsx
|
|
323
116
|
import { useUser } from '@authon/react';
|
|
324
117
|
|
|
325
|
-
function
|
|
118
|
+
function Profile() {
|
|
326
119
|
const { user, isLoading } = useUser();
|
|
327
|
-
|
|
328
120
|
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
|
-
|
|
121
|
+
if (!user) return <p>Not signed in</p>;
|
|
490
122
|
return (
|
|
491
123
|
<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>}
|
|
124
|
+
<p>Email: {user.email}</p>
|
|
125
|
+
<p>Name: {user.displayName}</p>
|
|
499
126
|
</div>
|
|
500
127
|
);
|
|
501
128
|
}
|
|
502
129
|
```
|
|
503
130
|
|
|
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.
|
|
131
|
+
### Add Email/Password Auth
|
|
523
132
|
|
|
524
133
|
```tsx
|
|
525
|
-
import {
|
|
134
|
+
import { useAuthon } from '@authon/react';
|
|
526
135
|
import { useState } from 'react';
|
|
527
136
|
|
|
528
|
-
function
|
|
529
|
-
const {
|
|
137
|
+
function EmailSignIn() {
|
|
138
|
+
const { client } = useAuthon();
|
|
530
139
|
const [email, setEmail] = useState('');
|
|
531
|
-
const [
|
|
532
|
-
const [code, setCode] = useState('');
|
|
140
|
+
const [password, setPassword] = useState('');
|
|
533
141
|
|
|
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!');
|
|
142
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
await client?.signInWithEmail(email, password);
|
|
553
145
|
};
|
|
554
146
|
|
|
555
147
|
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>
|
|
148
|
+
<form onSubmit={handleSubmit}>
|
|
149
|
+
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" />
|
|
150
|
+
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)} placeholder="Password" />
|
|
151
|
+
<button type="submit">Sign In</button>
|
|
152
|
+
</form>
|
|
570
153
|
);
|
|
571
154
|
}
|
|
572
155
|
```
|
|
573
156
|
|
|
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).
|
|
157
|
+
### Handle Sign Out
|
|
591
158
|
|
|
592
159
|
```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
|
|
160
|
+
import { useAuthon } from '@authon/react';
|
|
782
161
|
|
|
783
|
-
|
|
162
|
+
function SignOutButton() {
|
|
163
|
+
const { signOut } = useAuthon();
|
|
164
|
+
return <button onClick={() => signOut()}>Sign Out</button>;
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## Environment Variables
|
|
169
|
+
|
|
170
|
+
| Variable | Required | Description |
|
|
171
|
+
|----------|----------|-------------|
|
|
172
|
+
| `AUTHON_API_URL` | Yes | Your Authon server URL |
|
|
173
|
+
| `AUTHON_PUBLISHABLE_KEY` | Yes | Project publishable key (`pk_live_...` or `pk_test_...`) |
|
|
174
|
+
|
|
175
|
+
## API Reference
|
|
176
|
+
|
|
177
|
+
### Components
|
|
178
|
+
|
|
179
|
+
| Component | Description |
|
|
180
|
+
|-----------|-------------|
|
|
181
|
+
| `<AuthonProvider>` | Context provider. Props: `publishableKey`, `config?` |
|
|
182
|
+
| `<SignIn>` | Pre-built sign-in form (`mode="popup"` or `"embedded"`) |
|
|
183
|
+
| `<SignUp>` | Pre-built sign-up form |
|
|
184
|
+
| `<UserButton>` | Avatar dropdown with user info and sign-out |
|
|
185
|
+
| `<UserProfile>` | Full user profile management |
|
|
186
|
+
| `<SignedIn>` | Renders children only when signed in |
|
|
187
|
+
| `<SignedOut>` | Renders children only when signed out |
|
|
188
|
+
| `<Protect>` | Conditional render with `fallback` and `condition` |
|
|
189
|
+
| `<SocialButton>` | Single OAuth provider button |
|
|
190
|
+
| `<SocialButtons>` | All enabled OAuth provider buttons |
|
|
191
|
+
|
|
192
|
+
### Hooks
|
|
193
|
+
|
|
194
|
+
| Hook | Returns |
|
|
195
|
+
|------|---------|
|
|
196
|
+
| `useAuthon()` | `{ isSignedIn, isLoading, user, signOut, openSignIn, openSignUp, getToken, client }` |
|
|
197
|
+
| `useUser()` | `{ user, isLoading }` |
|
|
198
|
+
| `useAuthonMfa()` | MFA setup, verification, and management |
|
|
199
|
+
| `useAuthonPasskeys()` | Passkey registration and authentication |
|
|
200
|
+
| `useAuthonPasswordless()` | Magic link and email OTP |
|
|
201
|
+
| `useAuthonWeb3()` | Web3 wallet sign-in and management |
|
|
202
|
+
| `useAuthonSessions()` | List and revoke active sessions |
|
|
203
|
+
| `useOrganization()` | Active organization management |
|
|
204
|
+
| `useOrganizationList()` | List and switch organizations |
|
|
205
|
+
|
|
206
|
+
## Comparison
|
|
207
|
+
|
|
208
|
+
| Feature | Authon | Clerk | Auth.js |
|
|
209
|
+
|---------|--------|-------|---------|
|
|
210
|
+
| Self-hosted | Yes | No | Partial |
|
|
211
|
+
| Pricing | Free | $25/mo+ | Free |
|
|
212
|
+
| OAuth providers | 10+ | 20+ | 80+ |
|
|
213
|
+
| ShadowDOM modal | Yes | No | No |
|
|
214
|
+
| MFA/Passkeys | Yes | Yes | Plugin |
|
|
215
|
+
| Web3 auth | Yes | No | No |
|
|
216
|
+
| Organizations | Yes | Yes | No |
|
|
784
217
|
|
|
785
218
|
## License
|
|
786
219
|
|
|
787
|
-
|
|
220
|
+
MIT
|