diff --git a/apps/docs/content/guides/integrate/identity-providers/jwt_idp.mdx b/apps/docs/content/guides/integrate/identity-providers/jwt_idp.mdx
index f710fa765..8b48a72f9 100644
--- a/apps/docs/content/guides/integrate/identity-providers/jwt_idp.mdx
+++ b/apps/docs/content/guides/integrate/identity-providers/jwt_idp.mdx
@@ -6,12 +6,27 @@ sidebar_label: JWT IdP
## In simple terms
-A **JWT Identity Provider (JWT IdP)** allows ZITADEL to accept a JSON Web Token (JWT) issued and signed by an external system as proof that a user has already been authenticated elsewhere. In this flow, ZITADEL does not perform authentication itself, but relies on the trustworthiness and validity of the provided JWT.
+A **JWT Identity Provider (JWT IdP)** allows ZITADEL to accept a JSON Web Token (JWT) issued and signed by an external
+system as proof that a user has already been authenticated elsewhere. In this flow, ZITADEL does not perform
+authentication itself, but relies on the trustworthiness and validity of the provided JWT.
+
+### Step-by-Step Process
+
+
+
+1. User opens new application
+2. New app initiates an authorization code flow via ZITADEL
+3. If no session is found, ZITADEL redirects user to JWT endpoint
+4. JWT endpoint forwards token to ZITADEL
+5. ZITADEL responds to the JWT endpoint with an authorization code
+6. JWT endpoint redirects user back to the new app
+7. New app completes the authorization code flow
## When should I use JWT IdP?
Use JWT IdP if:
-- You have an existing application or Web Application Firewall (WAF) that authenticates users and is able to generate a JWT for them.
+- You have an existing application, gateway, Web Application Firewall (WAF),... that authenticates users and is able to
+ generate a JWT for them.
- You want users to access new applications via ZITADEL without re-authentication, effectively reusing an existing session.
- You want to enable silent single sign-on (SSO) between legacy and new apps.
- You wish to federate authentication with a system that can't act as a full OpenID Connect provider, but can issue JWTs.
@@ -24,139 +39,156 @@ Do **not** use JWT IdP if:
## Configuring a JWT as an Identity Provider in ZITADEL
-Configuring JWT IdP enables ZITADEL to accept a JWT generated by an external authentication system, such as your WAF or a legacy app. The typical setup involves obtaining the JWT from the user's previously established session.
+Configuring JWT IdP enables ZITADEL to accept a JWT generated by an external authentication system, such as your WAF or
+a legacy app. The typical setup involves obtaining the JWT from the user's previously established session.
-To configure JWT IdP in ZITADEL, you need to provide:
-- The endpoint to obtain the JWT.
-- Information ZITADEL requires to validate the JWT, including issuer, signature keys, and which HTTP header carries the JWT.
+To configure JWT IdP in ZITADEL, you need to provide the following parameters.
+
+### Issuer
+
+The party who issued the token. This is used to validate the `iss` claim in the JWT.
+
+*Example: `https://issuer.test.internal`*
+
+### Header name
+
+The name of the header in which the JWT will be sent in the proxy request to ZITADEL.
+
+### Keys Endpoint
+
+Where ZITADEL fetches the public keys for signature validation of the JWTs. This endpoint should return the keys in
+the format specified in [RFC7517](https://datatracker.ietf.org/doc/html/rfc7517).
+
+*Example: `https://issuer.test.internal/keys`*
+
+### JWT endpoint
+
+Where ZITADEL redirects users to retrieve the JWT.
+
+*Example: `https://apps.test.com/existing/auth-new`*
+
+The old app generates a JWT which it sends to ZITADEL. ZITADEL passes some query parameters in the request to the JWT
+endpoint. These parameters should be included in the proxy request to ZITADEL. They ensure the request is authentic.
+
+Example implementation of the endpoint:
+
+```js
+export default {
+ async fetch(request, env, ctx) {
+ // 1. Obtain the JWT for the current session (implementation will vary)
+ const jwt = await getJwtForCurrentSession(request, env);
+
+ // 2. Prepare the ZITADEL endpoint URL and copy all query params
+ const userUrl = new URL(request.url);
+ // Important: ZITADEL_JWT_IDP_ENDPOINT must match your configured JWT IdP endpoint (differs between Login V1 and V2)
+ // For example, Login V2: https://accounts.test.com/idps/jwt, Login V1: https://accounts.test.com/ui/login/idps/jwt
+ const zitadelUrl = new URL(env.ZITADEL_JWT_IDP_ENDPOINT);
+ userUrl.searchParams.forEach((v, k) => zitadelUrl.searchParams.set(k, v));
+
+ // 3. Proxy request, attaching JWT in the configured HTTP header
+ const zitadelReq = new Request(zitadelUrl.toString(), {
+ method: "GET",
+ headers: {
+ "x-custom-tkn": jwt, // Use header name as set in ZITADEL JWT IdP settings
+ "Accept": request.headers.get("Accept") || "*/*",
+ },
+ redirect: "manual",
+ });
+
+ // 4. Send to ZITADEL and relay its response to the browser
+ const zitadelResp = await fetch(zitadelReq);
+ return new Response(zitadelResp.body, {
+ status: zitadelResp.status,
+ headers: zitadelResp.headers,
+ });
+ }
+}
+```
+*Replace `getJwtForCurrentSession` with your logic for retrieving/creating a JWT from the user's WAF session.*
+
+Note: `env.ZITADEL_JWT_IDP_ENDPOINT` should be set to the custom domain of your ZITADEL instance
+with the `/idps/jwt` path, e.g. `https://accounts.test.com/idps/jwt`.
+
+In case you have a WAF/gateway which handles the authentication and forwards the JWT to services behind the WAF, the
+JWT endpoint would exist behind the WAF and `getJwtForCurrentSession` would get the token from that header.
+
+
+
+`ZITADEL_JWT_IDP_ENDPOINT` should be set to the custom domain of your ZITADEL instance with the respective path of
+the login callback:
+
+Login V1: `https://accounts.test.com/ui/login/login/jwt/authorize`
+
+Login V2: `https://accounts.test.com/idps/jwt`
+
+
+
+After receiving the JWT, ZITADEL will validate that the JWT is valid. It does that by ensuring that:
+- the **signature** matches using keys from the Keys Endpoint.
+- the `issuer` (`iss` claim) matches the configured issuer.
+- The JWT is **not expired** using the `exp` claim.
+
+
+- ZITADEL **does not** re-authenticate the user.
+- ZITADEL **does not** issue this JWT.
+- ZITADEL only verifies authenticity and validity.
+- The JWT is treated as an external ID token, similar to third-party IdPs.
+
---
-## Authentication using JWT IdP
+## Use-cases
-The authentication flow with JWT IdP looks like this:
+### Legacy app in internal network
-
+You have an existing application which exists in an internal network which handles its own user authentication. A new
+application is built using ZITADEL but the userbase of the existing application needs to be able to open the new
+application from within the existing one without creating a new account or having to sign in.
-**Step-by-Step Process**
+- Existing application: `apps.test.com/existing/`
+- Existing internal auth service: `issuer.test.internal`
+- New application: `new.test.com`
+- ZITADEL Login UI: `accounts.test.com`
-1. **User authenticates in the existing app (Server-side & Browser):**
- - The user logs in to the current application, with authentication managed (often) by a WAF or legacy system.
- - The WAF or app creates a session and can issue a JWT representing that session.
+#### What needs to happen
-2. **User accesses new app (Browser):**
- - The user visits the new application, possibly via a link from the existing app.
- - If no session is found, the new app initiates an OIDC login via ZITADEL.
- - The user either selects the JWT IdP in the ZITADEL UI,
- - **or** the application uses a [custom scope](/apis/openidoauth/scopes#reserved-scopes) to pre-select the JWT IdP in the OIDC Authorization Request.
+- Add a link in the existing application which directs the user to `new.test.com`.
+- Create an application on the domain of the existing application which serves the JWT and keys endpoints.
+ - JWT endpoint: `apps.test.com/existing/auth-new`
+ - Keys endpoint: `issuer.test.internal/keys`
+- Configure the JWT endpoint to send the JWT in the specified header
+ - Header name: `x-custom-tkn`
+- Configure the JWT IdP in ZITADEL with the parameters above
+ - JWT endpoint: `apps.test.com/existing/auth-new`
+ - Keys endpoint: `issuer.test.internal/keys`
+ - Header name: `x-custom-tkn`
+ - Issuer: `issuer.test.internal`
-3. **ZITADEL redirects to the JWT Endpoint (Browser):**
- - ZITADEL redirects the user's browser to the configured JWT Endpoint (usually behind the WAF).
+#### Result flow
- - **Purpose:**
- Redirecting the browser ensures that any existing session cookies for the previous app/WAF are available, letting the WAF determine the authenticated user and issue the appropriate JWT.
+1. User opens existing app
+2. User authenticates against existing IdP
+3. User clicks on link to new app
+4. User is redirected to ZITADEL
+5. ZITADEL redirects to JWT endpoint
+6. JWT endpoint creates JWT from existing session (using cookie)
+7. JWT endpoint forwards request with JWT to ZITADEL
+8. ZITADEL responds with authorization code
+9. New app can exchange authorization code with access-token
-4. **WAF/session gateway attaches JWT and proxies request to ZITADEL (Server-side):**
- - The JWT Endpoint (behind the WAF) uses the session (from browser cookies) to ascertain the authenticated user.
- - The WAF/server injects the JWT into a specific HTTP header (never as a URL parameter) and forwards (proxies) the user’s request with **all original query parameters intact** to ZITADEL’s JWT receiving endpoint.
- - This preserves critical OIDC flow state.
+## Clarifications and best practices
- - **How to proxy the request (example):**
- The example below shows a simplified "serverless/WAF proxy" approach to:
- - obtain/generate the JWT,
- - forward all the user's query params,
- - and place the JWT in the appropriate HTTP header when proxying to ZITADEL.
+#### Why must the JWT Endpoint be on the same domain as the existing app?
- ```js
- export default {
- async fetch(request, env, ctx) {
- // 1. Obtain the JWT for the current session (implementation will vary)
- const jwt = await getJwtForCurrentSession(request, env);
+This ensures browser sessions (cookies) are correctly sent and recognized by the WAF or app, so the right JWT can be
+generated.
- // 2. Prepare the ZITADEL endpoint URL and copy all query params
- const userUrl = new URL(request.url);
- // Important: The ZITADEL_JWT_IDP_ENDPOINT should be your ZITADEL custom domain plus "/idps/jwt"
- // For example: https://accounts.test.com/idps/jwt
- const zitadelUrl = new URL(env.ZITADEL_JWT_IDP_ENDPOINT);
- userUrl.searchParams.forEach((v, k) => zitadelUrl.searchParams.set(k, v));
+#### Why are cookies sent automatically?
- // 3. Proxy request, attaching JWT in the configured HTTP header
- const zitadelReq = new Request(zitadelUrl.toString(), {
- method: "GET",
- headers: {
- "x-custom-tkn": jwt, // Use header name as set in ZITADEL JWT IdP settings
- "Accept": request.headers.get("Accept") || "*/*",
- },
- redirect: "manual",
- });
+Browsers automatically include relevant cookies when redirecting within the same domain, enabling server-side
+authentication seamlessly.
- // 4. Send to ZITADEL and relay its response to the browser
- const zitadelResp = await fetch(zitadelReq);
- return new Response(zitadelResp.body, {
- status: zitadelResp.status,
- headers: zitadelResp.headers,
- });
- }
- }
- ```
- *(Replace `getJwtForCurrentSession` with your logic for retrieving/creating a JWT from the user's WAF session.
- Note: `env.ZITADEL_JWT_IDP_ENDPOINT` should be set to the custom domain of your ZITADEL instance
- with the `/idps/jwt` path, e.g. `https://accounts.test.com/idps/jwt`.)*
+#### Why send the JWT in a header, not as a parameter?
-5. **ZITADEL receives and validates the JWT (Server-side):**
- - ZITADEL extracts the JWT from the configured HTTP header.
- - ZITADEL verifies that:
- - The **signature** matches using keys from the Keys Endpoint.
- - The `issuer` (`iss` claim) matches the configured issuer.
- - The JWT is **not expired**.
- - **Note:**
- - ZITADEL **does not** re-authenticate the user.
- - ZITADEL **does not** issue this JWT.
- - ZITADEL only verifies authenticity and validity.
- - The JWT is treated as an external ID token, similar to third-party IdPs.
-
-6. **Login completes (Browser):**
- - ZITADEL finishes the OIDC flow.
- - The browser is redirected to the application’s callback endpoint.
- - The app exchanges the code for tokens, and the user is now logged in—without extra authentication steps.
-
----
-
-## Terms and Example Values
-
-Here’s a reference integration scenario:
-
-- **Existing Application:** `apps.test.com/existing/`
-- **New Application:** `new.test.com`
-- **ZITADEL Login UI:** `accounts.test.com`
-
-Sample **JWT IdP Configuration**:
-
-- **JWT Endpoint:**
- Where ZITADEL redirects users to retrieve the JWT:
- `https://apps.test.com/existing/auth-new`
-
-- **Issuer:**
- The `iss` claim ZITADEL expects in the JWT:
- `https://issuer.test.internal`
-
-- **Keys Endpoint:**
- Where ZITADEL fetches public keys for signature verification:
- `https://issuer.test.internal/keys`
-
-- **Header Name:**
- The HTTP header delivering the JWT (defaults to `Authorization` if not set):
- `x-custom-tkn`
-
-**Clarifications and Best Practices:**
-- **Why must the JWT Endpoint be on the same domain as the existing app?**
- This ensures browser sessions (cookies) are correctly sent and recognized by the WAF or app, so the right JWT can be generated.
-- **Why are cookies sent automatically?**
- Browsers automatically include relevant cookies when redirecting within the same domain, enabling server-side authentication seamlessly.
-- **Why send the JWT in a header, not as a parameter?**
- HTTP headers are more secure for transmitting sensitive tokens, prevent them from being exposed in URLs or logs, and avoid user tampering.
-
-When ZITADEL performs signature validation, it uses the Keys Endpoint to:
-- Verify the JWT’s signature
-- Check the issuer matches the JWT IdP settings
-- Ensure the JWT is not expired
+HTTP headers are more secure for transmitting sensitive tokens, prevent them from being exposed in URLs or logs, and
+avoid user tampering.
diff --git a/apps/docs/public/img/guides/jwt_idp.png b/apps/docs/public/img/guides/jwt_idp.png
index 218996aef..8730d56cd 100644
Binary files a/apps/docs/public/img/guides/jwt_idp.png and b/apps/docs/public/img/guides/jwt_idp.png differ