@zitadel/sdk-next 0.1.0-alpha.9 → 1.0.0-alpha.20
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 +132 -24
- package/dist/auth.d.ts +54 -13
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +152 -7
- package/dist/client.d.ts +23 -2
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +21 -9
- 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 +9 -10
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +294 -9
- package/dist/provider.d.ts +52 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +70 -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 +12 -2
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +10 -7
- package/dist/session.d.ts +61 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +115 -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 +28 -13
- 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
|
@@ -8,6 +8,23 @@ Next.js middleware and helpers for Nextgen Auth.
|
|
|
8
8
|
pnpm add @zitadel/sdk-next
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
+
## Entry points
|
|
12
|
+
|
|
13
|
+
| Import | Runs in | Provides |
|
|
14
|
+
| ------------------------------ | -------------------------------- | ------------------------------------------------ |
|
|
15
|
+
| `@zitadel/sdk-next/middleware` | Edge middleware | `nextgenMiddleware`, `createProxy` |
|
|
16
|
+
| `@zitadel/sdk-next/server` | Server Components, Route Handlers | `auth()`, `NextgenProvider` |
|
|
17
|
+
| `@zitadel/sdk-next/react` | Client Components | `useAuth()`, `AuthContextProvider` |
|
|
18
|
+
| `@zitadel/sdk-next/session` | Client Components | `getSession()` |
|
|
19
|
+
| `@zitadel/sdk-next/client` | Client boundary | Web-component registration, `configureZitadel()` |
|
|
20
|
+
|
|
21
|
+
The package root re-exports the server and client surfaces together for use
|
|
22
|
+
in server modules. Do not import the root from a `"use client"` module: it
|
|
23
|
+
pulls in the server-only `auth()`, which fails the build with an import trace
|
|
24
|
+
(exactly how depends on the bundler's tree shaking — the supported client
|
|
25
|
+
imports are `/react` and `/session`). `NextgenProvider` is itself server-only
|
|
26
|
+
because it accepts the token-bearing `auth()` result — see section 3.
|
|
27
|
+
|
|
11
28
|
## Setup
|
|
12
29
|
|
|
13
30
|
### 1. Middleware
|
|
@@ -40,7 +57,7 @@ The middleware runs on every matched route and does three things in one pass:
|
|
|
40
57
|
### 2. Reading auth in a Server Component
|
|
41
58
|
|
|
42
59
|
```ts
|
|
43
|
-
import { auth } from "@zitadel/sdk-next";
|
|
60
|
+
import { auth } from "@zitadel/sdk-next/server";
|
|
44
61
|
|
|
45
62
|
export default async function Page() {
|
|
46
63
|
const session = await auth();
|
|
@@ -49,30 +66,65 @@ export default async function Page() {
|
|
|
49
66
|
}
|
|
50
67
|
```
|
|
51
68
|
|
|
69
|
+
`auth()` reads the token the middleware tunnelled into the request headers
|
|
70
|
+
and **verifies it before trusting it** — JWTs cryptographically via JWKS,
|
|
71
|
+
opaque tokens against the backend's `GET /sessions/me` (which also supplies
|
|
72
|
+
the user's identity). A forged `x-nextgen-auth-token` header sent directly by
|
|
73
|
+
a client to a route outside the middleware `matcher` is rejected. Two things
|
|
74
|
+
follow:
|
|
75
|
+
|
|
76
|
+
- `auth()` only reports a session on routes the `matcher` covers — on other
|
|
77
|
+
routes the token never reaches it (see section 4 for chrome on public
|
|
78
|
+
pages).
|
|
79
|
+
- If the middleware runs with custom verification options (`audience`,
|
|
80
|
+
`allowedAlgorithms`, …), pass the same values to `auth()` so both layers
|
|
81
|
+
accept the same tokens.
|
|
82
|
+
|
|
83
|
+
`session.token` is the raw session token, available server-side for calling
|
|
84
|
+
upstream APIs. Never forward it into client components yourself —
|
|
85
|
+
`NextgenProvider` strips it for you (next section).
|
|
86
|
+
|
|
52
87
|
### 3. Reading auth in a Client Component
|
|
53
88
|
|
|
54
|
-
|
|
89
|
+
Seed the client tree once in your root layout (a Server Component), then
|
|
90
|
+
read the state with `useAuth()` anywhere below it:
|
|
55
91
|
|
|
56
92
|
```tsx
|
|
57
|
-
import { NextgenProvider } from '@zitadel/sdk-next';
|
|
93
|
+
import { auth, NextgenProvider } from '@zitadel/sdk-next/server';
|
|
58
94
|
|
|
59
95
|
export default async function RootLayout({ children }) {
|
|
60
96
|
const session = await auth();
|
|
61
97
|
return (
|
|
62
98
|
<html>
|
|
63
99
|
<body>
|
|
64
|
-
<NextgenProvider
|
|
100
|
+
<NextgenProvider session={session}>{children}</NextgenProvider>
|
|
65
101
|
</body>
|
|
66
102
|
</html>
|
|
67
103
|
);
|
|
68
104
|
}
|
|
69
105
|
```
|
|
70
106
|
|
|
107
|
+
`NextgenProvider` converts the `auth()` result to the client-safe shape
|
|
108
|
+
**before** it crosses the server→client boundary: client components receive
|
|
109
|
+
`userId` / `email` / `name`, and the raw session token never enters the RSC
|
|
110
|
+
flight payload, where any script on the page could read it.
|
|
111
|
+
|
|
112
|
+
That strip only protects you while it runs on the server, which is why
|
|
113
|
+
`NextgenProvider` is server-only and **must not be re-exported through a
|
|
114
|
+
`"use client"` wrapper** (the common `providers.tsx` pattern): the wrapper
|
|
115
|
+
would become the client boundary, and its still-unstripped `session` prop —
|
|
116
|
+
token included — would serialise into the flight payload before the provider
|
|
117
|
+
ever ran. The `server-only` guard turns that wrapper into a build error
|
|
118
|
+
instead of a silent leak. To seed the context from client-side state (e.g. a
|
|
119
|
+
`getSession()` read), render `AuthContextProvider` from
|
|
120
|
+
`@zitadel/sdk-next/react` — it only accepts the token-less
|
|
121
|
+
`ClientAuthResult`.
|
|
122
|
+
|
|
71
123
|
Then in any client component:
|
|
72
124
|
|
|
73
125
|
```tsx
|
|
74
126
|
'use client';
|
|
75
|
-
import { useAuth } from '@zitadel/sdk-next';
|
|
127
|
+
import { useAuth } from '@zitadel/sdk-next/react';
|
|
76
128
|
|
|
77
129
|
export function UserBadge() {
|
|
78
130
|
const auth = useAuth();
|
|
@@ -80,7 +132,58 @@ export function UserBadge() {
|
|
|
80
132
|
}
|
|
81
133
|
```
|
|
82
134
|
|
|
83
|
-
|
|
135
|
+
`useAuth()` returns the same client-safe `ClientAuthResult` shape as
|
|
136
|
+
`getSession()` and sdk-nuxt's `useAuth()`. It reflects what the server knew
|
|
137
|
+
when the page rendered — which, like `auth()`, is only a live session on
|
|
138
|
+
routes the middleware `matcher` covers.
|
|
139
|
+
|
|
140
|
+
### 4. Session state for your own UI (any page)
|
|
141
|
+
|
|
142
|
+
`auth()` only sees a session on routes the middleware `matcher` covers, and the
|
|
143
|
+
scaffolded matcher covers just the proxy path and the protected routes — so a
|
|
144
|
+
header on a public page would always look signed out. For your app's own chrome
|
|
145
|
+
(header navigation, account menus), read the session client-side with
|
|
146
|
+
`getSession()` from `@zitadel/sdk-next/session`. It fetches the same-origin
|
|
147
|
+
`{proxyPath}/sessions/me` — the same read the `<zitadel-session>` card performs —
|
|
148
|
+
so it works on every page and the answer is the server's:
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
'use client';
|
|
152
|
+
import { useEffect, useState } from 'react';
|
|
153
|
+
import { getSession, type ClientAuthResult } from '@zitadel/sdk-next/session';
|
|
154
|
+
|
|
155
|
+
export function HeaderNav() {
|
|
156
|
+
// undefined = not yet known — render neutral chrome, not "Sign in".
|
|
157
|
+
const [auth, setAuth] = useState<ClientAuthResult>();
|
|
158
|
+
const [error, setError] = useState<Error>();
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
getSession().then(setAuth, setError);
|
|
161
|
+
}, []);
|
|
162
|
+
if (error) return <span role="alert">Session unavailable</span>;
|
|
163
|
+
if (!auth) return null;
|
|
164
|
+
return auth.isAuthenticated ? (
|
|
165
|
+
<a href="/profile">{auth.session.name ?? auth.session.email ?? 'Account'}</a>
|
|
166
|
+
) : (
|
|
167
|
+
<a href="/login">Sign in</a>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
A rejected `getSession()` means the state is *unknown* (broken proxy, network,
|
|
173
|
+
5xx) — render a neutral or error state, never the signed-out CTAs.
|
|
174
|
+
|
|
175
|
+
A `200` with a non-empty `user_id` resolves to
|
|
176
|
+
`{ isAuthenticated: true, session: { userId, email, name } }` (client-safe —
|
|
177
|
+
no token); the canonical `401/auth.unauthorized`,
|
|
178
|
+
`404/sess.not_found`, and anonymous sessions resolve to signed out. The request
|
|
179
|
+
and response are both marked no-store. Any other response — including malformed
|
|
180
|
+
JSON or a framework's HTML 404 page from a misrouted proxy — throws so a broken
|
|
181
|
+
proxy doesn't silently render as signed out. Sign-in and sign-out navigate
|
|
182
|
+
(`post-sign-in-url` / `post-sign-out-url`), so chrome re-reads on the next page
|
|
183
|
+
load without extra wiring; to react in place, listen for the widgets'
|
|
184
|
+
`zitadel-signout` / `zitadel-flow-complete` events.
|
|
185
|
+
|
|
186
|
+
### 5. Login page
|
|
84
187
|
|
|
85
188
|
The `<zitadel-login>` web component (from `@zitadel/components`) must be rendered client-side only. Split it into a server wrapper and a client widget:
|
|
86
189
|
|
|
@@ -104,15 +207,17 @@ import dynamic from 'next/dynamic';
|
|
|
104
207
|
|
|
105
208
|
const ZitadelLogin = dynamic(
|
|
106
209
|
async () => {
|
|
107
|
-
|
|
210
|
+
// The /client entry registers the custom elements AND re-exports
|
|
211
|
+
// configureZitadel, so an app that only declares @zitadel/sdk-next
|
|
212
|
+
// resolves everything it needs (strict package managers would not
|
|
213
|
+
// resolve @zitadel/api or @zitadel/components transitively).
|
|
214
|
+
const { configureZitadel } = await import('@zitadel/sdk-next/client');
|
|
215
|
+
configureZitadel({
|
|
216
|
+
projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID!,
|
|
217
|
+
proxyPath: '/__nextgen',
|
|
218
|
+
});
|
|
108
219
|
return function ZitadelLoginElement() {
|
|
109
|
-
return
|
|
110
|
-
<zitadel-login
|
|
111
|
-
api-base="/__nextgen"
|
|
112
|
-
project-id="demo"
|
|
113
|
-
post-sign-in-url="/admin"
|
|
114
|
-
/>
|
|
115
|
-
);
|
|
220
|
+
return <zitadel-login post-sign-in-url="/admin" />;
|
|
116
221
|
};
|
|
117
222
|
},
|
|
118
223
|
{ ssr: false },
|
|
@@ -123,6 +228,10 @@ export function LoginWidget() {
|
|
|
123
228
|
}
|
|
124
229
|
```
|
|
125
230
|
|
|
231
|
+
There is no `api-base` attribute — the element reads the global handle from
|
|
232
|
+
`configureZitadel()` (or you can assign the returned handle to the element's
|
|
233
|
+
`project` property).
|
|
234
|
+
|
|
126
235
|
## Middleware options
|
|
127
236
|
|
|
128
237
|
| Option | Type | Default | Description |
|
|
@@ -136,17 +245,16 @@ export function LoginWidget() {
|
|
|
136
245
|
| `allowedTokenTypes` | `string[]` | `["JWT", "at+JWT"]` | Accepted `typ` header values (case-insensitive). Set to `[]` to disable this check |
|
|
137
246
|
| `clockSkewMs` | `number` | `5000` | Clock skew tolerance in ms for `exp`, `nbf`, `iat` |
|
|
138
247
|
| `jwksTimeoutMs` | `number` | `5000` | Timeout in ms for JWKS endpoint requests. Token is rejected if the fetch exceeds this window |
|
|
248
|
+
| `opaqueTokenTimeoutMs` | `number` | `5000` | Timeout in ms for opaque (non-JWT) session validation via `GET /sessions/me`. Also accepted by `auth()` |
|
|
249
|
+
| `proxyTimeoutMs` | `number` | `5000` | Timeout in ms for upstream proxy requests; requests exceeding it abort with a network error |
|
|
250
|
+
| `jwtKey` | `string` | unset | Reserved for future use — local PEM public key for offline JWT verification |
|
|
139
251
|
| `audience` | `string \| string[]` | not validated | Expected `aud` claim value(s). When omitted, audience is not checked |
|
|
140
252
|
|
|
141
253
|
## How JWT verification works
|
|
142
254
|
|
|
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
|
|
255
|
+
The verification pipeline is shared across SDKs and documented once in
|
|
256
|
+
[`@zitadel/sdk-core`](https://github.com/zitadel/nextgen/tree/main/packages/sdk-core#how-jwt-verification-works).
|
|
257
|
+
On top of that pipeline, the Next.js middleware adds:
|
|
258
|
+
|
|
259
|
+
1. The `x-nextgen-auth-token` header is stripped from all proxied requests to prevent internal state leakage
|
|
260
|
+
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,56 @@
|
|
|
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" | "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 neutralise 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
|
+
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
35
|
+
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
36
|
+
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
|
37
|
+
* live session exists. Either extend the `matcher` to every route that calls
|
|
38
|
+
* `auth()`, or — for app chrome like headers and account menus — read the
|
|
39
|
+
* session client-side with `getSession()` from `@zitadel/sdk-next/session`,
|
|
40
|
+
* which works on any page.
|
|
41
|
+
*
|
|
42
|
+
* The backend URL comes from `options.url`, falling back to the same
|
|
43
|
+
* `ZITADEL_URL` environment variable the middleware uses. If the middleware
|
|
44
|
+
* runs with custom verification options (`audience`, `allowedAlgorithms`, …),
|
|
45
|
+
* pass the same values here; see {@link AuthOptions}.
|
|
46
|
+
*
|
|
47
|
+
* This module is server-only: importing it (or the package root) from a
|
|
48
|
+
* client component fails at build time. Client components read auth state
|
|
49
|
+
* from `@zitadel/sdk-next/react` (`useAuth()`) or
|
|
50
|
+
* `@zitadel/sdk-next/session` (`getSession()`).
|
|
12
51
|
*
|
|
13
52
|
* ```ts
|
|
14
|
-
* import { auth } from "@zitadel/sdk-next";
|
|
53
|
+
* import { auth } from "@zitadel/sdk-next/server";
|
|
15
54
|
*
|
|
16
55
|
* export default async function Page() {
|
|
17
56
|
* const session = await auth();
|
|
@@ -20,8 +59,10 @@ import { AuthResult } from '@zitadel/sdk-core/types';
|
|
|
20
59
|
* }
|
|
21
60
|
* ```
|
|
22
61
|
*
|
|
23
|
-
* @
|
|
62
|
+
* @param options - Optional verification settings; see {@link AuthOptions}.
|
|
63
|
+
* @returns The current {@link AuthResult}. `session.token` is the raw session
|
|
64
|
+
* token for calling upstream APIs server-side — never forward it into
|
|
65
|
+
* client components (the `NextgenProvider` strips it for you).
|
|
24
66
|
*/
|
|
25
|
-
declare function auth(): Promise<AuthResult>;
|
|
26
|
-
|
|
27
|
-
export { auth };
|
|
67
|
+
export declare function auth(options?: AuthOptions): Promise<AuthResult>;
|
|
68
|
+
//# 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;AAOrB,OAAO,KAAK,EAAE,UAAU,EAAiB,wBAAwB,EAAE,MAAM,YAAY,CAAC;AAItF;;;;;;;;GAQG;AACH,MAAM,MAAM,WAAW,GAAG,IAAI,CAC5B,wBAAwB,EACtB,KAAK,GACL,mBAAmB,GACnB,aAAa,GACb,UAAU,GACV,mBAAmB,GACnB,eAAe,GACf,sBAAsB,CACzB,CAAC;AA8DF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqDG;AACH,wBAAsB,IAAI,CAAC,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA6DzE"}
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,152 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
} from "
|
|
4
|
-
import "./
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import "server-only";
|
|
2
|
+
import { headers } from "next/headers";
|
|
3
|
+
import { cache } from "react";
|
|
4
|
+
import { isJwtShaped, verifyJwt } from "./lib/jwt.js";
|
|
5
|
+
/**
|
|
6
|
+
* Validates an opaque (non-JWT) session token against the backend's
|
|
7
|
+
* `GET /sessions/me` and returns the client-safe identity, or `null` when the
|
|
8
|
+
* backend does not confirm a signed-in user.
|
|
9
|
+
*
|
|
10
|
+
* Wrapped in React's `cache()` so that many components calling `auth()` in
|
|
11
|
+
* the same render pass share a single backend round-trip. Arguments are
|
|
12
|
+
* primitives on purpose — `cache()` memoises by argument identity.
|
|
13
|
+
*
|
|
14
|
+
* Fail-closed: network errors, timeouts, and unexpected statuses all resolve
|
|
15
|
+
* to `null` (signed out). Unexpected failures are logged so a broken proxy or
|
|
16
|
+
* unreachable backend is distinguishable from a genuinely missing session.
|
|
17
|
+
*/
|
|
18
|
+
const validateOpaqueSession = cache(async (token, issuerUrl, timeoutMs) => {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${issuerUrl}/sessions/me`, {
|
|
21
|
+
method: "GET",
|
|
22
|
+
headers: { accept: "application/json", cookie: `__nextgen_session=${token}` },
|
|
23
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
24
|
+
});
|
|
25
|
+
// 401 = no/invalid session token, 404 = session gone (revoked/expired):
|
|
26
|
+
// both are the server's definitive "not signed in".
|
|
27
|
+
if (res.status === 401 || res.status === 404) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
if (!res.ok) {
|
|
31
|
+
console.warn(`[nextgen] auth(): session validation failed with HTTP ${res.status} from ` +
|
|
32
|
+
`${issuerUrl}/sessions/me — treating as signed out. If this persists, the ` +
|
|
33
|
+
`backend URL is likely misconfigured (ZITADEL_URL or the \`url\` option).`);
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const session = (await res.json());
|
|
37
|
+
// An anonymous session (no verified user factor yet) has no user_id —
|
|
38
|
+
// for server-rendered UI that is "not signed in".
|
|
39
|
+
if (!session.user_id) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
userId: session.user_id,
|
|
44
|
+
email: session.email ?? null,
|
|
45
|
+
name: session.name ?? null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
console.warn(`[nextgen] auth(): could not reach ${issuerUrl}/sessions/me to validate the ` +
|
|
50
|
+
`session token — treating as signed out.`);
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Reads the auth state in a React Server Component or Next.js Route Handler.
|
|
56
|
+
*
|
|
57
|
+
* The session token arrives on the `x-nextgen-auth-token` request header,
|
|
58
|
+
* tunnelled there by the middleware — and `auth()` **verifies it before
|
|
59
|
+
* trusting it**:
|
|
60
|
+
*
|
|
61
|
+
* - **JWT tokens** are verified cryptographically (signature via JWKS,
|
|
62
|
+
* issuer, expiry, algorithm and type allow-lists) using the same rules and
|
|
63
|
+
* defaults as the middleware. The JWKS is cached in-process, so this does
|
|
64
|
+
* not add a backend round-trip after the first call.
|
|
65
|
+
* - **Opaque encrypted tokens** are validated against the backend's
|
|
66
|
+
* `GET /sessions/me`, which also supplies the user's identity (`userId`,
|
|
67
|
+
* `email`, `name`). The lookup is deduplicated per render pass via React
|
|
68
|
+
* `cache()`.
|
|
69
|
+
*
|
|
70
|
+
* The header alone is never proof of anything: on routes the middleware
|
|
71
|
+
* `matcher` does not cover, the middleware cannot neutralise a forged
|
|
72
|
+
* client-supplied header, so `auth()` re-verifies every value it reads.
|
|
73
|
+
* A header value that fails verification is treated as signed out.
|
|
74
|
+
*
|
|
75
|
+
* **Matcher precondition (read this):** the middleware only tunnels the
|
|
76
|
+
* session token on routes covered by its `matcher`. On uncovered routes the
|
|
77
|
+
* cookie never reaches `auth()`, so `auth()` reports signed out even when a
|
|
78
|
+
* live session exists. Either extend the `matcher` to every route that calls
|
|
79
|
+
* `auth()`, or — for app chrome like headers and account menus — read the
|
|
80
|
+
* session client-side with `getSession()` from `@zitadel/sdk-next/session`,
|
|
81
|
+
* which works on any page.
|
|
82
|
+
*
|
|
83
|
+
* The backend URL comes from `options.url`, falling back to the same
|
|
84
|
+
* `ZITADEL_URL` environment variable the middleware uses. If the middleware
|
|
85
|
+
* runs with custom verification options (`audience`, `allowedAlgorithms`, …),
|
|
86
|
+
* pass the same values here; see {@link AuthOptions}.
|
|
87
|
+
*
|
|
88
|
+
* This module is server-only: importing it (or the package root) from a
|
|
89
|
+
* client component fails at build time. Client components read auth state
|
|
90
|
+
* from `@zitadel/sdk-next/react` (`useAuth()`) or
|
|
91
|
+
* `@zitadel/sdk-next/session` (`getSession()`).
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* import { auth } from "@zitadel/sdk-next/server";
|
|
95
|
+
*
|
|
96
|
+
* export default async function Page() {
|
|
97
|
+
* const session = await auth();
|
|
98
|
+
* if (!session.isAuthenticated) return <p>Not signed in</p>;
|
|
99
|
+
* return <p>Hello {session.session.userId}</p>;
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*
|
|
103
|
+
* @param options - Optional verification settings; see {@link AuthOptions}.
|
|
104
|
+
* @returns The current {@link AuthResult}. `session.token` is the raw session
|
|
105
|
+
* token for calling upstream APIs server-side — never forward it into
|
|
106
|
+
* client components (the `NextgenProvider` strips it for you).
|
|
107
|
+
*/
|
|
108
|
+
export async function auth(options = {}) {
|
|
109
|
+
const headerStore = await headers();
|
|
110
|
+
const token = headerStore.get("x-nextgen-auth-token");
|
|
111
|
+
if (!token) {
|
|
112
|
+
return { isAuthenticated: false, session: null };
|
|
113
|
+
}
|
|
114
|
+
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, opaqueTokenTimeoutMs = 5000, } = options;
|
|
115
|
+
if (isJwtShaped(token)) {
|
|
116
|
+
const payload = await verifyJwt(token, {
|
|
117
|
+
issuerUrl: url,
|
|
118
|
+
allowedAlgorithms,
|
|
119
|
+
clockSkewMs,
|
|
120
|
+
audience,
|
|
121
|
+
allowedTokenTypes,
|
|
122
|
+
jwksTimeoutMs,
|
|
123
|
+
});
|
|
124
|
+
if (payload?.sub) {
|
|
125
|
+
return {
|
|
126
|
+
isAuthenticated: true,
|
|
127
|
+
session: {
|
|
128
|
+
userId: payload.sub,
|
|
129
|
+
email: payload.email ?? null,
|
|
130
|
+
name: payload.name ?? null,
|
|
131
|
+
token,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
// A middleware-tunnelled JWT always verifies (the middleware applied the
|
|
136
|
+
// same checks moments earlier). Reaching this branch means the header was
|
|
137
|
+
// NOT vetted by the middleware — a route outside the `matcher` received a
|
|
138
|
+
// client-forged header — or the JWKS endpoint is unreachable from this
|
|
139
|
+
// runtime, or auth() runs with verification options that diverge from the
|
|
140
|
+
// middleware's.
|
|
141
|
+
console.warn("[nextgen] auth(): rejected an x-nextgen-auth-token header that failed JWT " +
|
|
142
|
+
"verification. If this route is not covered by the middleware matcher, this was " +
|
|
143
|
+
"a forged client header; otherwise check that auth() and the middleware use the " +
|
|
144
|
+
`same verification options and that the JWKS endpoint at ${url}/auth/keys is reachable.`);
|
|
145
|
+
return { isAuthenticated: false, session: null };
|
|
146
|
+
}
|
|
147
|
+
const session = await validateOpaqueSession(token, url, opaqueTokenTimeoutMs);
|
|
148
|
+
if (session) {
|
|
149
|
+
return { isAuthenticated: true, session: { ...session, token } };
|
|
150
|
+
}
|
|
151
|
+
return { isAuthenticated: false, session: null };
|
|
152
|
+
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the `<zitadel-login>` and `<zitadel-logout>` Lit web components
|
|
3
|
+
* 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
|
+
* SDK configuration is done via `configureZitadel()`, re-exported here so
|
|
14
|
+
* a consuming app that only declares `@zitadel/sdk-next` as a direct
|
|
15
|
+
* dependency can configure the SDK without reaching into
|
|
16
|
+
* `@zitadel/api/config` (which strict package managers would not
|
|
17
|
+
* resolve). Call it inside the same `"use client"` boundary before the
|
|
18
|
+
* components mount.
|
|
19
|
+
*/
|
|
20
|
+
export { ZitadelLogin, ZitadelLogout, ZitadelSession, businessLocales } from "@zitadel/components";
|
|
21
|
+
export { configureZitadel, getApi } from "@zitadel/api/config";
|
|
22
|
+
export type { ZitadelConfig, ZitadelProject } from "@zitadel/api/config";
|
|
23
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,qBAAqB,CAAC;AAC/D,YAAY,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC"}
|
package/dist/client.js
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports the `<zitadel-login>` and `<zitadel-logout>` Lit web components
|
|
3
|
+
* 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
|
+
* SDK configuration is done via `configureZitadel()`, re-exported here so
|
|
14
|
+
* a consuming app that only declares `@zitadel/sdk-next` as a direct
|
|
15
|
+
* dependency can configure the SDK without reaching into
|
|
16
|
+
* `@zitadel/api/config` (which strict package managers would not
|
|
17
|
+
* resolve). Call it inside the same `"use client"` boundary before the
|
|
18
|
+
* components mount.
|
|
19
|
+
*/
|
|
20
|
+
export { ZitadelLogin, ZitadelLogout, ZitadelSession, businessLocales } from "@zitadel/components";
|
|
21
|
+
export { configureZitadel, getApi } from "@zitadel/api/config";
|
package/dist/context.d.ts
CHANGED
|
@@ -1,11 +1,22 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { ClientAuthResult } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Client-side context carrier. In the standard server-seeded setup, render
|
|
5
|
+
* {@link NextgenProvider} from `@zitadel/sdk-next/server` instead — it
|
|
6
|
+
* normalises the server's `auth()` result to the client-safe shape (dropping
|
|
7
|
+
* the raw session token) *before* the value crosses the server→client
|
|
8
|
+
* component boundary, and its `server-only` guard keeps it out of client
|
|
9
|
+
* wrappers where that strip would come too late.
|
|
10
|
+
*
|
|
11
|
+
* Render this component directly only from client code that already holds a
|
|
12
|
+
* client-safe value — e.g. seeding from a `getSession()` read.
|
|
13
|
+
*
|
|
14
|
+
* The `value` prop is deliberately typed as {@link ClientAuthResult}: the
|
|
15
|
+
* raw session token must never enter client-side state.
|
|
16
|
+
*/
|
|
17
|
+
export declare function AuthContextProvider({ value, children, }: {
|
|
18
|
+
value: ClientAuthResult;
|
|
7
19
|
children: ReactNode;
|
|
8
|
-
}):
|
|
9
|
-
declare function useAuthContext():
|
|
10
|
-
|
|
11
|
-
export { NextgenProvider, useAuthContext };
|
|
20
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function useAuthContext(): ClientAuthResult;
|
|
22
|
+
//# sourceMappingURL=context.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../src/context.tsx"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAElE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAMnD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,QAAQ,GACT,EAAE;IACD,KAAK,EAAE,gBAAgB,CAAC;IACxB,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAEA;AAED,wBAAgB,cAAc,IAAI,gBAAgB,CAEjD"}
|
package/dist/context.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
const defaultValue = { isAuthenticated: false, session: null };
|
|
5
|
+
const NextgenAuthContext = createContext(defaultValue);
|
|
6
|
+
/**
|
|
7
|
+
* Client-side context carrier. In the standard server-seeded setup, render
|
|
8
|
+
* {@link NextgenProvider} from `@zitadel/sdk-next/server` instead — it
|
|
9
|
+
* normalises the server's `auth()` result to the client-safe shape (dropping
|
|
10
|
+
* the raw session token) *before* the value crosses the server→client
|
|
11
|
+
* component boundary, and its `server-only` guard keeps it out of client
|
|
12
|
+
* wrappers where that strip would come too late.
|
|
13
|
+
*
|
|
14
|
+
* Render this component directly only from client code that already holds a
|
|
15
|
+
* client-safe value — e.g. seeding from a `getSession()` read.
|
|
16
|
+
*
|
|
17
|
+
* The `value` prop is deliberately typed as {@link ClientAuthResult}: the
|
|
18
|
+
* raw session token must never enter client-side state.
|
|
19
|
+
*/
|
|
20
|
+
export function AuthContextProvider({ value, children, }) {
|
|
21
|
+
return _jsx(NextgenAuthContext.Provider, { value: value, children: children });
|
|
22
|
+
}
|
|
23
|
+
export function useAuthContext() {
|
|
24
|
+
return useContext(NextgenAuthContext);
|
|
25
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Package root — for **Server Components and server modules only**: it
|
|
3
|
+
* includes the server-only `auth()`, so importing it from a `"use client"`
|
|
4
|
+
* module fails at build time. Client components import from
|
|
5
|
+
* `@zitadel/sdk-next/react` (provider + hooks) or
|
|
6
|
+
* `@zitadel/sdk-next/session` (`getSession()`) instead.
|
|
7
|
+
*/
|
|
8
|
+
export * from "./types.js";
|
|
9
|
+
export { nextgenMiddleware, createProxy } from "./middleware.js";
|
|
10
|
+
export type { ProxyOptions, ProxyHandler } from "./middleware.js";
|
|
11
|
+
export { auth } from "./auth.js";
|
|
12
|
+
export type { AuthOptions } from "./auth.js";
|
|
13
|
+
export { NextgenProvider } from "./provider.js";
|
|
14
|
+
export { AuthContextProvider, useAuthContext } from "./context.js";
|
|
15
|
+
export { useAuth } from "./useAuth.js";
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,cAAc,YAAY,CAAC;AAC3B,OAAO,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACjE,YAAY,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
|