@zitadel/sdk-next 0.1.0-alpha.8 → 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/dist/client.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
+
// Registers <zitadel-login>, <zitadel-logout> and <zitadel-session> in the browser.
|
|
24
|
+
// The edge build of the components package has no elements, so this does nothing there.
|
|
25
|
+
import "@zitadel/components";
|
|
26
|
+
// Re-exported so scaffolded apps can wire the business copy overlay without a
|
|
27
|
+
// direct @zitadel/components dependency (strict package managers reject those).
|
|
28
|
+
export { businessLocales } from "@zitadel/components";
|
|
29
|
+
export { configureZitadel, getApi } from "@zitadel/api/config";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React components for the Zitadel widgets in Next.js apps.
|
|
3
|
+
*
|
|
4
|
+
* Use these instead of the raw `<zitadel-login>`, `<zitadel-logout>` and
|
|
5
|
+
* `<zitadel-session>` elements: the wrappers set `project` and `locales` as DOM
|
|
6
|
+
* properties on React 18 and 19, including after server-rendered hydration,
|
|
7
|
+
* where the raw elements lose them (React 19) or write `[object Object]`
|
|
8
|
+
* attributes (React 18).
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* import { ZitadelLogin } from "@zitadel/sdk-next/components";
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export { businessLocales, configureZitadel, ZitadelLogin, ZitadelLogout, ZitadelSession, } from "@zitadel/sdk-react";
|
|
15
|
+
export type { ZitadelLoginReactProps, ZitadelLogoutReactProps, ZitadelSessionReactProps, } from "@zitadel/sdk-react";
|
|
16
|
+
//# sourceMappingURL=components.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"components.d.ts","sourceRoot":"","sources":["../src/components.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,YAAY,EACZ,aAAa,EACb,cAAc,GACf,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,sBAAsB,EACtB,uBAAuB,EACvB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
/**
|
|
3
|
+
* React components for the Zitadel widgets in Next.js apps.
|
|
4
|
+
*
|
|
5
|
+
* Use these instead of the raw `<zitadel-login>`, `<zitadel-logout>` and
|
|
6
|
+
* `<zitadel-session>` elements: the wrappers set `project` and `locales` as DOM
|
|
7
|
+
* properties on React 18 and 19, including after server-rendered hydration,
|
|
8
|
+
* where the raw elements lose them (React 19) or write `[object Object]`
|
|
9
|
+
* attributes (React 18).
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* import { ZitadelLogin } from "@zitadel/sdk-next/components";
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export { businessLocales, configureZitadel, ZitadelLogin, ZitadelLogout, ZitadelSession, } from "@zitadel/sdk-react";
|
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"}
|
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.
|
|
@@ -20,6 +20,9 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
20
20
|
* });
|
|
21
21
|
* }
|
|
22
22
|
*
|
|
23
|
+
* A session counts only in the app's own project: `projectId`, else
|
|
24
|
+
* `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
|
|
25
|
+
*
|
|
23
26
|
* export const config = {
|
|
24
27
|
* matcher: ["/__nextgen/:path*", "/profile/:path*"],
|
|
25
28
|
* };
|
|
@@ -29,7 +32,7 @@ import { NextRequest, NextResponse } from 'next/server';
|
|
|
29
32
|
* @param options - Middleware configuration options.
|
|
30
33
|
* @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
|
|
31
34
|
*/
|
|
32
|
-
declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
|
|
35
|
+
export declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddlewareOptions): Promise<NextResponse | Response>;
|
|
33
36
|
/**
|
|
34
37
|
* Options for {@link createProxy} that are separate from the shared
|
|
35
38
|
* {@link ZitadelConfig}. These configure route protection, login
|
|
@@ -38,11 +41,11 @@ declare function nextgenMiddleware(req: NextRequest, options?: NextgenMiddleware
|
|
|
38
41
|
* `proxyPath` and `url` are omitted because they come from the
|
|
39
42
|
* {@link ZitadelConfig} passed as the first argument.
|
|
40
43
|
*/
|
|
41
|
-
type ProxyOptions = Omit<NextgenMiddlewareOptions,
|
|
44
|
+
export type ProxyOptions = Omit<NextgenMiddlewareOptions, "proxyPath" | "url">;
|
|
42
45
|
/**
|
|
43
46
|
* A pre-configured middleware handler returned by {@link createProxy}.
|
|
44
47
|
*/
|
|
45
|
-
type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
48
|
+
export type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
46
49
|
/**
|
|
47
50
|
* Creates a pre-configured middleware handler from the SDK config
|
|
48
51
|
* returned by `configureZitadel()`. This is the derived-service
|
|
@@ -54,7 +57,7 @@ type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
|
54
57
|
* import { createProxy } from "@zitadel/sdk-next/middleware";
|
|
55
58
|
*
|
|
56
59
|
* const zitadel = configureZitadel({
|
|
57
|
-
* projectId: "
|
|
60
|
+
* projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
58
61
|
* url: process.env.ZITADEL_URL,
|
|
59
62
|
* });
|
|
60
63
|
*
|
|
@@ -71,10 +74,15 @@ type ProxyHandler = (req: NextRequest) => Promise<NextResponse | Response>;
|
|
|
71
74
|
* export const middleware = proxy;
|
|
72
75
|
* ```
|
|
73
76
|
*
|
|
77
|
+
* Sessions are held to the handle's project unless `options.projectId` names
|
|
78
|
+
* another. `auth()` runs in the server runtime, where the middleware's handle
|
|
79
|
+
* may not be configured: it reads `ZITADEL_PROJECT_ID` /
|
|
80
|
+
* `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else a handle configured there. When the
|
|
81
|
+
* project lives only in this handle, pass it as `auth({ projectId })` too.
|
|
82
|
+
*
|
|
74
83
|
* @param config - The SDK handle from `configureZitadel()`.
|
|
75
84
|
* @param options - Route protection and JWT options.
|
|
76
85
|
* @returns A middleware handler function.
|
|
77
86
|
*/
|
|
78
|
-
declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
|
|
79
|
-
|
|
80
|
-
export { type ProxyHandler, type ProxyOptions, createProxy, nextgenMiddleware };
|
|
87
|
+
export declare function createProxy(config: ZitadelProject, options?: ProxyOptions): ProxyHandler;
|
|
88
|
+
//# 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,EAAyB,MAAM,8BAA8B,CAAC;AAEpG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAa/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAmC3C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,iBAAiB,CACrC,GAAG,EAAE,WAAW,EAChB,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,YAAY,GAAG,QAAQ,CAAC,CAyDlC;AAuMD;;;;;;;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;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AACH,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,GAAE,YAAiB,GAAG,YAAY,CAQ5F"}
|
package/dist/middleware.js
CHANGED
|
@@ -1,9 +1,304 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import { joinURL } from "ufo";
|
|
2
|
+
import { HOP_BY_HOP, INTERNAL_HEADERS, filterResponseHeaders, loginRedirectUrl, matchesRoutes, proxySessionCookies, SESSION_COOKIE_CACHE_CONTROL, requiresProjectSecret, resolveSession, } from "@zitadel/sdk-core/middleware";
|
|
3
|
+
import { NextResponse } from "next/server";
|
|
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
|
+
// This is the only writer of x-nextgen-auth-token: it injects the verified
|
|
23
|
+
// value for server components, and a client-sent one never reaches here.
|
|
24
|
+
for (const [name, value] of Object.entries(extra)) {
|
|
25
|
+
headers.set(name, value);
|
|
26
|
+
headers.set(`x-middleware-request-${name}`, value);
|
|
27
|
+
injectedNames.push(name);
|
|
28
|
+
}
|
|
29
|
+
headers.set("x-middleware-override-headers", injectedNames.join(","));
|
|
30
|
+
return headers;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Next.js Edge middleware that handles proxying, JWT verification, and route
|
|
34
|
+
* protection in a single pass.
|
|
35
|
+
*
|
|
36
|
+
* Place this in your `middleware.ts` file:
|
|
37
|
+
*
|
|
38
|
+
* ```ts
|
|
39
|
+
* import { nextgenMiddleware } from "@zitadel/sdk-next/middleware";
|
|
40
|
+
* import type { NextRequest } from "next/server";
|
|
41
|
+
*
|
|
42
|
+
* export function middleware(req: NextRequest) {
|
|
43
|
+
* return nextgenMiddleware(req, {
|
|
44
|
+
* url: process.env.ZITADEL_URL,
|
|
45
|
+
* protectedRoutes: ["/profile"],
|
|
46
|
+
* loginPath: "/login",
|
|
47
|
+
* });
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* A session counts only in the app's own project: `projectId`, else
|
|
51
|
+
* `ZITADEL_PROJECT_ID`, else `NEXT_PUBLIC_ZITADEL_PROJECT_ID`.
|
|
52
|
+
*
|
|
53
|
+
* export const config = {
|
|
54
|
+
* matcher: ["/__nextgen/:path*", "/profile/:path*"],
|
|
55
|
+
* };
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @param req - The incoming Next.js edge request.
|
|
59
|
+
* @param options - Middleware configuration options.
|
|
60
|
+
* @returns A `NextResponse` or `Response` to continue, redirect, or proxy.
|
|
61
|
+
*/
|
|
62
|
+
export async function nextgenMiddleware(req, options = {}) {
|
|
63
|
+
const { url = process.env.ZITADEL_URL ?? "http://localhost:8080", projectId = process.env.ZITADEL_PROJECT_ID || process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID, proxyPath = "/__nextgen", protectedRoutes = [], ignoredRoutes = [], loginPath = "/login", allowedAlgorithms = ["RS256", "ES256"], clockSkewMs = 5000, audience, allowedTokenTypes = ["JWT", "at+JWT"], jwksTimeoutMs, proxyTimeoutMs = 5000, opaqueTokenTimeoutMs = 5000, } = options;
|
|
64
|
+
// Guard against open-redirect: loginPath must be a relative path. An absolute
|
|
65
|
+
// URL (e.g. "https://evil.com/phish") or a protocol-relative URL
|
|
66
|
+
// (e.g. "//evil.com") would redirect the browser to an external host.
|
|
67
|
+
if (!loginPath.startsWith("/") || loginPath.startsWith("//")) {
|
|
68
|
+
throw new Error(`[nextgen] loginPath must be a relative path starting with a single "/". ` +
|
|
69
|
+
`Received: "${loginPath}". Using an absolute or protocol-relative URL ` +
|
|
70
|
+
`would allow open-redirect attacks.`);
|
|
71
|
+
}
|
|
72
|
+
// Clients never send x-nextgen-auth-token: it is an internal channel. Refuse any
|
|
73
|
+
// request that carries it, on every route, before anything else runs.
|
|
74
|
+
if (req.headers.has("x-nextgen-auth-token")) {
|
|
75
|
+
return new Response("Bad Request", { status: 400 });
|
|
76
|
+
}
|
|
77
|
+
const { pathname } = new URL(req.url);
|
|
78
|
+
if (matchesRoutes(pathname, ignoredRoutes)) {
|
|
79
|
+
return NextResponse.next();
|
|
80
|
+
}
|
|
81
|
+
if (pathname === proxyPath || pathname.startsWith(`${proxyPath}/`)) {
|
|
82
|
+
return proxyRequest(req, url, proxyPath, projectId, proxyTimeoutMs);
|
|
83
|
+
}
|
|
84
|
+
return handleAuth(req, {
|
|
85
|
+
url,
|
|
86
|
+
projectId,
|
|
87
|
+
protectedRoutes,
|
|
88
|
+
loginPath,
|
|
89
|
+
allowedAlgorithms,
|
|
90
|
+
clockSkewMs,
|
|
91
|
+
audience,
|
|
92
|
+
allowedTokenTypes,
|
|
93
|
+
jwksTimeoutMs,
|
|
94
|
+
opaqueTokenTimeoutMs,
|
|
95
|
+
pathname,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Forwards a `/__nextgen/*` request to the upstream auth backend and streams
|
|
100
|
+
* the response back, stripping hop-by-hop headers in both directions. The
|
|
101
|
+
* session cookie travels under this project's name in the browser and the
|
|
102
|
+
* server's name upstream ({@link proxySessionCookies}).
|
|
103
|
+
*
|
|
104
|
+
* All non-hop-by-hop headers from the incoming request are forwarded, including
|
|
105
|
+
* any `X-Forwarded-For` chain already set by an upstream CDN or load balancer.
|
|
106
|
+
* When `X-Forwarded-For` is absent (direct connection with no upstream proxy),
|
|
107
|
+
* it is initialised from the runtime-provided client IP so the auth server
|
|
108
|
+
* always receives origin information. `X-Forwarded-Host` and `X-Forwarded-Proto`
|
|
109
|
+
* are set only when absent, preserving values injected by an upstream CDN.
|
|
110
|
+
*
|
|
111
|
+
* @param req - The incoming edge request.
|
|
112
|
+
* @param authUrl - Base URL of the auth backend.
|
|
113
|
+
* @param proxyPath - The path prefix being proxied (e.g. `"/__nextgen"`).
|
|
114
|
+
* @param projectId - The project this app signs its users in to.
|
|
115
|
+
* @returns The proxied upstream `Response`.
|
|
116
|
+
*/
|
|
117
|
+
async function proxyRequest(req, authUrl, proxyPath, projectId, proxyTimeoutMs) {
|
|
118
|
+
const url = new URL(req.url);
|
|
119
|
+
const suffix = url.pathname.slice(proxyPath.length);
|
|
120
|
+
const target = `${joinURL(authUrl, suffix)}${url.search}`;
|
|
121
|
+
const upstreamHeaders = new Headers();
|
|
122
|
+
for (const [key, value] of req.headers.entries()) {
|
|
123
|
+
const lower = key.toLowerCase();
|
|
124
|
+
if (!HOP_BY_HOP.has(lower) && !INTERNAL_HEADERS.has(lower)) {
|
|
125
|
+
upstreamHeaders.set(key, value);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// Always append the direct client IP to the X-Forwarded-For chain so the
|
|
129
|
+
// upstream auth server sees the full proxy path. We never skip this even when
|
|
130
|
+
// a chain is already present — a CDN or load balancer may have set XFF before
|
|
131
|
+
// the request reached this edge node, and our hop must still be recorded.
|
|
132
|
+
const directIp = req.ip;
|
|
133
|
+
if (directIp) {
|
|
134
|
+
const existingXff = upstreamHeaders.get("x-forwarded-for");
|
|
135
|
+
upstreamHeaders.set("x-forwarded-for", existingXff ? `${existingXff}, ${directIp}` : directIp);
|
|
136
|
+
}
|
|
137
|
+
if (!upstreamHeaders.has("x-forwarded-host")) {
|
|
138
|
+
upstreamHeaders.set("x-forwarded-host", url.host);
|
|
139
|
+
}
|
|
140
|
+
if (!upstreamHeaders.has("x-forwarded-proto")) {
|
|
141
|
+
upstreamHeaders.set("x-forwarded-proto", url.protocol.replace(":", ""));
|
|
142
|
+
}
|
|
143
|
+
const sessionCookies = proxySessionCookies({
|
|
144
|
+
method: req.method,
|
|
145
|
+
path: suffix,
|
|
146
|
+
search: url.search,
|
|
147
|
+
cookie: req.headers.get("cookie"),
|
|
148
|
+
requestUrl: url,
|
|
149
|
+
forwardedProto: req.headers.get("x-forwarded-proto"),
|
|
150
|
+
forwardedHost: req.headers.get("x-forwarded-host"),
|
|
151
|
+
}, projectId, { url: authUrl, timeoutMs: proxyTimeoutMs });
|
|
152
|
+
if (sessionCookies.upstreamCookie === null) {
|
|
153
|
+
upstreamHeaders.delete("cookie");
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
upstreamHeaders.set("cookie", sessionCookies.upstreamCookie);
|
|
157
|
+
}
|
|
158
|
+
const hasBody = !["GET", "HEAD"].includes(req.method);
|
|
159
|
+
// ADR 036: the current browser flow needs the confidential project secret
|
|
160
|
+
// only for the handoff exchange. Stamping it onto every proxied operation
|
|
161
|
+
// turns this public request boundary into an operator-capable open relay.
|
|
162
|
+
// Preserve an explicit caller credential so the exchange can use the future
|
|
163
|
+
// publishable-key plane without being overwritten.
|
|
164
|
+
if (requiresProjectSecret(req.method, suffix) && !upstreamHeaders.has("authorization")) {
|
|
165
|
+
const secret = process.env.ZITADEL_PROJECT_SECRET;
|
|
166
|
+
if (secret) {
|
|
167
|
+
upstreamHeaders.set("authorization", `Bearer ${secret}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// Read body eagerly so undici receives a concrete buffer rather than a
|
|
171
|
+
// WinterCG ReadableStream, which is incompatible with Node.js fetch body
|
|
172
|
+
// extraction.
|
|
173
|
+
const bodyBuffer = hasBody ? await req.arrayBuffer() : undefined;
|
|
174
|
+
const upstream = await fetch(target, {
|
|
175
|
+
method: req.method,
|
|
176
|
+
headers: upstreamHeaders,
|
|
177
|
+
body: bodyBuffer,
|
|
178
|
+
redirect: "manual",
|
|
179
|
+
signal: AbortSignal.timeout(proxyTimeoutMs),
|
|
180
|
+
});
|
|
181
|
+
// The app's own request URL: the origin a proxied redirect may name, and
|
|
182
|
+
// the base a relative one resolves against.
|
|
183
|
+
const responseHeaders = filterResponseHeaders(upstream.headers, req.nextUrl.toString());
|
|
184
|
+
const answer = await sessionCookies.toBrowser(upstream);
|
|
185
|
+
for (const cookie of answer.setCookies) {
|
|
186
|
+
responseHeaders.append("set-cookie", cookie);
|
|
187
|
+
}
|
|
188
|
+
if (answer.setCookies.length > 0) {
|
|
189
|
+
responseHeaders.set("cache-control", SESSION_COOKIE_CACHE_CONTROL);
|
|
190
|
+
}
|
|
191
|
+
return new Response(answer.body, {
|
|
192
|
+
status: upstream.status,
|
|
193
|
+
headers: responseHeaders,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Judges the session and tunnels the token of a signed-in user to server
|
|
198
|
+
* components via a request header, where `auth()` verifies it again.
|
|
199
|
+
* Redirects to the login page when nobody is signed in on a protected route.
|
|
200
|
+
* Either way the response carries the session cookies the judgement asks for.
|
|
201
|
+
* When the backend cannot say whether the session is live, a protected route
|
|
202
|
+
* answers `503` instead.
|
|
203
|
+
*
|
|
204
|
+
* @param req - The incoming edge request.
|
|
205
|
+
* @param opts - Auth handler options.
|
|
206
|
+
* @returns A `NextResponse` to continue, redirect, or answer `503`.
|
|
207
|
+
*/
|
|
208
|
+
async function handleAuth(req, opts) {
|
|
209
|
+
const { protectedRoutes, loginPath, pathname, ...verification } = opts;
|
|
210
|
+
const authHeader = req.headers.get("authorization");
|
|
211
|
+
const bearerToken = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null;
|
|
212
|
+
const { auth, undecided, cookies } = await resolveSession({
|
|
213
|
+
bearer: bearerToken,
|
|
214
|
+
cookie: (name) => req.cookies.get(name)?.value,
|
|
215
|
+
requestUrl: req.url,
|
|
216
|
+
forwardedProto: req.headers.get("x-forwarded-proto"),
|
|
217
|
+
forwardedHost: req.headers.get("x-forwarded-host"),
|
|
218
|
+
}, verification);
|
|
219
|
+
let response;
|
|
220
|
+
if (auth.isAuthenticated) {
|
|
221
|
+
const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": auth.session.token });
|
|
222
|
+
response = NextResponse.next({ request: { headers: tunnelled } });
|
|
223
|
+
}
|
|
224
|
+
else if (undecided && matchesRoutes(pathname, protectedRoutes)) {
|
|
225
|
+
// The backend could not say whether the session is live. The user may well
|
|
226
|
+
// be signed in, so a protected route answers 503 rather than send them to
|
|
227
|
+
// the login page.
|
|
228
|
+
response = new NextResponse("Service Unavailable", { status: 503 });
|
|
229
|
+
}
|
|
230
|
+
else if (matchesRoutes(pathname, protectedRoutes)) {
|
|
231
|
+
response = NextResponse.redirect(loginRedirectUrl(loginPath, req.url), { status: 302 });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
const tunnelled = tunnelHeaders(req, { "x-nextgen-auth-token": "" });
|
|
235
|
+
response = NextResponse.next({ request: { headers: tunnelled } });
|
|
236
|
+
}
|
|
237
|
+
// A dead session's cookie is cleared so the browser stops replaying it, and a
|
|
238
|
+
// session of this project still under the shared name is copied to the
|
|
239
|
+
// project's own cookie. A live session that signs no one in here (an
|
|
240
|
+
// anonymous one, or one of another project) keeps its cookie, and so does one
|
|
241
|
+
// the backend gave no verdict on. The cookies go through the response's
|
|
242
|
+
// cookie jar, so a middleware composed around this one that sets a cookie of
|
|
243
|
+
// its own through it keeps them.
|
|
244
|
+
for (const cookie of cookies) {
|
|
245
|
+
response.cookies.set(cookie.name, cookie.value, {
|
|
246
|
+
path: "/",
|
|
247
|
+
httpOnly: true,
|
|
248
|
+
sameSite: "lax",
|
|
249
|
+
secure: cookie.secure,
|
|
250
|
+
...(cookie.maxAge === undefined ? {} : { maxAge: cookie.maxAge }),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
if (cookies.length > 0) {
|
|
254
|
+
response.headers.set("cache-control", SESSION_COOKIE_CACHE_CONTROL);
|
|
255
|
+
}
|
|
256
|
+
return response;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* Creates a pre-configured middleware handler from the SDK config
|
|
260
|
+
* returned by `configureZitadel()`. This is the derived-service
|
|
261
|
+
* derived service pattern:
|
|
262
|
+
*
|
|
263
|
+
* ```ts
|
|
264
|
+
* // src/zitadel.ts
|
|
265
|
+
* import { configureZitadel } from "@zitadel/api/config";
|
|
266
|
+
* import { createProxy } from "@zitadel/sdk-next/middleware";
|
|
267
|
+
*
|
|
268
|
+
* const zitadel = configureZitadel({
|
|
269
|
+
* projectId: process.env.NEXT_PUBLIC_ZITADEL_PROJECT_ID ?? "",
|
|
270
|
+
* url: process.env.ZITADEL_URL,
|
|
271
|
+
* });
|
|
272
|
+
*
|
|
273
|
+
* export const proxy = createProxy(zitadel, {
|
|
274
|
+
* protectedRoutes: ["/admin*"],
|
|
275
|
+
* loginPath: "/login",
|
|
276
|
+
* });
|
|
277
|
+
* ```
|
|
278
|
+
*
|
|
279
|
+
* Then in middleware.ts:
|
|
280
|
+
*
|
|
281
|
+
* ```ts
|
|
282
|
+
* import { proxy } from "./zitadel";
|
|
283
|
+
* export const middleware = proxy;
|
|
284
|
+
* ```
|
|
285
|
+
*
|
|
286
|
+
* Sessions are held to the handle's project unless `options.projectId` names
|
|
287
|
+
* another. `auth()` runs in the server runtime, where the middleware's handle
|
|
288
|
+
* may not be configured: it reads `ZITADEL_PROJECT_ID` /
|
|
289
|
+
* `NEXT_PUBLIC_ZITADEL_PROJECT_ID`, else a handle configured there. When the
|
|
290
|
+
* project lives only in this handle, pass it as `auth({ projectId })` too.
|
|
291
|
+
*
|
|
292
|
+
* @param config - The SDK handle from `configureZitadel()`.
|
|
293
|
+
* @param options - Route protection and JWT options.
|
|
294
|
+
* @returns A middleware handler function.
|
|
295
|
+
*/
|
|
296
|
+
export function createProxy(config, options = {}) {
|
|
297
|
+
const mergedOptions = {
|
|
298
|
+
...options,
|
|
299
|
+
projectId: options.projectId ?? (config.projectId || undefined),
|
|
300
|
+
proxyPath: config.proxyPath,
|
|
301
|
+
url: config.url,
|
|
302
|
+
};
|
|
303
|
+
return (req) => nextgenMiddleware(req, mergedOptions);
|
|
304
|
+
}
|