@zitadel/sdk-next 0.1.0-alpha.9 → 1.0.0-alpha.21
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 +134 -26
- package/dist/auth.d.ts +54 -13
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +154 -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 +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 +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 +116 -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/dist/index.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "./
|
|
13
|
-
|
|
14
|
-
NextgenProvider,
|
|
15
|
-
useAuthContext
|
|
16
|
-
} from "./chunk-2BFQLJQE.js";
|
|
17
|
-
export {
|
|
18
|
-
NextgenProvider,
|
|
19
|
-
auth,
|
|
20
|
-
createProxy,
|
|
21
|
-
nextgenMiddleware,
|
|
22
|
-
useAuth,
|
|
23
|
-
useAuthContext
|
|
24
|
-
};
|
|
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 { auth } from "./auth.js";
|
|
11
|
+
export { NextgenProvider } from "./provider.js";
|
|
12
|
+
export { AuthContextProvider, useAuthContext } from "./context.js";
|
|
13
|
+
export { useAuth } from "./useAuth.js";
|
package/dist/jsx.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Types-only shim: scaffolded apps depend on @zitadel/sdk-next, not
|
|
2
|
+
// @zitadel/components, so the React JSX declarations for the <zitadel-*>
|
|
3
|
+
// elements are re-exposed here as `@zitadel/sdk-next/jsx`. Copied verbatim
|
|
4
|
+
// into dist/ by the build script.
|
|
5
|
+
/// <reference types="@zitadel/components/jsx" />
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports shared JWT verification from `@zitadel/sdk-core`.
|
|
3
|
+
*
|
|
4
|
+
* The JWT module is runtime-agnostic (uses `atob` which is available in both
|
|
5
|
+
* Edge and Node.js 16+) and is defined once in sdk-core.
|
|
6
|
+
*/
|
|
7
|
+
export { JWKS_TTL_MS, base64UrlDecode, decodeJwt, isJwtShaped, verifyJwt, } from "@zitadel/sdk-core/jwt";
|
|
8
|
+
export type { JwtPayload, JwtHeader, DecodedJwt, VerifyJwtOptions } from "@zitadel/sdk-core/jwt";
|
|
9
|
+
//# sourceMappingURL=jwt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jwt.d.ts","sourceRoot":"","sources":["../../src/lib/jwt.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EACL,WAAW,EACX,eAAe,EACf,SAAS,EACT,WAAW,EACX,SAAS,GACV,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
|
package/dist/lib/jwt.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Re-exports shared JWT verification from `@zitadel/sdk-core`.
|
|
3
|
+
*
|
|
4
|
+
* The JWT module is runtime-agnostic (uses `atob` which is available in both
|
|
5
|
+
* Edge and Node.js 16+) and is defined once in sdk-core.
|
|
6
|
+
*/
|
|
7
|
+
export { JWKS_TTL_MS, base64UrlDecode, decodeJwt, isJwtShaped, verifyJwt, } from "@zitadel/sdk-core/jwt";
|
package/dist/middleware.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { ZitadelProject } from
|
|
2
|
-
import { NextgenMiddlewareOptions } from
|
|
3
|
-
import { NextRequest
|
|
4
|
-
|
|
1
|
+
import type { ZitadelProject } from "@zitadel/api/config";
|
|
2
|
+
import type { NextgenMiddlewareOptions } from "@zitadel/sdk-core/middleware";
|
|
3
|
+
import type { NextRequest } from "next/server";
|
|
4
|
+
import { NextResponse } from "next/server";
|
|
5
5
|
/**
|
|
6
6
|
* Next.js Edge middleware that handles proxying, JWT verification, and route
|
|
7
7
|
* protection in a single pass.
|
|
@@ -29,7 +29,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
29
29
|
* @param options - Middleware configuration options.
|
|
30
30
|
* @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
|
|
31
31
|
*/
|
|
32
|
-
declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
|
|
32
|
+
export declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
|
|
33
33
|
/**
|
|
34
34
|
* Options for {@link createProxy} that are separate from the shared
|
|
35
35
|
* {@link ZitadelConfig}. These configure route protection, login
|
|
@@ -38,11 +38,11 @@ declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddleware
|
|
|
38
38
|
* `proxyPath` and `url` are omitted because they come from the
|
|
39
39
|
* {@link ZitadelConfig} passed as the first argument.
|
|
40
40
|
*/
|
|
41
|
-
type ProxyOptions = Omit<NextgenMiddlewareOptions,
|
|
41
|
+
export type ProxyOptions = Omit<NextgenMiddlewareOptions, "proxyPath" | "url">;
|
|
42
42
|
/**
|
|
43
43
|
* A pre-configured middleware handler returned by {@link createProxy}.
|
|
44
44
|
*/
|
|
45
|
-
type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
45
|
+
export type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
46
46
|
/**
|
|
47
47
|
* Creates a pre-configured middleware handler from the SDK config
|
|
48
48
|
* returned by `configureZitadel()`. This is the derived-service
|
|
@@ -75,6 +75,5 @@ type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
|
75
75
|
* @param options - Route protection and JWT options.
|
|
76
76
|
* @returns A middleware handler function.
|
|
77
77
|
*/
|
|
78
|
-
declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
|
|
79
|
-
|
|
80
|
-
export { type ProxyHandler, type ProxyOptions, createProxy, nextgenMiddleware };
|
|
78
|
+
export declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
|
|
79
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,KAAK,EAAE,wBAAwB,EAAE,MAAM,8BAA8B,CAAC;AAC7E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAQ/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmC3C;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC,CAqDlC;AAqPD;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,wBAAwB,EAAE,WAAW,GAAG,KAAK,CAAC,CAAC;AAE/E;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC,CAAC;AAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+BG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,YAAiB,GAAG,YAAY,CAO5F"}
|
package/dist/middleware.js
CHANGED
|
@@ -1,9 +1,294 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { HOP_BY_HOP, INTERNAL_HEADERS, filterResponseHeaders, matchesRoutes, } from "@zitadel/sdk-core/middleware";
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
import { verifyJwt, isJwtShaped } from "./lib/jwt.js";
|
|
4
|
+
/**
|
|
5
|
+
* Clones the incoming request headers, injects `extra` key/value pairs,
|
|
6
|
+
* and registers them with the Next.js header-tunnelling mechanism so that
|
|
7
|
+
* server components can read them via `headers()`.
|
|
8
|
+
*
|
|
9
|
+
* @param req - The incoming edge request.
|
|
10
|
+
* @param extra - Additional headers to inject (e.g. `x-nextgen-auth-token`).
|
|
11
|
+
* @returns A new `Headers` instance with the injected values registered.
|
|
12
|
+
*/
|
|
13
|
+
function tunnelHeaders(req, extra) {
|
|
14
|
+
const headers = new Headers(req.headers);
|
|
15
|
+
// Security: strip any x-middleware-override-headers the client may have sent.
|
|
16
|
+
// Preserving the client value would allow an attacker to inject arbitrary
|
|
17
|
+
// header names into the Next.js override list, potentially bypassing the
|
|
18
|
+
// internal header-tunnelling safety checks. We always build this header from
|
|
19
|
+
// scratch using only the names we inject ourselves.
|
|
20
|
+
headers.delete("x-middleware-override-headers");
|
|
21
|
+
const injectedNames = [];
|
|
22
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
23
|
+
headers.set(name, value);
|
|
24
|
+
headers.set(`x-middleware-request-${name}`, value);
|
|
25
|
+
injectedNames.push(name);
|
|
26
|
+
}
|
|
27
|
+
headers.set("x-middleware-override-headers", injectedNames.join(","));
|
|
28
|
+
return headers;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Next.js Edge middleware that handles proxying, JWT verification, and route
|
|
32
|
+
* protection in a single pass.
|
|
33
|
+
*
|
|
34
|
+
* Place this in your `middleware.ts` file:
|
|
35
|
+
*
|
|
36
|
+
* ```ts
|
|
37
|
+
* import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
|
|
38
|
+
* import type { NextRequest } from "next/server";
|
|
39
|
+
*
|
|
40
|
+
* export function middleware(req: NextRequest) {
|
|
41
|
+
* return nextgenMiddleware(req, {
|
|
42
|
+
* url: process.env.ZITADEL_URL,
|
|
43
|
+
* protectedRoutes: ["/profile"],
|
|
44
|
+
* loginPath: "/login",
|
|
45
|
+
* });
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* export const config = {
|
|
49
|
+
* matcher: ["/__nextgen/:path*", "/profile/:path*"],
|
|
50
|
+
* };
|
|
51
|
+
* ```
|
|
52
|
+
*
|
|
53
|
+
* @param req - The incoming Next.js edge request.
|
|
54
|
+
* @param options - Middleware configuration options.
|
|
55
|
+
* @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
|
|
56
|
+
*/
|
|
57
|
+
export async function nextgenMiddleware(req, options = {}) {
|
|
58
|
+
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", proxyPath = "/__nextgen", protectedRoutes = [], ignoredRoutes = [], loginPath = "/login", allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, proxyTimeoutMs = 5000, opaqueTokenTimeoutMs = 5000, } = options;
|
|
59
|
+
// Guard against open-redirect: loginPath must be a relative path. An absolute
|
|
60
|
+
// URL (e.g. "https://evil.com/phish") or a protocol-relative URL
|
|
61
|
+
// (e.g. "//evil.com") would redirect the browser to an external host.
|
|
62
|
+
if (!loginPath.startsWith("/") || loginPath.startsWith("//")) {
|
|
63
|
+
throw new Error(`[nextgen] loginPath must be a relative path starting with a single "/". ` +
|
|
64
|
+
`Received: "${loginPath}". Using an absolute or protocol-relative URL ` +
|
|
65
|
+
`would allow open-redirect attacks.`);
|
|
66
|
+
}
|
|
67
|
+
const { pathname } = new URL(req.url);
|
|
68
|
+
if (matchesRoutes(pathname, ignoredRoutes)) {
|
|
69
|
+
// Neutralise any client-supplied x-nextgen-auth-token on ignored routes.
|
|
70
|
+
// handleAuth is skipped for ignored routes, so we must strip the header
|
|
71
|
+
// here to prevent a forged value from reaching server components.
|
|
72
|
+
const headers = tunnelHeaders(req, { "x-nextgen-auth-token": "" });
|
|
73
|
+
return NextResponse.next({ request: { headers } });
|
|
74
|
+
}
|
|
75
|
+
if (pathname === proxyPath || pathname.startsWith(`${proxyPath}/`)) {
|
|
76
|
+
return proxyRequest(req, url, proxyPath, proxyTimeoutMs);
|
|
77
|
+
}
|
|
78
|
+
return handleAuth(req, {
|
|
79
|
+
url,
|
|
80
|
+
protectedRoutes,
|
|
81
|
+
loginPath,
|
|
82
|
+
allowedAlgorithms,
|
|
83
|
+
clockSkewMs,
|
|
84
|
+
audience,
|
|
85
|
+
allowedTokenTypes,
|
|
86
|
+
jwksTimeoutMs,
|
|
87
|
+
opaqueTokenTimeoutMs,
|
|
88
|
+
pathname,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Validates an opaque (non-JWT) session token by calling the backend's
|
|
93
|
+
* `/sessions/me` endpoint. Returns `true` when the backend confirms the
|
|
94
|
+
* session is live (HTTP 200), `false` otherwise.
|
|
95
|
+
*
|
|
96
|
+
* This is the fallback path for backends that issue encrypted opaque tokens
|
|
97
|
+
* rather than self-contained JWTs.
|
|
98
|
+
*/
|
|
99
|
+
async function validateOpaqueSessionToken(token, issuerUrl, timeoutMs) {
|
|
100
|
+
try {
|
|
101
|
+
const res = await fetch(`${issuerUrl}/sessions/me`, {
|
|
102
|
+
method: "GET",
|
|
103
|
+
headers: { cookie: `__nextgen_session=${token}` },
|
|
104
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
105
|
+
});
|
|
106
|
+
return res.ok;
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Forwards a `/__nextgen/*` request to the upstream auth backend and streams
|
|
114
|
+
* the response back verbatim, stripping hop-by-hop headers in both directions.
|
|
115
|
+
*
|
|
116
|
+
* All non-hop-by-hop headers from the incoming request are forwarded, including
|
|
117
|
+
* any `X-Forwarded-For` chain already set by an upstream CDN or load balancer.
|
|
118
|
+
* When `X-Forwarded-For` is absent (direct connection with no upstream proxy),
|
|
119
|
+
* it is initialised from the runtime-provided client IP so the auth server
|
|
120
|
+
* always receives origin information. `X-Forwarded-Host` and `X-Forwarded-Proto`
|
|
121
|
+
* are set only when absent, preserving values injected by an upstream CDN.
|
|
122
|
+
*
|
|
123
|
+
* @param req - The incoming edge request.
|
|
124
|
+
* @param authUrl - Base URL of the auth backend.
|
|
125
|
+
* @param proxyPath - The path prefix being proxied (e.g. `"/__nextgen"`).
|
|
126
|
+
* @returns The proxied upstream `Response`.
|
|
127
|
+
*/
|
|
128
|
+
async function proxyRequest(req, authUrl, proxyPath, proxyTimeoutMs) {
|
|
129
|
+
const url = new URL(req.url);
|
|
130
|
+
const suffix = url.pathname.slice(proxyPath.length);
|
|
131
|
+
const target = `${authUrl}${suffix}${url.search}`;
|
|
132
|
+
const upstreamHeaders = new Headers();
|
|
133
|
+
for (const [key, value] of req.headers.entries()) {
|
|
134
|
+
const lower = key.toLowerCase();
|
|
135
|
+
if (!HOP_BY_HOP.has(lower) && !INTERNAL_HEADERS.has(lower)) {
|
|
136
|
+
upstreamHeaders.set(key, value);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Always append the direct client IP to the X-Forwarded-For chain so the
|
|
140
|
+
// upstream auth server sees the full proxy path. We never skip this even when
|
|
141
|
+
// a chain is already present — a CDN or load balancer may have set XFF before
|
|
142
|
+
// the request reached this edge node, and our hop must still be recorded.
|
|
143
|
+
const directIp = req.ip ?? req.headers.get("x-real-ip");
|
|
144
|
+
if (directIp) {
|
|
145
|
+
const existingXff = upstreamHeaders.get("x-forwarded-for");
|
|
146
|
+
upstreamHeaders.set("x-forwarded-for", existingXff ? `${existingXff}, ${directIp}` : directIp);
|
|
147
|
+
}
|
|
148
|
+
if (!upstreamHeaders.has("x-forwarded-host")) {
|
|
149
|
+
upstreamHeaders.set("x-forwarded-host", url.host);
|
|
150
|
+
}
|
|
151
|
+
if (!upstreamHeaders.has("x-forwarded-proto")) {
|
|
152
|
+
upstreamHeaders.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
153
|
+
}
|
|
154
|
+
const hasBody = !["GET", "HEAD"].includes(req.method);
|
|
155
|
+
// ADR 036: the current browser flow needs the confidential project secret
|
|
156
|
+
// only for the handoff exchange. Stamping it onto every proxied operation
|
|
157
|
+
// turns this public request boundary into an operator-capable open relay.
|
|
158
|
+
// Preserve an explicit caller credential so the exchange can use the future
|
|
159
|
+
// publishable-key plane without being overwritten.
|
|
160
|
+
if (requiresProjectSecret(req.method, suffix) && !upstreamHeaders.has("authorization")) {
|
|
161
|
+
const secret = process.env.ZITADEL_PROJECT_SECRET;
|
|
162
|
+
if (secret) {
|
|
163
|
+
upstreamHeaders.set("authorization", `Bearer ${secret}`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// Read body eagerly so undici receives a concrete buffer rather than a
|
|
167
|
+
// WinterCG ReadableStream, which is incompatible with Node.js fetch body
|
|
168
|
+
// extraction.
|
|
169
|
+
const bodyBuffer = hasBody ? await req.arrayBuffer() : undefined;
|
|
170
|
+
const upstream = await fetch(target, {
|
|
171
|
+
method: req.method,
|
|
172
|
+
headers: upstreamHeaders,
|
|
173
|
+
body: bodyBuffer,
|
|
174
|
+
redirect: "manual",
|
|
175
|
+
signal: AbortSignal.timeout(proxyTimeoutMs),
|
|
176
|
+
});
|
|
177
|
+
const responseHeaders = filterResponseHeaders(upstream.headers);
|
|
178
|
+
const setCookies = upstream.headers.getSetCookie?.() ?? [];
|
|
179
|
+
for (const cookie of setCookies) {
|
|
180
|
+
responseHeaders.append("set-cookie", cookie);
|
|
181
|
+
}
|
|
182
|
+
return new Response(upstream.body, {
|
|
183
|
+
status: upstream.status,
|
|
184
|
+
headers: responseHeaders,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function requiresProjectSecret(method, pathname) {
|
|
188
|
+
return method.toUpperCase() === "POST" && pathname === "/sessions/exchange";
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Verifies the session token and tunnels the result to server components via
|
|
192
|
+
* a request header. Redirects to the login page when the token is absent or
|
|
193
|
+
* invalid on a protected route.
|
|
194
|
+
*
|
|
195
|
+
* @param req - The incoming edge request.
|
|
196
|
+
* @param opts - Auth handler options.
|
|
197
|
+
* @returns A `NextResponse` to continue or redirect.
|
|
198
|
+
*/
|
|
199
|
+
async function handleAuth(req, opts) {
|
|
200
|
+
const { url, protectedRoutes, loginPath, allowedAlgorithms, clockSkewMs, audience, allowedTokenTypes, jwksTimeoutMs, opaqueTokenTimeoutMs, pathname, } = opts;
|
|
201
|
+
const authHeader = req.headers.get("authorization");
|
|
202
|
+
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
203
|
+
const cookieToken = req.cookies.get("__nextgen_session")?.value ?? null;
|
|
204
|
+
// Bearer token takes explicit precedence over the session cookie. API clients
|
|
205
|
+
// (e.g. mobile apps, CLIs) use Authorization headers while browsers use
|
|
206
|
+
// cookies; when both are present the caller clearly intended the Bearer token.
|
|
207
|
+
const token = bearerToken ?? cookieToken;
|
|
208
|
+
const payload = token
|
|
209
|
+
? await verifyJwt(token, {
|
|
210
|
+
issuerUrl: url,
|
|
211
|
+
allowedAlgorithms,
|
|
212
|
+
clockSkewMs,
|
|
213
|
+
audience,
|
|
214
|
+
allowedTokenTypes,
|
|
215
|
+
jwksTimeoutMs,
|
|
216
|
+
})
|
|
217
|
+
: null;
|
|
218
|
+
if (payload && token && payload.sub) {
|
|
219
|
+
const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": token });
|
|
220
|
+
return NextResponse.next({ request: { headers: tunnelled } });
|
|
221
|
+
}
|
|
222
|
+
// The backend issues opaque encrypted session tokens rather than JWTs.
|
|
223
|
+
// Only fall back to /sessions/me validation when the token is definitively
|
|
224
|
+
// not a JWT (non-JSON segments). A token with a valid JWT structure that
|
|
225
|
+
// failed verification (bad sig, wrong typ/alg) must be rejected — never
|
|
226
|
+
// accepted by a backend call that doesn't re-check the JWT claims.
|
|
227
|
+
if (!payload && cookieToken && !isJwtShaped(cookieToken)) {
|
|
228
|
+
const isValid = await validateOpaqueSessionToken(cookieToken, url, opaqueTokenTimeoutMs);
|
|
229
|
+
if (isValid) {
|
|
230
|
+
const tunnelled = tunnelHeaders(req, {
|
|
231
|
+
"x-nextgen-auth-token": cookieToken,
|
|
232
|
+
});
|
|
233
|
+
return NextResponse.next({ request: { headers: tunnelled } });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": "" });
|
|
237
|
+
const staleNextgenCookies = req.cookies
|
|
238
|
+
.getAll()
|
|
239
|
+
.filter((c) => c.name.startsWith("__nextgen"));
|
|
240
|
+
if (matchesRoutes(pathname, protectedRoutes)) {
|
|
241
|
+
const loginUrl = new URL(loginPath, req.url);
|
|
242
|
+
loginUrl.searchParams.set("next", pathname);
|
|
243
|
+
const redirect = NextResponse.redirect(loginUrl, { status: 302 });
|
|
244
|
+
for (const cookie of staleNextgenCookies) {
|
|
245
|
+
redirect.cookies.delete(cookie.name);
|
|
246
|
+
}
|
|
247
|
+
return redirect;
|
|
248
|
+
}
|
|
249
|
+
const response = NextResponse.next({ request: { headers: tunnelled } });
|
|
250
|
+
for (const cookie of staleNextgenCookies) {
|
|
251
|
+
response.cookies.delete(cookie.name);
|
|
252
|
+
}
|
|
253
|
+
return response;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Creates a pre-configured middleware handler from the SDK config
|
|
257
|
+
* returned by `configureZitadel()`. This is the derived-service
|
|
258
|
+
* derived service pattern:
|
|
259
|
+
*
|
|
260
|
+
* ```ts
|
|
261
|
+
* // src/zitadel.ts
|
|
262
|
+
* import { configureZitadel } from "@zitadel/api/config";
|
|
263
|
+
* import { createProxy } from "@zitadel/sdk-next/middleware";
|
|
264
|
+
*
|
|
265
|
+
* const zitadel = configureZitadel({
|
|
266
|
+
* projectId: "demo",
|
|
267
|
+
* url: process.env.ZITADEL_URL,
|
|
268
|
+
* });
|
|
269
|
+
*
|
|
270
|
+
* export const proxy = createProxy(zitadel, {
|
|
271
|
+
* protectedRoutes: ["/admin*"],
|
|
272
|
+
* loginPath: "/login",
|
|
273
|
+
* });
|
|
274
|
+
* ```
|
|
275
|
+
*
|
|
276
|
+
* Then in middleware.ts:
|
|
277
|
+
*
|
|
278
|
+
* ```ts
|
|
279
|
+
* import { proxy } from "./zitadel";
|
|
280
|
+
* export const middleware = proxy;
|
|
281
|
+
* ```
|
|
282
|
+
*
|
|
283
|
+
* @param config - The SDK handle from `configureZitadel()`.
|
|
284
|
+
* @param options - Route protection and JWT options.
|
|
285
|
+
* @returns A middleware handler function.
|
|
286
|
+
*/
|
|
287
|
+
export function createProxy(config, options = {}) {
|
|
288
|
+
const mergedOptions = {
|
|
289
|
+
...options,
|
|
290
|
+
proxyPath: config.proxyPath,
|
|
291
|
+
url: config.url,
|
|
292
|
+
};
|
|
293
|
+
return (req) => nextgenMiddleware(req, mergedOptions);
|
|
294
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import "server-only";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { AuthResult, ClientAuthResult, ClientSession, NextgenSession } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Seeds client components (the `useAuth()` hook) with the server-known auth
|
|
6
|
+
* state. Render it in a Server Component — typically the root layout — and
|
|
7
|
+
* pass the `auth()` result directly:
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* import { auth, NextgenProvider } from "@zitadel/sdk-next/server";
|
|
11
|
+
*
|
|
12
|
+
* export default async function RootLayout({ children }) {
|
|
13
|
+
* const session = await auth();
|
|
14
|
+
* return (
|
|
15
|
+
* <html>
|
|
16
|
+
* <body>
|
|
17
|
+
* <NextgenProvider session={session}>{children}</NextgenProvider>
|
|
18
|
+
* </body>
|
|
19
|
+
* </html>
|
|
20
|
+
* );
|
|
21
|
+
* }
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* Executed in a Server Component, it converts the input to the client-safe
|
|
25
|
+
* {@link ClientAuthResult} — dropping the raw session token — and only the
|
|
26
|
+
* stripped value is serialised across the server→client boundary. Passing the
|
|
27
|
+
* full `auth()` result straight into a `"use client"` component would embed
|
|
28
|
+
* the raw token in the RSC flight payload, readable by any script running on
|
|
29
|
+
* the page. (sdk-nuxt strips the token before seeding client state for the
|
|
30
|
+
* same reason.)
|
|
31
|
+
*
|
|
32
|
+
* This module is **server-only, on purpose**. The strip only protects the
|
|
33
|
+
* boundary when it runs on the server: if this component could be imported
|
|
34
|
+
* into a `"use client"` wrapper (the common `providers.tsx` pattern), the
|
|
35
|
+
* *wrapper* would become the boundary and its still-unstripped `session`
|
|
36
|
+
* prop — token included — would serialise into the flight payload before
|
|
37
|
+
* this code ever ran. The `server-only` import turns that wrapper into a
|
|
38
|
+
* build error instead of a silent leak. For a client-seeded tree (e.g. state
|
|
39
|
+
* from `getSession()`), use `AuthContextProvider` from
|
|
40
|
+
* `@zitadel/sdk-next/react`, which only accepts the token-less
|
|
41
|
+
* {@link ClientAuthResult}.
|
|
42
|
+
*
|
|
43
|
+
* Client components therefore only ever see `userId` / `email` / `name`. When
|
|
44
|
+
* the raw token is needed server-side (e.g. to call an upstream API), read it
|
|
45
|
+
* from `auth()` in a Server Component or Route Handler — never through this
|
|
46
|
+
* provider.
|
|
47
|
+
*/
|
|
48
|
+
export declare function NextgenProvider({ session, children, }: {
|
|
49
|
+
session: AuthResult | ClientAuthResult | ClientSession | NextgenSession | null;
|
|
50
|
+
children: ReactNode;
|
|
51
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
52
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC;AAErB,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,aAAa,EACb,cAAc,EAEf,MAAM,YAAY,CAAC;AAmBpB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AACH,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,QAAQ,GACT,EAAE;IACD,OAAO,EAAE,UAAU,GAAG,gBAAgB,GAAG,aAAa,GAAG,cAAc,GAAG,IAAI,CAAC;IAC/E,QAAQ,EAAE,SAAS,CAAC;CACrB,2CAaA"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import "server-only";
|
|
3
|
+
import { AuthContextProvider } from "./context.js";
|
|
4
|
+
const signedOut = { isAuthenticated: false, session: null };
|
|
5
|
+
/**
|
|
6
|
+
* Explicit field pick — never a spread. The raw session token (and any future
|
|
7
|
+
* server-only field) must not survive the conversion to the client shape.
|
|
8
|
+
*/
|
|
9
|
+
function toClientSession(session) {
|
|
10
|
+
return {
|
|
11
|
+
userId: session.userId,
|
|
12
|
+
identifier: session.identifier,
|
|
13
|
+
identifierProperty: session.identifierProperty,
|
|
14
|
+
display: session.display,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Seeds client components (the `useAuth()` hook) with the server-known auth
|
|
19
|
+
* state. Render it in a Server Component — typically the root layout — and
|
|
20
|
+
* pass the `auth()` result directly:
|
|
21
|
+
*
|
|
22
|
+
* ```tsx
|
|
23
|
+
* import { auth, NextgenProvider } from "@zitadel/sdk-next/server";
|
|
24
|
+
*
|
|
25
|
+
* export default async function RootLayout({ children }) {
|
|
26
|
+
* const session = await auth();
|
|
27
|
+
* return (
|
|
28
|
+
* <html>
|
|
29
|
+
* <body>
|
|
30
|
+
* <NextgenProvider session={session}>{children}</NextgenProvider>
|
|
31
|
+
* </body>
|
|
32
|
+
* </html>
|
|
33
|
+
* );
|
|
34
|
+
* }
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* Executed in a Server Component, it converts the input to the client-safe
|
|
38
|
+
* {@link ClientAuthResult} — dropping the raw session token — and only the
|
|
39
|
+
* stripped value is serialised across the server→client boundary. Passing the
|
|
40
|
+
* full `auth()` result straight into a `"use client"` component would embed
|
|
41
|
+
* the raw token in the RSC flight payload, readable by any script running on
|
|
42
|
+
* the page. (sdk-nuxt strips the token before seeding client state for the
|
|
43
|
+
* same reason.)
|
|
44
|
+
*
|
|
45
|
+
* This module is **server-only, on purpose**. The strip only protects the
|
|
46
|
+
* boundary when it runs on the server: if this component could be imported
|
|
47
|
+
* into a `"use client"` wrapper (the common `providers.tsx` pattern), the
|
|
48
|
+
* *wrapper* would become the boundary and its still-unstripped `session`
|
|
49
|
+
* prop — token included — would serialise into the flight payload before
|
|
50
|
+
* this code ever ran. The `server-only` import turns that wrapper into a
|
|
51
|
+
* build error instead of a silent leak. For a client-seeded tree (e.g. state
|
|
52
|
+
* from `getSession()`), use `AuthContextProvider` from
|
|
53
|
+
* `@zitadel/sdk-next/react`, which only accepts the token-less
|
|
54
|
+
* {@link ClientAuthResult}.
|
|
55
|
+
*
|
|
56
|
+
* Client components therefore only ever see `userId` / `email` / `name`. When
|
|
57
|
+
* the raw token is needed server-side (e.g. to call an upstream API), read it
|
|
58
|
+
* from `auth()` in a Server Component or Route Handler — never through this
|
|
59
|
+
* provider.
|
|
60
|
+
*/
|
|
61
|
+
export function NextgenProvider({ session, children, }) {
|
|
62
|
+
let value;
|
|
63
|
+
if (!session) {
|
|
64
|
+
value = signedOut;
|
|
65
|
+
}
|
|
66
|
+
else if ("isAuthenticated" in session) {
|
|
67
|
+
value = session.isAuthenticated
|
|
68
|
+
? { isAuthenticated: true, session: toClientSession(session.session) }
|
|
69
|
+
: signedOut;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
value = { isAuthenticated: true, session: toClientSession(session) };
|
|
73
|
+
}
|
|
74
|
+
return _jsx(AuthContextProvider, { value: value, children: children });
|
|
75
|
+
}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-safe React bindings for `@zitadel/sdk-next`.
|
|
3
|
+
*
|
|
4
|
+
* This is the entry point for `"use client"` components. It pulls in no
|
|
5
|
+
* server-only modules (`next/headers`, `auth()`, `NextgenProvider`), so it is
|
|
6
|
+
* safe to import anywhere.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* "use client";
|
|
10
|
+
* import { useAuth } from "@zitadel/sdk-next/react";
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* `NextgenProvider` is deliberately **not** exported here: it accepts the
|
|
14
|
+
* token-bearing `auth()` result, so it must execute in a Server Component —
|
|
15
|
+
* wrapping it in a `"use client"` file would serialise the raw token across
|
|
16
|
+
* the boundary before the provider strips it. Import it from
|
|
17
|
+
* `@zitadel/sdk-next/server` (or the package root) instead; the `server-only`
|
|
18
|
+
* guard turns a client-side import into a build error. To seed the context
|
|
19
|
+
* from client-side state (e.g. a `getSession()` read), render
|
|
20
|
+
* {@link AuthContextProvider}, which only accepts the token-less
|
|
21
|
+
* `ClientAuthResult`.
|
|
22
|
+
*/
|
|
23
|
+
export { AuthContextProvider, useAuthContext } from "./context.js";
|
|
24
|
+
export { useAuth } from "./useAuth.js";
|
|
25
|
+
export type { AuthResult, AuthState, ClientAuthResult, ClientAuthState, ClientSession, NextgenSession, UnauthState, } from "./types.js";
|
|
26
|
+
//# sourceMappingURL=react.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAE,mBAAmB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,YAAY,EACV,UAAU,EACV,SAAS,EACT,gBAAgB,EAChB,eAAe,EACf,aAAa,EACb,cAAc,EACd,WAAW,GACZ,MAAM,YAAY,CAAC"}
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-safe React bindings for `@zitadel/sdk-next`.
|
|
3
|
+
*
|
|
4
|
+
* This is the entry point for `"use client"` components. It pulls in no
|
|
5
|
+
* server-only modules (`next/headers`, `auth()`, `NextgenProvider`), so it is
|
|
6
|
+
* safe to import anywhere.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* "use client";
|
|
10
|
+
* import { useAuth } from "@zitadel/sdk-next/react";
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* `NextgenProvider` is deliberately **not** exported here: it accepts the
|
|
14
|
+
* token-bearing `auth()` result, so it must execute in a Server Component —
|
|
15
|
+
* wrapping it in a `"use client"` file would serialise the raw token across
|
|
16
|
+
* the boundary before the provider strips it. Import it from
|
|
17
|
+
* `@zitadel/sdk-next/server` (or the package root) instead; the `server-only`
|
|
18
|
+
* guard turns a client-side import into a build error. To seed the context
|
|
19
|
+
* from client-side state (e.g. a `getSession()` read), render
|
|
20
|
+
* {@link AuthContextProvider}, which only accepts the token-less
|
|
21
|
+
* `ClientAuthResult`.
|
|
22
|
+
*/
|
|
23
|
+
export { AuthContextProvider, useAuthContext } from "./context.js";
|
|
24
|
+
export { useAuth } from "./useAuth.js";
|
package/dist/server.d.ts
CHANGED
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Server-side surface of `@zitadel/sdk-next`: everything here is guarded by
|
|
3
|
+
* `server-only` and fails the build if pulled into a `"use client"` graph.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { auth, NextgenProvider } from "@zitadel/sdk-next/server";
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export { auth } from "./auth.js";
|
|
10
|
+
export type { AuthOptions } from "./auth.js";
|
|
11
|
+
export { NextgenProvider } from "./provider.js";
|
|
12
|
+
//# sourceMappingURL=server.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,YAAY,EAAE,WAAW,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/server.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Server-side surface of `@zitadel/sdk-next`: everything here is guarded by
|
|
3
|
+
* `server-only` and fails the build if pulled into a `"use client"` graph.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* import { auth, NextgenProvider } from "@zitadel/sdk-next/server";
|
|
7
|
+
* ```
|
|
8
|
+
*/
|
|
9
|
+
export { auth } from "./auth.js";
|
|
10
|
+
export { NextgenProvider } from "./provider.js";
|