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
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "تسجيل الدخول باستخدام AzureAD",
"signInWithGithub": "تسجيل الدخول باستخدام GitHub",
"signInWithGitlab": "تسجيل الدخول باستخدام GitLab",
"signInWithZitadel": "تسجيل الدخول باستخدام Zitadel",
"loginError": {
"title": "فشل تسجيل الدخول",
"description": "حدث خطأ أثناء محاولة تسجيل الدخول."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Mit AzureAD anmelden",
"signInWithGithub": "Mit GitHub anmelden",
"signInWithGitlab": "Mit GitLab anmelden",
"signInWithZitadel": "Mit Zitadel anmelden",
"loginError": {
"title": "Anmeldung fehlgeschlagen",
"description": "Beim Anmelden ist ein Fehler aufgetreten."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Sign in with AzureAD",
"signInWithGithub": "Sign in with GitHub",
"signInWithGitlab": "Sign in with GitLab",
"signInWithZitadel": "Sign in with Zitadel",
"loginError": {
"title": "Login failed",
"description": "An error occurred while trying to login."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Iniciar sesión con AzureAD",
"signInWithGithub": "Iniciar sesión con GitHub",
"signInWithGitlab": "Iniciar sesión con GitLab",
"signInWithZitadel": "Iniciar sesión con Zitadel",
"loginError": {
"title": "Error de inicio de sesión",
"description": "Ocurrió un error al intentar iniciar sesión."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Se connecter avec AzureAD",
"signInWithGithub": "Se connecter avec GitHub",
"signInWithGitlab": "Se connecter avec GitLab",
"signInWithZitadel": "Se connecter avec Zitadel",
"loginError": {
"title": "Échec de la connexion",
"description": "Une erreur est survenue lors de la tentative de connexion."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Bejelentkezés AzureAD-vel",
"signInWithGithub": "Bejelentkezés GitHubbal",
"signInWithGitlab": "Bejelentkezés GitLabbal",
"signInWithZitadel": "Bejelentkezés Zitadel-lel",
"loginError": {
"title": "Sikertelen bejelentkezés",
"description": "Hiba történt a bejelentkezés során."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Accedi con AzureAD",
"signInWithGithub": "Accedi con GitHub",
"signInWithGitlab": "Accedi con GitLab",
"signInWithZitadel": "Accedi con Zitadel",
"loginError": {
"title": "Accesso fallito",
"description": "Si è verificato un errore durante il tentativo di accesso."
+1
View File
@@ -157,6 +157,7 @@
"signInWithAzureAD": "AzureADでサインイン",
"signInWithGithub": "GitHubでサインイン",
"signInWithGitlab": "GitLabでサインイン",
"signInWithZitadel": "Zitadelでサインイン",
"loginError": {
"title": "ログイン失敗",
"description": "ログインの際にエラーが発生しました。"
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Aanmelden met AzureAD",
"signInWithGithub": "Aanmelden met GitHub",
"signInWithGitlab": "Aanmelden met GitLab",
"signInWithZitadel": "Aanmelden met Zitadel",
"loginError": {
"title": "Inloggen mislukt",
"description": "Er is een fout opgetreden tijdens het inloggen."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Zaloguj się przez AzureAD",
"signInWithGithub": "Zaloguj się przez GitHub",
"signInWithGitlab": "Zaloguj się przez GitLab",
"signInWithZitadel": "Zaloguj się przez Zitadel",
"loginError": {
"title": "Logowanie nieudane",
"description": "Wystąpił błąd podczas próby logowania."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Entrar com AzureAD",
"signInWithGithub": "Entrar com GitHub",
"signInWithGitlab": "Entrar com GitLab",
"signInWithZitadel": "Entrar com Zitadel",
"loginError": {
"title": "Falha no login",
"description": "Ocorreu um erro ao tentar fazer login."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Войти через AzureAD",
"signInWithGithub": "Войти через GitHub",
"signInWithGitlab": "Войти через GitLab",
"signInWithZitadel": "Войти через Zitadel",
"loginError": {
"title": "Ошибка входа",
"description": "Произошла ошибка при попытке входа."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "AzureAD ile Giriş",
"signInWithGithub": "GitHub ile Giriş",
"signInWithGitlab": "GitLab ile Giriş",
"signInWithZitadel": "Zitadel ile Giriş",
"loginError": {
"title": "Giriş başarısız",
"description": "Giriş yapılmaya çalışılırken bir hata oluştu."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "Увійти через AzureAD",
"signInWithGithub": "Увійти через GitHub",
"signInWithGitlab": "Увійти через GitLab",
"signInWithZitadel": "Увійти через Zitadel",
"loginError": {
"title": "Помилка входу",
"description": "Сталася помилка під час спроби входу."
+1
View File
@@ -164,6 +164,7 @@
"signInWithAzureAD": "用 AzureAD 登录",
"signInWithGithub": "用 GitHub 登录",
"signInWithGitlab": "用 GitLab 登录",
"signInWithZitadel": "用 Zitadel 登录",
"loginError": {
"title": "登录失败",
"description": "登录时发生错误。"
@@ -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];
+13
View File
@@ -65,6 +65,11 @@ describe("idp type mapping utilities", () => {
expect(result).toBe("jwt");
});
it("should map ZITADEL to 'zitadel'", () => {
const result = idpTypeToSlug(IdentityProviderType.ZITADEL);
expect(result).toBe("zitadel");
});
it("should throw error for unknown identity provider type", () => {
// Using a value that doesn't match any known type
const unknownType = 9999 as IdentityProviderType;
@@ -92,6 +97,7 @@ describe("idp type mapping utilities", () => {
IdentityProviderType.OIDC,
IdentityProviderType.LDAP,
IdentityProviderType.JWT,
IdentityProviderType.ZITADEL,
];
handledTypes.forEach((type) => {
@@ -178,6 +184,11 @@ describe("idp type mapping utilities", () => {
expect(result).toBe(IdentityProviderType.JWT);
});
it("should map IDP_TYPE_ZITADEL to ZITADEL", () => {
const result = idpTypeToIdentityProviderType(IDPType.IDP_TYPE_ZITADEL);
expect(result).toBe(IdentityProviderType.ZITADEL);
});
it("should throw error for unknown IDP type", () => {
// Using a value that doesn't match any known type
const unknownType = 9999 as IDPType;
@@ -203,6 +214,7 @@ describe("idp type mapping utilities", () => {
IDPType.IDP_TYPE_OAUTH,
IDPType.IDP_TYPE_OIDC,
IDPType.IDP_TYPE_JWT,
IDPType.IDP_TYPE_ZITADEL,
];
handledTypes.forEach((type) => {
@@ -310,6 +322,7 @@ describe("idp type mapping utilities", () => {
IdentityProviderType.OAUTH,
IdentityProviderType.LDAP,
IdentityProviderType.JWT,
IdentityProviderType.ZITADEL,
];
const slugs = types.map((type) => idpTypeToSlug(type));
+5
View File
@@ -28,6 +28,8 @@ export function idpTypeToSlug(idpType: IdentityProviderType) {
return "ldap";
case IdentityProviderType.JWT:
return "jwt";
case IdentityProviderType.ZITADEL:
return "zitadel";
default:
throw new Error("Unknown identity provider type");
}
@@ -69,6 +71,9 @@ export function idpTypeToIdentityProviderType(idpType: IDPType): IdentityProvide
case IDPType.IDP_TYPE_JWT:
return IdentityProviderType.JWT;
case IDPType.IDP_TYPE_ZITADEL:
return IdentityProviderType.ZITADEL;
default:
throw new Error("Unknown identity provider type");
}
+3
View File
@@ -35,6 +35,7 @@ import (
"github.com/zitadel/zitadel/internal/idp/providers/oauth"
openid "github.com/zitadel/zitadel/internal/idp/providers/oidc"
saml2 "github.com/zitadel/zitadel/internal/idp/providers/saml"
"github.com/zitadel/zitadel/internal/idp/providers/zitadel"
"github.com/zitadel/zitadel/internal/query"
"github.com/zitadel/zitadel/internal/zerrors"
)
@@ -555,6 +556,8 @@ func (h *Handler) fetchIDPUserFromCode(ctx context.Context, identityProvider idp
session = openid.NewSession(provider.Provider, code, idpArguments)
case *google.Provider:
session = openid.NewSession(provider.Provider, code, idpArguments)
case *zitadel.Provider:
session = openid.NewSession(provider.Provider, code, idpArguments)
case *apple.Provider:
session = apple.NewSession(provider, code, appleUser)
case *jwt.Provider, *ldap.Provider, *saml2.Provider: