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 + +![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. + + + +`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 -![](../../../../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. 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