Files
zitadel/apps/docs/content/guides/integrate/identity-providers/jwt_idp.mdx
T
Livio SpringandGitHub d184e976fc Merge commit from fork
* feat(jwt idp): manage and validate audience

* translations

* fix tests

* address comments

* update migration version

* fix merge
2026-06-15 15:27:47 +02:00

205 lines
8.1 KiB
Plaintext

---
title: JWT IdP
description: "Validate JSON Web Tokens from external systems as proof of user authentication in ZITADEL for single sign-on."
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.
### 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, 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.
Do **not** use JWT IdP if:
- You need ZITADEL to interactively authenticate users (e.g., password-based logins).
- There is no secure way to transfer or validate the JWT.
---
## 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.
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`*
### Audience (optional)
The expected audience (`aud` claim) in the JWT. If set, ZITADEL will validate that the JWT contains this audience.
It's recommended to set this to ensure the JWT is intended to be used by ZITADEL.
If the JWT contains multiple audiences, the configured audience must be one of them.
If no audience is configured, ZITADEL will not validate the `aud` claim.
*Example: `my-client`*
### 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/idps/jwt`
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>
---
## Use-cases
### 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.
- Existing application: `apps.test.com/existing/` (also used as audience)
- Existing internal auth service: `issuer.test.internal`
- New application: `new.test.com`
- ZITADEL Login UI: `accounts.test.com`
#### What needs to happen
- 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`
- Audience: `apps.test.com/existing/`
#### Result flow
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
## 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.