mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
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:
@@ -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.
|
||||
|
||||
<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
|
||||
|
||||

|
||||
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 |
Reference in New Issue
Block a user