@zitadel/sdk-next 0.1.0-alpha.9 → 0.1.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 +207 -37
- package/dist/auth.d.ts +66 -13
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +162 -7
- package/dist/client.d.ts +27 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +29 -9
- package/dist/components.d.ts +16 -0
- package/dist/components.d.ts.map +1 -0
- package/dist/components.js +15 -0
- package/dist/context.d.ts +21 -10
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +24 -8
- package/dist/index.d.ts +16 -9
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -24
- package/dist/jsx.d.ts +5 -0
- package/dist/lib/jwt.d.ts +9 -0
- package/dist/lib/jwt.d.ts.map +1 -0
- package/dist/lib/jwt.js +7 -0
- package/dist/middleware.d.ts +19 -11
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +304 -9
- package/dist/provider.d.ts +52 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +75 -0
- package/dist/react.d.ts +26 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +24 -0
- package/dist/server.d.ts +13 -2
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +10 -7
- package/dist/session.d.ts +77 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +156 -0
- package/dist/types.d.ts +8 -3
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -1
- package/dist/useAuth.d.ts +17 -5
- package/dist/useAuth.d.ts.map +1 -0
- package/dist/useAuth.js +18 -7
- package/package.json +46 -31
- package/dist/chunk-2BFQLJQE.js +0 -27
- package/dist/chunk-4KENHIG4.js +0 -222
- package/dist/chunk-6F4PWJZI.js +0 -0
- package/dist/chunk-B7S6XMT3.js +0 -41
- package/dist/chunk-OCZMYSFX.js +0 -13
- package/dist/chunk-XTCHTAIQ.js +0 -12
package/README.md
CHANGED
|
@@ -5,9 +5,37 @@ Next.js middleware and helpers for Nextgen Auth.
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
pnpm add @zitadel/sdk-next
|
|
8
|
+
pnpm add @zitadel/sdk-next@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
Requires Next.js 15.2.3 or later. Earlier 15.x releases skip middleware for a
|
|
12
|
+
request that sends an `x-middleware-subrequest` header (CVE-2025-29927), which
|
|
13
|
+
on self-hosted deployments would let any request past `protectedRoutes`.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
Peer dependencies: `next >=15.2.3`, `react >=18` and `react-dom >=18`.
|
|
18
|
+
|
|
19
|
+
TypeScript ≥ 5.0, the floor shared by every SDK; see the [compatibility notes](https://github.com/zitadel/nextgen/blob/main/packages/AGENTS.md).
|
|
20
|
+
|
|
21
|
+
## Entry points
|
|
22
|
+
|
|
23
|
+
| Import | Runs in | Provides |
|
|
24
|
+
| ------------------------------ | -------------------------------- | ------------------------------------------------ |
|
|
25
|
+
| `@zitadel/sdk-next/middleware` | Edge middleware | `nextgenMiddleware`, `createProxy` |
|
|
26
|
+
| `@zitadel/sdk-next/server` | Server Components, Route Handlers | `auth()`, `NextgenProvider` |
|
|
27
|
+
| `@zitadel/sdk-next/react` | Client Components | `useAuth()`, `AuthContextProvider` |
|
|
28
|
+
| `@zitadel/sdk-next/session` | Client Components | `getSession()` |
|
|
29
|
+
| `@zitadel/sdk-next/client` | Client boundary | Web-component registration, `configureZitadel()` |
|
|
30
|
+
| `@zitadel/sdk-next/components` | Client Components | `ZitadelLogin`, `ZitadelLogout`, `ZitadelSession` |
|
|
31
|
+
|
|
32
|
+
The package root re-exports the server and client surfaces together for use
|
|
33
|
+
in server modules. Do not import the root from a `"use client"` module: it
|
|
34
|
+
pulls in the server-only `auth()`, which fails the build with an import trace
|
|
35
|
+
(exactly how depends on the bundler's tree shaking — the supported client
|
|
36
|
+
imports are `/react` and `/session`). `NextgenProvider` is itself server-only
|
|
37
|
+
because it accepts the token-bearing `auth()` result — see section 3.
|
|
38
|
+
|
|
11
39
|
## Setup
|
|
12
40
|
|
|
13
41
|
### 1. Middleware
|
|
@@ -35,62 +63,180 @@ The middleware runs on every matched route and does three things in one pass:
|
|
|
35
63
|
|
|
36
64
|
1. **Proxies** `/__nextgen/*` requests to the auth backend
|
|
37
65
|
2. **Verifies** the session JWT via JWKS using the Web Crypto API
|
|
38
|
-
3. **Redirects** unauthenticated requests to `loginPath` for protected routes
|
|
66
|
+
3. **Redirects** unauthenticated requests to `loginPath` for protected routes, with the requested path and query string in `?next=`, where `<zitadel-login>` returns after sign-in
|
|
39
67
|
|
|
40
68
|
### 2. Reading auth in a Server Component
|
|
41
69
|
|
|
42
|
-
```
|
|
43
|
-
import { auth } from "@zitadel/sdk-next";
|
|
70
|
+
```tsx
|
|
71
|
+
import { auth } from "@zitadel/sdk-next/server";
|
|
72
|
+
import { Suspense } from "react";
|
|
44
73
|
|
|
45
|
-
export default
|
|
74
|
+
export default function Page() {
|
|
75
|
+
return (
|
|
76
|
+
<Suspense fallback={<p>Loading…</p>}>
|
|
77
|
+
<Greeting />
|
|
78
|
+
</Suspense>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function Greeting() {
|
|
46
83
|
const session = await auth();
|
|
47
84
|
if (!session.isAuthenticated) return <p>Not signed in</p>;
|
|
48
|
-
return <p>Hello {session.session.
|
|
85
|
+
return <p>Hello {session.session.display ?? session.session.identifier}</p>;
|
|
49
86
|
}
|
|
50
87
|
```
|
|
51
88
|
|
|
89
|
+
`auth()` reads the request, so whatever awaits it renders inside
|
|
90
|
+
`<Suspense>`: with Cache Components on (`cacheComponents`, which
|
|
91
|
+
`create-next-app` enables), Next.js refuses request data outside it.
|
|
92
|
+
|
|
93
|
+
`auth()` reads the token the middleware tunnelled into the request headers
|
|
94
|
+
and **verifies it before trusting it** — JWTs cryptographically via JWKS,
|
|
95
|
+
opaque tokens against the backend's `GET /sessions/me` (which also supplies
|
|
96
|
+
the user's identity). A forged `x-nextgen-auth-token` header sent directly by
|
|
97
|
+
a client to a route outside the middleware `matcher` is rejected. Two things
|
|
98
|
+
follow:
|
|
99
|
+
|
|
100
|
+
- `auth()` only reports a session on routes the `matcher` covers — on other
|
|
101
|
+
routes the token never reaches it (see section 4 for chrome on public
|
|
102
|
+
pages).
|
|
103
|
+
- If the middleware runs with custom verification options (`audience`,
|
|
104
|
+
`allowedAlgorithms`, …), pass the same values to `auth()` so both layers
|
|
105
|
+
accept the same tokens.
|
|
106
|
+
|
|
107
|
+
`session.token` is the raw session token, available server-side for calling
|
|
108
|
+
upstream APIs. Never forward it into client components yourself —
|
|
109
|
+
`NextgenProvider` strips it for you (next section).
|
|
110
|
+
|
|
52
111
|
### 3. Reading auth in a Client Component
|
|
53
112
|
|
|
54
|
-
|
|
113
|
+
Seed the client tree once in your root layout (a Server Component), then
|
|
114
|
+
read the state with `useAuth()` anywhere below it:
|
|
55
115
|
|
|
56
116
|
```tsx
|
|
57
|
-
import { NextgenProvider } from '@zitadel/sdk-next';
|
|
117
|
+
import { auth, NextgenProvider } from '@zitadel/sdk-next/server';
|
|
118
|
+
import { Suspense } from 'react';
|
|
58
119
|
|
|
59
|
-
export default
|
|
60
|
-
const session = await auth();
|
|
120
|
+
export default function RootLayout({ children }) {
|
|
61
121
|
return (
|
|
62
122
|
<html>
|
|
63
123
|
<body>
|
|
64
|
-
<
|
|
124
|
+
<Suspense>
|
|
125
|
+
<AuthProvider>{children}</AuthProvider>
|
|
126
|
+
</Suspense>
|
|
65
127
|
</body>
|
|
66
128
|
</html>
|
|
67
129
|
);
|
|
68
130
|
}
|
|
131
|
+
|
|
132
|
+
async function AuthProvider({ children }) {
|
|
133
|
+
const session = await auth();
|
|
134
|
+
return <NextgenProvider session={session}>{children}</NextgenProvider>;
|
|
135
|
+
}
|
|
69
136
|
```
|
|
70
137
|
|
|
138
|
+
`NextgenProvider` converts the `auth()` result to the client-safe shape
|
|
139
|
+
**before** it crosses the server→client boundary: client components receive
|
|
140
|
+
`userId` / `email` / `name`, and the raw session token never enters the RSC
|
|
141
|
+
flight payload, where any script on the page could read it.
|
|
142
|
+
|
|
143
|
+
That strip only protects you while it runs on the server, which is why
|
|
144
|
+
`NextgenProvider` is server-only and **must not be re-exported through a
|
|
145
|
+
`"use client"` wrapper** (the common `providers.tsx` pattern): the wrapper
|
|
146
|
+
would become the client boundary, and its still-unstripped `session` prop —
|
|
147
|
+
token included — would serialise into the flight payload before the provider
|
|
148
|
+
ever ran. The `server-only` guard turns that wrapper into a build error
|
|
149
|
+
instead of a silent leak. To seed the context from client-side state (e.g. a
|
|
150
|
+
`getSession()` read), render `AuthContextProvider` from
|
|
151
|
+
`@zitadel/sdk-next/react` — it only accepts the token-less
|
|
152
|
+
`ClientAuthResult`.
|
|
153
|
+
|
|
71
154
|
Then in any client component:
|
|
72
155
|
|
|
73
156
|
```tsx
|
|
74
157
|
'use client';
|
|
75
|
-
import { useAuth } from '@zitadel/sdk-next';
|
|
158
|
+
import { useAuth } from '@zitadel/sdk-next/react';
|
|
76
159
|
|
|
77
160
|
export function UserBadge() {
|
|
78
161
|
const auth = useAuth();
|
|
79
|
-
return <span>{auth.isAuthenticated ? auth.session.
|
|
162
|
+
return <span>{auth.isAuthenticated ? (auth.session.display ?? auth.session.identifier) : 'Guest'}</span>;
|
|
80
163
|
}
|
|
81
164
|
```
|
|
82
165
|
|
|
83
|
-
|
|
166
|
+
`useAuth()` returns the same client-safe `ClientAuthResult` shape as
|
|
167
|
+
`getSession()` and sdk-nuxt's `useAuth()`. It reflects what the server knew
|
|
168
|
+
when the page rendered — which, like `auth()`, is only a live session on
|
|
169
|
+
routes the middleware `matcher` covers.
|
|
170
|
+
|
|
171
|
+
### 4. Session state for your own UI (any page)
|
|
84
172
|
|
|
85
|
-
|
|
173
|
+
`auth()` only sees a session on routes the middleware `matcher` covers, and the
|
|
174
|
+
scaffolded matcher covers just the proxy path and the protected routes — so a
|
|
175
|
+
header on a public page would always look signed out. For your app's own chrome
|
|
176
|
+
(header navigation, account menus), read the session client-side with
|
|
177
|
+
`getSession()` from `@zitadel/sdk-next/session`. It fetches the same-origin
|
|
178
|
+
`{proxyPath}/sessions/me?project_id=…` — the same read the `<zitadel-session>`
|
|
179
|
+
card performs — so it works on every page and the answer is the server's. The
|
|
180
|
+
project is the `projectId` option, else the project of `configureZitadel()`,
|
|
181
|
+
else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`; with none, it reads as signed out:
|
|
182
|
+
|
|
183
|
+
```tsx
|
|
184
|
+
'use client';
|
|
185
|
+
import { useEffect, useState } from 'react';
|
|
186
|
+
import { getSession, type ClientAuthResult } from '@zitadel/sdk-next/session';
|
|
187
|
+
|
|
188
|
+
export function HeaderNav() {
|
|
189
|
+
// undefined = not yet known — render neutral chrome, not "Sign in".
|
|
190
|
+
const [auth, setAuth] = useState<ClientAuthResult>();
|
|
191
|
+
const [error, setError] = useState<Error>();
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
getSession({ projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID }).then(setAuth, setError);
|
|
194
|
+
}, []);
|
|
195
|
+
if (error) return <span role="alert">Session unavailable</span>;
|
|
196
|
+
if (!auth) return null;
|
|
197
|
+
return auth.isAuthenticated ? (
|
|
198
|
+
<a href="/profile">{auth.session.display ?? auth.session.identifier ?? 'Account'}</a>
|
|
199
|
+
) : (
|
|
200
|
+
<a href="/login">Sign in</a>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
A rejected `getSession()` means the state is *unknown* (broken proxy, network,
|
|
206
|
+
5xx) — render a neutral or error state, never the signed-out CTAs.
|
|
207
|
+
|
|
208
|
+
A `200` with a non-empty `user_id` resolves to
|
|
209
|
+
`{ isAuthenticated: true, session: { userId, email, name } }` (client-safe —
|
|
210
|
+
no token); the canonical `401/auth.unauthorized`, `403/sess.project_mismatch`
|
|
211
|
+
(a session of another project on the same host), `404/sess.not_found`, and
|
|
212
|
+
anonymous sessions resolve to signed out. The request
|
|
213
|
+
and response are both marked no-store. Any other response — including malformed
|
|
214
|
+
JSON or a framework's HTML 404 page from a misrouted proxy — throws so a broken
|
|
215
|
+
proxy doesn't silently render as signed out. Sign-in and sign-out navigate
|
|
216
|
+
(`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page
|
|
217
|
+
load without extra wiring; to react in place, listen for the widgets'
|
|
218
|
+
`zitadel-signout` / `zitadel-flow-complete` events.
|
|
219
|
+
|
|
220
|
+
### 5. Login page
|
|
221
|
+
|
|
222
|
+
The `ZitadelLogin` component (the `@zitadel/sdk-react` wrapper, re-exported from `@zitadel/sdk-next/components`) must be rendered client-side only. Split it into a server wrapper and a client widget:
|
|
86
223
|
|
|
87
224
|
```tsx
|
|
88
225
|
// app/login/page.tsx (server)
|
|
89
|
-
import { auth } from '@zitadel/sdk-next';
|
|
226
|
+
import { auth } from '@zitadel/sdk-next/server';
|
|
90
227
|
import { redirect } from 'next/navigation';
|
|
228
|
+
import { Suspense } from 'react';
|
|
91
229
|
import { LoginWidget } from './widget';
|
|
92
230
|
|
|
93
|
-
export default
|
|
231
|
+
export default function LoginPage() {
|
|
232
|
+
return (
|
|
233
|
+
<Suspense>
|
|
234
|
+
<SignedOutOnly />
|
|
235
|
+
</Suspense>
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function SignedOutOnly() {
|
|
94
240
|
const session = await auth();
|
|
95
241
|
if (session.isAuthenticated) redirect('/admin');
|
|
96
242
|
return <LoginWidget />;
|
|
@@ -104,15 +250,19 @@ import dynamic from 'next/dynamic';
|
|
|
104
250
|
|
|
105
251
|
const ZitadelLogin = dynamic(
|
|
106
252
|
async () => {
|
|
107
|
-
|
|
253
|
+
// The /components entry registers the custom elements AND re-exports
|
|
254
|
+
// configureZitadel, so an app that only declares @zitadel/sdk-next
|
|
255
|
+
// resolves everything it needs (strict package managers would not
|
|
256
|
+
// resolve @zitadel/api or @zitadel/components transitively).
|
|
257
|
+
const { ZitadelLogin: Login, configureZitadel } = await import(
|
|
258
|
+
'@zitadel/sdk-next/components'
|
|
259
|
+
);
|
|
260
|
+
const project = configureZitadel({
|
|
261
|
+
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID!,
|
|
262
|
+
proxyPath: '/__nextgen',
|
|
263
|
+
});
|
|
108
264
|
return function ZitadelLoginElement() {
|
|
109
|
-
return
|
|
110
|
-
<zitadel-login
|
|
111
|
-
api-base="/__nextgen"
|
|
112
|
-
project-id="demo"
|
|
113
|
-
post-sign-in-url="/admin"
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
265
|
+
return <Login project={project} postSignInUrl="/admin" />;
|
|
116
266
|
};
|
|
117
267
|
},
|
|
118
268
|
{ ssr: false },
|
|
@@ -123,12 +273,33 @@ export function LoginWidget() {
|
|
|
123
273
|
}
|
|
124
274
|
```
|
|
125
275
|
|
|
276
|
+
There is no `api-base` prop — pass the handle returned by `configureZitadel()`
|
|
277
|
+
as `project`.
|
|
278
|
+
|
|
279
|
+
## Widget events
|
|
280
|
+
|
|
281
|
+
Each widget event reaches the callback in the last column, which receives the event's `detail` payload.
|
|
282
|
+
|
|
283
|
+
| Widget event | Fires when | Detail | Widgets | Callback |
|
|
284
|
+
| --- | --- | --- | --- | --- |
|
|
285
|
+
| `zitadel-flow-step` | Login shows a step, the first one included. | `{ step }` | Login | `onFlowStep` |
|
|
286
|
+
| `zitadel-flow-input` | The visitor edits a field. | `{ name, value }` | Login | `onFlowInput` |
|
|
287
|
+
| `zitadel-flow-complete` | The flow reaches a terminal step. | `{ behavior, redirect_uri?, handoff_token?, handoff_token_expires_at? }` | Login | `onFlowComplete` |
|
|
288
|
+
| `zitadel-flow-redirect` | The widget hands the browser to an identity provider; nobody is signed in yet. | `{ redirect_url, step }` | Login | `onFlowRedirect` |
|
|
289
|
+
| `zitadel-flow-error` | The flow reports an error. | `{ message }` | Login | `onFlowError` |
|
|
290
|
+
| `zitadel-signout` | The user signs out. | `{ display, identifier }` | Logout, Session | `onSignout` |
|
|
291
|
+
|
|
292
|
+
```tsx
|
|
293
|
+
<ZitadelLogin project={project} theme="light" onFlowComplete={(detail) => track(detail)} />
|
|
294
|
+
```
|
|
295
|
+
|
|
126
296
|
## Middleware options
|
|
127
297
|
|
|
128
298
|
| Option | Type | Default | Description |
|
|
129
299
|
| ------------------- | -------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
|
|
130
|
-
| `url` | `string` | `ZITADEL_URL` env | Full URL of the Zitadel auth backend
|
|
131
|
-
| `
|
|
300
|
+
| `url` | `string` | `ZITADEL_URL` env | Full URL of the Zitadel auth backend. A trailing slash is ignored. |
|
|
301
|
+
| `projectId` | `string` | `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID` | The project this app signs its users in to. A session of any other project on the server reads as signed out and keeps its cookie; with no project, no session counts. Pass the same value to `auth()` |
|
|
302
|
+
| `proxyPath` | `string` | `"/__nextgen"` | Path prefix proxied to the auth backend. Sign-in with an external provider currently requires the default: the provider returns to `/__nextgen/idp/{slug}/callback` |
|
|
132
303
|
| `protectedRoutes` | `string[]` | `[]` | Paths requiring a valid session. Trailing `*` matches sub-paths |
|
|
133
304
|
| `ignoredRoutes` | `string[]` | `[]` | Paths skipped entirely — no JWT check, no tunnelling. Useful for webhooks or health checks. Trailing `*` matches sub-paths |
|
|
134
305
|
| `loginPath` | `string` | `"/login"` | Where to redirect unauthenticated users |
|
|
@@ -136,17 +307,16 @@ export function LoginWidget() {
|
|
|
136
307
|
| `allowedTokenTypes` | `string[]` | `["JWT", "at+JWT"]` | Accepted `typ` header values (case-insensitive). Set to `[]` to disable this check |
|
|
137
308
|
| `clockSkewMs` | `number` | `5000` | Clock skew tolerance in ms for `exp`, `nbf`, `iat` |
|
|
138
309
|
| `jwksTimeoutMs` | `number` | `5000` | Timeout in ms for JWKS endpoint requests. Token is rejected if the fetch exceeds this window |
|
|
310
|
+
| `opaqueTokenTimeoutMs` | `number` | `5000` | Timeout in ms for opaque (non-JWT) session validation via `GET /sessions/me`. Also accepted by `auth()` |
|
|
311
|
+
| `proxyTimeoutMs` | `number` | `5000` | Timeout in ms for upstream proxy requests; requests exceeding it abort with a network error |
|
|
312
|
+
| `jwtKey` | `string` | unset | Reserved for future use — local PEM public key for offline JWT verification |
|
|
139
313
|
| `audience` | `string \| string[]` | not validated | Expected `aud` claim value(s). When omitted, audience is not checked |
|
|
140
314
|
|
|
141
315
|
## How JWT verification works
|
|
142
316
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
7. `iss` must be present and must equal `url` — tokens without an issuer are rejected
|
|
150
|
-
8. `exp` must be present and must be in the future (with `clockSkewMs` tolerance) — tokens without an expiry are rejected
|
|
151
|
-
9. `nbf` and `iat` are validated with `clockSkewMs` tolerance when present
|
|
152
|
-
10. The `x-nextgen-auth-token` header is stripped from all proxied requests to prevent internal state leakage
|
|
317
|
+
The verification pipeline is shared across SDKs and documented once in
|
|
318
|
+
[`@zitadel/sdk-core`](https://github.com/zitadel/nextgen/tree/main/packages/sdk-core#how-jwt-verification-works).
|
|
319
|
+
On top of that pipeline, the Next.js middleware adds:
|
|
320
|
+
|
|
321
|
+
1. A request that carries an `x-nextgen-auth-token` header is refused with a 400 on every route, including ignored routes and the proxy path: the header is an internal channel that only the middleware writes, so a client never sends it
|
|
322
|
+
2. `auth()` re-applies the same verification to the tunnelled token in the server runtime — the header alone is never treated as proof of a session
|
package/dist/auth.d.ts
CHANGED
|
@@ -1,17 +1,68 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import "server-only";
|
|
2
|
+
import type { AuthResult, NextgenMiddlewareOptions } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Options for {@link auth}.
|
|
5
|
+
*
|
|
6
|
+
* These mirror the token-verification subset of the middleware options. When
|
|
7
|
+
* the middleware runs with custom verification settings (a non-default
|
|
8
|
+
* `audience`, `allowedAlgorithms`, …), pass the same values here — otherwise
|
|
9
|
+
* a token the middleware accepted can fail re-verification in `auth()` and
|
|
10
|
+
* render as signed out.
|
|
11
|
+
*/
|
|
12
|
+
export type AuthOptions = Pick<NextgenMiddlewareOptions, "url" | "projectId" | "allowedAlgorithms" | "clockSkewMs" | "audience" | "allowedTokenTypes" | "jwksTimeoutMs" | "opaqueTokenTimeoutMs">;
|
|
3
13
|
/**
|
|
4
14
|
* Reads the auth state in a React Server Component or Next.js Route Handler.
|
|
5
15
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
16
|
+
* The session token arrives on the `x-nextgen-auth-token` request header,
|
|
17
|
+
* tunnelled there by the middleware — and `auth()` **verifies it before
|
|
18
|
+
* trusting it**:
|
|
19
|
+
*
|
|
20
|
+
* - **JWT tokens** are verified cryptographically (signature via JWKS,
|
|
21
|
+
* issuer, expiry, algorithm and type allow-lists) using the same rules and
|
|
22
|
+
* defaults as the middleware. The JWKS is cached in-process, so this does
|
|
23
|
+
* not add a backend round-trip after the first call.
|
|
24
|
+
* - **Opaque encrypted tokens** are validated against the backend's
|
|
25
|
+
* `GET /sessions/me`, which also supplies the user's identity (`userId`,
|
|
26
|
+
* `email`, `name`). The lookup is deduplicated per render pass via React
|
|
27
|
+
* `cache()`.
|
|
28
|
+
*
|
|
29
|
+
* The header alone is never proof of anything: on routes the middleware
|
|
30
|
+
* `matcher` does not cover, the middleware cannot refuse a forged
|
|
31
|
+
* client-supplied header, so `auth()` re-verifies every value it reads.
|
|
32
|
+
* A header value that fails verification is treated as signed out.
|
|
33
|
+
*
|
|
34
|
+
* Either way a session counts only in the app's own project: `projectId`,
|
|
35
|
+
* else `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else the
|
|
36
|
+
* project of `configureZitadel()` when it has run in this server runtime. A
|
|
37
|
+
* JWT must name it in `aud`; an opaque token must be a session of that
|
|
38
|
+
* project. With no project, nobody is signed in. A middleware built with
|
|
39
|
+
* `createProxy(config)` holds sessions to `config.projectId`: pass the same
|
|
40
|
+
* project here unless it comes from one of those variables.
|
|
41
|
+
*
|
|
42
|
+
* A backend that answers with an unexpected status, or not at all, reads as
|
|
43
|
+
* signed out and is logged, so a broken proxy or an unreachable backend is
|
|
44
|
+
* distinguishable from a missing session.
|
|
45
|
+
*
|
|
46
|
+
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
47
|
+
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
48
|
+
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
|
49
|
+
* live session exists. Either extend the `matcher` to every route that calls
|
|
50
|
+
* `auth()`, or — for app chrome like headers and account menus — read the
|
|
51
|
+
* session client-side with `getSession()` from `@zitadel/sdk-next/session`,
|
|
52
|
+
* which works on any page.
|
|
53
|
+
*
|
|
54
|
+
* The backend URL comes from `options.url`, falling back to the same
|
|
55
|
+
* `ZITADEL_URL` environment variable the middleware uses. If the middleware
|
|
56
|
+
* runs with custom verification options (`audience`, `allowedAlgorithms`, …),
|
|
57
|
+
* pass the same values here; see {@link AuthOptions}.
|
|
58
|
+
*
|
|
59
|
+
* This module is server-only: importing it (or the package root) from a
|
|
60
|
+
* client component fails at build time. Client components read auth state
|
|
61
|
+
* from `@zitadel/sdk-next/react` (`useAuth()`) or
|
|
62
|
+
* `@zitadel/sdk-next/session` (`getSession()`).
|
|
12
63
|
*
|
|
13
64
|
* ```ts
|
|
14
|
-
* import { auth } from "@zitadel/sdk-next";
|
|
65
|
+
* import { auth } from "@zitadel/sdk-next/server";
|
|
15
66
|
*
|
|
16
67
|
* export default async function Page() {
|
|
17
68
|
* const session = await auth();
|
|
@@ -20,8 +71,10 @@ import { AuthResult } from '@zitadel/sdk-core/types';
|
|
|
20
71
|
* }
|
|
21
72
|
* ```
|
|
22
73
|
*
|
|
23
|
-
* @
|
|
74
|
+
* @param options - Optional verification settings; see {@link AuthOptions}.
|
|
75
|
+
* @returns The current {@link AuthResult}. `session.token` is the raw session
|
|
76
|
+
* token for calling upstream APIs server-side — never forward it into
|
|
77
|
+
* client components (the `NextgenProvider` strips it for you).
|
|
24
78
|
*/
|
|
25
|
-
declare function auth(): Promise<AuthResult>;
|
|
26
|
-
|
|
27
|
-
export { auth };
|
|
79
|
+
export declare function auth(options?: AuthOptions): Promise<AuthResult>;
|
|
80
|
+
//# sourceMappingURL=auth.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAQrB,OAAO,KAAK,EAAE,UAAU,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAIvE;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,wBAAwB,EACtB,KAAK,GACL,WAAW,GACX,mBAAmB,GACnB,aAAa,GACb,UAAU,GACV,mBAAmB,GACnB,eAAe,GACf,sBAAsB,CACzB,CAAC;AAgBF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiEG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CAsFzE"}
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,162 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
} from "
|
|
4
|
-
import "
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
};
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { getZitadelConfig } from "@zitadel/api/config";
|
|
3
|
+
import { checkOpaqueSessionToken, jwtNamesProject } from "@zitadel/sdk-core/middleware";
|
|
4
|
+
import { joinURL } from "ufo";
|
|
5
|
+
import { headers } from "next/headers";
|
|
6
|
+
import { cache } from "react";
|
|
7
|
+
import { isJwtShaped, verifyJwt } from "./lib/jwt.js";
|
|
8
|
+
/**
|
|
9
|
+
* Checks an opaque (non-JWT) session token against the backend's
|
|
10
|
+
* `GET /sessions/me`, held to `projectId` (see `checkOpaqueSessionToken` in
|
|
11
|
+
* `@zitadel/sdk-core`, which the middleware uses too).
|
|
12
|
+
*
|
|
13
|
+
* Wrapped in React's `cache()` so that many components calling `auth()` in
|
|
14
|
+
* the same render pass share a single backend round-trip. Arguments are
|
|
15
|
+
* primitives on purpose — `cache()` memoises by argument identity.
|
|
16
|
+
*/
|
|
17
|
+
const checkOpaqueSession = cache((token, issuerUrl, timeoutMs, projectId) => checkOpaqueSessionToken(token, issuerUrl, timeoutMs, projectId));
|
|
18
|
+
/**
|
|
19
|
+
* Reads the auth state in a React Server Component or Next.js Route Handler.
|
|
20
|
+
*
|
|
21
|
+
* The session token arrives on the `x-nextgen-auth-token` request header,
|
|
22
|
+
* tunnelled there by the middleware — and `auth()` **verifies it before
|
|
23
|
+
* trusting it**:
|
|
24
|
+
*
|
|
25
|
+
* - **JWT tokens** are verified cryptographically (signature via JWKS,
|
|
26
|
+
* issuer, expiry, algorithm and type allow-lists) using the same rules and
|
|
27
|
+
* defaults as the middleware. The JWKS is cached in-process, so this does
|
|
28
|
+
* not add a backend round-trip after the first call.
|
|
29
|
+
* - **Opaque encrypted tokens** are validated against the backend's
|
|
30
|
+
* `GET /sessions/me`, which also supplies the user's identity (`userId`,
|
|
31
|
+
* `email`, `name`). The lookup is deduplicated per render pass via React
|
|
32
|
+
* `cache()`.
|
|
33
|
+
*
|
|
34
|
+
* The header alone is never proof of anything: on routes the middleware
|
|
35
|
+
* `matcher` does not cover, the middleware cannot refuse a forged
|
|
36
|
+
* client-supplied header, so `auth()` re-verifies every value it reads.
|
|
37
|
+
* A header value that fails verification is treated as signed out.
|
|
38
|
+
*
|
|
39
|
+
* Either way a session counts only in the app's own project: `projectId`,
|
|
40
|
+
* else `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else the
|
|
41
|
+
* project of `configureZitadel()` when it has run in this server runtime. A
|
|
42
|
+
* JWT must name it in `aud`; an opaque token must be a session of that
|
|
43
|
+
* project. With no project, nobody is signed in. A middleware built with
|
|
44
|
+
* `createProxy(config)` holds sessions to `config.projectId`: pass the same
|
|
45
|
+
* project here unless it comes from one of those variables.
|
|
46
|
+
*
|
|
47
|
+
* A backend that answers with an unexpected status, or not at all, reads as
|
|
48
|
+
* signed out and is logged, so a broken proxy or an unreachable backend is
|
|
49
|
+
* distinguishable from a missing session.
|
|
50
|
+
*
|
|
51
|
+
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
52
|
+
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
53
|
+
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
|
54
|
+
* live session exists. Either extend the `matcher` to every route that calls
|
|
55
|
+
* `auth()`, or — for app chrome like headers and account menus — read the
|
|
56
|
+
* session client-side with `getSession()` from `@zitadel/sdk-next/session`,
|
|
57
|
+
* which works on any page.
|
|
58
|
+
*
|
|
59
|
+
* The backend URL comes from `options.url`, falling back to the same
|
|
60
|
+
* `ZITADEL_URL` environment variable the middleware uses. If the middleware
|
|
61
|
+
* runs with custom verification options (`audience`, `allowedAlgorithms`, …),
|
|
62
|
+
* pass the same values here; see {@link AuthOptions}.
|
|
63
|
+
*
|
|
64
|
+
* This module is server-only: importing it (or the package root) from a
|
|
65
|
+
* client component fails at build time. Client components read auth state
|
|
66
|
+
* from `@zitadel/sdk-next/react` (`useAuth()`) or
|
|
67
|
+
* `@zitadel/sdk-next/session` (`getSession()`).
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* import { auth } from "@zitadel/sdk-next/server";
|
|
71
|
+
*
|
|
72
|
+
* export default async function Page() {
|
|
73
|
+
* const session = await auth();
|
|
74
|
+
* if (!session.isAuthenticated) return <p>Not signed in</p>;
|
|
75
|
+
* return <p>Hello {session.session.userId}</p>;
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*
|
|
79
|
+
* @param options - Optional verification settings; see {@link AuthOptions}.
|
|
80
|
+
* @returns The current {@link AuthResult}. `session.token` is the raw session
|
|
81
|
+
* token for calling upstream APIs server-side — never forward it into
|
|
82
|
+
* client components (the `NextgenProvider` strips it for you).
|
|
83
|
+
*/
|
|
84
|
+
export async function auth(options = {}) {
|
|
85
|
+
const headerStore = await headers();
|
|
86
|
+
const token = headerStore.get("x-nextgen-auth-token");
|
|
87
|
+
if (!token) {
|
|
88
|
+
return { isAuthenticated: false, session: null };
|
|
89
|
+
}
|
|
90
|
+
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", projectId = process.env.ZITADEL_PROJECT_ID ||
|
|
91
|
+
process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ||
|
|
92
|
+
getZitadelConfig()?.projectId, allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, opaqueTokenTimeoutMs = 5000, } = options;
|
|
93
|
+
if (!projectId) {
|
|
94
|
+
warnMissingProject();
|
|
95
|
+
return { isAuthenticated: false, session: null };
|
|
96
|
+
}
|
|
97
|
+
if (isJwtShaped(token)) {
|
|
98
|
+
const payload = await verifyJwt(token, {
|
|
99
|
+
issuerUrl: url,
|
|
100
|
+
allowedAlgorithms,
|
|
101
|
+
clockSkewMs,
|
|
102
|
+
audience,
|
|
103
|
+
allowedTokenTypes,
|
|
104
|
+
jwksTimeoutMs,
|
|
105
|
+
});
|
|
106
|
+
if (payload?.sub && jwtNamesProject(payload, projectId)) {
|
|
107
|
+
return {
|
|
108
|
+
isAuthenticated: true,
|
|
109
|
+
session: {
|
|
110
|
+
userId: payload.sub,
|
|
111
|
+
identifier: payload.email ?? null,
|
|
112
|
+
identifierProperty: null,
|
|
113
|
+
display: payload.name ?? null,
|
|
114
|
+
token,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (payload?.sub) {
|
|
119
|
+
// Verified, but issued for another project: not a user of this one.
|
|
120
|
+
return { isAuthenticated: false, session: null };
|
|
121
|
+
}
|
|
122
|
+
// A middleware-tunnelled JWT always verifies (the middleware applied the
|
|
123
|
+
// same checks moments earlier). Reaching this branch means the header was
|
|
124
|
+
// NOT vetted by the middleware — a route outside the `matcher` received a
|
|
125
|
+
// client-forged header — or the JWKS endpoint is unreachable from this
|
|
126
|
+
// runtime, or auth() runs with verification options that diverge from the
|
|
127
|
+
// middleware's.
|
|
128
|
+
console.warn("[nextgen] auth(): rejected an x-nextgen-auth-token header that failed JWT " +
|
|
129
|
+
"verification. If this route is not covered by the middleware matcher, this was " +
|
|
130
|
+
"a forged client header; otherwise check that auth() and the middleware use the " +
|
|
131
|
+
`same verification options and that the JWKS endpoint at ${joinURL(url, "/auth/keys")} is reachable.`);
|
|
132
|
+
return { isAuthenticated: false, session: null };
|
|
133
|
+
}
|
|
134
|
+
// A refused session, an anonymous one and one of another project read as
|
|
135
|
+
// signed out here. So does a check without a verdict, which
|
|
136
|
+
// `checkOpaqueSessionToken` logs with its cause.
|
|
137
|
+
const check = await checkOpaqueSession(token, url, opaqueTokenTimeoutMs, projectId);
|
|
138
|
+
if (check.verdict === "live" && check.session.userId) {
|
|
139
|
+
return {
|
|
140
|
+
isAuthenticated: true,
|
|
141
|
+
session: {
|
|
142
|
+
userId: check.session.userId,
|
|
143
|
+
identifier: check.session.identifier,
|
|
144
|
+
identifierProperty: check.session.identifierProperty,
|
|
145
|
+
display: check.session.display,
|
|
146
|
+
token,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { isAuthenticated: false, session: null };
|
|
151
|
+
}
|
|
152
|
+
let warnedMissingProject = false;
|
|
153
|
+
/** Says once per process why `auth()` signs no one in: no project is configured. */
|
|
154
|
+
function warnMissingProject() {
|
|
155
|
+
if (warnedMissingProject) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
warnedMissingProject = true;
|
|
159
|
+
console.warn("[nextgen] auth(): no project id is configured, so no session counts as signed in. " +
|
|
160
|
+
"Set ZITADEL_PROJECT_ID, or pass the middleware's `projectId` to auth() (with " +
|
|
161
|
+
"createProxy(config), that is config.projectId).");
|
|
162
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Registers the `<zitadel-login>`, `<zitadel-logout>` and `<zitadel-session>`
|
|
3
|
+
* Lit web components for use in Next.js apps.
|
|
4
|
+
*
|
|
5
|
+
* Import this inside a `"use client"` boundary (e.g. a dynamic import
|
|
6
|
+
* with `{ ssr: false }`) to register the custom elements with the
|
|
7
|
+
* browser's global registry:
|
|
8
|
+
*
|
|
9
|
+
* ```ts
|
|
10
|
+
* await import("@zitadel/sdk-next/client");
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* On an edge runtime the components package resolves to an entry without the
|
|
14
|
+
* elements, so this entry still loads there.
|
|
15
|
+
*
|
|
16
|
+
* SDK configuration is done via `configureZitadel()`, re-exported here so
|
|
17
|
+
* a consuming app that only declares `@zitadel/sdk-next` as a direct
|
|
18
|
+
* dependency can configure the SDK without reaching into
|
|
19
|
+
* `@zitadel/api/config` (which strict package managers would not
|
|
20
|
+
* resolve). Call it inside the same `"use client"` boundary before the
|
|
21
|
+
* components mount.
|
|
22
|
+
*/
|
|
23
|
+
import "@zitadel/components";
|
|
24
|
+
export { businessLocales } from "@zitadel/components";
|
|
25
|
+
export { configureZitadel, getApi } from "@zitadel/api/config";
|
|
26
|
+
export type { ZitadelConfig, ZitadelProject } from "@zitadel/api/config";
|
|
27
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,qBAAqB,CAAC;AAI7B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|