docs: added documentation for jwt idp regarding callback uris (#11925)

# Which Problems Are Solved

The documentation did not reflect the difference between login v1 and v2
for configuring a JWT-idp

# How the Problems Are Solved

Added a warning in the docs which highlight that difference.

# Additional Changes

- Restructured the docs to reflect a more generic approach.
- Fixed indentation inside the `<Callout type="warning">` block so the
content renders as formatted text instead of a code block.
- Fixed inconsistent acronym casing in the use-case section: "jwt" →
"JWT" and "idp" → "IdP" to match the rest of the page and UI terms.

# Additional Context

- Closes: #11589

<!-- START COPILOT CODING AGENT TIPS -->
---

💡 You can make Copilot smarter by setting up custom instructions,
customizing its development environment and configuring Model Context
Protocol (MCP) servers. Learn more [Copilot coding agent
tips](https://gh.io/copilot-coding-agent-tips) in the docs.

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
This commit is contained in:
Wim Van Laer
2026-03-30 10:23:42 +02:00
committed by GitHub
co-authored by Copilot copilot-swe-agent[bot] <198982749+[email protected]>
parent 022bf74060
commit b145f1c85a
2 changed files with 152 additions and 120 deletions
@@ -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
![JWT-idp flow steps](../../../../public/img/guides/jwt_idp.png)
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.
<Callout type="warning" title="`ZITADEL_JWT_IDP_ENDPOINT` is different for login V1 and V2">
`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`
</Callout>
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.
<Callout type="info">
- 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.
</Callout>
---
## Authentication using JWT IdP
## Use-cases
The authentication flow with JWT IdP looks like this:
### Legacy app in internal network
![](../../../../public/img/guides/jwt_idp.png)
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.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 85 KiB