Files
zitadel/apps/login/src/lib/zitadel.ts
T
26f8238499 fix(login): correctly redirect to /password when ignoreUnknownUsername is set (#11130)
Closes #11006 

# Which Problems Are Solved

This PR addresses an issue where the ignoreUnknownUsernames setting was
not being respected in certain scenarios during the login flow.
Specifically:

- When a user was found but rejected due to login settings (e.g.,
disableLoginWithEmail), the system would return a "User not found" error
instead of redirecting to the password page as dictated by
ignoreUnknownUsernames.
- When a user was not found, and the flow fell through the registration
checks (e.g., IDP redirect failed), it would also return "User not
found" without checking ignoreUnknownUsernames.

# How the Problems Are Solved

- Introduced a helper function `handleUserNotFound` in 
`apps/login/src/lib/server/loginname.ts` to centralize the logic for
checking ignoreUnknownUsernames and redirecting to the password page.
- Updated the user validation logic to call handleUserNotFound when a
user is found but rejected by policy.
- Updated the registration fallback logic to call handleUserNotFound
instead of directly returning an error.
- Added new test cases in `apps/login/src/lib/server/loginname.test.ts`
to verify the fix and ensure no regressions.

# Additional Changes

- Fixed inconsistency in translation files

---------

Co-authored-by: Livio Spring <[email protected]>
2026-01-14 08:24:48 +00:00

1295 lines
34 KiB
TypeScript

import { Client, create, Duration } from "@zitadel/client";
import { createServerTransport as libCreateServerTransport } from "@zitadel/client/node";
import { makeReqCtx } from "@zitadel/client/v2";
import { IdentityProviderService } from "@zitadel/proto/zitadel/idp/v2/idp_service_pb";
import { OrganizationSchema, TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb";
import { CreateCallbackRequest, OIDCService } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
import { OrganizationService } from "@zitadel/proto/zitadel/org/v2/org_service_pb";
import { CreateResponseRequest, SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
import { RequestChallenges } from "@zitadel/proto/zitadel/session/v2/challenge_pb";
import { Checks, SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
import { LoginSettings } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
import { SendEmailVerificationCodeSchema } from "@zitadel/proto/zitadel/user/v2/email_pb";
import type { FormData, RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb";
import { NotificationType, SendPasswordResetLinkSchema } from "@zitadel/proto/zitadel/user/v2/password_pb";
import { SearchQuery, SearchQuerySchema } from "@zitadel/proto/zitadel/user/v2/query_pb";
import { SendInviteCodeSchema } from "@zitadel/proto/zitadel/user/v2/user_pb";
import {
AddHumanUserRequest,
AddHumanUserRequestSchema,
ResendEmailCodeRequest,
ResendEmailCodeRequestSchema,
SendEmailCodeRequestSchema,
SetPasswordRequest,
SetPasswordRequestSchema,
UpdateHumanUserRequest,
UserService,
VerifyPasskeyRegistrationRequest,
VerifyU2FRegistrationRequest,
} from "@zitadel/proto/zitadel/user/v2/user_service_pb";
import { unstable_cacheLife as cacheLife } from "next/cache";
import { getTranslations } from "next-intl/server";
import { getUserAgent } from "./fingerprint";
import { setSAMLFormCookie } from "./saml";
import { createServiceForHost } from "./service";
const useCache = process.env.DEBUG !== "true";
async function cacheWrapper<T>(callback: Promise<T>) {
"use cache";
cacheLife("hours");
return callback;
}
export async function getHostedLoginTranslation({
serviceConfig,
organization,
locale,
}: WithServiceConfig<{
organization?: string;
locale?: string;
}>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getHostedLoginTranslation(
{
level: organization
? {
case: "organizationId",
value: organization,
}
: {
case: "instance",
value: true,
},
locale: locale,
},
{},
)
.then((resp) => {
return resp.translations ? resp.translations : undefined;
});
return useCache ? cacheWrapper(callback) : callback;
}
export async function getBrandingSettings({
serviceConfig,
organization,
}: WithServiceConfig<{
organization?: string;
}>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getBrandingSettings({ ctx: makeReqCtx(organization) }, {})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLoginSettings({
serviceConfig,
organization,
}: WithServiceConfig<{
organization?: string;
}>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLoginSettings({ ctx: makeReqCtx(organization) }, {})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getSecuritySettings({ serviceConfig }: WithServiceConfig) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService.getSecuritySettings({}).then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLockoutSettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLockoutSettings({ ctx: makeReqCtx(orgId) }, {})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getPasswordExpirySettings({ serviceConfig, orgId }: WithServiceConfig<{ orgId?: string }>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getPasswordExpirySettings({ ctx: makeReqCtx(orgId) }, {})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function listIDPLinks({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.listIDPLinks({ userId }, {});
}
export async function addOTPEmail({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.addOTPEmail({ userId }, {});
}
export async function addOTPSMS({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.addOTPSMS({ userId }, {});
}
export async function registerTOTP({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.registerTOTP({ userId }, {});
}
export async function getGeneralSettings({ serviceConfig }: WithServiceConfig) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService.getGeneralSettings({}, {}).then((resp) => resp.supportedLanguages);
return useCache ? cacheWrapper(callback) : callback;
}
export async function getLegalAndSupportSettings({
serviceConfig,
organization,
}: WithServiceConfig<{
organization?: string;
}>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function getPasswordComplexitySettings({
serviceConfig,
organization,
}: WithServiceConfig<{
organization?: string;
}>) {
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
const callback = settingsService
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
.then((resp) => (resp.settings ? resp.settings : undefined));
return useCache ? cacheWrapper(callback) : callback;
}
export async function createSessionFromChecksAndChallenges({
serviceConfig,
checks,
challenges,
lifetime,
}: WithServiceConfig<{
checks: Checks;
challenges?: RequestChallenges;
lifetime: Duration;
}>) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
const userAgent = await getUserAgent();
return sessionService.createSession({ ...{ checks, lifetime, userAgent }, ...(challenges ? { challenges } : {}) }, {});
}
export async function createSessionForUserIdAndIdpIntent({
serviceConfig,
userId,
idpIntent,
lifetime,
}: WithServiceConfig<{
userId: string;
idpIntent: {
idpIntentId?: string | undefined;
idpIntentToken?: string | undefined;
};
lifetime: Duration;
}>) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
const userAgent = await getUserAgent();
return sessionService.createSession({
checks: {
user: {
search: {
case: "userId",
value: userId,
},
},
idpIntent,
},
lifetime,
userAgent,
});
}
export async function setSession({
serviceConfig,
sessionId,
sessionToken,
challenges,
checks,
lifetime,
}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
challenges: RequestChallenges | undefined;
checks?: Checks;
lifetime: Duration;
}>) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
return sessionService.setSession(
{
sessionId,
sessionToken,
challenges,
checks: checks ? checks : {},
metadata: {},
lifetime,
},
{},
);
}
export async function getSession({
serviceConfig,
sessionId,
sessionToken,
}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
}>) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
return sessionService.getSession({ sessionId, sessionToken }, {});
}
export async function deleteSession({
serviceConfig,
sessionId,
sessionToken,
}: WithServiceConfig<{
sessionId: string;
sessionToken: string;
}>) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
return sessionService.deleteSession({ sessionId, sessionToken }, {});
}
type ListSessionsCommand = WithServiceConfig<{
ids: string[];
}>;
export async function listSessions({ serviceConfig, ids }: ListSessionsCommand) {
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
return sessionService.listSessions(
{
queries: [
{
query: {
case: "idsQuery",
value: { ids },
},
},
],
},
{},
);
}
export type AddHumanUserData = WithServiceConfig<{
firstName: string;
lastName: string;
email: string;
password?: string;
organization: string;
}>;
export async function addHumanUser({ serviceConfig, email, firstName, lastName, password, organization }: AddHumanUserData) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
let addHumanUserRequest: AddHumanUserRequest = create(AddHumanUserRequestSchema, {
email: {
email,
verification: {
case: "isVerified",
value: false,
},
},
username: email,
profile: { givenName: firstName, familyName: lastName },
passwordType: password ? { case: "password", value: { password } } : undefined,
});
if (organization) {
const organizationSchema = create(OrganizationSchema, {
org: { case: "orgId", value: organization },
});
addHumanUserRequest = {
...addHumanUserRequest,
organization: organizationSchema,
};
}
return userService.addHumanUser(addHumanUserRequest);
}
export async function addHuman({ serviceConfig, request }: WithServiceConfig<{ request: AddHumanUserRequest }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.addHumanUser(request);
}
export async function updateHuman({
serviceConfig,
request,
}: WithServiceConfig<{
request: UpdateHumanUserRequest;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.updateHumanUser(request);
}
export async function verifyTOTPRegistration({
serviceConfig,
code,
userId,
}: WithServiceConfig<{
code: string;
userId: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.verifyTOTPRegistration({ code, userId }, {});
}
export async function getUserByID({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.getUserByID({ userId }, {});
}
export async function humanMFAInitSkipped({ serviceConfig, userId }: WithServiceConfig<{ userId: string }>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.humanMFAInitSkipped({ userId }, {});
}
export async function verifyInviteCode({
serviceConfig,
userId,
verificationCode,
}: WithServiceConfig<{
userId: string;
verificationCode: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.verifyInviteCode({ userId, verificationCode }, {});
}
export async function sendEmailCode({
serviceConfig,
userId,
urlTemplate,
}: WithServiceConfig<{
userId: string;
urlTemplate: string;
}>) {
let medium = create(SendEmailCodeRequestSchema, { userId });
medium = create(SendEmailCodeRequestSchema, {
...medium,
verification: {
case: "sendCode",
value: create(SendEmailVerificationCodeSchema, {
urlTemplate,
}),
},
});
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.sendEmailCode(medium, {});
}
export async function createInviteCode({
serviceConfig,
urlTemplate,
userId,
}: WithServiceConfig<{
urlTemplate: string;
userId: string;
}>) {
let medium = create(SendInviteCodeSchema, {
applicationName: process.env.NEXT_PUBLIC_APPLICATION_NAME || "Zitadel Login",
});
medium = {
...medium,
urlTemplate,
};
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.createInviteCode(
{
userId,
verification: {
case: "sendCode",
value: medium,
},
},
{},
);
}
export type ListUsersCommand = WithServiceConfig<{
loginName?: string;
userName?: string;
email?: string;
phone?: string;
organizationId?: string;
}>;
export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
const queries: SearchQuery[] = [];
// either use loginName or userName, email, phone
if (loginName) {
queries.push(
create(SearchQuerySchema, {
query: {
case: "loginNameQuery",
value: {
loginName,
method: TextQueryMethod.EQUALS,
},
},
}),
);
} else if (userName || email || phone) {
const orQueries: SearchQuery[] = [];
if (userName) {
const userNameQuery = create(SearchQuerySchema, {
query: {
case: "userNameQuery",
value: {
userName,
method: TextQueryMethod.EQUALS,
},
},
});
orQueries.push(userNameQuery);
}
if (email) {
const emailQuery = create(SearchQuerySchema, {
query: {
case: "emailQuery",
value: {
emailAddress: email,
method: TextQueryMethod.EQUALS,
},
},
});
orQueries.push(emailQuery);
}
if (phone) {
const phoneQuery = create(SearchQuerySchema, {
query: {
case: "phoneQuery",
value: {
number: phone,
method: TextQueryMethod.EQUALS,
},
},
});
orQueries.push(phoneQuery);
}
queries.push(
create(SearchQuerySchema, {
query: {
case: "orQuery",
value: {
queries: orQueries,
},
},
}),
);
}
if (organizationId) {
queries.push(
create(SearchQuerySchema, {
query: {
case: "organizationIdQuery",
value: {
organizationId,
},
},
}),
);
}
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.listUsers({ queries });
}
export type SearchUsersCommand = WithServiceConfig<{
searchValue: string;
loginSettings: LoginSettings;
organizationId?: string;
suffix?: string;
}>;
const PhoneQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "phoneQuery",
value: {
number: searchValue,
method: TextQueryMethod.EQUALS,
},
},
});
const LoginNameQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "loginNameQuery",
value: {
loginName: searchValue,
method: TextQueryMethod.EQUALS_IGNORE_CASE,
},
},
});
const EmailQuery = (searchValue: string) =>
create(SearchQuerySchema, {
query: {
case: "emailQuery",
value: {
emailAddress: searchValue,
method: TextQueryMethod.EQUALS_IGNORE_CASE,
},
},
});
/**
* this is a dedicated search function to search for users from the loginname page
* it searches users based on the loginName or userName and org suffix combination, and falls back to email and phone if no users are found
* */
export async function searchUsers({
serviceConfig,
searchValue,
loginSettings,
organizationId,
suffix,
}: SearchUsersCommand) {
const queries: SearchQuery[] = [];
const t = await getTranslations("zitadel");
// if a suffix is provided, we search for the userName concatenated with the suffix
if (suffix) {
const searchValueWithSuffix = `${searchValue}@${suffix}`;
const loginNameQuery = LoginNameQuery(searchValueWithSuffix);
queries.push(loginNameQuery);
} else {
const loginNameQuery = LoginNameQuery(searchValue);
queries.push(loginNameQuery);
}
if (organizationId) {
queries.push(
create(SearchQuerySchema, {
query: {
case: "organizationIdQuery",
value: {
organizationId,
},
},
}),
);
}
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
const loginNameResult = await userService.listUsers({ queries });
if (!loginNameResult || !loginNameResult.details) {
return { error: t("errors.errorOccured") };
}
if (loginNameResult.result.length > 1) {
return { error: t("errors.multipleUsersFound") };
}
if (loginNameResult.result.length == 1) {
return loginNameResult;
}
const emailAndPhoneQueries: SearchQuery[] = [];
if (loginSettings.disableLoginWithEmail && loginSettings.disableLoginWithPhone) {
// Both email and phone login are disabled, return empty result
return { result: [] };
} else if (loginSettings.disableLoginWithEmail && searchValue.length <= 20) {
const phoneQuery = PhoneQuery(searchValue);
emailAndPhoneQueries.push(phoneQuery);
} else if (loginSettings.disableLoginWithPhone) {
const emailQuery = EmailQuery(searchValue);
emailAndPhoneQueries.push(emailQuery);
} else {
const orQuery: SearchQuery[] = [];
const emailQuery = EmailQuery(searchValue);
orQuery.push(emailQuery);
let phoneQuery;
if (searchValue.length <= 20) {
phoneQuery = PhoneQuery(searchValue);
orQuery.push(phoneQuery);
}
emailAndPhoneQueries.push(
create(SearchQuerySchema, {
query: {
case: "orQuery",
value: {
queries: orQuery,
},
},
}),
);
}
if (organizationId) {
emailAndPhoneQueries.push(
create(SearchQuerySchema, {
query: {
case: "organizationIdQuery",
value: {
organizationId,
},
},
}),
);
}
const emailOrPhoneResult = await userService.listUsers({
queries: emailAndPhoneQueries,
});
if (!emailOrPhoneResult || !emailOrPhoneResult.details) {
return { error: t("errors.errorOccured") };
}
if (emailOrPhoneResult.result.length > 1) {
return { error: t("errors.multipleUsersFound") };
}
if (emailOrPhoneResult.result.length == 1) {
return emailOrPhoneResult;
}
// No users found - return empty result, not an error
return { result: [] };
}
export async function getDefaultOrg({ serviceConfig }: WithServiceConfig): Promise<Organization | null> {
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
return orgService
.listOrganizations(
{
queries: [
{
query: {
case: "defaultQuery",
value: {},
},
},
],
},
{},
)
.then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null));
}
export async function getOrgsByDomain({ serviceConfig, domain }: WithServiceConfig<{ domain: string }>) {
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
return orgService.listOrganizations(
{
queries: [
{
query: {
case: "domainQuery",
value: { domain, method: TextQueryMethod.EQUALS },
},
},
],
},
{},
);
}
export async function startIdentityProviderFlow({
serviceConfig,
idpId,
urls,
}: WithServiceConfig<{
idpId: string;
urls: RedirectURLsJson;
}>): Promise<string | null> {
// Use empty publicHost to avoid issues with redirect URIs pointing to the login UI instead of the zitadel API
const userService: Client<typeof UserService> = await createServiceForHost(UserService, {
...serviceConfig,
publicHost: "",
});
return userService
.startIdentityProviderIntent({
idpId,
content: {
case: "urls",
value: urls,
},
})
.then(async (resp) => {
if (resp.nextStep.case === "authUrl" && resp.nextStep.value) {
return resp.nextStep.value;
} else if (resp.nextStep.case === "formData" && resp.nextStep.value) {
const formData: FormData = resp.nextStep.value;
const redirectUrl = "/saml-post";
try {
const stringifiedFields = JSON.stringify(formData.fields);
// Check cookie size limits (typical limit is 4KB)
if (stringifiedFields.length > 4000) {
console.warn(
`SAML form cookie value is large (${stringifiedFields.length} characters), may exceed browser limits`,
);
}
const dataId = await setSAMLFormCookie(stringifiedFields);
const params = new URLSearchParams({ url: formData.url, id: dataId });
return `${redirectUrl}?${params.toString()}`;
} catch (stringifyError) {
throw new Error(
`Failed to serialize SAML form data: ${stringifyError instanceof Error ? stringifyError.message : String(stringifyError)}`,
);
}
} else {
return null;
}
});
}
export async function startLDAPIdentityProviderFlow({
serviceConfig,
idpId,
username,
password,
}: WithServiceConfig<{
idpId: string;
username: string;
password: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.startIdentityProviderIntent({
idpId,
content: {
case: "ldap",
value: {
username,
password,
},
},
});
}
export async function getAuthRequest({
serviceConfig,
authRequestId,
}: WithServiceConfig<{
authRequestId: string;
}>) {
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.getAuthRequest({
authRequestId,
});
}
export async function getDeviceAuthorizationRequest({
serviceConfig,
userCode,
}: WithServiceConfig<{
userCode: string;
}>) {
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.getDeviceAuthorizationRequest({
userCode,
});
}
export async function authorizeOrDenyDeviceAuthorization({
serviceConfig,
deviceAuthorizationId,
session,
}: WithServiceConfig<{
deviceAuthorizationId: string;
session?: { sessionId: string; sessionToken: string };
}>) {
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.authorizeOrDenyDeviceAuthorization({
deviceAuthorizationId,
decision: session
? {
case: "session",
value: session,
}
: {
case: "deny",
value: {},
},
});
}
export async function createCallback({ serviceConfig, req }: WithServiceConfig<{ req: CreateCallbackRequest }>) {
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
return oidcService.createCallback(req);
}
export async function getSAMLRequest({
serviceConfig,
samlRequestId,
}: WithServiceConfig<{
samlRequestId: string;
}>) {
const samlService = await createServiceForHost(SAMLService, serviceConfig);
return samlService.getSAMLRequest({
samlRequestId,
});
}
export async function createResponse({ serviceConfig, req }: WithServiceConfig<{ req: CreateResponseRequest }>) {
const samlService = await createServiceForHost(SAMLService, serviceConfig);
return samlService.createResponse(req);
}
export async function verifyEmail({
serviceConfig,
userId,
verificationCode,
}: WithServiceConfig<{
userId: string;
verificationCode: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.verifyEmail(
{
userId,
verificationCode,
},
{},
);
}
export async function resendEmailCode({
serviceConfig,
userId,
urlTemplate,
}: WithServiceConfig<{
userId: string;
urlTemplate: string;
}>) {
let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
userId,
});
const medium = create(SendEmailVerificationCodeSchema, {
urlTemplate,
});
request = { ...request, verification: { case: "sendCode", value: medium } };
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.resendEmailCode(request, {});
}
export async function retrieveIDPIntent({
serviceConfig,
id,
token,
}: WithServiceConfig<{
id: string;
token: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.retrieveIdentityProviderIntent({ idpIntentId: id, idpIntentToken: token }, {});
}
export async function getIDPByID({ serviceConfig, id }: WithServiceConfig<{ id: string }>) {
const idpService: Client<typeof IdentityProviderService> = await createServiceForHost(
IdentityProviderService,
serviceConfig,
);
return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp);
}
export async function addIDPLink({
serviceConfig,
idp,
userId,
}: WithServiceConfig<{
idp: { id: string; userId: string; userName: string };
userId: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.addIDPLink(
{
idpLink: {
userId: idp.userId,
idpId: idp.id,
userName: idp.userName,
},
userId,
},
{},
);
}
export async function passwordReset({
serviceConfig,
userId,
urlTemplate,
}: WithServiceConfig<{
userId: string;
urlTemplate?: string;
}>) {
let medium = create(SendPasswordResetLinkSchema, {
notificationType: NotificationType.Email,
});
medium = {
...medium,
urlTemplate,
};
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.passwordReset(
{
userId,
medium: {
case: "sendLink",
value: medium,
},
},
{},
);
}
export async function setUserPassword({
serviceConfig,
userId,
password,
code,
}: WithServiceConfig<{
userId: string;
password: string;
code?: string;
}>) {
let payload = create(SetPasswordRequestSchema, {
userId,
newPassword: {
password,
},
});
if (code) {
payload = {
...payload,
verification: {
case: "verificationCode",
value: code,
},
};
}
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.setPassword(payload, {}).catch((error) => {
// throw error if failed precondition (ex. User is not yet initialized)
if (error.code === 9 && error.message) {
return { error: error.message };
} else {
throw error;
}
});
}
export async function setPassword({
serviceConfig,
payload,
}: WithServiceConfig<{
payload: SetPasswordRequest;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.setPassword(payload, {});
}
/**
*
* @param host
* @param userId the id of the user where the email should be set
* @returns the newly set email
*/
export async function createPasskeyRegistrationLink({
serviceConfig,
userId,
}: WithServiceConfig<{
userId: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.createPasskeyRegistrationLink({
userId,
medium: {
case: "returnCode",
value: {},
},
});
}
/**
*
* @param host
* @param userId the id of the user where the email should be set
* @param domain the domain on which the factor is registered
* @returns the newly set email
*/
export async function registerU2F({
serviceConfig,
userId,
domain,
}: WithServiceConfig<{
userId: string;
domain: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.registerU2F({
userId,
domain,
});
}
/**
*
* @param host
* @param request the request object for verifying U2F registration
* @returns the result of the verification
*/
export async function verifyU2FRegistration({
serviceConfig,
request,
}: WithServiceConfig<{
request: VerifyU2FRegistrationRequest;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.verifyU2FRegistration(request, {});
}
/**
*
* @param host
* @param orgId the organization ID
* @param linking_allowed whether linking is allowed
* @returns the active identity providers
*/
export async function getActiveIdentityProviders({
serviceConfig,
orgId,
linking_allowed,
}: WithServiceConfig<{
orgId?: string;
linking_allowed?: boolean;
}>) {
const props: any = { ctx: makeReqCtx(orgId) };
if (linking_allowed) {
props.linkingAllowed = linking_allowed;
}
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
return settingsService.getActiveIdentityProviders(props, {});
}
/**
*
* @param host
* @param request the request object for verifying passkey registration
* @returns the result of the verification
*/
export async function verifyPasskeyRegistration({
serviceConfig,
request,
}: WithServiceConfig<{
request: VerifyPasskeyRegistrationRequest;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.verifyPasskeyRegistration(request, {});
}
/**
*
* @param host
* @param userId the id of the user where the email should be set
* @param code the code for registering the passkey
* @param domain the domain on which the factor is registered
* @returns the newly set email
*/
export async function registerPasskey({
serviceConfig,
userId,
code,
domain,
}: WithServiceConfig<{
userId: string;
code: { id: string; code: string };
domain: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.registerPasskey({
userId,
code,
domain,
});
}
/**
*
* @param host
* @param userId the id of the user where the email should be set
* @returns the list of authentication method types
*/
export async function listAuthenticationMethodTypes({
serviceConfig,
userId,
}: WithServiceConfig<{
userId: string;
}>) {
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
return userService.listAuthenticationMethodTypes({
userId,
});
}
export interface ServiceConfig {
baseUrl: string;
instanceHost?: string; // only for multi-tenant
publicHost?: string; // only for multi-tenant
}
/**
* Base type that all function parameters must extend to ensure serviceConfig is always required
*/
export type WithServiceConfig<T = {}> = T & {
serviceConfig: ServiceConfig;
};
export function createServerTransport(token: string, serviceConfig: ServiceConfig) {
return libCreateServerTransport(token, {
baseUrl: serviceConfig.baseUrl,
interceptors:
!process.env.CUSTOM_REQUEST_HEADERS && !serviceConfig.instanceHost && !serviceConfig.publicHost
? undefined
: [
(next) => {
return (req) => {
// Apply headers from serviceConfig
if (serviceConfig.instanceHost) {
req.header.set("x-zitadel-instance-host", serviceConfig.instanceHost);
}
if (serviceConfig.publicHost) {
req.header.set("x-zitadel-public-host", serviceConfig.publicHost);
}
// Apply headers from CUSTOM_REQUEST_HEADERS environment variable
if (process.env.CUSTOM_REQUEST_HEADERS) {
process.env.CUSTOM_REQUEST_HEADERS.split(",").forEach((header) => {
const kv = header.indexOf(":");
if (kv > 0) {
const key = header.slice(0, kv).trim();
const value = header.slice(kv + 1).trim();
if (value) {
req.header.set(key, value);
} else {
req.header.delete(key);
}
} else {
console.warn(`Skipping malformed header: ${header}`);
}
});
}
return next(req);
};
},
],
});
}