@ouim/logto-authkit 0.3.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/README.md +823 -0
- package/dist/bundler-config.cjs +1 -0
- package/dist/bundler-config.d.ts +81 -0
- package/dist/bundler-config.js +31 -0
- package/dist/callback.d.ts +3 -0
- package/dist/components/signin-button.d.ts +53 -0
- package/dist/components/ui/alert.d.ts +8 -0
- package/dist/components/ui/avatar.d.ts +6 -0
- package/dist/components/ui/badge.d.ts +9 -0
- package/dist/components/ui/button.d.ts +11 -0
- package/dist/components/ui/card.d.ts +8 -0
- package/dist/components/ui/dialog.d.ts +19 -0
- package/dist/components/ui/dropdown-menu.d.ts +17 -0
- package/dist/components/ui/loading-spinner.d.ts +2 -0
- package/dist/components/ui/skeleton.d.ts +2 -0
- package/dist/components/ui/tooltip.d.ts +7 -0
- package/dist/components/utils/utils.d.ts +2 -0
- package/dist/context.d.ts +57 -0
- package/dist/index.cjs +129 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.js +9583 -0
- package/dist/navigation.d.ts +10 -0
- package/dist/server/authorization.d.ts +53 -0
- package/dist/server/csrf.d.ts +247 -0
- package/dist/server/index.cjs +1 -0
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +431 -0
- package/dist/server/types.d.ts +62 -0
- package/dist/server/verify-auth.d.ts +296 -0
- package/dist/signin.d.ts +42 -0
- package/dist/types.d.ts +81 -0
- package/dist/useAuth.d.ts +55 -0
- package/dist/usePermission.d.ts +9 -0
- package/dist/user-center.d.ts +49 -0
- package/dist/utils.d.ts +169 -0
- package/package.json +111 -0
package/README.md
ADDED
|
@@ -0,0 +1,823 @@
|
|
|
1
|
+
# @ouim/logto-authkit
|
|
2
|
+
|
|
3
|
+
`@ouim/logto-authkit` is a batteries-included auth toolkit for Logto-powered React apps.
|
|
4
|
+
|
|
5
|
+
> Migration notice
|
|
6
|
+
> This package replaces `@ouim/simple-logto`.
|
|
7
|
+
> Import replacements:
|
|
8
|
+
> `@ouim/simple-logto` -> `@ouim/logto-authkit`
|
|
9
|
+
> `@ouim/simple-logto/backend` -> `@ouim/logto-authkit/server`
|
|
10
|
+
> `@ouim/simple-logto/bundler-config` -> `@ouim/logto-authkit/bundler-config`
|
|
11
|
+
> There will be no compatibility shim for `/backend`.
|
|
12
|
+
|
|
13
|
+
It wraps `@logto/react` with the pieces most teams end up building anyway:
|
|
14
|
+
|
|
15
|
+
- a higher-level React provider and hook
|
|
16
|
+
- ready-made sign-in, callback, and account UI
|
|
17
|
+
- backend token verification for Node and Next.js
|
|
18
|
+
- bundler fixes for the `jose` edge cases that usually slow setup down
|
|
19
|
+
|
|
20
|
+
If you want Logto without re-assembling the same frontend and backend auth plumbing from scratch, this package is the opinionated fast path.
|
|
21
|
+
|
|
22
|
+
> Wanna try it? checkout the library live on: [tstore.ouim.me](https://tstore.ouim.me/) & [mocka.ouim.me](https://mocka.ouim.me/)
|
|
23
|
+
|
|
24
|
+
## What It Actually Ships
|
|
25
|
+
|
|
26
|
+
### Frontend
|
|
27
|
+
|
|
28
|
+
- `AuthProvider` for wiring Logto into your app with less boilerplate
|
|
29
|
+
- `useAuth` for user state, auth actions, and route protection patterns
|
|
30
|
+
- `UserCenter` for a production-ready account dropdown
|
|
31
|
+
- `CallbackPage` for redirect and popup callback handling
|
|
32
|
+
- `SignInPage` for dedicated `/signin` routes
|
|
33
|
+
- `SignInButton` for drop-in sign-in triggers
|
|
34
|
+
- popup sign-in support
|
|
35
|
+
- guest mode support
|
|
36
|
+
- custom navigation support for SPA routers
|
|
37
|
+
|
|
38
|
+
### Backend
|
|
39
|
+
|
|
40
|
+
- JWT verification against Logto JWKS
|
|
41
|
+
- Express middleware via `createExpressAuthMiddleware`
|
|
42
|
+
- Next.js request verification via `verifyNextAuth`
|
|
43
|
+
- generic `verifyAuth` helper for custom servers and handlers
|
|
44
|
+
- optional scope checks
|
|
45
|
+
- cookie and bearer-token extraction
|
|
46
|
+
- guest-aware auth context support
|
|
47
|
+
|
|
48
|
+
### Build tooling
|
|
49
|
+
|
|
50
|
+
- Vite config helpers
|
|
51
|
+
- Webpack config helpers
|
|
52
|
+
- Next.js config helpers
|
|
53
|
+
- a dedicated `bundler-config` entrypoint for build-time imports
|
|
54
|
+
|
|
55
|
+
## Why We Use It
|
|
56
|
+
|
|
57
|
+
- Faster first integration: frontend and backend auth can be wired from one package.
|
|
58
|
+
- Better defaults: common auth screens and account UI are already handled.
|
|
59
|
+
- Less glue code: cookie syncing, callback handling, popup flows, and request verification are built in.
|
|
60
|
+
- Easier adoption: you still keep Logto underneath, so you are not boxed into a custom auth system.
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npm install @ouim/logto-authkit @logto/react
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Peer dependencies:
|
|
69
|
+
|
|
70
|
+
- `react`
|
|
71
|
+
- `react-dom`
|
|
72
|
+
- `@logto/react`
|
|
73
|
+
|
|
74
|
+
## Runtime Support
|
|
75
|
+
|
|
76
|
+
The package currently declares compatibility with:
|
|
77
|
+
|
|
78
|
+
- Node.js `18.18+`, `20.x`, `22.x`, and `24.x`
|
|
79
|
+
- React `17.x`, `18.x`, and `19.x`
|
|
80
|
+
- `@logto/react` `3.x` and `4.x`
|
|
81
|
+
|
|
82
|
+
GitHub Actions runs the default validation gate on Node `24`, which is the current Active LTS line as of March 29, 2026. The published `engines` field expresses the broader compatibility policy, while CI stays intentionally lighter for day-to-day pull requests.
|
|
83
|
+
|
|
84
|
+
## Examples
|
|
85
|
+
|
|
86
|
+
### Vite + React + Express
|
|
87
|
+
|
|
88
|
+
The [example_app/](./example_app/) directory contains a complete Vite React playground:
|
|
89
|
+
|
|
90
|
+
- **Frontend:** `AuthProvider`, `useAuth`, `UserCenter`, popup sign-in, guest mode
|
|
91
|
+
- **Backend:** Express.js with `createExpressAuthMiddleware`, scope checks, CSRF protection
|
|
92
|
+
- **Setup:** See [example_app/README.md](./example_app/README.md)
|
|
93
|
+
|
|
94
|
+
### Next.js App Router
|
|
95
|
+
|
|
96
|
+
[examples/nextjs-app-router/](./examples/nextjs-app-router/) demonstrates:
|
|
97
|
+
|
|
98
|
+
- `AuthProvider` wrapping the app
|
|
99
|
+
- Sign-in route with `SignInPage`
|
|
100
|
+
- Callback route with `CallbackPage`
|
|
101
|
+
- Protected API route using `verifyNextAuth`
|
|
102
|
+
- Role-based authorization
|
|
103
|
+
- See [examples/nextjs-app-router/README.md](./examples/nextjs-app-router/README.md)
|
|
104
|
+
|
|
105
|
+
### Smoke Tests
|
|
106
|
+
|
|
107
|
+
[smoke-fixtures/](./smoke-fixtures/) contains automated tests for:
|
|
108
|
+
|
|
109
|
+
- Vite + React integration
|
|
110
|
+
- Next.js App Router integration
|
|
111
|
+
- React Router integration
|
|
112
|
+
- Node.js backend verification
|
|
113
|
+
- Bundler compatibility (CommonJS + ESM)
|
|
114
|
+
|
|
115
|
+
These validate the package works across different bundler and framework combinations.
|
|
116
|
+
|
|
117
|
+
### Additional Notes
|
|
118
|
+
|
|
119
|
+
- [docs/notes/](./docs/notes/) contains working implementation notes and architecture decisions
|
|
120
|
+
- [AGENTS.md](./AGENTS.md) documents how AI agents can contribute to this project
|
|
121
|
+
|
|
122
|
+
## Quick Start
|
|
123
|
+
|
|
124
|
+
### 1. Wrap your app
|
|
125
|
+
|
|
126
|
+
```tsx
|
|
127
|
+
import { AuthProvider } from '@ouim/logto-authkit'
|
|
128
|
+
|
|
129
|
+
const logtoConfig = {
|
|
130
|
+
endpoint: 'https://your-tenant.logto.app',
|
|
131
|
+
appId: 'your-app-id',
|
|
132
|
+
resources: ['https://your-api.example.com'],
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function AppProviders({ children }: { children: React.ReactNode }) {
|
|
136
|
+
return (
|
|
137
|
+
<AuthProvider config={logtoConfig} callbackUrl="http://localhost:3000/callback">
|
|
138
|
+
{children}
|
|
139
|
+
</AuthProvider>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### 2. Add the callback route
|
|
145
|
+
|
|
146
|
+
```tsx
|
|
147
|
+
import { CallbackPage } from '@ouim/logto-authkit'
|
|
148
|
+
|
|
149
|
+
export default function CallbackRoute() {
|
|
150
|
+
return <CallbackPage />
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### 3. Add a sign-in entry point
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
import { SignInPage } from '@ouim/logto-authkit'
|
|
158
|
+
|
|
159
|
+
export default function SignInRoute() {
|
|
160
|
+
return <SignInPage />
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 4. Use auth anywhere
|
|
165
|
+
|
|
166
|
+
```tsx
|
|
167
|
+
import { useAuth } from '@ouim/logto-authkit'
|
|
168
|
+
|
|
169
|
+
export function Dashboard() {
|
|
170
|
+
const { user, isLoadingUser, signIn, signOut } = useAuth()
|
|
171
|
+
|
|
172
|
+
if (isLoadingUser) return <div>Loading...</div>
|
|
173
|
+
if (!user) return <button onClick={() => signIn()}>Sign in</button>
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div>
|
|
177
|
+
<p>Welcome, {user.name ?? user.id}</p>
|
|
178
|
+
<button onClick={() => signOut()}>Sign out</button>
|
|
179
|
+
</div>
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### 5. Add account UI
|
|
185
|
+
|
|
186
|
+
```tsx
|
|
187
|
+
import { UserCenter } from '@ouim/logto-authkit'
|
|
188
|
+
|
|
189
|
+
export function Navbar() {
|
|
190
|
+
return (
|
|
191
|
+
<nav className="flex items-center justify-between h-16 px-4 border-b">
|
|
192
|
+
<div className="font-bold">MyApp</div>
|
|
193
|
+
<UserCenter />
|
|
194
|
+
</nav>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+

|
|
200
|
+
|
|
201
|
+

|
|
202
|
+
|
|
203
|
+
## Frontend API
|
|
204
|
+
|
|
205
|
+
### `AuthProvider`
|
|
206
|
+
|
|
207
|
+
Main provider for the package. It wraps Logto, manages auth refresh, and keeps the browser cookie in sync for backend verification.
|
|
208
|
+
|
|
209
|
+
Props:
|
|
210
|
+
|
|
211
|
+
- `config`: Logto config object
|
|
212
|
+
- `callbackUrl?`: default auth callback URL
|
|
213
|
+
- `customNavigate?`: custom navigation function for React Router, Next.js, or other SPA routers
|
|
214
|
+
- `enablePopupSignIn?`: enables popup-based sign-in flow
|
|
215
|
+
- `onTokenRefresh?`: called when an already-authenticated session receives a different access token
|
|
216
|
+
- `onAuthError?`: called when auth loading hits a transient or definitive auth error
|
|
217
|
+
- `onSignOut?`: called immediately before the provider initiates local or global sign-out
|
|
218
|
+
|
|
219
|
+
Example with custom router navigation:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
<AuthProvider config={logtoConfig} callbackUrl="/callback" customNavigate={url => router.push(url)} enablePopupSignIn>
|
|
223
|
+
<App />
|
|
224
|
+
</AuthProvider>
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Lifecycle callback example:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
<AuthProvider
|
|
231
|
+
config={logtoConfig}
|
|
232
|
+
onTokenRefresh={({ expiresAt }) => analytics.track('token_refreshed', { expiresAt })}
|
|
233
|
+
onAuthError={({ error, isTransient }) => console.error('auth error', { message: error.message, isTransient })}
|
|
234
|
+
onSignOut={({ reason }) => analytics.track('signed_out', { reason })}
|
|
235
|
+
>
|
|
236
|
+
<App />
|
|
237
|
+
</AuthProvider>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### `useAuth`
|
|
241
|
+
|
|
242
|
+
Returns:
|
|
243
|
+
|
|
244
|
+
- `user`
|
|
245
|
+
- `isLoadingUser`
|
|
246
|
+
- `signIn`
|
|
247
|
+
- `signOut`
|
|
248
|
+
- `refreshAuth`
|
|
249
|
+
- `enablePopupSignIn`
|
|
250
|
+
|
|
251
|
+
You can also use it for lightweight route protection:
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const { user } = useAuth({
|
|
255
|
+
middleware: 'auth',
|
|
256
|
+
redirectTo: '/signin',
|
|
257
|
+
})
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
Guest-only route example:
|
|
261
|
+
|
|
262
|
+
```tsx
|
|
263
|
+
const auth = useAuth({
|
|
264
|
+
middleware: 'guest',
|
|
265
|
+
redirectIfAuthenticated: '/dashboard',
|
|
266
|
+
})
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### `usePermission`
|
|
270
|
+
|
|
271
|
+
Use `usePermission()` for client-side conditional rendering when the current frontend user claims already include permission data.
|
|
272
|
+
|
|
273
|
+
```tsx
|
|
274
|
+
import { usePermission } from '@ouim/logto-authkit'
|
|
275
|
+
|
|
276
|
+
function AdminActions() {
|
|
277
|
+
const canManageUsers = usePermission('manage:users')
|
|
278
|
+
|
|
279
|
+
if (!canManageUsers) {
|
|
280
|
+
return null
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return <button>Invite user</button>
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Notes:
|
|
288
|
+
|
|
289
|
+
- `usePermission()` reads the `user` object from `AuthProvider`, so it reflects frontend claims only.
|
|
290
|
+
- By default it checks `permissions`, then `scope`, then `scp`.
|
|
291
|
+
- Pass `claimKeys` if your tenant emits permissions under a custom claim name such as `https://example.com/permissions`.
|
|
292
|
+
- While auth state is loading the hook returns `false` so restricted UI does not flash before claims arrive.
|
|
293
|
+
|
|
294
|
+
### `UserCenter`
|
|
295
|
+
|
|
296
|
+
Prebuilt account dropdown for navbars and app shells.
|
|
297
|
+
|
|
298
|
+
Supports:
|
|
299
|
+
|
|
300
|
+
- signed-in and signed-out states
|
|
301
|
+
- local sign-out by default, or global sign-out when explicitly enabled
|
|
302
|
+
- custom account links
|
|
303
|
+
- custom theme class names
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
<UserCenter
|
|
307
|
+
signoutCallbackUrl="/"
|
|
308
|
+
globalSignOut={false}
|
|
309
|
+
additionalPages={[
|
|
310
|
+
{ link: '/settings', text: 'Settings' },
|
|
311
|
+
{ link: '/billing', text: 'Billing' },
|
|
312
|
+
]}
|
|
313
|
+
/>
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
Pass `globalSignOut={true}` only when you explicitly want the account menu to end the user's wider Logto tenant session, not just the current app session.
|
|
317
|
+
|
|
318
|
+
### `CallbackPage`
|
|
319
|
+
|
|
320
|
+
Drop this onto your callback route to complete the Logto auth flow.
|
|
321
|
+
|
|
322
|
+
Optional props:
|
|
323
|
+
|
|
324
|
+
- `onSuccess`
|
|
325
|
+
- `onError`
|
|
326
|
+
- `loadingComponent`
|
|
327
|
+
- `successComponent`
|
|
328
|
+
- `className`
|
|
329
|
+
|
|
330
|
+
### `SignInPage`
|
|
331
|
+
|
|
332
|
+
Use this when you want a dedicated `/signin` route that automatically initiates the auth flow. It also supports popup-based sign-in windows.
|
|
333
|
+
|
|
334
|
+
If you enable popup sign-in on `AuthProvider`, you should still define a real `/signin` route that renders `SignInPage`. The popup window navigates to that route first, and `SignInPage` is what kicks off the Logto flow inside the popup.
|
|
335
|
+
|
|
336
|
+
Optional props:
|
|
337
|
+
|
|
338
|
+
- `loadingComponent`
|
|
339
|
+
- `errorComponent`
|
|
340
|
+
- `className`
|
|
341
|
+
|
|
342
|
+
```tsx
|
|
343
|
+
<SignInPage
|
|
344
|
+
className="min-h-screen bg-slate-50"
|
|
345
|
+
loadingComponent={<div>Redirecting to Logto...</div>}
|
|
346
|
+
errorComponent={error => <div>Could not start sign-in: {error.message}</div>}
|
|
347
|
+
/>
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
### `SignInButton`
|
|
351
|
+
|
|
352
|
+
For cases where you want a reusable trigger instead of manually calling `signIn()`.
|
|
353
|
+
|
|
354
|
+
```tsx
|
|
355
|
+
import { SignInButton } from '@ouim/logto-authkit'
|
|
356
|
+
;<SignInButton />
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
## SSR And Router Boundaries
|
|
360
|
+
|
|
361
|
+
The frontend entrypoint is intentionally browser-oriented. `AuthProvider`, `useAuth`, `SignInPage`, `CallbackPage`, and `UserCenter` all rely on client-only behaviors such as cookies, `window`, popup messaging, focus events, or browser navigation.
|
|
362
|
+
|
|
363
|
+
Use these rules when integrating into SSR-capable apps:
|
|
364
|
+
|
|
365
|
+
- Render frontend auth components only from client components.
|
|
366
|
+
- Keep `/signin` and `/callback` as real browser routes, not server-only handlers.
|
|
367
|
+
- Expect the initial server render to be unauthenticated until the client hydrates and loads the Logto session.
|
|
368
|
+
- Do not treat server-rendered auth state from the frontend package as authoritative for backend access control; use the backend verifier helpers for that.
|
|
369
|
+
|
|
370
|
+
### React Router
|
|
371
|
+
|
|
372
|
+
Wrap the router tree in `AuthProvider` and pass a router-aware `customNavigate` callback so auth redirects stay inside the SPA router.
|
|
373
|
+
|
|
374
|
+
```tsx
|
|
375
|
+
'use client'
|
|
376
|
+
|
|
377
|
+
import { AuthProvider, CallbackPage, SignInPage } from '@ouim/logto-authkit'
|
|
378
|
+
import { BrowserRouter, Route, Routes, useNavigate } from 'react-router-dom'
|
|
379
|
+
|
|
380
|
+
function AuthShell() {
|
|
381
|
+
const navigate = useNavigate()
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<AuthProvider
|
|
385
|
+
config={logtoConfig}
|
|
386
|
+
callbackUrl={`${window.location.origin}/callback`}
|
|
387
|
+
customNavigate={url => navigate(url)}
|
|
388
|
+
enablePopupSignIn
|
|
389
|
+
>
|
|
390
|
+
<Routes>
|
|
391
|
+
<Route path="/signin" element={<SignInPage />} />
|
|
392
|
+
<Route path="/callback" element={<CallbackPage />} />
|
|
393
|
+
<Route path="/" element={<Dashboard />} />
|
|
394
|
+
</Routes>
|
|
395
|
+
</AuthProvider>
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
export function App() {
|
|
400
|
+
return (
|
|
401
|
+
<BrowserRouter>
|
|
402
|
+
<AuthShell />
|
|
403
|
+
</BrowserRouter>
|
|
404
|
+
)
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Next.js App Router
|
|
409
|
+
|
|
410
|
+
Keep the auth UI behind client components and let the backend subpath handle server-side request verification.
|
|
411
|
+
|
|
412
|
+
```tsx
|
|
413
|
+
// app/providers.tsx
|
|
414
|
+
'use client'
|
|
415
|
+
|
|
416
|
+
import { AuthProvider } from '@ouim/logto-authkit'
|
|
417
|
+
import { useRouter } from 'next/navigation'
|
|
418
|
+
|
|
419
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
420
|
+
const router = useRouter()
|
|
421
|
+
|
|
422
|
+
return (
|
|
423
|
+
<AuthProvider config={logtoConfig} callbackUrl={`${process.env.NEXT_PUBLIC_APP_URL}/callback`} customNavigate={url => router.push(url)}>
|
|
424
|
+
{children}
|
|
425
|
+
</AuthProvider>
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
```tsx
|
|
431
|
+
// app/signin/page.tsx
|
|
432
|
+
'use client'
|
|
433
|
+
|
|
434
|
+
import { SignInPage } from '@ouim/logto-authkit'
|
|
435
|
+
|
|
436
|
+
export default function SignIn() {
|
|
437
|
+
return <SignInPage />
|
|
438
|
+
}
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
// app/callback/page.tsx
|
|
443
|
+
'use client'
|
|
444
|
+
|
|
445
|
+
import { CallbackPage } from '@ouim/logto-authkit'
|
|
446
|
+
|
|
447
|
+
export default function Callback() {
|
|
448
|
+
return <CallbackPage />
|
|
449
|
+
}
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
For protected server routes, route handlers, or middleware, import from `@ouim/logto-authkit/server` instead of trying to read frontend auth state during SSR.
|
|
453
|
+
|
|
454
|
+
These React Router and Next.js examples are mirrored by packed smoke fixtures in `smoke-fixtures/` so CI catches export or packaging drift against the documented integration patterns.
|
|
455
|
+
|
|
456
|
+
## Backend API
|
|
457
|
+
|
|
458
|
+
Import backend helpers from the dedicated subpath:
|
|
459
|
+
|
|
460
|
+
```ts
|
|
461
|
+
import { createExpressAuthMiddleware, hasScopes, requireScopes, verifyAuth, verifyNextAuth } from '@ouim/logto-authkit/server'
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
### Express middleware
|
|
465
|
+
|
|
466
|
+
`createExpressAuthMiddleware` automatically parses cookies for you, so you do not need to add `cookie-parser` yourself.
|
|
467
|
+
|
|
468
|
+
```ts
|
|
469
|
+
import express from 'express'
|
|
470
|
+
import { createExpressAuthMiddleware } from '@ouim/logto-authkit/server'
|
|
471
|
+
|
|
472
|
+
const app = express()
|
|
473
|
+
|
|
474
|
+
const authMiddleware = createExpressAuthMiddleware({
|
|
475
|
+
logtoUrl: 'https://your-tenant.logto.app',
|
|
476
|
+
audience: 'https://your-api.example.com',
|
|
477
|
+
cookieName: 'logto_authtoken',
|
|
478
|
+
requiredScope: 'read:profile',
|
|
479
|
+
allowGuest: true,
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
app.get('/api/me', authMiddleware, (req, res) => {
|
|
483
|
+
res.json({
|
|
484
|
+
userId: req.auth?.userId,
|
|
485
|
+
isAuthenticated: req.auth?.isAuthenticated,
|
|
486
|
+
isGuest: req.auth?.isGuest,
|
|
487
|
+
})
|
|
488
|
+
})
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Next.js route handlers
|
|
492
|
+
|
|
493
|
+
```ts
|
|
494
|
+
import { verifyNextAuth } from '@ouim/logto-authkit/server'
|
|
495
|
+
|
|
496
|
+
export async function GET(request: Request) {
|
|
497
|
+
const result = await verifyNextAuth(request, {
|
|
498
|
+
logtoUrl: process.env.LOGTO_URL!,
|
|
499
|
+
audience: process.env.LOGTO_AUDIENCE!,
|
|
500
|
+
allowGuest: false,
|
|
501
|
+
})
|
|
502
|
+
|
|
503
|
+
if (!result.success) {
|
|
504
|
+
return Response.json({ error: result.error }, { status: 401 })
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return Response.json({
|
|
508
|
+
userId: result.auth.userId,
|
|
509
|
+
payload: result.auth.payload,
|
|
510
|
+
})
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
### SSR-safe backend verification pattern
|
|
515
|
+
|
|
516
|
+
For SSR frameworks, make authorization decisions on the server with the backend subpath and pass only the derived result to your rendered UI.
|
|
517
|
+
|
|
518
|
+
```ts
|
|
519
|
+
import { verifyAuth } from '@ouim/logto-authkit/server'
|
|
520
|
+
|
|
521
|
+
export async function loadUserFromRequest(request: Request) {
|
|
522
|
+
const auth = await verifyAuth(request, {
|
|
523
|
+
logtoUrl: process.env.LOGTO_URL!,
|
|
524
|
+
audience: process.env.LOGTO_AUDIENCE!,
|
|
525
|
+
allowGuest: true,
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
userId: auth.userId,
|
|
530
|
+
isAuthenticated: auth.isAuthenticated,
|
|
531
|
+
isGuest: auth.isGuest,
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Generic token verification
|
|
537
|
+
|
|
538
|
+
```ts
|
|
539
|
+
import { verifyAuth } from '@ouim/logto-authkit/server'
|
|
540
|
+
|
|
541
|
+
const auth = await verifyAuth('your-jwt-token', {
|
|
542
|
+
logtoUrl: 'https://your-tenant.logto.app',
|
|
543
|
+
audience: 'https://your-api.example.com',
|
|
544
|
+
})
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
### Backend options
|
|
548
|
+
|
|
549
|
+
- `logtoUrl`: required
|
|
550
|
+
- `audience`: required for protected API resources, accepts either a single audience string or an array of allowed audiences
|
|
551
|
+
- `cookieName?`: defaults to `logto_authtoken`
|
|
552
|
+
- `requiredScope?`: rejects requests missing the given scope
|
|
553
|
+
- `allowGuest?`: enables guest auth fallback
|
|
554
|
+
- `jwksCacheTtlMs?`: overrides the default 5 minute in-memory JWKS cache TTL
|
|
555
|
+
- `skipJwksCache?`: bypasses the in-memory JWKS cache for a single verifier/middleware instance
|
|
556
|
+
|
|
557
|
+
### Multi-scope and role authorization helpers
|
|
558
|
+
|
|
559
|
+
If you need more than the built-in single `requiredScope` check, use the backend authorization helpers after token verification:
|
|
560
|
+
|
|
561
|
+
```ts
|
|
562
|
+
import { hasRole, hasScopes, requireRole, requireScopes, verifyAuth } from '@ouim/logto-authkit/server'
|
|
563
|
+
|
|
564
|
+
const auth = await verifyAuth(request, {
|
|
565
|
+
logtoUrl: process.env.LOGTO_URL!,
|
|
566
|
+
audience: process.env.LOGTO_AUDIENCE!,
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
if (!hasScopes(auth, ['profile:read', 'profile:write'], { mode: 'any' })) {
|
|
570
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 })
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
requireScopes(auth, ['profile:read', 'profile:write'])
|
|
574
|
+
|
|
575
|
+
if (!hasRole(auth, 'admin')) {
|
|
576
|
+
return Response.json({ error: 'Forbidden' }, { status: 403 })
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
requireRole(auth, 'admin')
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Notes:
|
|
583
|
+
|
|
584
|
+
- `hasScopes(subject, scopes, { mode })` returns a boolean for either a raw `AuthPayload` or a full `AuthContext`.
|
|
585
|
+
- `requireScopes(subject, scopes, { mode })` throws an error you can map to `403`.
|
|
586
|
+
- `mode: 'all'` is the default; use `mode: 'any'` when any one of the scopes should be enough.
|
|
587
|
+
- Scope parsing follows the OAuth `scope` claim convention: a whitespace-delimited string.
|
|
588
|
+
- `hasRole(subject, role, { claimKeys })` and `requireRole(subject, role, { claimKeys })` check role claims without coupling the logic to Express or Next.js.
|
|
589
|
+
- Role helpers look for `roles` first and then `role` by default. If your Logto tenant maps roles into a custom claim, pass `claimKeys`, for example `['https://example.com/roles']`.
|
|
590
|
+
|
|
591
|
+
### JWKS cache controls
|
|
592
|
+
|
|
593
|
+
Backend verification uses a per-process in-memory JWKS cache by default. If you need tighter control during key rotation, debugging, or unusual network setups, you can tune or clear it explicitly:
|
|
594
|
+
|
|
595
|
+
```ts
|
|
596
|
+
import { clearJwksCache, invalidateJwksCache, verifyAuth } from '@ouim/logto-authkit/server'
|
|
597
|
+
|
|
598
|
+
await verifyAuth(token, {
|
|
599
|
+
logtoUrl: 'https://your-tenant.logto.app',
|
|
600
|
+
audience: 'https://your-api.example.com',
|
|
601
|
+
jwksCacheTtlMs: 60_000,
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
invalidateJwksCache('https://your-tenant.logto.app')
|
|
605
|
+
clearJwksCache()
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
### Auth context shape
|
|
609
|
+
|
|
610
|
+
```ts
|
|
611
|
+
interface AuthContext {
|
|
612
|
+
userId: string | null
|
|
613
|
+
isAuthenticated: boolean
|
|
614
|
+
payload: AuthPayload | null
|
|
615
|
+
isGuest?: boolean
|
|
616
|
+
guestId?: string
|
|
617
|
+
}
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
## Bundler Config
|
|
621
|
+
|
|
622
|
+
This package includes bundler helpers for the `jose` resolution issues that often show up during Logto integration.
|
|
623
|
+
|
|
624
|
+
For build-time scripts, prefer the dedicated subpath:
|
|
625
|
+
|
|
626
|
+
```ts
|
|
627
|
+
import { viteConfig, getBundlerConfig } from '@ouim/logto-authkit/bundler-config'
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### Vite
|
|
631
|
+
|
|
632
|
+
```ts
|
|
633
|
+
import { defineConfig } from 'vite'
|
|
634
|
+
import { viteConfig } from '@ouim/logto-authkit/bundler-config'
|
|
635
|
+
|
|
636
|
+
export default defineConfig({
|
|
637
|
+
...viteConfig,
|
|
638
|
+
})
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Webpack
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
import { webpackConfig } from '@ouim/logto-authkit/bundler-config'
|
|
645
|
+
|
|
646
|
+
export default {
|
|
647
|
+
...webpackConfig,
|
|
648
|
+
}
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
### Next.js
|
|
652
|
+
|
|
653
|
+
```ts
|
|
654
|
+
import { nextjsConfig } from '@ouim/logto-authkit/bundler-config'
|
|
655
|
+
|
|
656
|
+
const nextConfig = {
|
|
657
|
+
...nextjsConfig,
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
export default nextConfig
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
## TypeScript
|
|
664
|
+
|
|
665
|
+
The package ships typed frontend and backend exports.
|
|
666
|
+
|
|
667
|
+
```ts
|
|
668
|
+
import type {
|
|
669
|
+
LogtoUser,
|
|
670
|
+
AuthOptions,
|
|
671
|
+
AuthContextType,
|
|
672
|
+
AuthProviderProps,
|
|
673
|
+
CallbackPageProps,
|
|
674
|
+
SignInPageProps,
|
|
675
|
+
AdditionalPage,
|
|
676
|
+
SignInButtonProps,
|
|
677
|
+
} from '@ouim/logto-authkit'
|
|
678
|
+
|
|
679
|
+
import type {
|
|
680
|
+
AuthContext,
|
|
681
|
+
AuthPayload,
|
|
682
|
+
AuthorizationMode,
|
|
683
|
+
VerifyAuthOptions,
|
|
684
|
+
ExpressRequest,
|
|
685
|
+
ExpressResponse,
|
|
686
|
+
ExpressNext,
|
|
687
|
+
NextRequest,
|
|
688
|
+
NextResponse,
|
|
689
|
+
} from '@ouim/logto-authkit/server'
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
## Positioning
|
|
693
|
+
|
|
694
|
+
`@ouim/logto-authkit` is best thought of as the practical app-layer around Logto:
|
|
695
|
+
|
|
696
|
+
- Logto remains the identity platform
|
|
697
|
+
- `@logto/react` remains the core SDK
|
|
698
|
+
- this package adds the missing productized layer most app teams want on day one
|
|
699
|
+
|
|
700
|
+
If your team eventually needs lower-level control, you can still drop down to the official Logto APIs without throwing your whole auth model away.
|
|
701
|
+
|
|
702
|
+
## Security & Advanced Features
|
|
703
|
+
|
|
704
|
+
This package includes **security hardening** that most auth libraries leave to you:
|
|
705
|
+
|
|
706
|
+
- **CSRF Protection** — Double-submit cookie pattern for backend routes
|
|
707
|
+
- **Cookie Security** — All auth/guest cookies use `Secure`, `SameSite=Strict`
|
|
708
|
+
- **JWKS Cache Invalidation** — Automatic key rotation detection
|
|
709
|
+
- **Payload Validation** — JWT fields validated before use
|
|
710
|
+
- **Network Resilience** — Transient errors auto-retry; auth errors fail fast
|
|
711
|
+
- **Backend Cookie Upgrade** — Helper to set `HttpOnly` cookies from the backend
|
|
712
|
+
|
|
713
|
+
See [docs/SECURITY_AND_FEATURES.md](./docs/SECURITY_AND_FEATURES.md) for details.
|
|
714
|
+
|
|
715
|
+
### New in This Release
|
|
716
|
+
|
|
717
|
+
- `usePermission` hook for frontend permission checks
|
|
718
|
+
- `checkRoleAuthorization` and `checkMultiScopeAuthorization` backend helpers
|
|
719
|
+
- Provider lifecycle callbacks: `onTokenRefresh`, `onAuthError`, `onSignOut`
|
|
720
|
+
- Configurable post-callback redirect on `CallbackPage`
|
|
721
|
+
- Proactive token refresh before expiry
|
|
722
|
+
- Configurable JWKS cache TTL
|
|
723
|
+
|
|
724
|
+
### Breaking Changes
|
|
725
|
+
|
|
726
|
+
- `UserCenter` now defaults to **local sign-out** (`global: false`) — much safer. Opt into global logout with `globalSignOut={true}` if you need it.
|
|
727
|
+
|
|
728
|
+
See [docs/MIGRATION_GUIDE.md](./docs/MIGRATION_GUIDE.md) for upgrade instructions.
|
|
729
|
+
|
|
730
|
+
## Repository Notes
|
|
731
|
+
|
|
732
|
+
- frontend and backend helpers are published from the same package
|
|
733
|
+
- backend helpers are exposed from `@ouim/logto-authkit/server`
|
|
734
|
+
- bundler helpers are exposed from `@ouim/logto-authkit/bundler-config`
|
|
735
|
+
|
|
736
|
+
## Key Documentation
|
|
737
|
+
|
|
738
|
+
### Core Guides
|
|
739
|
+
|
|
740
|
+
- **[SECURITY_AND_FEATURES.md](./docs/SECURITY_AND_FEATURES.md)** — Security hardening, CSRF protection, role-based authorization
|
|
741
|
+
- **[PERMISSIONS_AND_AUTHORIZATION.md](./docs/PERMISSIONS_AND_AUTHORIZATION.md)** — Using `usePermission` and backend authorization helpers
|
|
742
|
+
- **[MIGRATION_GUIDE.md](./docs/MIGRATION_GUIDE.md)** — Upgrade guide for v0.1.9+
|
|
743
|
+
- **[src/server/README.md](./src/server/README.md)** — Backend verification API reference
|
|
744
|
+
|
|
745
|
+
### Project Information
|
|
746
|
+
|
|
747
|
+
- **[CONTRIBUTING.md](./CONTRIBUTING.md)** — Contributing guidelines and branch protection rules
|
|
748
|
+
- **[CI_CD_AND_RELEASES.md](./docs/CI_CD_AND_RELEASES.md)** — GitHub Actions workflows, smoke tests, release process
|
|
749
|
+
- **[CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md)** — Community standards
|
|
750
|
+
- **[CHANGELOG.md](./CHANGELOG.md)** — Version history and release notes
|
|
751
|
+
- **[SECURITY.md](./SECURITY.md)** — Vulnerability disclosure policy
|
|
752
|
+
|
|
753
|
+
## Troubleshooting
|
|
754
|
+
|
|
755
|
+
### CORS errors on your backend API
|
|
756
|
+
|
|
757
|
+
Cause: your API is rejecting the browser origin or not allowing credentialed requests, so auth cookies or bearer-token requests never reach the server correctly.
|
|
758
|
+
|
|
759
|
+
Fix:
|
|
760
|
+
|
|
761
|
+
- Allow your frontend origin in the backend CORS config.
|
|
762
|
+
- If you rely on cookies, enable credentials on both sides: backend `Access-Control-Allow-Credentials: true` and frontend `fetch(..., { credentials: 'include' })`.
|
|
763
|
+
- Keep the frontend app, callback route, and backend cookie domain aligned. A cookie set for one host will not be sent to another.
|
|
764
|
+
|
|
765
|
+
### JWKS fetch failures
|
|
766
|
+
|
|
767
|
+
Cause: the backend cannot reach `https://<your-logto-host>/oidc/jwks`, the `logtoUrl` is wrong, or the Logto tenant URL includes a typo or wrong environment.
|
|
768
|
+
|
|
769
|
+
Fix:
|
|
770
|
+
|
|
771
|
+
- Verify `logtoUrl` is the tenant base URL, for example `https://your-tenant.logto.app`.
|
|
772
|
+
- Open `https://your-tenant.logto.app/oidc/jwks` directly and confirm it returns JSON.
|
|
773
|
+
- Check outbound network rules, proxy settings, and TLS certificates on the server running `verifyAuth` / `verifyNextAuth`.
|
|
774
|
+
- If failures happen only after a deployment or key rotation, retry once first: the verifier already invalidates stale JWKS cache entries and refetches keys automatically.
|
|
775
|
+
|
|
776
|
+
### "Invalid audience"
|
|
777
|
+
|
|
778
|
+
Cause: the token's `aud` claim does not include the API resource identifier you passed as `audience` in the backend verifier.
|
|
779
|
+
|
|
780
|
+
Fix:
|
|
781
|
+
|
|
782
|
+
- Make sure the frontend Logto config requests the same resource in `resources`.
|
|
783
|
+
- Make sure the backend `audience` matches that resource exactly.
|
|
784
|
+
- If your API accepts multiple resources, pass `audience` as an array to backend helpers.
|
|
785
|
+
- Decode a failing token and compare its `aud` claim with your configured `audience` value instead of assuming they match.
|
|
786
|
+
|
|
787
|
+
### Popup sign-in is blocked
|
|
788
|
+
|
|
789
|
+
Cause: the browser blocked `window.open`, usually because the sign-in call was not triggered from a direct user interaction or the site is in a stricter popup policy context.
|
|
790
|
+
|
|
791
|
+
Fix:
|
|
792
|
+
|
|
793
|
+
- Trigger popup sign-in from a real click or tap handler.
|
|
794
|
+
- Keep a real `/signin` route that renders `SignInPage`; popup flow depends on it.
|
|
795
|
+
- If popup restrictions are unavoidable, disable popup flow and use the default redirect flow instead.
|
|
796
|
+
- Test with browser extensions disabled if a popup blocker is interfering during development.
|
|
797
|
+
|
|
798
|
+
### Infinite redirect loop
|
|
799
|
+
|
|
800
|
+
Cause: the app is repeatedly sending unauthenticated users to sign-in without successfully finishing the callback or persisting the token.
|
|
801
|
+
|
|
802
|
+
Fix:
|
|
803
|
+
|
|
804
|
+
- Confirm both `/signin` and `/callback` routes exist and render `SignInPage` and `CallbackPage`.
|
|
805
|
+
- Ensure `callbackUrl` in `AuthProvider` exactly matches the redirect URI configured in Logto.
|
|
806
|
+
- Do not protect the callback route itself with `useAuth({ middleware: 'auth' })`.
|
|
807
|
+
- If you use custom navigation, verify it does not rewrite the callback URL or strip query parameters before `CallbackPage` runs.
|
|
808
|
+
- Check whether auth cookies are being cleared or blocked after callback, especially across different domains, subdomains, or HTTP/non-HTTPS environments.
|
|
809
|
+
|
|
810
|
+
### Local `file:` linked package issues
|
|
811
|
+
|
|
812
|
+
Cause: when `@ouim/logto-authkit` is consumed from a local path or symlink, the app can resolve a different React instance than the linked package. That usually shows up as invalid hook calls in Vite apps, or client/server boundary issues in Next.js App Router.
|
|
813
|
+
|
|
814
|
+
Fix:
|
|
815
|
+
|
|
816
|
+
- For Vite consumers, dedupe `react` and `react-dom` and alias them to the app's own `node_modules`.
|
|
817
|
+
- When merging `viteConfig`, merge `resolve` and `resolve.alias` instead of replacing them.
|
|
818
|
+
- For Next.js App Router, make sure your local build preserves the frontend entry's `'use client'` directive.
|
|
819
|
+
- See the dedicated guide: [docs/LINKED_LOCAL_PACKAGE_TROUBLESHOOTING.md](./docs/LINKED_LOCAL_PACKAGE_TROUBLESHOOTING.md)
|
|
820
|
+
|
|
821
|
+
## License
|
|
822
|
+
|
|
823
|
+
MIT
|