feat(login): Sign in with Zitadel (#12530)

Closes #11824

# Which Problems Are Solved

The backend supports the dedicated ZITADEL identity provider template
(`AddZitadelProvider`, `IDENTITY_PROVIDER_TYPE_ZITADEL`), but the new
login (login v2)
cannot yet handle providers of this type:

A configured and activated ZITADEL IdP is not rendered as a sign-in
option, because the provider type is missing from the login's component
mapping.

# How the Problems Are Solved

- Regenerated `@zitadel/proto` (`settings/v2/login_settings_pb` in
`types`, `es` and `cjs`)
so `IdentityProviderType.ZITADEL = 13` is available (`idp/v2` was
already up to date).
- `apps/login/src/lib/idp.ts`:
- `idpTypeToSlug` maps `ZITADEL` to the `zitadel` slug, used by the
generic
    `/idp/[provider]/process|failure` routes (no route changes needed).
  - `idpTypeToIdentityProviderType` maps `IDPType.IDP_TYPE_ZITADEL` to
    `IdentityProviderType.ZITADEL`.
- Added `SignInWithZitadel` button
(`apps/login/src/components/idps/sign-in-with-zitadel.tsx`)
with the Zitadel logo as inline SVG: the brand gradient is
theme-independent, accents use
`currentColor` to adapt to light/dark, consistent with the other
provider buttons.
- Registered the Zitadel type in the provider-to-component mapping in
`sign-in-with-idp.tsx`.

No changes to the IdP intent handling are required: user
auto-creation/auto-update consume
the pre-mapped `user_action.create_user` / `update_user` from
`RetrieveIdentityProviderIntent`,
which the backend fills for the Zitadel provider like for any generic
OIDC provider.

# Additional Changes

- Added the `signInWithZitadel` i18n key ("Sign in with Zitadel") to all
15 locale files.
- Extended `idp.test.ts` with the new slug and type mappings and added
the Zitadel type to
  the exhaustive enum-coverage tests (45 tests passing).

# Additional Context

- Part of the "Zitadel as an Identity Provider" epic: #5127
- Enables the customer-portal "support access" flow, where support staff
sign in to a
  customer instance via a pre-configured Zitadel Support IdP

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
This commit is contained in:
Max Peintner
2026-08-10 10:27:34 +02:00
committed by GitHub
co-authored by Copilot Autofix powered by AI
parent 83854e80e0
commit 30434d176c
20 changed files with 225 additions and 0 deletions
@@ -0,0 +1,187 @@
"use client";
import { forwardRef, useId } from "react";
import { Translated } from "../translated";
import { BaseButton, SignInWithIdentityProviderProps } from "./base-button";
export const SignInWithZitadel = forwardRef<HTMLButtonElement, SignInWithIdentityProviderProps>(
function SignInWithZitadel(props, ref) {
const { children, name, ...restProps } = props;
// Unique per component instance: multiple ZITADEL IdPs can be rendered on
// the same page, and duplicate SVG gradient ids would be invalid HTML.
const gradientId = useId().replace(/:/g, "");
return (
<BaseButton {...restProps} ref={ref}>
<div className="flex h-12 w-12 items-center justify-center">
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 467 467"
fillRule="evenodd"
clipRule="evenodd"
aria-hidden="true"
focusable="false"
>
<g transform="matrix(0.564847,0,0,0.659318,-1282.85,0)">
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5923.46,-2258.26)">
<path
d="M1493.5,1056.38L1493.5,1037L1496.5,1037L1496.5,1061.62L1426.02,1020.38L1496.5,979.392L1496.5,1004L1493.5,1004L1493.5,984.608L1431.98,1020.39L1493.5,1056.38Z"
fill="currentColor"
/>
</g>
<g transform="matrix(31.0036,0,0,15.0393,-397275,-666.457)">
<g transform="matrix(-0.0429306,-0.282967,0.160219,-0.0758207,12884.5,137.392)">
<path
d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z"
fill={`url(#${gradientId}-g1)`}
/>
</g>
<g transform="matrix(0.160219,0.0758207,-0.0429306,0.282967,12878.9,10.8747)">
<path
d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z"
fill={`url(#${gradientId}-g2)`}
/>
</g>
<g transform="matrix(-0.117289,0.207146,-0.117289,-0.207146,12943.8,65.7)">
<path
d="M212.517,110L200.392,110L190,92L179.608,110L167.483,110L190,71L212.517,110Z"
fill={`url(#${gradientId}-g3)`}
/>
</g>
<g transform="matrix(-0.160219,-0.0758207,0.0429306,-0.282967,12917.4,132.195)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" fill={`url(#${gradientId}-g4)`} />
</g>
<g transform="matrix(-0.117289,0.207146,0.117289,0.207146,12897.8,5.87512)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" fill={`url(#${gradientId}-g5)`} />
</g>
<g transform="matrix(-0.0429306,-0.282967,-0.160219,0.0758207,12936.8,97.6441)">
<path d="M139.622,117L149,142L130.244,142L139.622,117Z" fill={`url(#${gradientId}-g6)`} />
</g>
</g>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5928.43,-2257.12)">
<circle cx="1496" cy="1004" r="7" fill="currentColor" />
</g>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5884.5,-2116.69)">
<circle cx="1496" cy="1004" r="7" fill="currentColor" />
</g>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5855.22,-2023.06)">
<circle cx="1496" cy="1004" r="7" fill="currentColor" />
</g>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-6234.47,-2112.14)">
<circle cx="1496" cy="1004" r="7" fill="currentColor" />
</g>
<g transform="matrix(4.96737,-1.14029,1.331,4.25561,-5957.71,-2350.75)">
<circle cx="1496" cy="1004" r="7" fill="currentColor" />
</g>
<g transform="matrix(4.96737,-1.14029,1.16463,3.72366,-5477.99,-831.33)">
<path
d="M1499.26,757.787C1499.26,757.787 1497.37,756.489 1497,755.2C1496.71,754.182 1496.57,750.662 1496.54,750C1496.41,747.303 1499.21,745.644 1499.21,745.644L1490.01,745.835C1490.01,745.835 1493.15,745.713 1493.46,750C1493.51,750.661 1493.23,753.476 1493,755.2C1492.91,756.447 1491.2,757.668 1491.2,757.668L1499.26,757.787Z"
fill="currentColor"
/>
</g>
<g transform="matrix(4.96737,-1.14029,1.16463,3.72366,-5404.79,-597.271)">
<path
d="M1498.27,757.077C1498.27,757.077 1496.71,756.46 1496.65,754.8C1496.65,753.658 1496.64,753.281 1496.65,752.016C1496.62,751.334 1496.59,750.608 1496.65,749.949C1496.78,746.836 1498.5,746.156 1498.5,746.156L1491.46,745.931C1491.46,745.931 1493.37,746.719 1493.65,749.83C1493.71,750.489 1493.69,751.528 1493.65,752.209C1493.64,753.331 1493.64,753.413 1493.65,754.518C1493.68,756.334 1492.58,756.827 1492.58,756.827L1498.27,757.077Z"
fill="currentColor"
/>
</g>
<g transform="matrix(4.96737,-1.14029,1.16463,3.72366,-5770.62,-677.495)">
<path
d="M1500.86,762.056C1500.86,762.056 1499.86,760.4 1503.09,757.456C1504.91,755.797 1507.33,754.151 1509.98,752.255C1514.82,748.79 1520.68,744.94 1526.52,741.049C1531.45,737.766 1536.38,734.479 1540.82,731.68C1544.52,729.349 1547.85,727.296 1550.54,725.8C1551.07,725.506 1551.6,725.329 1552.05,725.029C1554.73,723.257 1556.85,724.968 1556.85,724.968L1552.23,716.282C1552.23,716.282 1551.99,719.454 1550,720.997C1549.57,721.333 1549.15,721.741 1548.67,722.12C1546.2,724.053 1542.99,726.344 1539.39,728.867C1535.06,731.898 1530.13,735.166 1525.19,738.438C1519.35,742.314 1513.52,746.234 1508.49,749.329C1505.74,751.023 1503.28,752.577 1501.13,753.598C1497.99,755.086 1495.28,753.617 1495.28,753.617L1500.86,762.056Z"
fill="currentColor"
/>
</g>
<g transform="matrix(4.96737,-1.14029,-1.16463,-3.72366,-3997,4993.28)">
<path
d="M1496.1,754.362C1496.1,754.362 1497.2,755.607 1501.13,753.598C1503.25,752.509 1505.74,751.023 1508.49,749.329C1513.52,746.234 1519.35,742.314 1525.19,738.438C1530.13,735.166 1534.94,731.832 1539.27,728.802C1542.87,726.279 1549.36,722.059 1549.81,721.75C1552.75,719.73 1552.18,718.196 1552.18,718.196L1555.28,724.152C1555.28,724.152 1553.77,722.905 1551.37,724.681C1550.93,725.006 1544.52,729.349 1540.82,731.68C1536.38,734.479 1531.45,737.766 1526.52,741.049C1520.68,744.94 1514.82,748.79 1509.98,752.255C1507.33,754.151 1504.89,755.771 1503.09,757.456C1499.47,760.841 1501.26,763.283 1501.26,763.283L1496.1,754.362Z"
fill="currentColor"
/>
</g>
</g>
<defs>
<linearGradient
id={`${gradientId}-g1`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-41.5984,155.247,-155.247,-41.5984,201.516,76.8392)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
<linearGradient
id={`${gradientId}-g2`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(155.247,-41.5984,41.5984,155.247,110.08,195.509)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
<linearGradient
id={`${gradientId}-g3`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-113.649,-113.649,113.649,-113.649,258.31,215.618)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
<linearGradient
id={`${gradientId}-g4`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-155.247,41.5984,-41.5984,-155.247,220.914,144.546)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
<linearGradient
id={`${gradientId}-g5`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-113.649,113.649,113.649,113.649,206.837,124.661)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
<linearGradient
id={`${gradientId}-g6`}
x1="0"
y1="0"
x2="1"
y2="0"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-41.5984,-155.247,-155.247,41.5984,152.054,262.8)"
>
<stop offset="0" stopColor="rgb(255,143,0)" stopOpacity={1} />
<stop offset="1" stopColor="rgb(254,0,255)" stopOpacity={1} />
</linearGradient>
</defs>
</svg>
</div>
{children ? (
children
) : (
<span className="ml-4">{name ? name : <Translated i18nKey="signInWithZitadel" namespace="idp" />}</span>
)}
</BaseButton>
);
},
);
@@ -13,6 +13,7 @@ import { SignInWithGeneric } from "./idps/sign-in-with-generic";
import { SignInWithGithub } from "./idps/sign-in-with-github";
import { SignInWithGitlab } from "./idps/sign-in-with-gitlab";
import { SignInWithGoogle } from "./idps/sign-in-with-google";
import { SignInWithZitadel } from "./idps/sign-in-with-zitadel";
import { Translated } from "./translated";
export interface SignInWithIDPProps {
@@ -51,6 +52,7 @@ export function SignInWithIdp({
[IdentityProviderType.SAML]: SignInWithGeneric,
[IdentityProviderType.LDAP]: SignInWithGeneric,
[IdentityProviderType.JWT]: SignInWithGeneric,
[IdentityProviderType.ZITADEL]: SignInWithZitadel,
};
const Component = components[type];