mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
Merge commit from fork
* fix: Improve host header handling and deployment mode detection * ZITADEL_CLOUD env * refactor service config * service config * cleanup * cleanup * util for original and public host * instanceHost, publicHost * cleanup constructUrl * cleanup middleware * add x-zitadel-forward-host as fallback for current cloud setup * fix build * fix: lint * fix: update test mocks * fix: provide basepath to unit tests * host handling * unit tests * imports * fix: getPublicHost instead of getInstanceHost * unit tests * update nextjs, implement jwt verify for logout_token * pass logout information in jwt to login v2 * handle hosts in jwks url * point idp callbacks to the api and not the login UI * logout_hint out of logout token, typecheck * rm unused imports * fix oidc integration test --------- Co-authored-by: Livio Spring <[email protected]>
This commit is contained in:
co-authored by
Livio Spring
parent
f9e3871bb3
commit
df75be96ff
Vendored
+1
@@ -1,5 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference path="./.next/types/routes.d.ts" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -37,12 +37,9 @@ const nextConfig = {
|
||||
output: process.env.NEXT_OUTPUT_MODE || undefined,
|
||||
reactStrictMode: true,
|
||||
experimental: {
|
||||
dynamicIO: true,
|
||||
// Add React 19 compatibility optimizations
|
||||
optimizePackageImports: ['@radix-ui/react-tooltip', '@heroicons/react'],
|
||||
},
|
||||
eslint: {
|
||||
ignoreDuringBuilds: true,
|
||||
useCache: true,
|
||||
},
|
||||
// Improve SSR stability - not actually needed for React 19 SSR issues
|
||||
// onDemandEntries: {
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
"deepmerge": "^4.3.1",
|
||||
"lucide-react": "0.469.0",
|
||||
"moment": "^2.29.4",
|
||||
"next": "15.4.0-canary.86",
|
||||
"next": "15.5.7",
|
||||
"next-intl": "^3.25.1",
|
||||
"next-themes": "^0.2.1",
|
||||
"nice-grpc": "2.0.1",
|
||||
@@ -75,7 +75,7 @@
|
||||
"dotenv-cli": "^8.0.0",
|
||||
"env-cmd": "^10.1.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-next": "15.4.0-canary.86",
|
||||
"eslint-config-next": "15.5.7",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"gaxios": "^7.1.0",
|
||||
"grpc-tools": "1.13.0",
|
||||
|
||||
@@ -2,8 +2,8 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SessionsList } from "@/components/sessions-list";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getAllSessionCookieIds } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
|
||||
import { UserPlusIcon } from "@heroicons/react/24/outline";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
@@ -17,13 +17,11 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
return { title: t("title") };
|
||||
}
|
||||
|
||||
async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
|
||||
async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
|
||||
const cookieIds = await getAllSessionCookieIds();
|
||||
|
||||
if (cookieIds && cookieIds.length) {
|
||||
const response = await listSessions({
|
||||
serviceUrl,
|
||||
ids: cookieIds.filter((id) => !!id) as string[],
|
||||
const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[],
|
||||
});
|
||||
return response?.sessions ?? [];
|
||||
} else {
|
||||
@@ -39,23 +37,19 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const organization = searchParams?.organization;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
let sessions = await loadSessions({ serviceUrl });
|
||||
let sessions = await loadSessions({ serviceConfig });
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -6,7 +6,7 @@ import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { checkUserVerification } from "@/lib/verify-helper";
|
||||
import {
|
||||
@@ -35,7 +35,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, requestId, organization, sessionId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionWithData = sessionId
|
||||
? await loadSessionById(sessionId, organization)
|
||||
@@ -52,11 +52,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
throw Error("Could not get user id from session");
|
||||
}
|
||||
|
||||
return listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
return listAuthenticationMethodTypes({ serviceConfig, userId,
|
||||
}).then((methods) => {
|
||||
return getUserByID({ serviceUrl, userId }).then((user) => {
|
||||
return getUserByID({ serviceConfig, userId }).then((user) => {
|
||||
const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
|
||||
|
||||
return {
|
||||
@@ -71,25 +69,21 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
}
|
||||
|
||||
async function loadSessionByLoginname(loginName?: string, organization?: string) {
|
||||
return loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
return loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
}).then((session) => {
|
||||
return getAuthMethodsAndUser(serviceUrl, session);
|
||||
return getAuthMethodsAndUser(serviceConfig.baseUrl, session);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadSessionById(sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((sessionResponse) => {
|
||||
return getAuthMethodsAndUser(serviceUrl, sessionResponse.session);
|
||||
return getAuthMethodsAndUser(serviceConfig.baseUrl, sessionResponse.session);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -101,14 +95,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
);
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: sessionWithData.factors.user?.organizationId,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: sessionWithData.factors.user?.organizationId,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: sessionWithData.factors.user?.organizationId,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionWithData.factors.user?.organizationId,
|
||||
});
|
||||
|
||||
// check if user was verified recently
|
||||
@@ -132,9 +122,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
redirect(`/verify?` + params);
|
||||
}
|
||||
|
||||
const identityProviders = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: sessionWithData.factors?.user?.organizationId,
|
||||
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: sessionWithData.factors?.user?.organizationId,
|
||||
linking_allowed: true,
|
||||
}).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConsentScreen } from "@/components/consent";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg, getDeviceAuthorizationRequest } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { headers } from "next/headers";
|
||||
@@ -22,11 +22,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({
|
||||
serviceUrl,
|
||||
userCode,
|
||||
const { deviceAuthorizationRequest } = await getDeviceAuthorizationRequest({ serviceConfig, userCode,
|
||||
});
|
||||
|
||||
if (!deviceAuthorizationRequest) {
|
||||
@@ -39,17 +37,13 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DeviceCodeForm } from "@/components/device-code-form";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
@@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const organization = searchParams?.organization;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
|
||||
import { Button } from "@/components/button";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
@@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { organization, postErrorRedirectUrl } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { RegisterFormIDPIncomplete } from "@/components/register-form-idp-incomplete";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings } from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
@@ -16,11 +16,9 @@ export default async function CompleteRegistrationPage(props: {
|
||||
const { id, token, requestId, organization, idpId, idpUserId, idpUserName, givenName, familyName, email } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
if (!id || !token || !idpId || !organization || !idpUserId || !idpUserName) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChooseAuthenticatorToLogin } from "@/components/choose-authenticator-to
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getLoginSettings, getUserByID, listAuthenticationMethodTypes } from "@/lib/zitadel";
|
||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
@@ -18,16 +18,12 @@ export default async function Page(props: {
|
||||
const { organization, userId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
let authMethods: AuthenticationMethodType[] = [];
|
||||
@@ -43,9 +39,7 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
if (userId) {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId,
|
||||
const userResponse = await getUserByID({ serviceConfig, userId,
|
||||
});
|
||||
if (userResponse) {
|
||||
user = userResponse.user;
|
||||
@@ -58,9 +52,7 @@ export default async function Page(props: {
|
||||
}
|
||||
}
|
||||
|
||||
const authMethodsResponse = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
const authMethodsResponse = await listAuthenticationMethodTypes({ serviceConfig, userId,
|
||||
});
|
||||
if (authMethodsResponse.authMethodTypes) {
|
||||
authMethods = authMethodsResponse.authMethodTypes;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings } from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
@@ -15,11 +15,9 @@ export default async function LinkingFailedPage(props: {
|
||||
const { organization, error } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Alert, AlertType } from "@/components/alert";
|
||||
import { Button } from "@/components/button";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
@@ -20,21 +20,17 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { organization, postErrorRedirectUrl } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { LDAPUsernamePasswordForm } from "@/components/ldap-username-password-form";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { headers } from "next/headers";
|
||||
@@ -18,21 +18,17 @@ export default async function Page(props: {
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
// return login failed if no linking or creation is allowed and no user was found
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -19,18 +19,14 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const organization = searchParams?.organization;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const identityProviders = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: organization,
|
||||
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
|
||||
}).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UsernameForm } from "@/components/username-form";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getActiveIdentityProviders, getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
@@ -24,38 +24,28 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const submit: boolean = searchParams?.submit === "true";
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
const contextLoginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const contextLoginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const identityProviders = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: organization ?? defaultOrganization,
|
||||
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ?? defaultOrganization,
|
||||
}).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings } from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
@@ -8,13 +8,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const { organization } = searchParams;
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -2,8 +2,9 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SessionsClearList } from "@/components/sessions-clear-list";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getAllSessionCookieIds } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg, listSessions } from "@/lib/zitadel";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getBrandingSettings, getDefaultOrg, listSessions, ServiceConfig } from "@/lib/zitadel";
|
||||
import { verifyJwt } from "@zitadel/client/node";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
@@ -11,17 +12,14 @@ import { headers } from "next/headers";
|
||||
|
||||
export async function generateMetadata(): Promise<Metadata> {
|
||||
const t = await getTranslations("logout");
|
||||
return { title: t('title')};
|
||||
return { title: t("title") };
|
||||
}
|
||||
|
||||
async function loadSessions({ serviceUrl }: { serviceUrl: string }) {
|
||||
async function loadSessions({ serviceConfig }: { serviceConfig: ServiceConfig }) {
|
||||
const cookieIds = await getAllSessionCookieIds();
|
||||
|
||||
if (cookieIds && cookieIds.length) {
|
||||
const response = await listSessions({
|
||||
serviceUrl,
|
||||
ids: cookieIds.filter((id) => !!id) as string[],
|
||||
});
|
||||
const response = await listSessions({ serviceConfig, ids: cookieIds.filter((id) => !!id) as string[] });
|
||||
return response?.sessions ?? [];
|
||||
} else {
|
||||
console.info("No session cookie found.");
|
||||
@@ -33,30 +31,46 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const organization = searchParams?.organization;
|
||||
const postLogoutRedirectUri = searchParams?.post_logout_redirect || searchParams?.post_logout_redirect_uri;
|
||||
const logoutHint = searchParams?.logout_hint;
|
||||
// TODO implement with new translation service
|
||||
// const UILocales = searchParams?.ui_locales;
|
||||
const logoutToken = searchParams?.logout_token;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let postLogoutRedirectUri, logoutHint;
|
||||
if (logoutToken) {
|
||||
try {
|
||||
const payload = await verifyJwt<{ post_logout_redirect_uri?: string; logoutHint?: string }>(
|
||||
logoutToken,
|
||||
`${serviceConfig.baseUrl}/oauth/v2/keys`,
|
||||
{
|
||||
instanceHost: serviceConfig.instanceHost,
|
||||
publicHost: serviceConfig.publicHost,
|
||||
},
|
||||
);
|
||||
console.log("logout token payload", payload);
|
||||
|
||||
if (payload.post_logout_redirect_uri && typeof payload.post_logout_redirect_uri === "string") {
|
||||
postLogoutRedirectUri = payload.post_logout_redirect_uri;
|
||||
}
|
||||
if (payload.logout_hint && typeof payload.logout_hint === "string") {
|
||||
logoutHint = payload.logout_hint;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to verify logout token", error);
|
||||
}
|
||||
}
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig });
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
let sessions = await loadSessions({ serviceUrl });
|
||||
let sessions = await loadSessions({ serviceConfig });
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
});
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization });
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getSession, listAuthenticationMethodTypes } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -23,24 +23,20 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, requestId, organization, sessionId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionFactors = sessionId
|
||||
? await loadSessionById(serviceUrl, sessionId, organization)
|
||||
: await loadSessionByLoginname(serviceUrl, loginName, organization);
|
||||
? await loadSessionById(serviceConfig.baseUrl, sessionId, organization)
|
||||
: await loadSessionByLoginname(serviceConfig.baseUrl, loginName, organization);
|
||||
|
||||
async function loadSessionByLoginname(serviceUrl: string, loginName?: string, organization?: string) {
|
||||
return loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
return loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
}).then((session) => {
|
||||
if (session && session.factors?.user?.id) {
|
||||
return listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
return listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
|
||||
}).then((methods) => {
|
||||
return {
|
||||
factors: session?.factors,
|
||||
@@ -53,15 +49,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
|
||||
async function loadSessionById(host: string, sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
if (response?.session && response.session.factors?.user?.id) {
|
||||
return listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: response.session.factors.user.id,
|
||||
return listAuthenticationMethodTypes({ serviceConfig, userId: response.session.factors.user.id,
|
||||
}).then((methods) => {
|
||||
return {
|
||||
factors: response.session?.factors,
|
||||
@@ -72,9 +64,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import {
|
||||
getBrandingSettings,
|
||||
@@ -46,7 +46,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, checkAfter, force, requestId, organization, sessionId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionWithData = sessionId
|
||||
? await loadSessionById(sessionId, organization)
|
||||
@@ -59,11 +59,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
throw Error("Could not get user id from session");
|
||||
}
|
||||
|
||||
return listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
return listAuthenticationMethodTypes({ serviceConfig, userId,
|
||||
}).then((methods) => {
|
||||
return getUserByID({ serviceUrl, userId }).then((user) => {
|
||||
return getUserByID({ serviceConfig, userId }).then((user) => {
|
||||
const humanUser = user.user?.type.case === "human" ? user.user?.type.value : undefined;
|
||||
|
||||
return {
|
||||
@@ -79,9 +77,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
}
|
||||
|
||||
async function loadSessionByLoginname(loginName?: string, organization?: string) {
|
||||
return loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
return loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
@@ -92,22 +88,16 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
|
||||
async function loadSessionById(sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((sessionResponse) => {
|
||||
return getAuthMethodsAndUser(sessionResponse.session);
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: sessionWithData.factors?.user?.organizationId,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionWithData.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
const { valid } = isSessionValid(sessionWithData);
|
||||
|
||||
@@ -4,8 +4,8 @@ import { LoginOTP } from "@/components/login-otp";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getOriginalHost } from "@/lib/server/host";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getPublicHost } from "@/lib/server/host";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getLoginSettings, getSession } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -25,8 +25,8 @@ export default async function Page(props: {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
const {
|
||||
loginName, // send from password page
|
||||
@@ -40,18 +40,11 @@ export default async function Page(props: {
|
||||
|
||||
const session = sessionId
|
||||
? await loadSessionById(sessionId, organization)
|
||||
: await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: { loginName, organization },
|
||||
});
|
||||
: await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization } });
|
||||
|
||||
async function loadSessionById(sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((response) => {
|
||||
if (response?.session) {
|
||||
return response.session;
|
||||
}
|
||||
@@ -60,12 +53,12 @@ export default async function Page(props: {
|
||||
|
||||
// email links do not come with organization, thus we need to use the session's organization
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization: organization ?? session?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization: organization ?? session?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { TotpRegister } from "@/components/totp-register";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { addOTPEmail, addOTPSMS, getBrandingSettings, getLoginSettings, registerTOTP } from "@/lib/zitadel";
|
||||
import { RegisterTOTPResponse } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
@@ -24,20 +24,14 @@ export default async function Page(props: {
|
||||
const { method } = params;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const session = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
const session = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
@@ -46,9 +40,7 @@ export default async function Page(props: {
|
||||
let totpResponse: RegisterTOTPResponse | undefined, error: Error | undefined;
|
||||
if (session && session.factors?.user?.id) {
|
||||
if (method === "time-based") {
|
||||
await registerTOTP({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
await registerTOTP({ serviceConfig, userId: session.factors.user.id,
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp) {
|
||||
@@ -59,17 +51,13 @@ export default async function Page(props: {
|
||||
error = err;
|
||||
});
|
||||
} else if (method === "sms") {
|
||||
await addOTPSMS({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
await addOTPSMS({ serviceConfig, userId: session.factors.user.id,
|
||||
}).catch((_error) => {
|
||||
// TODO: Throw this error?
|
||||
new Error("Could not add OTP via SMS");
|
||||
});
|
||||
} else if (method === "email") {
|
||||
await addOTPEmail({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
await addOTPEmail({ serviceConfig, userId: session.factors.user.id,
|
||||
}).catch((_error) => {
|
||||
// TODO: Throw this error?
|
||||
new Error("Could not add OTP via Email");
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -22,20 +22,16 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, altPassword, requestId, organization, sessionId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionFactors = sessionId
|
||||
? await loadSessionById(serviceUrl, sessionId, organization)
|
||||
: await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: { loginName, organization },
|
||||
? await loadSessionById(serviceConfig.baseUrl, sessionId, organization)
|
||||
: await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
|
||||
});
|
||||
|
||||
async function loadSessionById(serviceUrl: string, sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
if (response?.session) {
|
||||
@@ -44,9 +40,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { RegisterPasskey } from "@/components/register-passkey";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
@@ -23,31 +23,25 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { userId, loginName, prompt, organization, requestId, code, codeId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
// also allow no session to be found for userId-based flows
|
||||
let session: Session | undefined;
|
||||
if (loginName) {
|
||||
session = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
session = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
let user: User | undefined;
|
||||
let displayName: string | undefined;
|
||||
if (userId) {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId,
|
||||
const userResponse = await getUserByID({ serviceConfig, userId,
|
||||
});
|
||||
user = userResponse.user;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { ChangePasswordForm } from "@/components/change-password-form";
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -17,34 +17,26 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
|
||||
export default async function Page(props: { searchParams: Promise<Record<string | number | symbol, string | undefined>> }) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const { loginName, organization, requestId } = searchParams;
|
||||
|
||||
// also allow no session to be found (ignoreUnkownUsername)
|
||||
const sessionFactors = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const passwordComplexity = await getPasswordComplexitySettings({
|
||||
serviceUrl,
|
||||
organization: sessionFactors?.factors?.user?.organizationId,
|
||||
const passwordComplexity = await getPasswordComplexitySettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: sessionFactors?.factors?.user?.organizationId,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: sessionFactors?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { PasswordForm } from "@/components/password-form";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getDefaultOrg, getLoginSettings } from "@/lib/zitadel";
|
||||
import { Organization } from "@zitadel/proto/zitadel/org/v2/org_pb";
|
||||
@@ -21,13 +21,11 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
let { loginName, organization, requestId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let defaultOrganization;
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
|
||||
if (org) {
|
||||
defaultOrganization = org.id;
|
||||
@@ -37,9 +35,7 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
// also allow no session to be found (ignoreUnkownUsername)
|
||||
let sessionFactors;
|
||||
try {
|
||||
sessionFactors = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
@@ -49,13 +45,9 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
console.warn(error);
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: organization ?? defaultOrganization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization ?? defaultOrganization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SetPasswordForm } from "@/components/set-password-form";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getLoginSettings, getPasswordComplexitySettings, getUserByID } from "@/lib/zitadel";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
@@ -23,41 +23,31 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { userId, loginName, organization, requestId, code, initial } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
// also allow no session to be found (ignoreUnkownUsername)
|
||||
let session: Session | undefined;
|
||||
if (loginName) {
|
||||
session = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
session = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const passwordComplexity = await getPasswordComplexitySettings({
|
||||
serviceUrl,
|
||||
organization: session?.factors?.user?.organizationId,
|
||||
const passwordComplexity = await getPasswordComplexitySettings({ serviceConfig, organization: session?.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
let user: User | undefined;
|
||||
let displayName: string | undefined;
|
||||
if (userId) {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId,
|
||||
const userResponse = await getUserByID({ serviceConfig, userId,
|
||||
});
|
||||
user = userResponse.user;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { RegisterForm } from "@/components/register-form";
|
||||
import { SignInWithIdp } from "@/components/sign-in-with-idp";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import {
|
||||
getActiveIdentityProviders,
|
||||
getBrandingSettings,
|
||||
@@ -29,39 +29,27 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
let { firstname, lastname, email, organization, requestId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
organization = org.id;
|
||||
}
|
||||
}
|
||||
|
||||
const legal = await getLegalAndSupportSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
|
||||
});
|
||||
const passwordComplexitySettings = await getPasswordComplexitySettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const identityProviders = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: organization,
|
||||
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization,
|
||||
}).then((resp) => {
|
||||
return resp.identityProviders.filter((idp) => {
|
||||
return idp.options?.isAutoCreation || idp.options?.isCreationAllowed; // check if IDP allows to create account automatically or manual creation is allowed
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { SetRegisterPasswordForm } from "@/components/set-register-password-form";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import {
|
||||
getBrandingSettings,
|
||||
getDefaultOrg,
|
||||
@@ -18,12 +18,10 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
let { firstname, lastname, email, organization, requestId } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
if (!organization) {
|
||||
const org: Organization | null = await getDefaultOrg({
|
||||
serviceUrl,
|
||||
});
|
||||
const org: Organization | null = await getDefaultOrg({ serviceConfig, });
|
||||
if (org) {
|
||||
organization = org.id;
|
||||
}
|
||||
@@ -31,23 +29,15 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
|
||||
const missingData = !firstname || !lastname || !email || !organization;
|
||||
|
||||
const legal = await getLegalAndSupportSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const legal = await getLegalAndSupportSettings({ serviceConfig, organization,
|
||||
});
|
||||
const passwordComplexitySettings = await getPasswordComplexitySettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const passwordComplexitySettings = await getPasswordComplexitySettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return missingData ? (
|
||||
|
||||
@@ -5,9 +5,9 @@ import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getMostRecentCookieWithLoginname, getSessionCookieById } from "@/lib/cookies";
|
||||
import { completeDeviceAuthorization } from "@/lib/server/device";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getLoginSettings, getSession } from "@/lib/zitadel";
|
||||
import { getBrandingSettings, getLoginSettings, getSession, ServiceConfig } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { headers } from "next/headers";
|
||||
@@ -18,11 +18,9 @@ export async function generateMetadata(): Promise<Metadata> {
|
||||
return { title: t("title", { user: "" }) };
|
||||
}
|
||||
|
||||
async function loadSessionById(serviceUrl: string, sessionId: string, organization?: string) {
|
||||
async function loadSessionById(serviceConfig: ServiceConfig, sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
if (response?.session) {
|
||||
@@ -35,13 +33,11 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const { loginName, requestId, organization, sessionId } = searchParams;
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
// complete device authorization flow if device requestId is present
|
||||
@@ -75,17 +71,13 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
}
|
||||
|
||||
const sessionFactors = sessionId
|
||||
? await loadSessionById(serviceUrl, sessionId, organization)
|
||||
: await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: { loginName, organization },
|
||||
? await loadSessionById(serviceConfig, sessionId, organization)
|
||||
: await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
|
||||
});
|
||||
|
||||
let loginSettings;
|
||||
if (!requestId) {
|
||||
loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
loginSettings = await getLoginSettings({ serviceConfig, organization,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { LoginPasskey } from "@/components/login-passkey";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getSessionCookieById } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getSession } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -22,25 +22,19 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, requestId, sessionId, organization } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const sessionFactors = sessionId
|
||||
? await loadSessionById(sessionId, organization)
|
||||
: await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: { loginName, organization },
|
||||
: await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
|
||||
});
|
||||
|
||||
async function loadSessionById(sessionId: string, organization?: string) {
|
||||
const recent = await getSessionCookieById({ sessionId, organization });
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
return getSession({ serviceConfig, sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((response) => {
|
||||
if (response?.session) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { RegisterU2f } from "@/components/register-u2f";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings } from "@/lib/zitadel";
|
||||
import { Metadata } from "next";
|
||||
@@ -21,19 +21,15 @@ export default async function Page(props: { searchParams: Promise<Record<string
|
||||
const { loginName, organization, requestId, checkAfter } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionFactors = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: {
|
||||
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
});
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -3,9 +3,9 @@ import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { VerifyForm } from "@/components/verify-form";
|
||||
import { getPublicHostWithProtocol } from "@/lib/server/host";
|
||||
import { sendEmailCode, sendInviteEmailCode } from "@/lib/server/verify";
|
||||
import { getOriginalHostWithProtocol } from "@/lib/server/host";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
@@ -24,12 +24,9 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const { userId, loginName, code, organization, requestId, invite, send } = searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
});
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization });
|
||||
|
||||
let sessionFactors;
|
||||
let user: User | undefined;
|
||||
@@ -43,7 +40,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
async function sendEmail(userId: string) {
|
||||
const hostWithProtocol = await getOriginalHostWithProtocol();
|
||||
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
|
||||
|
||||
if (invite === "true") {
|
||||
await sendInviteEmailCode({
|
||||
@@ -70,7 +67,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
|
||||
if ("loginName" in searchParams) {
|
||||
sessionFactors = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
@@ -85,10 +82,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
await sendEmail(userId);
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId });
|
||||
if (userResponse) {
|
||||
user = userResponse.user;
|
||||
if (user?.type.case === "human") {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DynamicTheme } from "@/components/dynamic-theme";
|
||||
import { Translated } from "@/components/translated";
|
||||
import { UserAvatar } from "@/components/user-avatar";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { loadMostRecentSession } from "@/lib/session";
|
||||
import { getBrandingSettings, getUserByID } from "@/lib/zitadel";
|
||||
import { HumanUser, User } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
@@ -11,18 +11,14 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
const searchParams = await props.searchParams;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const { loginName, organization, userId } = searchParams;
|
||||
|
||||
const branding = await getBrandingSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
const branding = await getBrandingSettings({ serviceConfig, organization,
|
||||
});
|
||||
|
||||
const sessionFactors = await loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams: { loginName, organization },
|
||||
const sessionFactors = await loadMostRecentSession({ serviceConfig, sessionParams: { loginName, organization },
|
||||
}).catch((error) => {
|
||||
console.warn("Error loading session:", error);
|
||||
});
|
||||
@@ -33,9 +29,7 @@ export default async function Page(props: { searchParams: Promise<any> }) {
|
||||
throw Error("Failed to get user id");
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: id,
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: id,
|
||||
});
|
||||
|
||||
let user: User | undefined;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { getAllSessions } from "@/lib/cookies";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { validateAuthRequest, isRSCRequest } from "@/lib/auth-utils";
|
||||
import { handleOIDCFlowInitiation, handleSAMLFlowInitiation, FlowInitiationParams } from "@/lib/server/flow-initiation";
|
||||
import { listSessions } from "@/lib/zitadel";
|
||||
import { listSessions, ServiceConfig } from "@/lib/zitadel";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
@@ -13,18 +13,15 @@ export const fetchCache = "default-no-store";
|
||||
// Add this to prevent RSC requests
|
||||
export const runtime = "nodejs";
|
||||
|
||||
async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise<Session[]> {
|
||||
const response = await listSessions({
|
||||
serviceUrl,
|
||||
ids: ids.filter((id: string | undefined) => !!id),
|
||||
});
|
||||
async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise<Session[]> {
|
||||
const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id) });
|
||||
|
||||
return response?.sessions ?? [];
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const searchParams = request.nextUrl.searchParams;
|
||||
|
||||
@@ -43,17 +40,11 @@ export async function GET(request: NextRequest) {
|
||||
const ids = sessionCookies.map((s) => s.id);
|
||||
let sessions: Session[] = [];
|
||||
if (ids && ids.length) {
|
||||
sessions = await loadSessions({ serviceUrl, ids });
|
||||
sessions = await loadSessions({ serviceConfig, ids });
|
||||
}
|
||||
|
||||
// Flow initiation - delegate to appropriate handler
|
||||
const flowParams: FlowInitiationParams = {
|
||||
serviceUrl,
|
||||
requestId,
|
||||
sessions,
|
||||
sessionCookies,
|
||||
request,
|
||||
};
|
||||
const flowParams: FlowInitiationParams = { serviceConfig, requestId, sessions, sessionCookies, request };
|
||||
|
||||
if (requestId.startsWith("oidc_")) {
|
||||
return handleOIDCFlowInitiation(flowParams);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createServiceForHost } from "@/lib/service";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { Client } from "@zitadel/client";
|
||||
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||
import { headers } from "next/headers";
|
||||
@@ -7,10 +7,10 @@ import { NextResponse } from "next/server";
|
||||
|
||||
export async function GET() {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const settingsService: Client<typeof SettingsService> =
|
||||
await createServiceForHost(SettingsService, serviceUrl);
|
||||
await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
const settings = await settingsService
|
||||
.getSecuritySettings({})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { LANGS, LANGUAGE_COOKIE_NAME, LANGUAGE_HEADER_NAME } from "@/lib/i18n";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { getHostedLoginTranslation } from "@/lib/zitadel";
|
||||
import { JsonObject } from "@zitadel/client";
|
||||
import deepmerge from "deepmerge";
|
||||
@@ -13,7 +13,7 @@ export default getRequestConfig(async () => {
|
||||
let locale: string = fallback;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const languageHeader = await (await headers()).get(LANGUAGE_HEADER_NAME);
|
||||
if (languageHeader) {
|
||||
@@ -34,9 +34,7 @@ export default getRequestConfig(async () => {
|
||||
|
||||
let translations: JsonObject | {} = {};
|
||||
try {
|
||||
const i18nJSON = await getHostedLoginTranslation({
|
||||
serviceUrl,
|
||||
locale,
|
||||
const i18nJSON = await getHostedLoginTranslation({ serviceConfig, locale,
|
||||
organization: i18nOrganization,
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { describe, expect, test, beforeEach, afterEach } from "vitest";
|
||||
import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
|
||||
|
||||
describe("Deployment utilities", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset environment before each test
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe("hasSystemUserCredentials", () => {
|
||||
test("should return true when all system user credentials are present", () => {
|
||||
process.env.AUDIENCE = "https://api.zitadel.cloud";
|
||||
process.env.SYSTEM_USER_ID = "12345";
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
|
||||
|
||||
expect(hasSystemUserCredentials()).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false when AUDIENCE is missing", () => {
|
||||
process.env.AUDIENCE = undefined as any;
|
||||
process.env.SYSTEM_USER_ID = "12345";
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
|
||||
|
||||
expect(hasSystemUserCredentials()).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when SYSTEM_USER_ID is missing", () => {
|
||||
process.env.AUDIENCE = "https://api.zitadel.cloud";
|
||||
process.env.SYSTEM_USER_ID = undefined as any;
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY = "-----BEGIN PRIVATE KEY-----\n...";
|
||||
|
||||
expect(hasSystemUserCredentials()).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when SYSTEM_USER_PRIVATE_KEY is missing", () => {
|
||||
process.env.AUDIENCE = "https://api.zitadel.cloud";
|
||||
process.env.SYSTEM_USER_ID = "12345";
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
|
||||
|
||||
expect(hasSystemUserCredentials()).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when all credentials are missing", () => {
|
||||
process.env.AUDIENCE = undefined as any;
|
||||
process.env.SYSTEM_USER_ID = undefined as any;
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY = undefined as any;
|
||||
|
||||
expect(hasSystemUserCredentials()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hasServiceUserToken", () => {
|
||||
test("should return true when ZITADEL_SERVICE_USER_TOKEN is present", () => {
|
||||
process.env.ZITADEL_SERVICE_USER_TOKEN = "token123";
|
||||
|
||||
expect(hasServiceUserToken()).toBe(true);
|
||||
});
|
||||
|
||||
test("should return false when ZITADEL_SERVICE_USER_TOKEN is not set", () => {
|
||||
process.env.ZITADEL_SERVICE_USER_TOKEN = undefined as any;
|
||||
|
||||
expect(hasServiceUserToken()).toBe(false);
|
||||
});
|
||||
|
||||
test("should return false when ZITADEL_SERVICE_USER_TOKEN is empty string", () => {
|
||||
process.env.ZITADEL_SERVICE_USER_TOKEN = "";
|
||||
|
||||
expect(hasServiceUserToken()).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/**
|
||||
* Checks if system user credentials are available for JWT authentication.
|
||||
*
|
||||
* System user authentication requires:
|
||||
* - AUDIENCE: The API audience for JWT authentication
|
||||
* - SYSTEM_USER_ID: The system user's ID
|
||||
* - SYSTEM_USER_PRIVATE_KEY: The private key for JWT signing
|
||||
*
|
||||
* Both multi-tenant and self-hosted deployments can use system user authentication.
|
||||
*
|
||||
* @returns true if system user credentials are present, false otherwise
|
||||
*/
|
||||
export function hasSystemUserCredentials(): boolean {
|
||||
return !!process.env.AUDIENCE && !!process.env.SYSTEM_USER_ID && !!process.env.SYSTEM_USER_PRIVATE_KEY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if service user token is available for authentication.
|
||||
*
|
||||
* @returns true if ZITADEL_SERVICE_USER_TOKEN is present, false otherwise
|
||||
*/
|
||||
export function hasServiceUserToken(): boolean {
|
||||
return !!process.env.ZITADEL_SERVICE_USER_TOKEN;
|
||||
}
|
||||
@@ -1,25 +1,19 @@
|
||||
import { Cookie } from "@/lib/cookies";
|
||||
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
||||
import { createCallback, getLoginSettings } from "@/lib/zitadel";
|
||||
import { createCallback, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { CreateCallbackRequestSchema, SessionSchema } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { isSessionValid } from "./session";
|
||||
|
||||
type LoginWithOIDCAndSession = {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
authRequest: string;
|
||||
sessionId: string;
|
||||
sessions: Session[];
|
||||
sessionCookies: Cookie[];
|
||||
};
|
||||
export async function loginWithOIDCAndSession({
|
||||
serviceUrl,
|
||||
authRequest,
|
||||
sessionId,
|
||||
sessions,
|
||||
sessionCookies,
|
||||
}: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
|
||||
export async function loginWithOIDCAndSession({ serviceConfig, authRequest, sessionId, sessions, sessionCookies }: LoginWithOIDCAndSession): Promise<{ error: string } | { redirect: string }> {
|
||||
console.log(`Login with session: ${sessionId} and authRequest: ${authRequest}`);
|
||||
|
||||
const selectedSession = sessions.find((s) => s.id === sessionId);
|
||||
@@ -27,10 +21,7 @@ export async function loginWithOIDCAndSession({
|
||||
if (selectedSession && selectedSession.id) {
|
||||
console.log(`Found session ${selectedSession.id}`);
|
||||
|
||||
const isValid = await isSessionValid({
|
||||
serviceUrl,
|
||||
session: selectedSession,
|
||||
});
|
||||
const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
|
||||
|
||||
console.log("Session is valid:", isValid);
|
||||
|
||||
@@ -61,9 +52,7 @@ export async function loginWithOIDCAndSession({
|
||||
};
|
||||
|
||||
try {
|
||||
const { callbackUrl } = await createCallback({
|
||||
serviceUrl,
|
||||
req: create(CreateCallbackRequestSchema, {
|
||||
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
|
||||
authRequestId: authRequest,
|
||||
callbackKind: {
|
||||
case: "session",
|
||||
@@ -81,10 +70,7 @@ export async function loginWithOIDCAndSession({
|
||||
// handle already handled gracefully as these could come up if old emails with requestId are used (reset password, register emails etc.)
|
||||
console.error(error);
|
||||
if (error && typeof error === "object" && "code" in error && error?.code === 9) {
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: selectedSession.factors?.user?.organizationId,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
|
||||
|
||||
if (loginSettings?.defaultRedirectUri) {
|
||||
return { redirect: loginSettings.defaultRedirectUri };
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Cookie } from "@/lib/cookies";
|
||||
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
||||
import { createResponse, getLoginSettings } from "@/lib/zitadel";
|
||||
import { createResponse, getLoginSettings, ServiceConfig } from "@/lib/zitadel";
|
||||
import { create } from "@zitadel/client";
|
||||
import { CreateResponseRequestSchema } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
@@ -9,7 +9,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { isSessionValid } from "./session";
|
||||
|
||||
type LoginWithSAMLAndSession = {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
samlRequest: string;
|
||||
sessionId: string;
|
||||
sessions: Session[];
|
||||
@@ -82,13 +82,7 @@ export async function getSAMLFormCookie(uid: string): Promise<string | null> {
|
||||
}
|
||||
}
|
||||
|
||||
export async function loginWithSAMLAndSession({
|
||||
serviceUrl,
|
||||
samlRequest,
|
||||
sessionId,
|
||||
sessions,
|
||||
sessionCookies,
|
||||
}: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
|
||||
export async function loginWithSAMLAndSession({ serviceConfig, samlRequest, sessionId, sessions, sessionCookies }: LoginWithSAMLAndSession): Promise<{ error: string } | { redirect: string }> {
|
||||
console.log(`Login with session: ${sessionId} and samlRequest: ${samlRequest}`);
|
||||
|
||||
const selectedSession = sessions.find((s) => s.id === sessionId);
|
||||
@@ -96,10 +90,7 @@ export async function loginWithSAMLAndSession({
|
||||
if (selectedSession && selectedSession.id) {
|
||||
console.log(`Found session ${selectedSession.id}`);
|
||||
|
||||
const isValid = await isSessionValid({
|
||||
serviceUrl,
|
||||
session: selectedSession,
|
||||
});
|
||||
const isValid = await isSessionValid({ serviceConfig, session: selectedSession });
|
||||
|
||||
console.log("Session is valid:", isValid);
|
||||
|
||||
@@ -129,9 +120,7 @@ export async function loginWithSAMLAndSession({
|
||||
|
||||
// works not with _rsc request
|
||||
try {
|
||||
const { url } = await createResponse({
|
||||
serviceUrl,
|
||||
req: create(CreateResponseRequestSchema, {
|
||||
const { url } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
|
||||
samlRequestId: samlRequest,
|
||||
responseKind: {
|
||||
case: "session",
|
||||
@@ -149,10 +138,7 @@ export async function loginWithSAMLAndSession({
|
||||
console.error(error);
|
||||
|
||||
if (error && typeof error === "object" && "code" in error && error?.code === 9) {
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: selectedSession.factors?.user?.organizationId,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: selectedSession.factors?.user?.organizationId });
|
||||
|
||||
if (loginSettings?.defaultRedirectUri) {
|
||||
return { redirect: loginSettings.defaultRedirectUri };
|
||||
|
||||
@@ -3,14 +3,11 @@
|
||||
import { createUserServiceClient } from "@zitadel/client/v2";
|
||||
import { headers } from "next/headers";
|
||||
import { getSessionCookieById } from "./cookies";
|
||||
import { getServiceUrlFromHeaders } from "./service-url";
|
||||
import { createServerTransport, getSession } from "./zitadel";
|
||||
import { getServiceConfig } from "./service-url";
|
||||
import { createServerTransport, getSession, ServiceConfig } from "./zitadel";
|
||||
|
||||
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
||||
const transportPromise = await createServerTransport(
|
||||
sessionToken,
|
||||
serviceUrl,
|
||||
);
|
||||
const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
|
||||
const transportPromise = await createServerTransport(sessionToken, serviceConfig);
|
||||
return createUserServiceClient(transportPromise);
|
||||
};
|
||||
|
||||
@@ -22,13 +19,11 @@ export async function setMyPassword({
|
||||
password: string;
|
||||
}) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionCookie = await getSessionCookieById({ sessionId });
|
||||
|
||||
const { session } = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
const { session } = await getSession({ serviceConfig, sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
|
||||
@@ -36,7 +31,7 @@ export async function setMyPassword({
|
||||
return { error: "Could not load session" };
|
||||
}
|
||||
|
||||
const service = await myUserService(serviceUrl, `${sessionCookie.token}`);
|
||||
const service = await myUserService(serviceConfig, `${sessionCookie.token}`);
|
||||
|
||||
if (!session?.factors?.user?.id) {
|
||||
return { error: "No user id found in session" };
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import { getAllSessions } from "@/lib/cookies";
|
||||
import { loginWithOIDCAndSession } from "@/lib/oidc";
|
||||
import { loginWithSAMLAndSession } from "@/lib/saml";
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { listSessions } from "@/lib/zitadel";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import { listSessions, ServiceConfig } from "@/lib/zitadel";
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { headers } from "next/headers";
|
||||
|
||||
@@ -14,10 +14,8 @@ export interface AuthFlowParams {
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
async function loadSessions({ serviceUrl, ids }: { serviceUrl: string; ids: string[] }): Promise<Session[]> {
|
||||
const response = await listSessions({
|
||||
serviceUrl,
|
||||
ids: ids.filter((id: string | undefined) => !!id),
|
||||
async function loadSessions({ serviceConfig, ids }: { serviceConfig: ServiceConfig; ids: string[] }): Promise<Session[]> {
|
||||
const response = await listSessions({ serviceConfig, ids: ids.filter((id: string | undefined) => !!id),
|
||||
});
|
||||
|
||||
return response?.sessions ?? [];
|
||||
@@ -33,21 +31,19 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
|
||||
const { sessionId, requestId } = command;
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const sessionCookies = await getAllSessions();
|
||||
const ids = sessionCookies.map((s) => s.id);
|
||||
let sessions: Session[] = [];
|
||||
|
||||
if (ids && ids.length) {
|
||||
sessions = await loadSessions({ serviceUrl, ids });
|
||||
sessions = await loadSessions({ serviceConfig, ids });
|
||||
}
|
||||
|
||||
if (requestId.startsWith("oidc_")) {
|
||||
// Complete OIDC flow
|
||||
const result = await loginWithOIDCAndSession({
|
||||
serviceUrl,
|
||||
authRequest: requestId.replace("oidc_", ""),
|
||||
const result = await loginWithOIDCAndSession({ serviceConfig, authRequest: requestId.replace("oidc_", ""),
|
||||
sessionId,
|
||||
sessions,
|
||||
sessionCookies,
|
||||
@@ -62,9 +58,7 @@ export async function completeAuthFlow(command: AuthFlowParams): Promise<{ error
|
||||
return result;
|
||||
} else if (requestId.startsWith("saml_")) {
|
||||
// Complete SAML flow
|
||||
const result = await loginWithSAMLAndSession({
|
||||
serviceUrl,
|
||||
samlRequest: requestId.replace("saml_", ""),
|
||||
const result = await loginWithSAMLAndSession({ serviceConfig, samlRequest: requestId.replace("saml_", ""),
|
||||
sessionId,
|
||||
sessions,
|
||||
sessionCookies,
|
||||
|
||||
@@ -14,7 +14,7 @@ import { Challenges, RequestChallenges } from "@zitadel/proto/zitadel/session/v2
|
||||
import { Session } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { Checks } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { headers } from "next/headers";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
|
||||
type CustomCookieData = {
|
||||
id: string;
|
||||
@@ -46,7 +46,7 @@ export async function createSessionAndUpdateCookie(command: {
|
||||
lifetime?: Duration;
|
||||
}): Promise<Session> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let sessionLifetime = command.lifetime;
|
||||
|
||||
@@ -59,16 +59,12 @@ export async function createSessionAndUpdateCookie(command: {
|
||||
} as Duration; // for usecases where the lifetime is not specified (user discovery)
|
||||
}
|
||||
|
||||
const createdSession = await createSessionFromChecks({
|
||||
serviceUrl,
|
||||
checks: command.checks,
|
||||
const createdSession = await createSessionFromChecks({ serviceConfig, checks: command.checks,
|
||||
lifetime: sessionLifetime,
|
||||
});
|
||||
|
||||
if (createdSession) {
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: createdSession.sessionId,
|
||||
return getSession({ serviceConfig, sessionId: createdSession.sessionId,
|
||||
sessionToken: createdSession.sessionToken,
|
||||
}).then(async (response) => {
|
||||
if (response?.session && response.session?.factors?.user?.loginName) {
|
||||
@@ -89,7 +85,7 @@ export async function createSessionAndUpdateCookie(command: {
|
||||
sessionCookie.organization = response.session.factors.user.organizationId;
|
||||
}
|
||||
|
||||
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||
const securitySettings = await getSecuritySettings({ serviceConfig });
|
||||
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
|
||||
|
||||
await addSessionToCookie({ session: sessionCookie, iFrameEnabled });
|
||||
@@ -119,7 +115,7 @@ export async function createSessionForIdpAndUpdateCookie({
|
||||
lifetime?: Duration;
|
||||
}): Promise<Session> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
let sessionLifetime = lifetime;
|
||||
|
||||
@@ -132,9 +128,7 @@ export async function createSessionForIdpAndUpdateCookie({
|
||||
} as Duration;
|
||||
}
|
||||
|
||||
const createdSession = await createSessionForUserIdAndIdpIntent({
|
||||
serviceUrl,
|
||||
userId,
|
||||
const createdSession = await createSessionForUserIdAndIdpIntent({ serviceConfig, userId,
|
||||
idpIntent,
|
||||
lifetime: sessionLifetime,
|
||||
}).catch((error: ErrorDetail | CredentialsCheckError) => {
|
||||
@@ -152,9 +146,7 @@ export async function createSessionForIdpAndUpdateCookie({
|
||||
throw "Could not create session";
|
||||
}
|
||||
|
||||
const { session } = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: createdSession.sessionId,
|
||||
const { session } = await getSession({ serviceConfig, sessionId: createdSession.sessionId,
|
||||
sessionToken: createdSession.sessionToken,
|
||||
});
|
||||
|
||||
@@ -180,7 +172,7 @@ export async function createSessionForIdpAndUpdateCookie({
|
||||
sessionCookie.organization = session.factors.user.organizationId;
|
||||
}
|
||||
|
||||
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||
const securitySettings = await getSecuritySettings({ serviceConfig });
|
||||
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
|
||||
|
||||
return addSessionToCookie({ session: sessionCookie, iFrameEnabled }).then(() => {
|
||||
@@ -200,11 +192,9 @@ export async function setSessionAndUpdateCookie(command: {
|
||||
lifetime: Duration;
|
||||
}) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
return setSession({
|
||||
serviceUrl,
|
||||
sessionId: command.recentCookie.id,
|
||||
return setSession({ serviceConfig, sessionId: command.recentCookie.id,
|
||||
sessionToken: command.recentCookie.token,
|
||||
challenges: command.challenges,
|
||||
checks: command.checks,
|
||||
@@ -227,9 +217,7 @@ export async function setSessionAndUpdateCookie(command: {
|
||||
sessionCookie.requestId = command.requestId;
|
||||
}
|
||||
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
return getSession({ serviceConfig, sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
}).then(async (response) => {
|
||||
if (!response?.session || !response.session.factors?.user?.loginName) {
|
||||
@@ -252,7 +240,7 @@ export async function setSessionAndUpdateCookie(command: {
|
||||
newCookie.requestId = sessionCookie.requestId;
|
||||
}
|
||||
|
||||
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||
const securitySettings = await getSecuritySettings({ serviceConfig });
|
||||
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
|
||||
|
||||
return updateSessionCookie({
|
||||
|
||||
@@ -2,19 +2,17 @@
|
||||
|
||||
import { authorizeOrDenyDeviceAuthorization } from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
|
||||
export async function completeDeviceAuthorization(
|
||||
deviceAuthorizationId: string,
|
||||
session?: { sessionId: string; sessionToken: string },
|
||||
) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
// without the session, device auth request is denied
|
||||
return authorizeOrDenyDeviceAuthorization({
|
||||
serviceUrl,
|
||||
deviceAuthorizationId,
|
||||
return authorizeOrDenyDeviceAuthorization({ serviceConfig, deviceAuthorizationId,
|
||||
session,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
getSAMLRequest,
|
||||
getSecuritySettings,
|
||||
startIdentityProviderFlow,
|
||||
ServiceConfig,
|
||||
} from "@/lib/zitadel";
|
||||
import { sendLoginname, SendLoginnameCommand } from "@/lib/server/loginname";
|
||||
import { idpTypeToSlug } from "@/lib/idp";
|
||||
@@ -47,7 +48,7 @@ const gotoAccounts = ({
|
||||
};
|
||||
|
||||
export interface FlowInitiationParams {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
requestId: string;
|
||||
sessions: Session[];
|
||||
sessionCookies: any[];
|
||||
@@ -58,11 +59,9 @@ export interface FlowInitiationParams {
|
||||
* Handle OIDC flow initiation
|
||||
*/
|
||||
export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
|
||||
const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
|
||||
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
|
||||
|
||||
const { authRequest } = await getAuthRequest({
|
||||
serviceUrl,
|
||||
authRequestId: requestId.replace("oidc_", ""),
|
||||
const { authRequest } = await getAuthRequest({ serviceConfig, authRequestId: requestId.replace("oidc_", ""),
|
||||
});
|
||||
|
||||
let organization = "";
|
||||
@@ -85,9 +84,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
|
||||
console.log("Extracted org domain:", orgDomain);
|
||||
if (orgDomain) {
|
||||
const orgs = await getOrgsByDomain({
|
||||
serviceUrl,
|
||||
domain: orgDomain,
|
||||
const orgs = await getOrgsByDomain({ serviceConfig, domain: orgDomain,
|
||||
});
|
||||
|
||||
if (orgs.result && orgs.result.length === 1) {
|
||||
@@ -102,9 +99,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
const matched = IDP_SCOPE_REGEX.exec(idpScope);
|
||||
idpId = matched?.[1] ?? "";
|
||||
|
||||
const identityProviders = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: organization ? organization : undefined,
|
||||
const identityProviders = await getActiveIdentityProviders({ serviceConfig, orgId: organization ? organization : undefined,
|
||||
}).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
});
|
||||
@@ -137,9 +132,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
params.set("organization", organization);
|
||||
}
|
||||
|
||||
let url: string | null = await startIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
idpId,
|
||||
let url: string | null = await startIdentityProviderFlow({ serviceConfig, idpId,
|
||||
urls: {
|
||||
successUrl: `${origin}/idp/${provider}/process?` + new URLSearchParams(params),
|
||||
failureUrl: `${origin}/idp/${provider}/failure?` + new URLSearchParams(params),
|
||||
@@ -215,13 +208,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
}
|
||||
return NextResponse.redirect(loginNameUrl);
|
||||
} else if (authRequest.prompt.includes(Prompt.NONE)) {
|
||||
const securitySettings = await getSecuritySettings({
|
||||
serviceUrl,
|
||||
});
|
||||
const securitySettings = await getSecuritySettings({ serviceConfig, });
|
||||
|
||||
const selectedSession = await findValidSession({
|
||||
serviceUrl,
|
||||
sessions,
|
||||
const selectedSession = await findValidSession({ serviceConfig, sessions,
|
||||
authRequest,
|
||||
});
|
||||
|
||||
@@ -251,9 +240,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
sessionToken: cookie.token,
|
||||
};
|
||||
|
||||
const { callbackUrl } = await createCallback({
|
||||
serviceUrl,
|
||||
req: create(CreateCallbackRequestSchema, {
|
||||
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
|
||||
authRequestId: requestId.replace("oidc_", ""),
|
||||
callbackKind: {
|
||||
case: "session",
|
||||
@@ -275,9 +262,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
|
||||
return callbackResponse;
|
||||
} else {
|
||||
let selectedSession = await findValidSession({
|
||||
serviceUrl,
|
||||
sessions,
|
||||
let selectedSession = await findValidSession({ serviceConfig, sessions,
|
||||
authRequest,
|
||||
});
|
||||
|
||||
@@ -305,9 +290,7 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
};
|
||||
|
||||
try {
|
||||
const { callbackUrl } = await createCallback({
|
||||
serviceUrl,
|
||||
req: create(CreateCallbackRequestSchema, {
|
||||
const { callbackUrl } = await createCallback({ serviceConfig, req: create(CreateCallbackRequestSchema, {
|
||||
authRequestId: requestId.replace("oidc_", ""),
|
||||
callbackKind: {
|
||||
case: "session",
|
||||
@@ -359,11 +342,9 @@ export async function handleOIDCFlowInitiation(params: FlowInitiationParams): Pr
|
||||
* Handle SAML flow initiation
|
||||
*/
|
||||
export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Promise<NextResponse> {
|
||||
const { serviceUrl, requestId, sessions, sessionCookies, request } = params;
|
||||
const { serviceConfig, requestId, sessions, sessionCookies, request } = params;
|
||||
|
||||
const { samlRequest } = await getSAMLRequest({
|
||||
serviceUrl,
|
||||
samlRequestId: requestId.replace("saml_", ""),
|
||||
const { samlRequest } = await getSAMLRequest({ serviceConfig, samlRequestId: requestId.replace("saml_", ""),
|
||||
});
|
||||
|
||||
if (!samlRequest) {
|
||||
@@ -378,9 +359,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
|
||||
}
|
||||
|
||||
// Try to find a valid session
|
||||
let selectedSession = await findValidSession({
|
||||
serviceUrl,
|
||||
sessions,
|
||||
let selectedSession = await findValidSession({ serviceConfig, sessions,
|
||||
samlRequest,
|
||||
});
|
||||
|
||||
@@ -410,9 +389,7 @@ export async function handleSAMLFlowInitiation(params: FlowInitiationParams): Pr
|
||||
};
|
||||
|
||||
try {
|
||||
const { url, binding } = await createResponse({
|
||||
serviceUrl,
|
||||
req: create(CreateResponseRequestSchema, {
|
||||
const { url, binding } = await createResponse({ serviceConfig, req: create(CreateResponseRequestSchema, {
|
||||
samlRequestId: requestId.replace("saml_", ""),
|
||||
responseKind: {
|
||||
case: "session",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { describe, expect, test, vi, beforeEach, afterEach } from "vitest";
|
||||
import { getOriginalHost, getOriginalHostWithProtocol } from "./host";
|
||||
|
||||
// Mock the Next.js headers function
|
||||
vi.mock("next/headers", () => ({
|
||||
headers: vi.fn(),
|
||||
}));
|
||||
import { getInstanceHost, getPublicHostWithProtocol, getPublicHost } from "./host";
|
||||
|
||||
describe("Host utility functions", () => {
|
||||
beforeEach(() => {
|
||||
@@ -15,151 +10,94 @@ describe("Host utility functions", () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("getOriginalHost", () => {
|
||||
test("should return x-forwarded-host when available", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
describe("getInstanceHost", () => {
|
||||
test("should use x-zitadel-instance-host when available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-forwarded-host") return "zitadel.com";
|
||||
if (key === "x-original-host") return "backup.com";
|
||||
if (key === "host") return "internal.vercel.app";
|
||||
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
|
||||
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHost();
|
||||
expect(result).toBe("zitadel.com");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
|
||||
const result = getInstanceHost(mockHeaders);
|
||||
expect(result).toBe("instance.zitadel.cloud");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
|
||||
});
|
||||
|
||||
test("should fall back to x-original-host when x-forwarded-host is not available", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should use x-zitadel-forward-host when x-zitadel-instance-host is not available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-forwarded-host") return null;
|
||||
if (key === "x-original-host") return "original.com";
|
||||
if (key === "host") return "internal.vercel.app";
|
||||
if (key === "x-zitadel-instance-host") return null;
|
||||
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHost();
|
||||
expect(result).toBe("original.com");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
|
||||
const result = getInstanceHost(mockHeaders);
|
||||
expect(result).toBe("forward.zitadel.cloud");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
|
||||
});
|
||||
|
||||
test("should fall back to host when forwarded headers are not available", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should return null when neither x-zitadel-instance-host nor x-zitadel-forward-host are available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-forwarded-host") return null;
|
||||
if (key === "x-original-host") return null;
|
||||
if (key === "host") return "fallback.com";
|
||||
if (key === "x-zitadel-instance-host") return null;
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "x-forwarded-host") return "accounts.mycompany.com";
|
||||
if (key === "host") return "internal.server";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHost();
|
||||
expect(result).toBe("fallback.com");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-original-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("host");
|
||||
});
|
||||
|
||||
test("should throw error when no host is found", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => null),
|
||||
};
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
|
||||
});
|
||||
|
||||
test("should throw error when host is empty string", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => ""),
|
||||
};
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
|
||||
});
|
||||
|
||||
test("should throw error when host is not a string", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => 123),
|
||||
};
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
await expect(getOriginalHost()).rejects.toThrow("No host found in headers");
|
||||
const result = getInstanceHost(mockHeaders);
|
||||
expect(result).toBeNull();
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-instance-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getOriginalHostWithProtocol", () => {
|
||||
test("should return https for production domain", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
describe("getPublicHostWithProtocol", () => {
|
||||
test("should return https for production domain", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "zitadel.com"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://zitadel.com");
|
||||
});
|
||||
|
||||
test("should return http for localhost", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should return http for localhost", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "localhost:3000"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("http://localhost:3000");
|
||||
});
|
||||
|
||||
test("should return http for localhost without port", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should return http for localhost without port", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "localhost"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("http://localhost");
|
||||
});
|
||||
|
||||
test("should return https for custom domain", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should return https for custom domain", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "auth.company.com"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://auth.company.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Real-world scenarios", () => {
|
||||
test("should handle Vercel rewrite scenario", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle Vercel rewrite scenario", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
// Simulate Vercel rewrite: zitadel.com/login -> login-zitadel-qa.vercel.app
|
||||
@@ -167,16 +105,13 @@ describe("Host utility functions", () => {
|
||||
if (key === "host") return "login-zitadel-qa.vercel.app";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://zitadel.com");
|
||||
});
|
||||
|
||||
test("should handle CloudFlare proxy scenario", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle CloudFlare proxy scenario", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-forwarded-host") return "auth.company.com";
|
||||
@@ -184,114 +119,180 @@ describe("Host utility functions", () => {
|
||||
if (key === "host") return "cloudflare-worker.workers.dev";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHost();
|
||||
const result = getPublicHost(mockHeaders);
|
||||
expect(result).toBe("auth.company.com");
|
||||
});
|
||||
|
||||
test("should handle development environment", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle development environment", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "host") return "localhost:3000";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("http://localhost:3000");
|
||||
});
|
||||
|
||||
test("should handle staging environment with subdomain", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle staging environment with subdomain", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-forwarded-host") return "staging-auth.company.com";
|
||||
if (key === "host") return "staging-internal.vercel.app";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://staging-auth.company.com");
|
||||
});
|
||||
|
||||
test("should prioritize x-zitadel-instance-host in multi-tenant scenario", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-instance-host") return "customer.zitadel.cloud";
|
||||
if (key === "x-forwarded-host") return "accounts.company.com";
|
||||
if (key === "host") return "internal.vercel.app";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getInstanceHost(mockHeaders);
|
||||
expect(result).toBe("customer.zitadel.cloud");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edge cases", () => {
|
||||
test("should handle IPv4 addresses", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle IPv4 addresses", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "192.168.1.100:3000"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://192.168.1.100:3000");
|
||||
});
|
||||
|
||||
test("should handle IPv6 addresses", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle IPv6 addresses", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "[::1]:3000"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://[::1]:3000");
|
||||
});
|
||||
|
||||
test("should handle hosts with ports", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle hosts with ports", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "zitadel.com:8080"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("https://zitadel.com:8080");
|
||||
});
|
||||
|
||||
test("should handle localhost with different ports", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
test("should handle localhost with different ports", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => "localhost:8080"),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
|
||||
const result = await getOriginalHostWithProtocol();
|
||||
const result = getPublicHostWithProtocol(mockHeaders);
|
||||
expect(result).toBe("http://localhost:8080");
|
||||
});
|
||||
});
|
||||
|
||||
test("should handle priority order correctly", async () => {
|
||||
const { headers } = await import("next/headers");
|
||||
describe("getPublicHost", () => {
|
||||
test("should use x-zitadel-public-host when available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
// All headers are present, should return x-forwarded-host (highest priority)
|
||||
if (key === "x-forwarded-host") return "priority1.com";
|
||||
if (key === "x-original-host") return "priority2.com";
|
||||
if (key === "host") return "priority3.com";
|
||||
if (key === "x-zitadel-public-host") return "public.zitadel.cloud";
|
||||
if (key === "x-forwarded-host") return "accounts.company.com";
|
||||
return null;
|
||||
}),
|
||||
};
|
||||
} as any;
|
||||
|
||||
vi.mocked(headers).mockResolvedValue(mockHeaders as any);
|
||||
const result = getPublicHost(mockHeaders);
|
||||
expect(result).toBe("public.zitadel.cloud");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
|
||||
});
|
||||
|
||||
const result = await getOriginalHost();
|
||||
expect(result).toBe("priority1.com");
|
||||
// Should only call x-forwarded-host since it's available
|
||||
test("should use x-zitadel-forward-host when x-zitadel-public-host is not available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-public-host") return null;
|
||||
if (key === "x-zitadel-forward-host") return "forward.zitadel.cloud";
|
||||
if (key === "x-forwarded-host") return "accounts.company.com";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getPublicHost(mockHeaders);
|
||||
expect(result).toBe("forward.zitadel.cloud");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
|
||||
});
|
||||
|
||||
test("should use x-forwarded-host when neither x-zitadel-public-host nor x-zitadel-forward-host is available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-public-host") return null;
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "x-forwarded-host") return "accounts.company.com";
|
||||
if (key === "host") return "internal.server";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getPublicHost(mockHeaders);
|
||||
expect(result).toBe("accounts.company.com");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-public-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-zitadel-forward-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test("should fall back to host when x-forwarded-host is not available", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-public-host") return null;
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "x-forwarded-host") return null;
|
||||
if (key === "host") return "localhost:3000";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getPublicHost(mockHeaders);
|
||||
expect(result).toBe("localhost:3000");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("x-forwarded-host");
|
||||
expect(mockHeaders.get).toHaveBeenCalledWith("host");
|
||||
});
|
||||
|
||||
test("should throw error when no host is found", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => null),
|
||||
} as any;
|
||||
|
||||
expect(() => getPublicHost(mockHeaders)).toThrow("No host found in headers");
|
||||
});
|
||||
|
||||
test("should differ from getInstanceHost when x-zitadel-instance-host is present", () => {
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-instance-host") return "instance.zitadel.cloud";
|
||||
if (key === "x-forwarded-host") return "accounts.company.com";
|
||||
if (key === "host") return "internal.server";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const instanceHost = getInstanceHost(mockHeaders);
|
||||
const publicHost = getPublicHost(mockHeaders);
|
||||
|
||||
expect(instanceHost).toBe("instance.zitadel.cloud");
|
||||
expect(publicHost).toBe("accounts.company.com");
|
||||
expect(instanceHost).not.toBe(publicHost);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { headers } from "next/headers";
|
||||
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||
|
||||
/**
|
||||
* Gets the original host that the user sees in their browser URL.
|
||||
* When using rewrites this function prioritizes forwarded headers that preserve the original host.
|
||||
*
|
||||
* ⚠️ SERVER-SIDE ONLY: This function can only be used in:
|
||||
* - Server Actions (functions with "use server")
|
||||
* - Server Components (React components that run on the server)
|
||||
* - Route Handlers (API routes)
|
||||
* - Middleware
|
||||
*
|
||||
* @returns The host string (e.g., "zitadel.com")
|
||||
* @throws Error if no host is found
|
||||
*/
|
||||
export async function getOriginalHost(): Promise<string> {
|
||||
const _headers = await headers();
|
||||
export function getInstanceHost(headers: ReadonlyHeaders): string | null {
|
||||
// use standard proxy headers (x-forwarded-host → host) for both multi-tenant and self-hosted, do not use x-zitadel-instance-host
|
||||
const instanceHost = headers.get("x-zitadel-instance-host") || headers.get("x-zitadel-forward-host");
|
||||
|
||||
// Priority order:
|
||||
// 1. x-forwarded-host - Set by proxies/CDNs with the original host
|
||||
// 2. x-original-host - Alternative header sometimes used
|
||||
// 3. host - Fallback to the current host header
|
||||
const host = _headers.get("x-forwarded-host") || _headers.get("x-original-host") || _headers.get("host");
|
||||
|
||||
if (!host || typeof host !== "string") {
|
||||
throw new Error("No host found in headers");
|
||||
}
|
||||
|
||||
return host;
|
||||
return instanceHost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the original host with protocol prefix.
|
||||
* Automatically detects if localhost should use http:// or https://
|
||||
* Gets the public host that the user sees in their browser URL.
|
||||
* Only considers standard proxy headers (x-forwarded-host and host).
|
||||
* Does NOT include x-zitadel-instance-host.
|
||||
*
|
||||
* ⚠️ SERVER-SIDE ONLY: This function can only be used in:
|
||||
* - Server Actions (functions with "use server")
|
||||
* - Server Components (React components that run on the server)
|
||||
* - Route Handlers (API routes)
|
||||
* - Middleware
|
||||
* Use this when you need the public-facing host that the user actually sees,
|
||||
* not the internal instance host used for API routing.
|
||||
*
|
||||
* @returns The full URL prefix (e.g., "https://zitadel.com")
|
||||
* @returns The public host string (e.g., "accounts.company.com")
|
||||
* @throws Error if no host is found
|
||||
*/
|
||||
export async function getOriginalHostWithProtocol(): Promise<string> {
|
||||
const host = await getOriginalHost();
|
||||
export function getPublicHost(headers: ReadonlyHeaders): string {
|
||||
// Only use standard proxy headers (x-zitadel-public-host → x-zitadel-forward-host → x-forwarded-host → host)
|
||||
// Do NOT use x-zitadel-instance-host as it may differ from what the user sees
|
||||
const publicHost =
|
||||
headers.get("x-zitadel-public-host") ||
|
||||
headers.get("x-zitadel-forward-host") ||
|
||||
headers.get("x-forwarded-host") ||
|
||||
headers.get("host");
|
||||
|
||||
if (!publicHost || typeof publicHost !== "string") {
|
||||
throw new Error("No host found in headers");
|
||||
}
|
||||
|
||||
return publicHost;
|
||||
}
|
||||
|
||||
export function getPublicHostWithProtocol(headers: ReadonlyHeaders): string {
|
||||
const host = getPublicHost(headers);
|
||||
const protocol = host.includes("localhost") ? "http://" : "https://";
|
||||
return `${protocol}${host}`;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ vi.mock("@zitadel/client", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../service-url", () => ({
|
||||
getServiceUrlFromHeaders: vi.fn(),
|
||||
getServiceConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../zitadel", () => ({
|
||||
@@ -111,7 +111,7 @@ describe("processIDPCallback", () => {
|
||||
|
||||
// Import mocked modules
|
||||
const { headers } = await import("next/headers");
|
||||
const { getServiceUrlFromHeaders } = await import("../service-url");
|
||||
const { getServiceConfig } = await import("../service-url");
|
||||
const {
|
||||
retrieveIDPIntent,
|
||||
getIDPByID,
|
||||
@@ -129,7 +129,7 @@ describe("processIDPCallback", () => {
|
||||
|
||||
// Setup mocks
|
||||
mockHeaders = vi.mocked(headers);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
|
||||
mockRetrieveIDPIntent = vi.mocked(retrieveIDPIntent);
|
||||
mockGetIDPByID = vi.mocked(getIDPByID);
|
||||
mockUpdateHuman = vi.mocked(updateHuman);
|
||||
@@ -146,7 +146,7 @@ describe("processIDPCallback", () => {
|
||||
// Default mock implementations
|
||||
mockHeaders.mockResolvedValue({} as any);
|
||||
mockGetServiceUrlFromHeaders.mockReturnValue({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
});
|
||||
mockRetrieveIDPIntent.mockResolvedValue(defaultIntent);
|
||||
mockGetIDPByID.mockResolvedValue(defaultIdp);
|
||||
@@ -257,7 +257,7 @@ describe("processIDPCallback", () => {
|
||||
const result = await processIDPCallback(defaultParams);
|
||||
|
||||
expect(mockRetrieveIDPIntent).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
id: "intent123",
|
||||
token: "token123",
|
||||
});
|
||||
@@ -287,7 +287,7 @@ describe("processIDPCallback", () => {
|
||||
await processIDPCallback(defaultParams);
|
||||
|
||||
expect(mockUpdateHuman).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
request: expect.objectContaining({
|
||||
userId: "user123",
|
||||
profile: defaultIntent.updateHumanUser.profile,
|
||||
@@ -353,7 +353,7 @@ describe("processIDPCallback", () => {
|
||||
const result = await processIDPCallback(linkParams);
|
||||
|
||||
expect(mockAddIDPLink).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
idp: {
|
||||
id: "idp123",
|
||||
userId: "user123",
|
||||
@@ -442,12 +442,12 @@ describe("processIDPCallback", () => {
|
||||
const result = await processIDPCallback(defaultParams);
|
||||
|
||||
expect(mockListUsers).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
email: "[email protected]",
|
||||
organizationId: "org123",
|
||||
});
|
||||
expect(mockAddIDPLink).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
idp: {
|
||||
id: "idp123",
|
||||
userId: "user123",
|
||||
@@ -523,7 +523,7 @@ describe("processIDPCallback", () => {
|
||||
const result = await processIDPCallback(defaultParams);
|
||||
|
||||
expect(mockListUsers).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userName: "testuser",
|
||||
organizationId: "org123",
|
||||
});
|
||||
@@ -557,7 +557,7 @@ describe("processIDPCallback", () => {
|
||||
const result = await processIDPCallback(defaultParams);
|
||||
|
||||
expect(mockAddHuman).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
request: expect.objectContaining({
|
||||
username: "testuser",
|
||||
profile: defaultIntent.addHumanUser.profile,
|
||||
@@ -602,11 +602,11 @@ describe("processIDPCallback", () => {
|
||||
});
|
||||
|
||||
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
domain: "example.com",
|
||||
});
|
||||
expect(mockAddHuman).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
request: expect.objectContaining({
|
||||
organization: expect.objectContaining({
|
||||
org: { case: "orgId", value: "org-from-domain" },
|
||||
@@ -625,10 +625,10 @@ describe("processIDPCallback", () => {
|
||||
});
|
||||
|
||||
expect(mockGetDefaultOrg).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
});
|
||||
expect(mockAddHuman).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
request: expect.objectContaining({
|
||||
organization: expect.objectContaining({
|
||||
org: { case: "orgId", value: "default-org" },
|
||||
@@ -901,14 +901,14 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockGetLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
organization: "org123",
|
||||
});
|
||||
expect(mockGetActiveIdentityProviders).not.toHaveBeenCalled();
|
||||
@@ -918,7 +918,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
mockGetLoginSettings.mockResolvedValue(undefined);
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
@@ -930,7 +930,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
mockGetLoginSettings.mockResolvedValue({});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
@@ -952,14 +952,14 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
orgId: "org123",
|
||||
linking_allowed: true,
|
||||
});
|
||||
@@ -974,7 +974,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
@@ -989,7 +989,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
mockGetActiveIdentityProviders.mockResolvedValue({});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
@@ -1011,18 +1011,18 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(mockGetLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
organization: "org123",
|
||||
});
|
||||
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
orgId: "org123",
|
||||
linking_allowed: true,
|
||||
});
|
||||
@@ -1042,7 +1042,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
@@ -1057,7 +1057,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
|
||||
await expect(
|
||||
validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
}),
|
||||
@@ -1072,7 +1072,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
|
||||
await expect(
|
||||
validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
}),
|
||||
@@ -1088,7 +1088,7 @@ describe("validateIDPLinkingPermissions", () => {
|
||||
});
|
||||
|
||||
const result = await validateIDPLinkingPermissions({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userOrganizationId: "org123",
|
||||
idpId: "idp123",
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import { getServiceUrlFromHeaders } from "@/lib/service-url";
|
||||
import { getServiceConfig } from "@/lib/service-url";
|
||||
import {
|
||||
retrieveIDPIntent,
|
||||
getIDPByID,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
getActiveIdentityProviders,
|
||||
getUserByID,
|
||||
getDefaultOrg,
|
||||
ServiceConfig,
|
||||
} from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
import { create } from "@zitadel/client";
|
||||
@@ -31,11 +32,11 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||
async function resolveOrganizationForUser({
|
||||
organization,
|
||||
addHumanUser,
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
}: {
|
||||
organization?: string;
|
||||
addHumanUser?: AddHumanUserRequest;
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
}): Promise<string | undefined> {
|
||||
if (organization) return organization;
|
||||
|
||||
@@ -43,16 +44,12 @@ async function resolveOrganizationForUser({
|
||||
const matched = ORG_SUFFIX_REGEX.exec(addHumanUser.username);
|
||||
const suffix = matched?.[1] ?? "";
|
||||
|
||||
const orgs = await getOrgsByDomain({
|
||||
serviceUrl,
|
||||
domain: suffix,
|
||||
const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix,
|
||||
});
|
||||
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
|
||||
|
||||
if (orgToCheckForDiscovery) {
|
||||
const orgLoginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: orgToCheckForDiscovery,
|
||||
const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery,
|
||||
});
|
||||
if (orgLoginSettings?.allowDomainDiscovery) {
|
||||
return orgToCheckForDiscovery;
|
||||
@@ -61,7 +58,7 @@ async function resolveOrganizationForUser({
|
||||
}
|
||||
|
||||
// Fallback to default organization if no org was resolved through discovery
|
||||
const defaultOrg = await getDefaultOrg({ serviceUrl });
|
||||
const defaultOrg = await getDefaultOrg({ serviceConfig });
|
||||
return defaultOrg?.id;
|
||||
}
|
||||
|
||||
@@ -72,19 +69,15 @@ async function resolveOrganizationForUser({
|
||||
* 2. The specific IDP is activated for the organization
|
||||
*
|
||||
*/
|
||||
export async function validateIDPLinkingPermissions({
|
||||
serviceUrl,
|
||||
userOrganizationId,
|
||||
export async function validateIDPLinkingPermissions({ serviceConfig, userOrganizationId,
|
||||
idpId,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
userOrganizationId: string;
|
||||
idpId: string;
|
||||
}): Promise<boolean> {
|
||||
// Check organization login settings
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: userOrganizationId,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: userOrganizationId,
|
||||
});
|
||||
|
||||
if (!loginSettings?.allowExternalIdp) {
|
||||
@@ -92,9 +85,7 @@ export async function validateIDPLinkingPermissions({
|
||||
}
|
||||
|
||||
// Check if the IDP is activated for the organization and allows linking
|
||||
const activeIDPs = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: userOrganizationId,
|
||||
const activeIDPs = await getActiveIdentityProviders({ serviceConfig, orgId: userOrganizationId,
|
||||
linking_allowed: true,
|
||||
});
|
||||
|
||||
@@ -132,7 +123,7 @@ export async function processIDPCallback({
|
||||
postErrorRedirectUrl?: string;
|
||||
}): Promise<{ redirect?: string; error?: string }> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const t = await getTranslations("idp");
|
||||
|
||||
@@ -155,9 +146,7 @@ export async function processIDPCallback({
|
||||
});
|
||||
|
||||
// Consume the single-use token ONCE
|
||||
const intent = await retrieveIDPIntent({
|
||||
serviceUrl,
|
||||
id,
|
||||
const intent = await retrieveIDPIntent({ serviceConfig, id,
|
||||
token,
|
||||
});
|
||||
|
||||
@@ -171,9 +160,7 @@ export async function processIDPCallback({
|
||||
}
|
||||
|
||||
// Get IDP configuration
|
||||
const idp = await getIDPByID({
|
||||
serviceUrl,
|
||||
id: idpInformation.idpId,
|
||||
const idp = await getIDPByID({ serviceConfig, id: idpInformation.idpId,
|
||||
});
|
||||
|
||||
if (!idp) {
|
||||
@@ -207,9 +194,7 @@ export async function processIDPCallback({
|
||||
// Auto-update user if enabled
|
||||
if (options?.isAutoUpdate && updateHumanUser) {
|
||||
try {
|
||||
await updateHuman({
|
||||
serviceUrl,
|
||||
request: create(UpdateHumanUserRequestSchema, {
|
||||
await updateHuman({ serviceConfig, request: create(UpdateHumanUserRequestSchema, {
|
||||
userId: userId,
|
||||
profile: updateHumanUser.profile,
|
||||
email: updateHumanUser.email,
|
||||
@@ -259,7 +244,7 @@ export async function processIDPCallback({
|
||||
|
||||
try {
|
||||
// Get user to retrieve their organization
|
||||
const targetUser = await getUserByID({ serviceUrl, userId });
|
||||
const targetUser = await getUserByID({ serviceConfig, userId });
|
||||
|
||||
if (!targetUser || !targetUser.details?.resourceOwner) {
|
||||
console.error("[IDP Process] User not found or missing organization");
|
||||
@@ -268,9 +253,7 @@ export async function processIDPCallback({
|
||||
}
|
||||
|
||||
// Validate IDP linking permissions
|
||||
const isAllowed = await validateIDPLinkingPermissions({
|
||||
serviceUrl,
|
||||
userOrganizationId: targetUser.details.resourceOwner,
|
||||
const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: targetUser.details.resourceOwner,
|
||||
idpId: idpInformation.idpId,
|
||||
});
|
||||
|
||||
@@ -280,9 +263,7 @@ export async function processIDPCallback({
|
||||
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
|
||||
}
|
||||
|
||||
await addIDPLink({
|
||||
serviceUrl,
|
||||
idp: {
|
||||
await addIDPLink({ serviceConfig, idp: {
|
||||
id: idpInformation.idpId,
|
||||
userId: idpInformation.userId,
|
||||
userName: idpInformation.userName,
|
||||
@@ -328,21 +309,17 @@ export async function processIDPCallback({
|
||||
const email = addHumanUser?.email?.email;
|
||||
|
||||
if (options.autoLinking === AutoLinkingOption.EMAIL && email) {
|
||||
foundUser = await listUsers({ serviceUrl, email, organizationId: organization }).then((response) => {
|
||||
foundUser = await listUsers({ serviceConfig, email, organizationId: organization }).then((response) => {
|
||||
return response.result ? response.result[0] : null;
|
||||
});
|
||||
} else if (options.autoLinking === AutoLinkingOption.USERNAME) {
|
||||
foundUser = await listUsers({
|
||||
serviceUrl,
|
||||
userName: idpInformation.userName,
|
||||
foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
|
||||
organizationId: organization,
|
||||
}).then((response) => {
|
||||
return response.result ? response.result[0] : null;
|
||||
});
|
||||
} else {
|
||||
foundUser = await listUsers({
|
||||
serviceUrl,
|
||||
userName: idpInformation.userName,
|
||||
foundUser = await listUsers({ serviceConfig, userName: idpInformation.userName,
|
||||
email,
|
||||
organizationId: organization,
|
||||
}).then((response) => {
|
||||
@@ -359,9 +336,7 @@ export async function processIDPCallback({
|
||||
}
|
||||
|
||||
// Validate IDP linking permissions
|
||||
const isAllowed = await validateIDPLinkingPermissions({
|
||||
serviceUrl,
|
||||
userOrganizationId: foundUser.details.resourceOwner,
|
||||
const isAllowed = await validateIDPLinkingPermissions({ serviceConfig, userOrganizationId: foundUser.details.resourceOwner,
|
||||
idpId: idpInformation.idpId,
|
||||
});
|
||||
|
||||
@@ -371,9 +346,7 @@ export async function processIDPCallback({
|
||||
return { redirect: `/idp/${provider}/linking-failed?${params}&error=validation_failed` };
|
||||
}
|
||||
|
||||
await addIDPLink({
|
||||
serviceUrl,
|
||||
idp: {
|
||||
await addIDPLink({ serviceConfig, idp: {
|
||||
id: idpInformation.idpId,
|
||||
userId: idpInformation.userId,
|
||||
userName: idpInformation.userName,
|
||||
@@ -419,7 +392,7 @@ export async function processIDPCallback({
|
||||
const orgToRegisterOn = await resolveOrganizationForUser({
|
||||
organization,
|
||||
addHumanUser,
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
});
|
||||
|
||||
if (!orgToRegisterOn) {
|
||||
@@ -438,9 +411,7 @@ export async function processIDPCallback({
|
||||
});
|
||||
|
||||
try {
|
||||
const newUser = await addHuman({
|
||||
serviceUrl,
|
||||
request: addHumanUserWithOrganization,
|
||||
const newUser = await addHuman({ serviceConfig, request: addHumanUserWithOrganization,
|
||||
});
|
||||
console.log("[IDP Process] User auto-created successfully, creating session");
|
||||
|
||||
@@ -480,7 +451,7 @@ export async function processIDPCallback({
|
||||
const orgToRegisterOn = await resolveOrganizationForUser({
|
||||
organization,
|
||||
addHumanUser,
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
});
|
||||
|
||||
if (!orgToRegisterOn) {
|
||||
|
||||
@@ -13,11 +13,12 @@ vi.mock("next/navigation", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../service-url", () => ({
|
||||
getServiceUrlFromHeaders: vi.fn(),
|
||||
getServiceConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./host", () => ({
|
||||
getOriginalHost: vi.fn(),
|
||||
getInstanceHost: vi.fn(),
|
||||
getPublicHost: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../zitadel", () => ({
|
||||
@@ -27,7 +28,8 @@ vi.mock("../zitadel", () => ({
|
||||
describe("redirectToIdp", () => {
|
||||
let mockHeaders: any;
|
||||
let mockGetServiceUrlFromHeaders: any;
|
||||
let mockGetOriginalHost: any;
|
||||
let mockGetInstanceHost: any;
|
||||
let mockGetPublicHost: any;
|
||||
let mockStartIdentityProviderFlow: any;
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -35,22 +37,24 @@ describe("redirectToIdp", () => {
|
||||
|
||||
// Import mocked modules
|
||||
const { headers } = await import("next/headers");
|
||||
const { getServiceUrlFromHeaders } = await import("../service-url");
|
||||
const { getOriginalHost } = await import("./host");
|
||||
const { getServiceConfig } = await import("../service-url");
|
||||
const { getInstanceHost, getPublicHost } = await import("./host");
|
||||
const { startIdentityProviderFlow } = await import("../zitadel");
|
||||
|
||||
// Setup mocks
|
||||
mockHeaders = vi.mocked(headers);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
|
||||
mockGetOriginalHost = vi.mocked(getOriginalHost);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
|
||||
mockGetInstanceHost = vi.mocked(getInstanceHost);
|
||||
mockGetPublicHost = vi.mocked(getPublicHost);
|
||||
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
|
||||
|
||||
// Default mock implementations
|
||||
mockHeaders.mockResolvedValue({} as any);
|
||||
mockGetServiceUrlFromHeaders.mockReturnValue({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
});
|
||||
mockGetOriginalHost.mockResolvedValue("example.com");
|
||||
mockGetInstanceHost.mockReturnValue("example.com");
|
||||
mockGetPublicHost.mockReturnValue("example.com");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -76,7 +80,7 @@ describe("redirectToIdp", () => {
|
||||
}
|
||||
|
||||
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
idpId: "idp123",
|
||||
urls: {
|
||||
successUrl: expect.stringContaining("postErrorRedirectUrl=https%3A%2F%2Fapp.example.com%2Ferror"),
|
||||
@@ -115,7 +119,7 @@ describe("redirectToIdp", () => {
|
||||
}
|
||||
|
||||
expect(mockStartIdentityProviderFlow).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
idpId: "idp123",
|
||||
urls: {
|
||||
successUrl: expect.not.stringContaining("postErrorRedirectUrl"),
|
||||
|
||||
@@ -6,21 +6,22 @@ import {
|
||||
listAuthenticationMethodTypes,
|
||||
startIdentityProviderFlow,
|
||||
startLDAPIdentityProviderFlow,
|
||||
ServiceConfig,
|
||||
} from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
import { redirect } from "next/navigation";
|
||||
import { completeFlowOrGetUrl } from "../client";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
|
||||
import { createSessionForIdpAndUpdateCookie } from "./cookie";
|
||||
import { getOriginalHost } from "./host";
|
||||
import { getPublicHost } from "./host";
|
||||
|
||||
export type RedirectToIdpState = { error?: string | null } | undefined;
|
||||
|
||||
export async function redirectToIdp(prevState: RedirectToIdpState, formData: FormData): Promise<RedirectToIdpState> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
const params = new URLSearchParams();
|
||||
|
||||
@@ -43,7 +44,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
|
||||
}
|
||||
|
||||
const response = await startIDPFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
host,
|
||||
idpId,
|
||||
successUrl: `/idp/${provider}/process?` + params.toString(),
|
||||
@@ -62,7 +63,7 @@ export async function redirectToIdp(prevState: RedirectToIdpState, formData: For
|
||||
}
|
||||
|
||||
export type StartIDPFlowCommand = {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
host: string;
|
||||
idpId: string;
|
||||
successUrl: string;
|
||||
@@ -73,7 +74,7 @@ async function startIDPFlow(command: StartIDPFlowCommand) {
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
const url = await startIdentityProviderFlow({
|
||||
serviceUrl: command.serviceUrl,
|
||||
serviceConfig: command.serviceConfig,
|
||||
idpId: command.idpId,
|
||||
urls: {
|
||||
successUrl: `${command.host.includes("localhost") ? "http://" : "https://"}${command.host}${basePath}${command.successUrl}`,
|
||||
@@ -103,25 +104,19 @@ export type CreateNewSessionCommand = {
|
||||
export async function createNewSessionFromIdpIntent(command: CreateNewSessionCommand) {
|
||||
const _headers = await headers();
|
||||
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
if (!command.userId || !command.idpIntent) {
|
||||
throw new Error("No userId or loginName provided");
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: "User not found in the system" };
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: userResponse.user.details?.resourceOwner,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: userResponse.user.details?.resourceOwner });
|
||||
|
||||
const session = await createSessionForIdpAndUpdateCookie({
|
||||
userId: command.userId,
|
||||
@@ -146,17 +141,14 @@ export async function createNewSessionFromIdpIntent(command: CreateNewSessionCom
|
||||
// check if user has MFA methods
|
||||
let authMethods;
|
||||
if (session.factors?.user?.id) {
|
||||
const response = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
||||
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||
authMethods = response.authMethodTypes;
|
||||
}
|
||||
}
|
||||
|
||||
const mfaFactorCheck = await checkMFAFactors(
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
session,
|
||||
loginSettings,
|
||||
authMethods || [], // Pass empty array if no auth methods
|
||||
@@ -193,14 +185,14 @@ type createNewSessionForLDAPCommand = {
|
||||
export async function createNewSessionForLDAP(command: createNewSessionForLDAPCommand) {
|
||||
const _headers = await headers();
|
||||
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
if (!command.username || !command.password) {
|
||||
return { error: "No username or password provided" };
|
||||
}
|
||||
|
||||
const response = await startLDAPIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idpId: command.idpId,
|
||||
username: command.username,
|
||||
password: command.password,
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("@zitadel/client", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../service-url", () => ({
|
||||
getServiceUrlFromHeaders: vi.fn(),
|
||||
getServiceConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../idp", () => ({
|
||||
@@ -39,7 +39,8 @@ vi.mock("./cookie", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("./host", () => ({
|
||||
getOriginalHost: vi.fn(),
|
||||
getInstanceHost: vi.fn(),
|
||||
getPublicHost: vi.fn(),
|
||||
}));
|
||||
|
||||
// this returns the key itself that can be checked not the translated value
|
||||
@@ -57,7 +58,8 @@ describe("sendLoginname", () => {
|
||||
let mockCreateSessionAndUpdateCookie: any;
|
||||
let mockListAuthenticationMethodTypes: any;
|
||||
let mockListIDPLinks: any;
|
||||
let mockGetOriginalHost: any;
|
||||
let mockGetInstanceHost: any;
|
||||
let mockGetPublicHost: any;
|
||||
let mockStartIdentityProviderFlow: any;
|
||||
let mockGetActiveIdentityProviders: any;
|
||||
let mockGetIDPByID: any;
|
||||
@@ -70,7 +72,7 @@ describe("sendLoginname", () => {
|
||||
// Import mocked modules
|
||||
const { headers } = await import("next/headers");
|
||||
const { create } = await import("@zitadel/client");
|
||||
const { getServiceUrlFromHeaders } = await import("../service-url");
|
||||
const { getServiceConfig } = await import("../service-url");
|
||||
const {
|
||||
getLoginSettings,
|
||||
searchUsers,
|
||||
@@ -81,19 +83,20 @@ describe("sendLoginname", () => {
|
||||
getOrgsByDomain,
|
||||
} = await import("../zitadel");
|
||||
const { createSessionAndUpdateCookie } = await import("./cookie");
|
||||
const { getOriginalHost } = await import("./host");
|
||||
const { getInstanceHost, getPublicHost } = await import("./host");
|
||||
const { idpTypeToSlug } = await import("../idp");
|
||||
|
||||
// Setup mocks
|
||||
mockHeaders = vi.mocked(headers);
|
||||
mockCreate = vi.mocked(create);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
|
||||
mockGetLoginSettings = vi.mocked(getLoginSettings);
|
||||
mockSearchUsers = vi.mocked(searchUsers);
|
||||
mockCreateSessionAndUpdateCookie = vi.mocked(createSessionAndUpdateCookie);
|
||||
mockListAuthenticationMethodTypes = vi.mocked(listAuthenticationMethodTypes);
|
||||
mockListIDPLinks = vi.mocked(listIDPLinks);
|
||||
mockGetOriginalHost = vi.mocked(getOriginalHost);
|
||||
mockGetInstanceHost = vi.mocked(getInstanceHost);
|
||||
mockGetPublicHost = vi.mocked(getPublicHost);
|
||||
mockStartIdentityProviderFlow = vi.mocked(startIdentityProviderFlow);
|
||||
mockGetActiveIdentityProviders = vi.mocked(getActiveIdentityProviders);
|
||||
mockGetIDPByID = vi.mocked(getIDPByID);
|
||||
@@ -102,8 +105,9 @@ describe("sendLoginname", () => {
|
||||
|
||||
// Default mock implementations
|
||||
mockHeaders.mockResolvedValue({} as any);
|
||||
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceUrl: "https://api.example.com" });
|
||||
mockGetOriginalHost.mockResolvedValue("example.com");
|
||||
mockGetServiceUrlFromHeaders.mockReturnValue({ serviceConfig: { baseUrl: "https://api.example.com" } });
|
||||
mockGetInstanceHost.mockReturnValue("example.com");
|
||||
mockGetPublicHost.mockReturnValue("example.com");
|
||||
mockIdpTypeToSlug.mockReturnValue("google");
|
||||
mockGetIDPByID.mockResolvedValue({
|
||||
id: "idp123",
|
||||
@@ -243,7 +247,7 @@ describe("sendLoginname", () => {
|
||||
|
||||
expect(result).toEqual({ redirect: "https://idp.example.com/auth" });
|
||||
expect(mockListIDPLinks).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
userId: "user123",
|
||||
});
|
||||
});
|
||||
@@ -283,7 +287,7 @@ describe("sendLoginname", () => {
|
||||
|
||||
expect(result).toEqual({ redirect: "https://org-idp.example.com/auth" });
|
||||
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
orgId: "org123", // User's organization from resourceOwner
|
||||
});
|
||||
});
|
||||
@@ -523,7 +527,7 @@ describe("sendLoginname", () => {
|
||||
|
||||
// Verify org discovery was called with correct domain
|
||||
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
domain: "example.com",
|
||||
});
|
||||
});
|
||||
@@ -562,13 +566,13 @@ describe("sendLoginname", () => {
|
||||
|
||||
// Verify org discovery was called
|
||||
expect(mockGetOrgsByDomain).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
domain: "company.com",
|
||||
});
|
||||
|
||||
// Verify IDP redirect was called with discovered org
|
||||
expect(mockGetActiveIdentityProviders).toHaveBeenCalledWith({
|
||||
serviceUrl: "https://api.example.com",
|
||||
serviceConfig: { baseUrl: "https://api.example.com" },
|
||||
orgId: "discovered-org-456",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import { idpTypeToIdentityProviderType, idpTypeToSlug } from "../idp";
|
||||
|
||||
import { PasskeysType } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb";
|
||||
import { UserState } from "@zitadel/proto/zitadel/user/v2/user_pb";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import {
|
||||
getActiveIdentityProviders,
|
||||
getIDPByID,
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
startIdentityProviderFlow,
|
||||
} from "../zitadel";
|
||||
import { createSessionAndUpdateCookie } from "./cookie";
|
||||
import { getOriginalHost } from "./host";
|
||||
import { getPublicHost } from "./host";
|
||||
import { IDPLink } from "@zitadel/proto/zitadel/user/v2/idp_pb";
|
||||
|
||||
export type SendLoginnameCommand = {
|
||||
@@ -36,21 +36,18 @@ const ORG_SUFFIX_REGEX = /(?<=@)(.+)/;
|
||||
|
||||
export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const t = await getTranslations("loginname");
|
||||
|
||||
const loginSettingsByContext = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: command.organization,
|
||||
});
|
||||
const loginSettingsByContext = await getLoginSettings({ serviceConfig, organization: command.organization });
|
||||
|
||||
if (!loginSettingsByContext) {
|
||||
return { error: t("errors.couldNotGetLoginSettings") };
|
||||
}
|
||||
|
||||
let searchUsersRequest: SearchUsersCommand = {
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
searchValue: command.loginName,
|
||||
organizationId: command.organization,
|
||||
loginSettings: loginSettingsByContext,
|
||||
@@ -88,28 +85,22 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
// If userId is provided, check for user-specific IDP links first
|
||||
let identityProviders: IDPLink[] = [];
|
||||
if (userId) {
|
||||
identityProviders = await listIDPLinks({
|
||||
serviceUrl,
|
||||
userId,
|
||||
}).then((resp) => {
|
||||
identityProviders = await listIDPLinks({ serviceConfig, userId }).then((resp) => {
|
||||
return resp.result;
|
||||
});
|
||||
}
|
||||
|
||||
// If no IDP links exist for the user (or no userId provided), try to get active IDPs from the organization
|
||||
if (identityProviders.length === 0) {
|
||||
const activeIdps = await getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
orgId: organization,
|
||||
}).then((resp) => {
|
||||
const activeIdps = await getActiveIdentityProviders({ serviceConfig, orgId: organization }).then((resp) => {
|
||||
return resp.identityProviders;
|
||||
});
|
||||
|
||||
// If exactly one active IDP exists in the organization, redirect to it
|
||||
if (activeIdps.length === 1) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
const identityProviderType = activeIdps[0].type;
|
||||
const provider = idpTypeToSlug(identityProviderType);
|
||||
@@ -131,7 +122,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
const url = await startIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idpId: activeIdps[0].id,
|
||||
urls: {
|
||||
successUrl:
|
||||
@@ -153,15 +144,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
|
||||
if (identityProviders.length === 1) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
const identityProviderId = identityProviders[0].idpId;
|
||||
|
||||
const idp = await getIDPByID({
|
||||
serviceUrl,
|
||||
id: identityProviderId,
|
||||
});
|
||||
const idp = await getIDPByID({ serviceConfig, id: identityProviderId });
|
||||
|
||||
const idpType = idp?.type;
|
||||
|
||||
@@ -189,7 +177,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
const url = await startIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idpId: idp.id,
|
||||
urls: {
|
||||
successUrl:
|
||||
@@ -216,10 +204,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
const user = users[0];
|
||||
const userId = users[0].userId;
|
||||
|
||||
const userLoginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: user.details?.resourceOwner,
|
||||
});
|
||||
const userLoginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
|
||||
|
||||
// compare with the concatenated suffix when set
|
||||
const concatLoginname = command.suffix ? `${command.loginName}@${command.suffix}` : command.loginName;
|
||||
@@ -273,10 +258,7 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
// Resolve organization from command or session
|
||||
const organization = command.organization ?? session.factors?.user?.organizationId;
|
||||
|
||||
const methods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors?.user?.id,
|
||||
});
|
||||
const methods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors?.user?.id });
|
||||
|
||||
// always resend invite if user has no auth method set
|
||||
if (!methods.authMethodTypes || !methods.authMethodTypes.length) {
|
||||
@@ -418,18 +400,12 @@ export async function sendLoginname(command: SendLoginnameCommand) {
|
||||
const suffix = matched?.[1] ?? "";
|
||||
|
||||
// this just returns orgs where the suffix is set as primary domain
|
||||
const orgs = await getOrgsByDomain({
|
||||
serviceUrl,
|
||||
domain: suffix,
|
||||
});
|
||||
const orgs = await getOrgsByDomain({ serviceConfig, domain: suffix });
|
||||
|
||||
const orgToCheckForDiscovery = orgs.result && orgs.result.length === 1 ? orgs.result[0].id : undefined;
|
||||
|
||||
if (orgToCheckForDiscovery) {
|
||||
const orgLoginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: orgToCheckForDiscovery,
|
||||
});
|
||||
const orgLoginSettings = await getLoginSettings({ serviceConfig, organization: orgToCheckForDiscovery });
|
||||
|
||||
if (orgLoginSettings?.allowDomainDiscovery) {
|
||||
console.log("org discovery successful, using org:", orgToCheckForDiscovery);
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
import { getDeviceAuthorizationRequest as zitadelGetDeviceAuthorizationRequest } from "@/lib/zitadel";
|
||||
import { headers } from "next/headers";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
|
||||
export async function getDeviceAuthorizationRequest(userCode: string) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
return zitadelGetDeviceAuthorizationRequest({
|
||||
serviceUrl,
|
||||
userCode,
|
||||
return zitadelGetDeviceAuthorizationRequest({ serviceConfig, userCode,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ vi.mock("@zitadel/client", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("../service-url", () => ({
|
||||
getServiceUrlFromHeaders: vi.fn(),
|
||||
getServiceConfig: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("../zitadel", () => ({
|
||||
@@ -62,7 +62,7 @@ describe("sendPasskey", () => {
|
||||
|
||||
// Import mocked modules
|
||||
const { headers } = await import("next/headers");
|
||||
const { getServiceUrlFromHeaders } = await import("../service-url");
|
||||
const { getServiceConfig } = await import("../service-url");
|
||||
const { getLoginSettings, getUserByID } = await import("../zitadel");
|
||||
const { setSessionAndUpdateCookie } = await import("./cookie");
|
||||
const { getSessionCookieById, getSessionCookieByLoginName, getMostRecentSessionCookie } = await import("../cookies");
|
||||
@@ -71,7 +71,7 @@ describe("sendPasskey", () => {
|
||||
|
||||
// Setup mocks
|
||||
mockHeaders = vi.mocked(headers);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceUrlFromHeaders);
|
||||
mockGetServiceUrlFromHeaders = vi.mocked(getServiceConfig);
|
||||
mockGetLoginSettings = vi.mocked(getLoginSettings);
|
||||
mockGetUserByID = vi.mocked(getUserByID);
|
||||
mockSetSessionAndUpdateCookie = vi.mocked(setSessionAndUpdateCookie);
|
||||
|
||||
@@ -20,10 +20,10 @@ import { headers } from "next/headers";
|
||||
import { userAgent } from "next/server";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getMostRecentSessionCookie, getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { checkEmailVerification, checkUserVerification } from "../verify-helper";
|
||||
import { createSessionAndUpdateCookie, setSessionAndUpdateCookie } from "./cookie";
|
||||
import { getOriginalHost } from "./host";
|
||||
import { getPublicHost } from "./host";
|
||||
import { completeFlowOrGetUrl } from "../client";
|
||||
|
||||
type VerifyPasskeyCommand = {
|
||||
@@ -63,8 +63,8 @@ export async function registerPasskeyLink(
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
let session: GetSessionResponse | undefined;
|
||||
let createdSession: Session | undefined;
|
||||
@@ -74,11 +74,7 @@ export async function registerPasskeyLink(
|
||||
if (command.sessionId) {
|
||||
// Session-based flow (existing logic)
|
||||
const sessionCookie = await getSessionCookieById({ sessionId: command.sessionId });
|
||||
session = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
|
||||
|
||||
if (!session?.session?.factors?.user?.id) {
|
||||
return { error: "Could not determine user from session" };
|
||||
@@ -89,10 +85,7 @@ export async function registerPasskeyLink(
|
||||
const sessionValid = isSessionValid(session.session);
|
||||
|
||||
if (!sessionValid.valid) {
|
||||
const authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: currentUserId });
|
||||
|
||||
// if the user has no authmethods set, we need to check if the user was verified
|
||||
if (authmethods.authMethodTypes.length !== 0) {
|
||||
@@ -117,10 +110,7 @@ export async function registerPasskeyLink(
|
||||
code: command.code,
|
||||
};
|
||||
} else {
|
||||
const codeResponse = await createPasskeyRegistrationLink({
|
||||
serviceUrl,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const codeResponse = await createPasskeyRegistrationLink({ serviceConfig, userId: currentUserId });
|
||||
|
||||
if (!codeResponse?.code?.code) {
|
||||
return { error: "Could not create registration link" };
|
||||
@@ -136,10 +126,7 @@ export async function registerPasskeyLink(
|
||||
};
|
||||
|
||||
// Check if user exists
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: "User not found" };
|
||||
@@ -179,17 +166,12 @@ export async function registerPasskeyLink(
|
||||
throw new Error("Could not determine user");
|
||||
}
|
||||
|
||||
return registerPasskey({
|
||||
serviceUrl,
|
||||
userId: currentUserId,
|
||||
code: registerCode,
|
||||
domain: hostname,
|
||||
});
|
||||
return registerPasskey({ serviceConfig, userId: currentUserId, code: registerCode, domain: hostname });
|
||||
}
|
||||
|
||||
export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
if (!command.sessionId && !command.userId) {
|
||||
throw new Error("Either sessionId or userId must be provided");
|
||||
@@ -214,11 +196,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
|
||||
const sessionCookie = await getSessionCookieById({
|
||||
sessionId: command.sessionId,
|
||||
});
|
||||
const session = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
|
||||
const userId = session?.session?.factors?.user?.id;
|
||||
|
||||
if (!userId) {
|
||||
@@ -231,10 +209,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
|
||||
currentUserId = command.userId!;
|
||||
|
||||
// Verify user exists
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: currentUserId,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: currentUserId });
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
throw new Error("User not found");
|
||||
@@ -242,7 +217,7 @@ export async function verifyPasskeyRegistration(command: VerifyPasskeyCommand) {
|
||||
}
|
||||
|
||||
return zitadelVerifyPasskeyRegistration({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
request: create(VerifyPasskeyRegistrationRequestSchema, {
|
||||
passkeyId: command.passkeyId,
|
||||
publicKeyCredential: command.publicKeyCredential,
|
||||
@@ -279,12 +254,9 @@ export async function sendPasskey(command: SendPasskeyCommand) {
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization });
|
||||
|
||||
let lifetime = command.lifetime; // Use provided lifetime first
|
||||
|
||||
@@ -318,10 +290,7 @@ export async function sendPasskey(command: SendPasskeyCommand) {
|
||||
|
||||
let userResponse;
|
||||
try {
|
||||
userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: session?.factors?.user?.id,
|
||||
});
|
||||
userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
|
||||
} catch (error) {
|
||||
console.error("Error fetching user by ID:", error);
|
||||
return { error: t("verify.errors.couldNotGetUser") };
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
listAuthenticationMethodTypes,
|
||||
listUsers,
|
||||
passwordReset,
|
||||
ServiceConfig,
|
||||
setPassword,
|
||||
setUserPassword,
|
||||
} from "@/lib/zitadel";
|
||||
@@ -22,8 +23,8 @@ import { SetPasswordRequestSchema } from "@zitadel/proto/zitadel/user/v2/user_se
|
||||
import { headers } from "next/headers";
|
||||
import { completeFlowOrGetUrl } from "../client";
|
||||
import { getSessionCookieById, getSessionCookieByLoginName } from "../cookies";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getOriginalHostWithProtocol } from "./host";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { getPublicHostWithProtocol } from "./host";
|
||||
import {
|
||||
checkEmailVerification,
|
||||
checkMFAFactors,
|
||||
@@ -41,18 +42,14 @@ type ResetPasswordCommand = {
|
||||
|
||||
export async function resetPassword(command: ResetPasswordCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const t = await getTranslations("password");
|
||||
|
||||
// Get the original host that the user sees with protocol
|
||||
const hostWithProtocol = await getOriginalHostWithProtocol();
|
||||
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
|
||||
|
||||
const users = await listUsers({
|
||||
serviceUrl,
|
||||
loginName: command.loginName,
|
||||
organizationId: command.organization,
|
||||
});
|
||||
const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
|
||||
|
||||
if (!users.details || users.details.totalResult !== BigInt(1) || !users.result[0].userId) {
|
||||
return { error: t("errors.couldNotSendResetLink") };
|
||||
@@ -62,7 +59,7 @@ export async function resetPassword(command: ResetPasswordCommand) {
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
return passwordReset({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
urlTemplate:
|
||||
`${hostWithProtocol}${basePath}/password/set?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
@@ -79,7 +76,7 @@ export type UpdateSessionCommand = {
|
||||
|
||||
export async function sendPassword(command: UpdateSessionCommand): Promise<{ error: string } | { redirect: string }> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const t = await getTranslations("password");
|
||||
|
||||
let sessionCookie = await getSessionCookieByLoginName({
|
||||
@@ -94,11 +91,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
let loginSettings: LoginSettings | undefined;
|
||||
|
||||
if (!sessionCookie) {
|
||||
const users = await listUsers({
|
||||
serviceUrl,
|
||||
loginName: command.loginName,
|
||||
organizationId: command.organization,
|
||||
});
|
||||
const users = await listUsers({ serviceConfig, loginName: command.loginName, organizationId: command.organization });
|
||||
|
||||
if (users.details?.totalResult == BigInt(1) && users.result[0].userId) {
|
||||
user = users.result[0];
|
||||
@@ -108,10 +101,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
password: { password: command.checks.password?.password },
|
||||
});
|
||||
|
||||
loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: command.organization,
|
||||
});
|
||||
loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization });
|
||||
|
||||
try {
|
||||
session = await createSessionAndUpdateCookie({
|
||||
@@ -121,10 +111,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
});
|
||||
} catch (error: any) {
|
||||
if ("failedAttempts" in error && error.failedAttempts) {
|
||||
const lockoutSettings = await getLockoutSettings({
|
||||
serviceUrl,
|
||||
orgId: command.organization,
|
||||
});
|
||||
const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
|
||||
|
||||
const hasLimit =
|
||||
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
|
||||
@@ -149,10 +136,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
// this is a fake error message to hide that the user does not even exist
|
||||
return { error: t("errors.couldNotVerifyPassword") };
|
||||
} else {
|
||||
loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: sessionCookie.organization,
|
||||
});
|
||||
loginSettings = await getLoginSettings({ serviceConfig, organization: sessionCookie.organization });
|
||||
|
||||
if (!loginSettings) {
|
||||
return { error: "Could not load login settings" };
|
||||
@@ -177,10 +161,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
});
|
||||
} catch (error: any) {
|
||||
if ("failedAttempts" in error && error.failedAttempts) {
|
||||
const lockoutSettings = await getLockoutSettings({
|
||||
serviceUrl,
|
||||
orgId: command.organization,
|
||||
});
|
||||
const lockoutSettings = await getLockoutSettings({ serviceConfig, orgId: command.organization });
|
||||
|
||||
const hasLimit =
|
||||
lockoutSettings?.maxPasswordAttempts !== undefined && lockoutSettings?.maxPasswordAttempts > BigInt(0);
|
||||
@@ -202,10 +183,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
return { error: t("errors.couldNotCreateSessionForUser") };
|
||||
}
|
||||
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: session?.factors?.user?.id,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id });
|
||||
|
||||
if (!userResponse.user) {
|
||||
return { error: t("errors.userNotFound") };
|
||||
@@ -216,7 +194,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
|
||||
if (!loginSettings) {
|
||||
loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization: command.organization ?? session.factors?.user?.organizationId,
|
||||
});
|
||||
}
|
||||
@@ -228,7 +206,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
const humanUser = user.type.case === "human" ? user.type.value : undefined;
|
||||
|
||||
const expirySettings = await getPasswordExpirySettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
orgId: command.organization ?? session.factors?.user?.organizationId,
|
||||
});
|
||||
|
||||
@@ -260,10 +238,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
// if password, check if user has MFA methods
|
||||
let authMethods;
|
||||
if (command.checks && command.checks.password && session.factors?.user?.id) {
|
||||
const response = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
||||
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||
authMethods = response.authMethodTypes;
|
||||
}
|
||||
@@ -274,7 +249,7 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
}
|
||||
|
||||
const mfaFactorCheck = await checkMFAFactors(
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
session,
|
||||
loginSettings,
|
||||
authMethods,
|
||||
@@ -331,14 +306,11 @@ export async function sendPassword(command: UpdateSessionCommand): Promise<{ err
|
||||
// this function lets users with code set a password or users with valid User Verification Check
|
||||
export async function changePassword(command: { code?: string; userId: string; password: string }) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const t = await getTranslations("password");
|
||||
|
||||
// check for init state
|
||||
const { user } = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
});
|
||||
const { user } = await getUserByID({ serviceConfig, userId: command.userId });
|
||||
|
||||
if (!user || user.userId !== command.userId) {
|
||||
return { error: t("errors.couldNotSendResetLink") };
|
||||
@@ -351,10 +323,7 @@ export async function changePassword(command: { code?: string; userId: string; p
|
||||
|
||||
// check if the user has no password set in order to set a password
|
||||
if (!command.code) {
|
||||
const authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId,
|
||||
});
|
||||
const authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId });
|
||||
|
||||
// if the user has no authmethods set, we need to check if the user was verified
|
||||
if (authmethods.authMethodTypes.length !== 0) {
|
||||
@@ -371,12 +340,7 @@ export async function changePassword(command: { code?: string; userId: string; p
|
||||
}
|
||||
}
|
||||
|
||||
return setUserPassword({
|
||||
serviceUrl,
|
||||
userId,
|
||||
password: command.password,
|
||||
code: command.code,
|
||||
});
|
||||
return setUserPassword({ serviceConfig, userId, password: command.password, code: command.code });
|
||||
}
|
||||
|
||||
type CheckSessionAndSetPasswordCommand = {
|
||||
@@ -386,7 +350,7 @@ type CheckSessionAndSetPasswordCommand = {
|
||||
|
||||
export async function checkSessionAndSetPassword({ sessionId, password }: CheckSessionAndSetPasswordCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const t = await getTranslations("password");
|
||||
|
||||
let sessionCookie;
|
||||
@@ -400,7 +364,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
|
||||
let session;
|
||||
try {
|
||||
const sessionResponse = await getSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
@@ -424,10 +388,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
|
||||
// check if the user has no password set in order to set a password
|
||||
let authmethods;
|
||||
try {
|
||||
authmethods = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
authmethods = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
||||
} catch (error) {
|
||||
console.error("Error getting auth methods:", error);
|
||||
return { error: "Could not load auth methods" };
|
||||
@@ -439,10 +400,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
|
||||
|
||||
let loginSettings;
|
||||
try {
|
||||
loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: session.factors.user.organizationId,
|
||||
});
|
||||
loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors.user.organizationId });
|
||||
} catch (error) {
|
||||
console.error("Error getting login settings:", error);
|
||||
return { error: "Could not load login settings" };
|
||||
@@ -453,7 +411,7 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
|
||||
// if the user has no MFA but MFA is enforced, we can set a password otherwise we use the token of the user
|
||||
if (forceMfa) {
|
||||
console.log("Set password using service account due to enforced MFA without existing MFA methods");
|
||||
return setPassword({ serviceUrl, payload }).catch((error) => {
|
||||
return setPassword({ serviceConfig, payload }).catch((error) => {
|
||||
// throw error if failed precondition (ex. User is not yet initialized)
|
||||
if (error.code === 9 && error.message) {
|
||||
return { error: t("errors.failedPrecondition") };
|
||||
@@ -461,16 +419,16 @@ export async function checkSessionAndSetPassword({ sessionId, password }: CheckS
|
||||
return { error: "Could not set password" };
|
||||
});
|
||||
} else {
|
||||
const transport = async (serviceUrl: string, token: string) => {
|
||||
return createServerTransport(token, serviceUrl);
|
||||
const transport = async (serviceConfig: ServiceConfig, token: string) => {
|
||||
return createServerTransport(token, serviceConfig);
|
||||
};
|
||||
|
||||
const myUserService = async (serviceUrl: string, sessionToken: string) => {
|
||||
const transportPromise = await transport(serviceUrl, sessionToken);
|
||||
const myUserService = async (serviceConfig: ServiceConfig, sessionToken: string) => {
|
||||
const transportPromise = await transport(serviceConfig, sessionToken);
|
||||
return createUserServiceClient(transportPromise);
|
||||
};
|
||||
|
||||
const selfService = await myUserService(serviceUrl, `${sessionCookie.token}`);
|
||||
const selfService = await myUserService(serviceConfig, sessionCookie.token);
|
||||
|
||||
return selfService
|
||||
.setPassword(
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Factors } from "@zitadel/proto/zitadel/session/v2/session_pb";
|
||||
import { ChecksJson, ChecksSchema } from "@zitadel/proto/zitadel/session/v2/session_service_pb";
|
||||
import { cookies, headers } from "next/headers";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { checkEmailVerification, checkMFAFactors } from "../verify-helper";
|
||||
import { getOrSetFingerprintId } from "../fingerprint";
|
||||
import crypto from "crypto";
|
||||
@@ -30,11 +30,9 @@ export type RegisterUserResponse = {
|
||||
export async function registerUser(command: RegisterUserCommand) {
|
||||
const t = await getTranslations("register");
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const addResponse = await addHumanUser({
|
||||
serviceUrl,
|
||||
email: command.email,
|
||||
const addResponse = await addHumanUser({ serviceConfig, email: command.email,
|
||||
firstName: command.firstName,
|
||||
lastName: command.lastName,
|
||||
password: command.password ? command.password : undefined,
|
||||
@@ -45,9 +43,7 @@ export async function registerUser(command: RegisterUserCommand) {
|
||||
return { error: t("errors.couldNotCreateUser") };
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: command.organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
|
||||
});
|
||||
|
||||
let checkPayload: any = {
|
||||
@@ -100,9 +96,7 @@ export async function registerUser(command: RegisterUserCommand) {
|
||||
|
||||
return { redirect: "/passkey/set?" + params };
|
||||
} else {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: session?.factors?.user?.id,
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: session?.factors?.user?.id,
|
||||
});
|
||||
|
||||
if (!userResponse.user) {
|
||||
@@ -162,11 +156,9 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
|
||||
const t = await getTranslations("register");
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const addUserResponse = await addHumanUser({
|
||||
serviceUrl,
|
||||
email: command.email,
|
||||
const addUserResponse = await addHumanUser({ serviceConfig, email: command.email,
|
||||
firstName: command.firstName,
|
||||
lastName: command.lastName,
|
||||
organization: command.organization,
|
||||
@@ -176,14 +168,10 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
|
||||
return { error: t("errors.couldNotCreateUser") };
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: command.organization,
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: command.organization,
|
||||
});
|
||||
|
||||
const idpLink = await addIDPLink({
|
||||
serviceUrl,
|
||||
idp: {
|
||||
const idpLink = await addIDPLink({ serviceConfig, idp: {
|
||||
id: command.idpId,
|
||||
userId: command.idpUserId,
|
||||
userName: command.idpUserName,
|
||||
@@ -207,7 +195,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
|
||||
}
|
||||
|
||||
// const userResponse = await getUserByID({
|
||||
// serviceUrl,
|
||||
// serviceConfig.baseUrl,
|
||||
// userId: session?.factors?.user?.id,
|
||||
// });
|
||||
|
||||
@@ -227,9 +215,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
|
||||
// check if user has MFA methods
|
||||
let authMethods;
|
||||
if (session.factors?.user?.id) {
|
||||
const response = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id,
|
||||
});
|
||||
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||
authMethods = response.authMethodTypes;
|
||||
@@ -239,7 +225,7 @@ export async function registerUserAndLinkToIDP(command: RegisterUserAndLinkToIDP
|
||||
// Always check MFA factors, even if no auth methods are configured
|
||||
// This ensures that force MFA settings are respected
|
||||
const mfaFactorCheck = await checkMFAFactors(
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
session,
|
||||
loginSettings,
|
||||
authMethods || [], // Pass empty array if no auth methods
|
||||
|
||||
@@ -21,8 +21,8 @@ import {
|
||||
getSessionCookieByLoginName,
|
||||
removeSessionFromCookie,
|
||||
} from "../cookies";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getOriginalHost } from "./host";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { getPublicHost } from "./host";
|
||||
|
||||
export async function skipMFAAndContinueWithNextUrl({
|
||||
userId,
|
||||
@@ -38,14 +38,11 @@ export async function skipMFAAndContinueWithNextUrl({
|
||||
organization?: string;
|
||||
}): Promise<{ redirect: string } | { error: string }> {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: organization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: organization });
|
||||
|
||||
await humanMFAInitSkipped({ serviceUrl, userId });
|
||||
await humanMFAInitSkipped({ serviceConfig, userId });
|
||||
|
||||
if (requestId && sessionId) {
|
||||
return completeFlowOrGetUrl(
|
||||
@@ -73,14 +70,11 @@ export type ContinueWithSessionCommand = Session & { requestId?: string };
|
||||
|
||||
export async function continueWithSession({ requestId, ...session }: ContinueWithSessionCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const t = await getTranslations("error");
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: session.factors?.user?.organizationId,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
|
||||
|
||||
if (requestId && session.id && session.factors?.user) {
|
||||
return completeFlowOrGetUrl(
|
||||
@@ -130,8 +124,8 @@ export async function updateSession(options: UpdateSessionCommand) {
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
if (!host) {
|
||||
return { error: "Could not get host" };
|
||||
@@ -143,10 +137,7 @@ export async function updateSession(options: UpdateSessionCommand) {
|
||||
challenges.webAuthN.domain = hostname;
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization });
|
||||
|
||||
let lifetime = checks?.webAuthN
|
||||
? loginSettings?.multiFactorCheckLifetime // TODO different lifetime for webauthn u2f/passkey
|
||||
@@ -177,10 +168,7 @@ export async function updateSession(options: UpdateSessionCommand) {
|
||||
// if password, check if user has MFA methods
|
||||
let authMethods;
|
||||
if (checks && checks.password && session.factors?.user?.id) {
|
||||
const response = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
const response = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
||||
if (response.authMethodTypes && response.authMethodTypes.length) {
|
||||
authMethods = response.authMethodTypes;
|
||||
}
|
||||
@@ -200,19 +188,19 @@ type ClearSessionOptions = {
|
||||
|
||||
export async function clearSession(options: ClearSessionOptions) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const { sessionId } = options;
|
||||
|
||||
const sessionCookie = await getSessionCookieById({ sessionId });
|
||||
|
||||
const deleteResponse = await deleteSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
|
||||
const securitySettings = await getSecuritySettings({ serviceUrl });
|
||||
const securitySettings = await getSecuritySettings({ serviceConfig });
|
||||
const iFrameEnabled = !!securitySettings?.embeddedIframe?.enabled;
|
||||
|
||||
if (!deleteResponse) {
|
||||
|
||||
@@ -6,8 +6,8 @@ import { VerifyU2FRegistrationRequestSchema } from "@zitadel/proto/zitadel/user/
|
||||
import { headers } from "next/headers";
|
||||
import { userAgent } from "next/server";
|
||||
import { getSessionCookieById } from "../cookies";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getOriginalHost } from "./host";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { getPublicHost } from "./host";
|
||||
|
||||
type RegisterU2FCommand = {
|
||||
sessionId: string;
|
||||
@@ -22,8 +22,8 @@ type VerifyU2FCommand = {
|
||||
|
||||
export async function addU2F(command: RegisterU2FCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const host = await getOriginalHost();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const host = getPublicHost(_headers);
|
||||
|
||||
const sessionCookie = await getSessionCookieById({
|
||||
sessionId: command.sessionId,
|
||||
@@ -33,11 +33,7 @@ export async function addU2F(command: RegisterU2FCommand) {
|
||||
return { error: "Could not get session" };
|
||||
}
|
||||
|
||||
const session = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
|
||||
|
||||
const [hostname] = host.split(":");
|
||||
|
||||
@@ -51,12 +47,12 @@ export async function addU2F(command: RegisterU2FCommand) {
|
||||
return { error: "Could not get session" };
|
||||
}
|
||||
|
||||
return registerU2F({ serviceUrl, userId, domain: hostname });
|
||||
return registerU2F({ serviceConfig, userId, domain: hostname });
|
||||
}
|
||||
|
||||
export async function verifyU2F(command: VerifyU2FCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
let passkeyName = command.passkeyName;
|
||||
if (!passkeyName) {
|
||||
const headersList = await headers();
|
||||
@@ -71,11 +67,7 @@ export async function verifyU2F(command: VerifyU2FCommand) {
|
||||
sessionId: command.sessionId,
|
||||
});
|
||||
|
||||
const session = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
});
|
||||
const session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token });
|
||||
|
||||
const userId = session?.session?.factors?.user?.id;
|
||||
|
||||
@@ -90,5 +82,5 @@ export async function verifyU2F(command: VerifyU2FCommand) {
|
||||
userId,
|
||||
});
|
||||
|
||||
return verifyU2FRegistration({ serviceUrl, request });
|
||||
return verifyU2FRegistration({ serviceConfig, request });
|
||||
}
|
||||
|
||||
@@ -20,30 +20,26 @@ import { cookies, headers } from "next/headers";
|
||||
import { completeFlowOrGetUrl } from "../client";
|
||||
import { getSessionCookieByLoginName } from "../cookies";
|
||||
import { getOrSetFingerprintId } from "../fingerprint";
|
||||
import { getServiceUrlFromHeaders } from "../service-url";
|
||||
import { getServiceConfig } from "../service-url";
|
||||
import { loadMostRecentSession } from "../session";
|
||||
import { checkMFAFactors } from "../verify-helper";
|
||||
import { createSessionAndUpdateCookie } from "./cookie";
|
||||
import { getOriginalHostWithProtocol } from "./host";
|
||||
import { getPublicHostWithProtocol } from "./host";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
export async function verifyTOTP(code: string, loginName?: string, organization?: string) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
return loadMostRecentSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionParams: {
|
||||
loginName,
|
||||
organization,
|
||||
},
|
||||
}).then((session) => {
|
||||
if (session?.factors?.user?.id) {
|
||||
return verifyTOTPRegistration({
|
||||
serviceUrl,
|
||||
code,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
return verifyTOTPRegistration({ serviceConfig, code, userId: session.factors.user.id });
|
||||
} else {
|
||||
throw Error("No user id found in session.");
|
||||
}
|
||||
@@ -62,22 +58,14 @@ type VerifyUserByEmailCommand = {
|
||||
export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
const t = await getTranslations("verify");
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const verifyResponse = command.isInvite
|
||||
? await verifyInviteCode({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
verificationCode: command.code,
|
||||
}).catch((error) => {
|
||||
? await verifyInviteCode({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
|
||||
console.warn(error);
|
||||
return { error: t("errors.couldNotVerifyInvite") };
|
||||
})
|
||||
: await verifyEmail({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
verificationCode: command.code,
|
||||
}).catch((error) => {
|
||||
: await verifyEmail({ serviceConfig, userId: command.userId, verificationCode: command.code }).catch((error) => {
|
||||
console.warn(error);
|
||||
return { error: t("errors.couldNotVerifyEmail") };
|
||||
});
|
||||
@@ -91,10 +79,7 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
}
|
||||
|
||||
let session: Session | undefined;
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: command.userId });
|
||||
|
||||
if (!userResponse || !userResponse.user) {
|
||||
return { error: t("errors.couldNotLoadUser") };
|
||||
@@ -110,22 +95,17 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
});
|
||||
|
||||
if (sessionCookie) {
|
||||
session = await getSession({
|
||||
serviceUrl,
|
||||
sessionId: sessionCookie.id,
|
||||
sessionToken: sessionCookie.token,
|
||||
}).then((response) => {
|
||||
session = await getSession({ serviceConfig, sessionId: sessionCookie.id, sessionToken: sessionCookie.token }).then(
|
||||
(response) => {
|
||||
if (response?.session) {
|
||||
return response.session;
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// load auth methods for user
|
||||
const authMethodResponse = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: user.userId,
|
||||
});
|
||||
const authMethodResponse = await listAuthenticationMethodTypes({ serviceConfig, userId: user.userId });
|
||||
|
||||
if (!authMethodResponse || !authMethodResponse.authMethodTypes) {
|
||||
return { error: t("errors.couldNotLoadAuthenticators") };
|
||||
@@ -203,14 +183,11 @@ export async function sendVerification(command: VerifyUserByEmailCommand) {
|
||||
return { redirect: `/verify/success?${verifySuccessParams}` };
|
||||
}
|
||||
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: user.details?.resourceOwner,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: user.details?.resourceOwner });
|
||||
|
||||
// redirect to mfa factor if user has one, or redirect to set one up
|
||||
const mfaFactorCheck = await checkMFAFactors(
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
session,
|
||||
loginSettings,
|
||||
authMethodResponse.authMethodTypes,
|
||||
@@ -253,14 +230,14 @@ type resendVerifyEmailCommand = {
|
||||
export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||
const t = await getTranslations("verify");
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const hostWithProtocol = await getOriginalHostWithProtocol();
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
const hostWithProtocol = await getPublicHostWithProtocol(_headers);
|
||||
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH ?? "";
|
||||
|
||||
return command.isInvite
|
||||
? createInviteCode({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId: command.userId,
|
||||
urlTemplate:
|
||||
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}&invite=true` +
|
||||
@@ -272,8 +249,8 @@ export async function resendVerification(command: resendVerifyEmailCommand) {
|
||||
return { error: t("errors.couldNotResendInvite") };
|
||||
})
|
||||
: zitadelSendEmailCode({
|
||||
serviceConfig,
|
||||
userId: command.userId,
|
||||
serviceUrl,
|
||||
urlTemplate:
|
||||
`${hostWithProtocol}${basePath}/verify?code={{.Code}}&userId={{.UserID}}&organization={{.OrgID}}` +
|
||||
(command.requestId ? `&requestId=${command.requestId}` : ""),
|
||||
@@ -287,22 +264,14 @@ type SendEmailCommand = {
|
||||
|
||||
export async function sendEmailCode(command: SendEmailCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
return zitadelSendEmailCode({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
urlTemplate: command.urlTemplate,
|
||||
});
|
||||
return zitadelSendEmailCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
|
||||
}
|
||||
|
||||
export async function sendInviteEmailCode(command: SendEmailCommand) {
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
return createInviteCode({
|
||||
serviceUrl,
|
||||
userId: command.userId,
|
||||
urlTemplate: command.urlTemplate,
|
||||
});
|
||||
return createInviteCode({ serviceConfig, userId: command.userId, urlTemplate: command.urlTemplate });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,217 @@
|
||||
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
|
||||
import { getServiceConfig, constructUrl } from "./service-url";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
describe("Service URL utilities", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
describe("getServiceConfig", () => {
|
||||
test("should throw when ZITADEL_API_URL is not set", () => {
|
||||
process.env.ZITADEL_API_URL = undefined as any;
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn(() => null),
|
||||
} as any;
|
||||
|
||||
expect(() => getServiceConfig(mockHeaders)).toThrow("ZITADEL_API_URL is not set");
|
||||
});
|
||||
|
||||
test("should return only baseUrl when x-zitadel-forward-host is not present (self-hosted)", () => {
|
||||
process.env.ZITADEL_API_URL = "https://zitadel.mycompany.com";
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "host") return "mycompany.com";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getServiceConfig(mockHeaders);
|
||||
|
||||
expect(result.serviceConfig.baseUrl).toBe("https://zitadel.mycompany.com");
|
||||
expect(result.serviceConfig.instanceHost).toBeUndefined();
|
||||
expect(result.serviceConfig.publicHost).toBe("mycompany.com");
|
||||
});
|
||||
|
||||
test("should use x-zitadel-forward-host when present (multi-tenant)", () => {
|
||||
process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
|
||||
if (key === "host") return "customer.zitadel.cloud";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getServiceConfig(mockHeaders);
|
||||
|
||||
expect(result.serviceConfig.baseUrl).toBe("https://api.zitadel.cloud");
|
||||
expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
|
||||
expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
|
||||
});
|
||||
|
||||
test("should strip protocol from instanceHost and publicHost", () => {
|
||||
process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return "https://customer.zitadel.cloud";
|
||||
if (key === "host") return "customer.zitadel.cloud";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getServiceConfig(mockHeaders);
|
||||
|
||||
expect(result.serviceConfig.instanceHost).toBe("customer.zitadel.cloud");
|
||||
expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud");
|
||||
});
|
||||
|
||||
test("should throw when host header is missing", () => {
|
||||
process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "host") return null;
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
expect(() => getServiceConfig(mockHeaders)).toThrow("No host found in headers");
|
||||
});
|
||||
|
||||
test("should handle host with port number", () => {
|
||||
process.env.ZITADEL_API_URL = "https://api.zitadel.cloud";
|
||||
|
||||
const mockHeaders = {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud:443";
|
||||
if (key === "host") return "customer.zitadel.cloud:443";
|
||||
return null;
|
||||
}),
|
||||
} as any;
|
||||
|
||||
const result = getServiceConfig(mockHeaders);
|
||||
|
||||
expect(result.serviceConfig.publicHost).toBe("customer.zitadel.cloud:443");
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructUrl", () => {
|
||||
test("should construct URL with x-zitadel-forward-host when present", () => {
|
||||
process.env.NEXT_PUBLIC_BASE_PATH = "";
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return "customer.zitadel.cloud";
|
||||
if (key === "host") return "customer.zitadel.cloud";
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
nextUrl: {
|
||||
protocol: "https:",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = constructUrl(mockRequest as NextRequest, "/test");
|
||||
|
||||
expect(result.hostname).toBe("customer.zitadel.cloud");
|
||||
expect(result.pathname).toBe("/test");
|
||||
expect(result.protocol).toBe("https:");
|
||||
});
|
||||
|
||||
test("should fall back to x-forwarded-host when x-zitadel-forward-host is not present", () => {
|
||||
process.env.NEXT_PUBLIC_BASE_PATH = "";
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "x-forwarded-host") return "mycompany.com";
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
nextUrl: {
|
||||
protocol: "https:",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = constructUrl(mockRequest as NextRequest, "/oauth/authorize");
|
||||
|
||||
expect(result.hostname).toBe("mycompany.com");
|
||||
expect(result.pathname).toBe("/oauth/authorize");
|
||||
});
|
||||
|
||||
test("should fall back to host header when no forwarded headers present", () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "x-zitadel-forward-host") return null;
|
||||
if (key === "x-forwarded-host") return null;
|
||||
if (key === "host") return "localhost:3000";
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
nextUrl: {
|
||||
protocol: "http:",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = constructUrl(mockRequest as NextRequest, "/test");
|
||||
|
||||
expect(result.hostname).toBe("localhost");
|
||||
expect(result.port).toBe("3000");
|
||||
});
|
||||
|
||||
test("should use protocol from nextUrl.protocol (not from headers)", () => {
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
get: vi.fn((key: string) => {
|
||||
// Even if x-forwarded-proto is present, it should be ignored
|
||||
if (key === "x-forwarded-proto") return "http";
|
||||
if (key === "host") return "example.com";
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
nextUrl: {
|
||||
protocol: "https:", // This should be used
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = constructUrl(mockRequest as NextRequest, "/test");
|
||||
|
||||
// Should use https: from nextUrl.protocol, not http from header
|
||||
expect(result.protocol).toBe("https:");
|
||||
});
|
||||
|
||||
test("should include base path when NEXT_PUBLIC_BASE_PATH is set", () => {
|
||||
process.env.NEXT_PUBLIC_BASE_PATH = "/login";
|
||||
|
||||
const mockRequest = {
|
||||
headers: {
|
||||
get: vi.fn((key: string) => {
|
||||
if (key === "host") return "example.com";
|
||||
return null;
|
||||
}),
|
||||
},
|
||||
nextUrl: {
|
||||
protocol: "https:",
|
||||
},
|
||||
} as any;
|
||||
|
||||
const result = constructUrl(mockRequest as NextRequest, "/test");
|
||||
|
||||
expect(result.pathname).toBe("/login/test");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,58 +1,47 @@
|
||||
import { ReadonlyHeaders } from "next/dist/server/web/spec-extension/adapters/headers";
|
||||
import { NextRequest } from "next/server";
|
||||
import { ServiceConfig } from "./zitadel";
|
||||
import { getInstanceHost, getPublicHost } from "./server/host";
|
||||
|
||||
/**
|
||||
* Extracts the service url and region from the headers if used in a multitenant context (host, x-zitadel-forward-host header)
|
||||
* or falls back to the ZITADEL_API_URL for a self hosting deployment
|
||||
* or falls back to the host header for a self hosting deployment using custom domains
|
||||
* @param headers
|
||||
* @returns the service url and region from the headers
|
||||
* @throws if the service url could not be determined
|
||||
* Extracts the service URL based on deployment mode and configuration.
|
||||
*
|
||||
* Priority:
|
||||
* 1. ZITADEL_API_URL (required) - Used by both self-hosted and multi-tenant
|
||||
* 2. x-zitadel-forward-host (multi-tenant only) - Set by Zitadel proxy
|
||||
* 3. host header (multi-tenant fallback) - For dynamic host resolution
|
||||
*
|
||||
* @param headers - Request headers
|
||||
* @returns Object containing the service Configuration
|
||||
* @throws Error if the service Configuration could not be determined
|
||||
*/
|
||||
export function getServiceUrlFromHeaders(headers: ReadonlyHeaders): {
|
||||
serviceUrl: string;
|
||||
} {
|
||||
let instanceUrl;
|
||||
|
||||
const forwardedHost = headers.get("x-zitadel-forward-host");
|
||||
// use the forwarded host if available (multitenant), otherwise fall back to the host of the deployment itself
|
||||
if (forwardedHost) {
|
||||
instanceUrl = forwardedHost;
|
||||
instanceUrl = instanceUrl.startsWith("http://")
|
||||
? instanceUrl
|
||||
: `https://${instanceUrl}`;
|
||||
} else if (process.env.ZITADEL_API_URL) {
|
||||
instanceUrl = process.env.ZITADEL_API_URL;
|
||||
} else {
|
||||
const host = headers.get("host");
|
||||
|
||||
if (host) {
|
||||
const [hostname] = host.split(":");
|
||||
if (hostname !== "localhost") {
|
||||
instanceUrl = host.startsWith("http") ? host : `https://${host}`;
|
||||
}
|
||||
}
|
||||
function stripProtocol(url: string): string {
|
||||
return url.replace(/^https?:\/\//, "");
|
||||
}
|
||||
|
||||
if (!instanceUrl) {
|
||||
throw new Error("Service URL could not be determined");
|
||||
export function getServiceConfig(headers: ReadonlyHeaders): { serviceConfig: ServiceConfig } {
|
||||
if (!process.env.ZITADEL_API_URL) {
|
||||
throw new Error("ZITADEL_API_URL is not set");
|
||||
}
|
||||
|
||||
// use forwarded host from proxy - headers are forwarded to the APIs.
|
||||
const instanceHost = getInstanceHost(headers);
|
||||
const publicHost = getPublicHost(headers);
|
||||
|
||||
return {
|
||||
serviceUrl: instanceUrl,
|
||||
serviceConfig: {
|
||||
baseUrl: process.env.ZITADEL_API_URL,
|
||||
...(instanceHost && { instanceHost: stripProtocol(instanceHost) }),
|
||||
...(publicHost && { publicHost: stripProtocol(publicHost) }),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function constructUrl(request: NextRequest, path: string) {
|
||||
const forwardedProto = request.headers.get("x-forwarded-proto")
|
||||
? `${request.headers.get("x-forwarded-proto")}:`
|
||||
: request.nextUrl.protocol;
|
||||
const protocol = request.nextUrl.protocol;
|
||||
|
||||
const forwardedHost =
|
||||
request.headers.get("x-zitadel-forward-host") ??
|
||||
request.headers.get("x-forwarded-host") ??
|
||||
request.headers.get("host");
|
||||
const forwardedHost = getPublicHost(request.headers);
|
||||
const basePath = process.env.NEXT_PUBLIC_BASE_PATH || "";
|
||||
return new URL(`${basePath}${path}`, `${forwardedProto}//${forwardedHost}`);
|
||||
return new URL(`${basePath}${path}`, `${protocol}//${forwardedHost}`);
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_servic
|
||||
import { SettingsService } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb";
|
||||
import { UserService } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { systemAPIToken } from "./api";
|
||||
import { createServerTransport } from "./zitadel";
|
||||
import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment";
|
||||
import { createServerTransport, ServiceConfig } from "./zitadel";
|
||||
|
||||
type ServiceClass =
|
||||
| typeof IdentityProviderService
|
||||
@@ -18,32 +19,27 @@ type ServiceClass =
|
||||
| typeof SettingsService
|
||||
| typeof SAMLService;
|
||||
|
||||
export async function createServiceForHost<T extends ServiceClass>(
|
||||
service: T,
|
||||
serviceUrl: string,
|
||||
) {
|
||||
export async function createServiceForHost<T extends ServiceClass>(service: T, serviceConfig: ServiceConfig) {
|
||||
let token;
|
||||
|
||||
// if we are running in a multitenancy context, use the system user token
|
||||
if (
|
||||
process.env.AUDIENCE &&
|
||||
process.env.SYSTEM_USER_ID &&
|
||||
process.env.SYSTEM_USER_PRIVATE_KEY
|
||||
) {
|
||||
// Determine authentication method based on available credentials
|
||||
// Prefer system user JWT if available, fallback to service user token
|
||||
if (hasSystemUserCredentials()) {
|
||||
token = await systemAPIToken();
|
||||
} else if (process.env.ZITADEL_SERVICE_USER_TOKEN) {
|
||||
} else if (hasServiceUserToken()) {
|
||||
// Use service user token authentication (self-hosted)
|
||||
token = process.env.ZITADEL_SERVICE_USER_TOKEN;
|
||||
} else {
|
||||
throw new Error(
|
||||
"No authentication credentials found. Set either system user credentials (AUDIENCE, SYSTEM_USER_ID, SYSTEM_USER_PRIVATE_KEY) or ZITADEL_SERVICE_USER_TOKEN",
|
||||
);
|
||||
}
|
||||
|
||||
if (!serviceUrl) {
|
||||
throw new Error("No instance url found");
|
||||
if (!serviceConfig) {
|
||||
throw new Error("No service config found");
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
throw new Error("No token found");
|
||||
}
|
||||
|
||||
const transport = createServerTransport(token, serviceUrl);
|
||||
const transport = createServerTransport(token, serviceConfig);
|
||||
|
||||
return createClientFor<T>(service)(transport);
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ describe("isSessionValid", () => {
|
||||
factors: undefined,
|
||||
});
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Session has no user");
|
||||
@@ -120,7 +120,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Session is expired", expect.any(String));
|
||||
@@ -147,7 +147,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
@@ -179,7 +179,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: false,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -207,7 +207,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: false,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -242,7 +242,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
@@ -278,7 +278,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.OTP_EMAIL],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -307,7 +307,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.U2F],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -342,7 +342,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -372,7 +372,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -409,7 +409,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -447,7 +447,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
@@ -499,7 +499,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
// This should be true - if it's false, the original bug still exists
|
||||
expect(result).toBe(true);
|
||||
@@ -539,7 +539,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
// With our fix, this should be true (session is valid)
|
||||
// With the old logic, this would have been false (bug)
|
||||
@@ -576,7 +576,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -611,7 +611,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith("Session has no valid multifactor", expect.any(Object));
|
||||
@@ -647,7 +647,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: false,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -681,7 +681,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: true,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -716,15 +716,15 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(true);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
organization: mockOrganizationId,
|
||||
});
|
||||
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
userId: mockUserId,
|
||||
});
|
||||
consoleSpy.mockRestore();
|
||||
@@ -777,7 +777,7 @@ describe("isSessionValid", () => {
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
@@ -831,7 +831,7 @@ describe("isSessionValid", () => {
|
||||
},
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -866,7 +866,7 @@ describe("isSessionValid", () => {
|
||||
|
||||
vi.mocked(verifyHelperModule.shouldEnforceMFA).mockReturnValue(false);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
// getUserByID should not be called when EMAIL_VERIFICATION is disabled
|
||||
@@ -902,7 +902,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: false,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -938,12 +938,12 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
|
||||
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
organization: mockOrganizationId,
|
||||
});
|
||||
});
|
||||
@@ -979,16 +979,16 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(false);
|
||||
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
|
||||
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
organization: mockOrganizationId,
|
||||
});
|
||||
expect(zitadelModule.listAuthenticationMethodTypes).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
userId: mockUserId,
|
||||
});
|
||||
});
|
||||
@@ -1024,12 +1024,12 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.TOTP, AuthenticationMethodType.OTP_EMAIL],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
|
||||
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
organization: mockOrganizationId,
|
||||
});
|
||||
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
|
||||
@@ -1067,7 +1067,7 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.TOTP],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
@@ -1105,12 +1105,12 @@ describe("isSessionValid", () => {
|
||||
authMethodTypes: [AuthenticationMethodType.TOTP],
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
expect(verifyHelperModule.shouldEnforceMFA).toHaveBeenCalledWith(session, expect.any(Object));
|
||||
expect(zitadelModule.getLoginSettings).toHaveBeenCalledWith({
|
||||
serviceUrl: mockServiceUrl,
|
||||
serviceConfig: { baseUrl: mockServiceUrl },
|
||||
organization: mockOrganizationId,
|
||||
});
|
||||
// Should not call listAuthenticationMethodTypes since shouldEnforceMFA returned false
|
||||
@@ -1146,7 +1146,7 @@ describe("isSessionValid", () => {
|
||||
forceMfaLocalOnly: false,
|
||||
} as any);
|
||||
|
||||
const result = await isSessionValid({ serviceUrl: mockServiceUrl, session });
|
||||
const result = await isSessionValid({ serviceConfig: { baseUrl: mockServiceUrl }, session });
|
||||
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
@@ -6,37 +6,30 @@ import { GetSessionResponse } from "@zitadel/proto/zitadel/session/v2/session_se
|
||||
import { AuthenticationMethodType } from "@zitadel/proto/zitadel/user/v2/user_service_pb";
|
||||
import { getMostRecentCookieWithLoginname } from "./cookies";
|
||||
import { shouldEnforceMFA } from "./verify-helper";
|
||||
import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes } from "./zitadel";
|
||||
import { getLoginSettings, getSession, getUserByID, listAuthenticationMethodTypes, ServiceConfig } from "./zitadel";
|
||||
|
||||
type LoadMostRecentSessionParams = {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
sessionParams: {
|
||||
loginName?: string;
|
||||
organization?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export async function loadMostRecentSession({
|
||||
serviceUrl,
|
||||
sessionParams,
|
||||
}: LoadMostRecentSessionParams): Promise<Session | undefined> {
|
||||
export async function loadMostRecentSession({ serviceConfig, sessionParams }: LoadMostRecentSessionParams): Promise<Session | undefined> {
|
||||
const recent = await getMostRecentCookieWithLoginname({
|
||||
loginName: sessionParams.loginName,
|
||||
organization: sessionParams.organization,
|
||||
});
|
||||
|
||||
return getSession({
|
||||
serviceUrl,
|
||||
sessionId: recent.id,
|
||||
sessionToken: recent.token,
|
||||
}).then((resp: GetSessionResponse) => resp.session);
|
||||
return getSession({ serviceConfig, sessionId: recent.id, sessionToken: recent.token }).then((resp: GetSessionResponse) => resp.session);
|
||||
}
|
||||
|
||||
/**
|
||||
* mfa is required, session is not valid anymore (e.g. session expired, user logged out, etc.)
|
||||
* to check for mfa for automatically selected session -> const response = await listAuthenticationMethodTypes(userId);
|
||||
**/
|
||||
export async function isSessionValid({ serviceUrl, session }: { serviceUrl: string; session: Session }): Promise<boolean> {
|
||||
export async function isSessionValid({ serviceConfig, session }: { serviceConfig: ServiceConfig; session: Session }): Promise<boolean> {
|
||||
// session can't be checked without user
|
||||
if (!session.factors?.user) {
|
||||
console.warn("Session has no user");
|
||||
@@ -51,20 +44,14 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
|
||||
const validPasskey = session?.factors?.webAuthN?.verifiedAt;
|
||||
|
||||
// Get login settings to determine if MFA is actually required by policy
|
||||
const loginSettings = await getLoginSettings({
|
||||
serviceUrl,
|
||||
organization: session.factors?.user?.organizationId,
|
||||
});
|
||||
const loginSettings = await getLoginSettings({ serviceConfig, organization: session.factors?.user?.organizationId });
|
||||
|
||||
// Use the existing shouldEnforceMFA function to determine if MFA is required
|
||||
const isMfaRequired = shouldEnforceMFA(session, loginSettings);
|
||||
|
||||
// Only enforce MFA validation if MFA is required by policy
|
||||
if (isMfaRequired) {
|
||||
const authMethodTypes = await listAuthenticationMethodTypes({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
const authMethodTypes = await listAuthenticationMethodTypes({ serviceConfig, userId: session.factors.user.id });
|
||||
|
||||
const authMethods = authMethodTypes.authMethodTypes;
|
||||
// Filter to only MFA methods (exclude PASSWORD and PASSKEY)
|
||||
@@ -134,10 +121,7 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
|
||||
|
||||
// Check email verification if EMAIL_VERIFICATION environment variable is enabled
|
||||
if (process.env.EMAIL_VERIFICATION === "true") {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: session.factors.user.id,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: session.factors.user.id });
|
||||
|
||||
const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
|
||||
|
||||
@@ -151,12 +135,12 @@ export async function isSessionValid({ serviceUrl, session }: { serviceUrl: stri
|
||||
}
|
||||
|
||||
export async function findValidSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessions,
|
||||
authRequest,
|
||||
samlRequest,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
serviceConfig: ServiceConfig;
|
||||
sessions: Session[];
|
||||
authRequest?: AuthRequest;
|
||||
samlRequest?: SAMLRequest;
|
||||
@@ -189,7 +173,7 @@ export async function findValidSession({
|
||||
|
||||
// return the first valid session according to settings
|
||||
for (const session of sessionsWithHint) {
|
||||
if (await isSessionValid({ serviceUrl, session })) {
|
||||
if (await isSessionValid({ serviceConfig, session })) {
|
||||
return session;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import crypto from "crypto";
|
||||
import moment from "moment";
|
||||
import { cookies } from "next/headers";
|
||||
import { getFingerprintIdCookie } from "./fingerprint";
|
||||
import { getUserByID } from "./zitadel";
|
||||
import { getUserByID, ServiceConfig } from "./zitadel";
|
||||
|
||||
export function checkPasswordChangeRequired(
|
||||
expirySettings: PasswordExpirySettings | undefined,
|
||||
@@ -82,7 +82,7 @@ export function checkEmailVerification(session: Session, humanUser?: HumanUser,
|
||||
}
|
||||
|
||||
export async function checkMFAFactors(
|
||||
serviceUrl: string,
|
||||
serviceConfig: ServiceConfig,
|
||||
session: Session,
|
||||
loginSettings: LoginSettings | undefined,
|
||||
authMethods: AuthenticationMethodType[],
|
||||
@@ -179,10 +179,7 @@ export async function checkMFAFactors(
|
||||
session?.factors?.user?.id &&
|
||||
shouldEnforceMFA(session, loginSettings)
|
||||
) {
|
||||
const userResponse = await getUserByID({
|
||||
serviceUrl,
|
||||
userId: session.factors?.user?.id,
|
||||
});
|
||||
const userResponse = await getUserByID({ serviceConfig, userId: session.factors?.user?.id });
|
||||
|
||||
const humanUser = userResponse?.user?.type.case === "human" ? userResponse?.user.type.value : undefined;
|
||||
|
||||
|
||||
+265
-198
@@ -45,15 +45,14 @@ async function cacheWrapper<T>(callback: Promise<T>) {
|
||||
}
|
||||
|
||||
export async function getHostedLoginTranslation({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization,
|
||||
locale,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
organization?: string;
|
||||
locale?: string;
|
||||
}) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
}>) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
const callback = settingsService
|
||||
.getHostedLoginTranslation(
|
||||
@@ -78,8 +77,13 @@ export async function getHostedLoginTranslation({
|
||||
return useCache ? cacheWrapper(callback) : callback;
|
||||
}
|
||||
|
||||
export async function getBrandingSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
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) }, {})
|
||||
@@ -88,8 +92,13 @@ export async function getBrandingSettings({ serviceUrl, organization }: { servic
|
||||
return useCache ? cacheWrapper(callback) : callback;
|
||||
}
|
||||
|
||||
export async function getLoginSettings({ serviceUrl, organization }: { serviceUrl: string; organization?: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
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) }, {})
|
||||
@@ -98,16 +107,16 @@ export async function getLoginSettings({ serviceUrl, organization }: { serviceUr
|
||||
return useCache ? cacheWrapper(callback) : callback;
|
||||
}
|
||||
|
||||
export async function getSecuritySettings({ serviceUrl }: { serviceUrl: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
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({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
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) }, {})
|
||||
@@ -116,8 +125,8 @@ export async function getLockoutSettings({ serviceUrl, orgId }: { serviceUrl: st
|
||||
return useCache ? cacheWrapper(callback) : callback;
|
||||
}
|
||||
|
||||
export async function getPasswordExpirySettings({ serviceUrl, orgId }: { serviceUrl: string; orgId?: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
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) }, {})
|
||||
@@ -126,32 +135,32 @@ export async function getPasswordExpirySettings({ serviceUrl, orgId }: { service
|
||||
return useCache ? cacheWrapper(callback) : callback;
|
||||
}
|
||||
|
||||
export async function listIDPLinks({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl }: { serviceUrl: string }) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
export async function getGeneralSettings({ serviceConfig }: WithServiceConfig) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
const callback = settingsService.getGeneralSettings({}, {}).then((resp) => resp.supportedLanguages);
|
||||
|
||||
@@ -159,13 +168,12 @@ export async function getGeneralSettings({ serviceUrl }: { serviceUrl: string })
|
||||
}
|
||||
|
||||
export async function getLegalAndSupportSettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
organization?: string;
|
||||
}) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
}>) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
const callback = settingsService
|
||||
.getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {})
|
||||
@@ -175,13 +183,12 @@ export async function getLegalAndSupportSettings({
|
||||
}
|
||||
|
||||
export async function getPasswordComplexitySettings({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
organization,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
organization?: string;
|
||||
}) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceUrl);
|
||||
}>) {
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
const callback = settingsService
|
||||
.getPasswordComplexitySettings({ ctx: makeReqCtx(organization) })
|
||||
@@ -191,15 +198,14 @@ export async function getPasswordComplexitySettings({
|
||||
}
|
||||
|
||||
export async function createSessionFromChecks({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
checks,
|
||||
lifetime,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
checks: Checks;
|
||||
lifetime: Duration;
|
||||
}) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
}>) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
const userAgent = await getUserAgent();
|
||||
|
||||
@@ -207,21 +213,20 @@ export async function createSessionFromChecks({
|
||||
}
|
||||
|
||||
export async function createSessionForUserIdAndIdpIntent({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
idpIntent,
|
||||
lifetime,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
idpIntent: {
|
||||
idpIntentId?: string | undefined;
|
||||
idpIntentToken?: string | undefined;
|
||||
};
|
||||
lifetime: Duration;
|
||||
}) {
|
||||
}>) {
|
||||
console.log("Creating session for userId and IDP intent", { userId, idpIntent, lifetime });
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
const userAgent = await getUserAgent();
|
||||
|
||||
@@ -241,21 +246,20 @@ export async function createSessionForUserIdAndIdpIntent({
|
||||
}
|
||||
|
||||
export async function setSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionId,
|
||||
sessionToken,
|
||||
challenges,
|
||||
checks,
|
||||
lifetime,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
sessionId: string;
|
||||
sessionToken: string;
|
||||
challenges: RequestChallenges | undefined;
|
||||
checks?: Checks;
|
||||
lifetime: Duration;
|
||||
}) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
}>) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
return sessionService.setSession(
|
||||
{
|
||||
@@ -271,40 +275,37 @@ export async function setSession({
|
||||
}
|
||||
|
||||
export async function getSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionId,
|
||||
sessionToken,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
sessionId: string;
|
||||
sessionToken: string;
|
||||
}) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
}>) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
return sessionService.getSession({ sessionId, sessionToken }, {});
|
||||
}
|
||||
|
||||
export async function deleteSession({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
sessionId,
|
||||
sessionToken,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
sessionId: string;
|
||||
sessionToken: string;
|
||||
}) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
}>) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
return sessionService.deleteSession({ sessionId, sessionToken }, {});
|
||||
}
|
||||
|
||||
type ListSessionsCommand = {
|
||||
serviceUrl: string;
|
||||
type ListSessionsCommand = WithServiceConfig<{
|
||||
ids: string[];
|
||||
};
|
||||
}>;
|
||||
|
||||
export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceUrl);
|
||||
export async function listSessions({ serviceConfig, ids }: ListSessionsCommand) {
|
||||
const sessionService: Client<typeof SessionService> = await createServiceForHost(SessionService, serviceConfig);
|
||||
|
||||
return sessionService.listSessions(
|
||||
{
|
||||
@@ -321,17 +322,16 @@ export async function listSessions({ serviceUrl, ids }: ListSessionsCommand) {
|
||||
);
|
||||
}
|
||||
|
||||
export type AddHumanUserData = {
|
||||
serviceUrl: string;
|
||||
export type AddHumanUserData = WithServiceConfig<{
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
email: string;
|
||||
password?: string;
|
||||
organization: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export async function addHumanUser({ serviceUrl, email, firstName, lastName, password, organization }: AddHumanUserData) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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: {
|
||||
@@ -360,67 +360,69 @@ export async function addHumanUser({ serviceUrl, email, firstName, lastName, pas
|
||||
return userService.addHumanUser(addHumanUserRequest);
|
||||
}
|
||||
|
||||
export async function addHuman({ serviceUrl, request }: { serviceUrl: string; request: AddHumanUserRequest }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, request }: { serviceUrl: string; request: UpdateHumanUserRequest }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
code,
|
||||
userId,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
code: string;
|
||||
userId: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.verifyTOTPRegistration({ code, userId }, {});
|
||||
}
|
||||
|
||||
export async function getUserByID({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
verificationCode,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
verificationCode: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.verifyInviteCode({ userId, verificationCode }, {});
|
||||
}
|
||||
|
||||
export async function sendEmailCode({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
urlTemplate,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
urlTemplate: string;
|
||||
}) {
|
||||
}>) {
|
||||
let medium = create(SendEmailCodeRequestSchema, { userId });
|
||||
|
||||
medium = create(SendEmailCodeRequestSchema, {
|
||||
@@ -433,20 +435,19 @@ export async function sendEmailCode({
|
||||
},
|
||||
});
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.sendEmailCode(medium, {});
|
||||
}
|
||||
|
||||
export async function createInviteCode({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
urlTemplate,
|
||||
userId,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
urlTemplate: string;
|
||||
userId: string;
|
||||
}) {
|
||||
}>) {
|
||||
let medium = create(SendInviteCodeSchema, {
|
||||
applicationName: process.env.NEXT_PUBLIC_APPLICATION_NAME || "Zitadel Login",
|
||||
});
|
||||
@@ -456,7 +457,7 @@ export async function createInviteCode({
|
||||
urlTemplate,
|
||||
};
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.createInviteCode(
|
||||
{
|
||||
@@ -470,16 +471,15 @@ export async function createInviteCode({
|
||||
);
|
||||
}
|
||||
|
||||
export type ListUsersCommand = {
|
||||
serviceUrl: string;
|
||||
export type ListUsersCommand = WithServiceConfig<{
|
||||
loginName?: string;
|
||||
userName?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
organizationId?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
export async function listUsers({ serviceUrl, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
|
||||
export async function listUsers({ serviceConfig, loginName, userName, phone, email, organizationId }: ListUsersCommand) {
|
||||
const queries: SearchQuery[] = [];
|
||||
|
||||
// either use loginName or userName, email, phone
|
||||
@@ -562,18 +562,17 @@ export async function listUsers({ serviceUrl, loginName, userName, phone, email,
|
||||
);
|
||||
}
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.listUsers({ queries });
|
||||
}
|
||||
|
||||
export type SearchUsersCommand = {
|
||||
serviceUrl: string;
|
||||
export type SearchUsersCommand = WithServiceConfig<{
|
||||
searchValue: string;
|
||||
loginSettings: LoginSettings;
|
||||
organizationId?: string;
|
||||
suffix?: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
const PhoneQuery = (searchValue: string) =>
|
||||
create(SearchQuerySchema, {
|
||||
@@ -612,7 +611,13 @@ const EmailQuery = (searchValue: string) =>
|
||||
* 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({ serviceUrl, searchValue, loginSettings, organizationId, suffix }: SearchUsersCommand) {
|
||||
export async function searchUsers({
|
||||
serviceConfig,
|
||||
searchValue,
|
||||
loginSettings,
|
||||
organizationId,
|
||||
suffix,
|
||||
}: SearchUsersCommand) {
|
||||
const queries: SearchQuery[] = [];
|
||||
|
||||
const t = await getTranslations("zitadel");
|
||||
@@ -640,7 +645,7 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
|
||||
);
|
||||
}
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
const loginNameResult = await userService.listUsers({ queries });
|
||||
|
||||
@@ -723,8 +728,8 @@ export async function searchUsers({ serviceUrl, searchValue, loginSettings, orga
|
||||
return { result: [] };
|
||||
}
|
||||
|
||||
export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Promise<Organization | null> {
|
||||
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceUrl);
|
||||
export async function getDefaultOrg({ serviceConfig }: WithServiceConfig): Promise<Organization | null> {
|
||||
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
|
||||
|
||||
return orgService
|
||||
.listOrganizations(
|
||||
@@ -743,8 +748,8 @@ export async function getDefaultOrg({ serviceUrl }: { serviceUrl: string }): Pro
|
||||
.then((resp) => (resp?.result && resp.result[0] ? resp.result[0] : null));
|
||||
}
|
||||
|
||||
export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: string; domain: string }) {
|
||||
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceUrl);
|
||||
export async function getOrgsByDomain({ serviceConfig, domain }: WithServiceConfig<{ domain: string }>) {
|
||||
const orgService: Client<typeof OrganizationService> = await createServiceForHost(OrganizationService, serviceConfig);
|
||||
|
||||
return orgService.listOrganizations(
|
||||
{
|
||||
@@ -762,15 +767,15 @@ export async function getOrgsByDomain({ serviceUrl, domain }: { serviceUrl: stri
|
||||
}
|
||||
|
||||
export async function startIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idpId,
|
||||
urls,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
idpId: string;
|
||||
urls: RedirectURLsJson;
|
||||
}): Promise<string | null> {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>): 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({
|
||||
@@ -823,17 +828,16 @@ export async function startIdentityProviderFlow({
|
||||
}
|
||||
|
||||
export async function startLDAPIdentityProviderFlow({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idpId,
|
||||
username,
|
||||
password,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
idpId: string;
|
||||
username: string;
|
||||
password: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.startIdentityProviderIntent({
|
||||
idpId,
|
||||
@@ -847,16 +851,26 @@ export async function startLDAPIdentityProviderFlow({
|
||||
});
|
||||
}
|
||||
|
||||
export async function getAuthRequest({ serviceUrl, authRequestId }: { serviceUrl: string; authRequestId: string }) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||
export async function getAuthRequest({
|
||||
serviceConfig,
|
||||
authRequestId,
|
||||
}: WithServiceConfig<{
|
||||
authRequestId: string;
|
||||
}>) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
|
||||
|
||||
return oidcService.getAuthRequest({
|
||||
authRequestId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getDeviceAuthorizationRequest({ serviceUrl, userCode }: { serviceUrl: string; userCode: string }) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||
export async function getDeviceAuthorizationRequest({
|
||||
serviceConfig,
|
||||
userCode,
|
||||
}: WithServiceConfig<{
|
||||
userCode: string;
|
||||
}>) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
|
||||
|
||||
return oidcService.getDeviceAuthorizationRequest({
|
||||
userCode,
|
||||
@@ -864,15 +878,14 @@ export async function getDeviceAuthorizationRequest({ serviceUrl, userCode }: {
|
||||
}
|
||||
|
||||
export async function authorizeOrDenyDeviceAuthorization({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
deviceAuthorizationId,
|
||||
session,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
deviceAuthorizationId: string;
|
||||
session?: { sessionId: string; sessionToken: string };
|
||||
}) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||
}>) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
|
||||
|
||||
return oidcService.authorizeOrDenyDeviceAuthorization({
|
||||
deviceAuthorizationId,
|
||||
@@ -888,36 +901,40 @@ export async function authorizeOrDenyDeviceAuthorization({
|
||||
});
|
||||
}
|
||||
|
||||
export async function createCallback({ serviceUrl, req }: { serviceUrl: string; req: CreateCallbackRequest }) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceUrl);
|
||||
export async function createCallback({ serviceConfig, req }: WithServiceConfig<{ req: CreateCallbackRequest }>) {
|
||||
const oidcService = await createServiceForHost(OIDCService, serviceConfig);
|
||||
|
||||
return oidcService.createCallback(req);
|
||||
}
|
||||
|
||||
export async function getSAMLRequest({ serviceUrl, samlRequestId }: { serviceUrl: string; samlRequestId: string }) {
|
||||
const samlService = await createServiceForHost(SAMLService, serviceUrl);
|
||||
export async function getSAMLRequest({
|
||||
serviceConfig,
|
||||
samlRequestId,
|
||||
}: WithServiceConfig<{
|
||||
samlRequestId: string;
|
||||
}>) {
|
||||
const samlService = await createServiceForHost(SAMLService, serviceConfig);
|
||||
|
||||
return samlService.getSAMLRequest({
|
||||
samlRequestId,
|
||||
});
|
||||
}
|
||||
|
||||
export async function createResponse({ serviceUrl, req }: { serviceUrl: string; req: CreateResponseRequest }) {
|
||||
const samlService = await createServiceForHost(SAMLService, serviceUrl);
|
||||
export async function createResponse({ serviceConfig, req }: WithServiceConfig<{ req: CreateResponseRequest }>) {
|
||||
const samlService = await createServiceForHost(SAMLService, serviceConfig);
|
||||
|
||||
return samlService.createResponse(req);
|
||||
}
|
||||
|
||||
export async function verifyEmail({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
verificationCode,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
verificationCode: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.verifyEmail(
|
||||
{
|
||||
@@ -929,14 +946,13 @@ export async function verifyEmail({
|
||||
}
|
||||
|
||||
export async function resendEmailCode({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
urlTemplate,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
urlTemplate: string;
|
||||
}) {
|
||||
}>) {
|
||||
let request: ResendEmailCodeRequest = create(ResendEmailCodeRequestSchema, {
|
||||
userId,
|
||||
});
|
||||
@@ -947,33 +963,42 @@ export async function resendEmailCode({
|
||||
|
||||
request = { ...request, verification: { case: "sendCode", value: medium } };
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.resendEmailCode(request, {});
|
||||
}
|
||||
|
||||
export async function retrieveIDPIntent({ serviceUrl, id, token }: { serviceUrl: string; id: string; token: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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({ serviceUrl, id }: { serviceUrl: string; id: string }) {
|
||||
const idpService: Client<typeof IdentityProviderService> = await createServiceForHost(IdentityProviderService, serviceUrl);
|
||||
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({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
idp,
|
||||
userId,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
idp: { id: string; userId: string; userName: string };
|
||||
userId: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.addIDPLink(
|
||||
{
|
||||
@@ -989,14 +1014,13 @@ export async function addIDPLink({
|
||||
}
|
||||
|
||||
export async function passwordReset({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
urlTemplate,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
urlTemplate?: string;
|
||||
}) {
|
||||
}>) {
|
||||
let medium = create(SendPasswordResetLinkSchema, {
|
||||
notificationType: NotificationType.Email,
|
||||
});
|
||||
@@ -1006,7 +1030,7 @@ export async function passwordReset({
|
||||
urlTemplate,
|
||||
};
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.passwordReset(
|
||||
{
|
||||
@@ -1021,16 +1045,15 @@ export async function passwordReset({
|
||||
}
|
||||
|
||||
export async function setUserPassword({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
password,
|
||||
code,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
password: string;
|
||||
code?: string;
|
||||
}) {
|
||||
}>) {
|
||||
let payload = create(SetPasswordRequestSchema, {
|
||||
userId,
|
||||
newPassword: {
|
||||
@@ -1048,7 +1071,7 @@ export async function setUserPassword({
|
||||
};
|
||||
}
|
||||
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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)
|
||||
@@ -1060,8 +1083,13 @@ export async function setUserPassword({
|
||||
});
|
||||
}
|
||||
|
||||
export async function setPassword({ serviceUrl, payload }: { serviceUrl: string; payload: SetPasswordRequest }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
export async function setPassword({
|
||||
serviceConfig,
|
||||
payload,
|
||||
}: WithServiceConfig<{
|
||||
payload: SetPasswordRequest;
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.setPassword(payload, {});
|
||||
}
|
||||
@@ -1072,8 +1100,13 @@ export async function setPassword({ serviceUrl, payload }: { serviceUrl: string;
|
||||
* @param userId the id of the user where the email should be set
|
||||
* @returns the newly set email
|
||||
*/
|
||||
export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
export async function createPasskeyRegistrationLink({
|
||||
serviceConfig,
|
||||
userId,
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.createPasskeyRegistrationLink({
|
||||
userId,
|
||||
@@ -1091,8 +1124,15 @@ export async function createPasskeyRegistrationLink({ serviceUrl, userId }: { se
|
||||
* @param domain the domain on which the factor is registered
|
||||
* @returns the newly set email
|
||||
*/
|
||||
export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl: string; userId: string; domain: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
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,
|
||||
@@ -1107,13 +1147,12 @@ export async function registerU2F({ serviceUrl, userId, domain }: { serviceUrl:
|
||||
* @returns the result of the verification
|
||||
*/
|
||||
export async function verifyU2FRegistration({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
request,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
request: VerifyU2FRegistrationRequest;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.verifyU2FRegistration(request, {});
|
||||
}
|
||||
@@ -1126,19 +1165,18 @@ export async function verifyU2FRegistration({
|
||||
* @returns the active identity providers
|
||||
*/
|
||||
export async function getActiveIdentityProviders({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
orgId,
|
||||
linking_allowed,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: 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, serviceUrl);
|
||||
const settingsService: Client<typeof SettingsService> = await createServiceForHost(SettingsService, serviceConfig);
|
||||
|
||||
return settingsService.getActiveIdentityProviders(props, {});
|
||||
}
|
||||
@@ -1150,13 +1188,12 @@ export async function getActiveIdentityProviders({
|
||||
* @returns the result of the verification
|
||||
*/
|
||||
export async function verifyPasskeyRegistration({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
request,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
request: VerifyPasskeyRegistrationRequest;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.verifyPasskeyRegistration(request, {});
|
||||
}
|
||||
@@ -1170,17 +1207,16 @@ export async function verifyPasskeyRegistration({
|
||||
* @returns the newly set email
|
||||
*/
|
||||
export async function registerPasskey({
|
||||
serviceUrl,
|
||||
serviceConfig,
|
||||
userId,
|
||||
code,
|
||||
domain,
|
||||
}: {
|
||||
serviceUrl: string;
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
code: { id: string; code: string };
|
||||
domain: string;
|
||||
}) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.registerPasskey({
|
||||
userId,
|
||||
@@ -1195,23 +1231,52 @@ export async function registerPasskey({
|
||||
* @param userId the id of the user where the email should be set
|
||||
* @returns the list of authentication method types
|
||||
*/
|
||||
export async function listAuthenticationMethodTypes({ serviceUrl, userId }: { serviceUrl: string; userId: string }) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceUrl);
|
||||
export async function listAuthenticationMethodTypes({
|
||||
serviceConfig,
|
||||
userId,
|
||||
}: WithServiceConfig<{
|
||||
userId: string;
|
||||
}>) {
|
||||
const userService: Client<typeof UserService> = await createServiceForHost(UserService, serviceConfig);
|
||||
|
||||
return userService.listAuthenticationMethodTypes({
|
||||
userId,
|
||||
});
|
||||
}
|
||||
|
||||
export function createServerTransport(token: string, baseUrl: string) {
|
||||
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,
|
||||
interceptors: !process.env.CUSTOM_REQUEST_HEADERS
|
||||
baseUrl: serviceConfig.baseUrl,
|
||||
interceptors:
|
||||
!process.env.CUSTOM_REQUEST_HEADERS && !serviceConfig.instanceHost && !serviceConfig.publicHost
|
||||
? undefined
|
||||
: [
|
||||
(next) => {
|
||||
return (req) => {
|
||||
process.env.CUSTOM_REQUEST_HEADERS!.split(",").forEach((header) => {
|
||||
// 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) {
|
||||
req.header.set(header.slice(0, kv).trim(), header.slice(kv + 1).trim());
|
||||
@@ -1219,6 +1284,8 @@ export function createServerTransport(token: string, baseUrl: string) {
|
||||
console.warn(`Skipping malformed header: ${header}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return next(req);
|
||||
};
|
||||
},
|
||||
|
||||
@@ -3,28 +3,16 @@ import { SecuritySettings } from "@zitadel/proto/zitadel/settings/v2/security_se
|
||||
import { headers } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { DEFAULT_CSP } from "../constants/csp";
|
||||
import { getServiceUrlFromHeaders } from "./lib/service-url";
|
||||
import { getServiceConfig } from "./lib/service-url";
|
||||
export const config = {
|
||||
matcher: [
|
||||
"/.well-known/:path*",
|
||||
"/oauth/:path*",
|
||||
"/oidc/:path*",
|
||||
"/idps/callback/:path*",
|
||||
"/saml/:path*",
|
||||
"/:path*",
|
||||
],
|
||||
matcher: ["/.well-known/:path*", "/oauth/:path*", "/oidc/:path*", "/idps/callback/:path*", "/saml/:path*", "/:path*"],
|
||||
};
|
||||
|
||||
async function loadSecuritySettings(
|
||||
request: NextRequest,
|
||||
): Promise<SecuritySettings | null> {
|
||||
async function loadSecuritySettings(request: NextRequest): Promise<SecuritySettings | null> {
|
||||
const securityResponse = await fetch(`${request.nextUrl.origin}/security`);
|
||||
|
||||
if (!securityResponse.ok) {
|
||||
console.error(
|
||||
"Failed to fetch security settings:",
|
||||
securityResponse.statusText,
|
||||
);
|
||||
console.error("Failed to fetch security settings:", securityResponse.statusText);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -49,40 +37,28 @@ export async function middleware(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Only run the rest of the logic for the original matcher paths
|
||||
const proxyPaths = [
|
||||
"/.well-known/",
|
||||
"/oauth/",
|
||||
"/oidc/",
|
||||
"/idps/callback/",
|
||||
"/saml/",
|
||||
];
|
||||
const proxyPaths = ["/.well-known/", "/oauth/", "/oidc/", "/idps/callback/", "/saml/"];
|
||||
|
||||
const isMatched = proxyPaths.some((prefix) =>
|
||||
request.nextUrl.pathname.startsWith(prefix),
|
||||
);
|
||||
const isMatched = proxyPaths.some((prefix) => request.nextUrl.pathname.startsWith(prefix));
|
||||
|
||||
// escape proxy if the environment is setup for multitenancy
|
||||
if (
|
||||
!isMatched ||
|
||||
!process.env.ZITADEL_API_URL ||
|
||||
!process.env.ZITADEL_SERVICE_USER_TOKEN
|
||||
) {
|
||||
// For all other routes, just add the header and continue
|
||||
// Only proxy in self-hosted mode
|
||||
if (!isMatched) {
|
||||
// For multi-tenant or non-proxied routes, just add the header and continue
|
||||
return NextResponse.next({
|
||||
request: { headers: requestHeaders },
|
||||
});
|
||||
}
|
||||
|
||||
const _headers = await headers();
|
||||
const { serviceUrl } = getServiceUrlFromHeaders(_headers);
|
||||
const { serviceConfig } = getServiceConfig(_headers);
|
||||
|
||||
const instanceHost = `${serviceUrl}`
|
||||
.replace("https://", "")
|
||||
.replace("http://", "");
|
||||
|
||||
// Add additional headers as before
|
||||
requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`);
|
||||
requestHeaders.set("x-zitadel-instance-host", instanceHost);
|
||||
// add additional headers if available
|
||||
if (serviceConfig.publicHost) {
|
||||
requestHeaders.set("x-zitadel-public-host", serviceConfig.publicHost);
|
||||
}
|
||||
if (serviceConfig.instanceHost) {
|
||||
requestHeaders.set("x-zitadel-instance-host", serviceConfig.instanceHost);
|
||||
}
|
||||
|
||||
const responseHeaders = new Headers();
|
||||
responseHeaders.set("Access-Control-Allow-Origin", "*");
|
||||
@@ -98,7 +74,7 @@ export async function middleware(request: NextRequest) {
|
||||
responseHeaders.delete("X-Frame-Options");
|
||||
}
|
||||
|
||||
request.nextUrl.href = `${serviceUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
|
||||
request.nextUrl.href = `${serviceConfig.baseUrl}${request.nextUrl.pathname}${request.nextUrl.search}`;
|
||||
|
||||
return NextResponse.rewrite(request.nextUrl, {
|
||||
request: {
|
||||
|
||||
@@ -28,6 +28,16 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules", "integration", "acceptance"]
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"integration",
|
||||
"acceptance"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -10,10 +10,11 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/zitadel/logging"
|
||||
"github.com/zitadel/oidc/v3/pkg/crypto"
|
||||
"github.com/zitadel/oidc/v3/pkg/oidc"
|
||||
"github.com/zitadel/oidc/v3/pkg/op"
|
||||
"golang.org/x/text/language"
|
||||
|
||||
"github.com/zitadel/zitadel/internal/api/authz"
|
||||
http_utils "github.com/zitadel/zitadel/internal/api/http"
|
||||
@@ -33,6 +34,7 @@ const (
|
||||
LoginPostLogoutRedirectParam = "post_logout_redirect"
|
||||
LoginLogoutHintParam = "logout_hint"
|
||||
LoginUILocalesParam = "ui_locales"
|
||||
LoginLogoutTokenParam = "logout_token"
|
||||
LoginPath = "/login"
|
||||
LogoutPath = "/logout"
|
||||
LogoutDonePath = "/logout/done"
|
||||
@@ -298,7 +300,11 @@ func (o *OPStorage) TerminateSessionFromRequest(ctx context.Context, endSessionR
|
||||
} else {
|
||||
logoutURI = logoutURI.JoinPath(LogoutPath)
|
||||
}
|
||||
return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales), nil
|
||||
signer, _, err := GetSignerOnce(o.query.GetActiveSigningWebKey)(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buildLoginV2LogoutURL(logoutURI, redirectURI, endSessionRequest.LogoutHint, endSessionRequest.UILocales, signer)
|
||||
}
|
||||
|
||||
// V1:
|
||||
@@ -375,7 +381,13 @@ func (o *OPStorage) federatedLogout(ctx context.Context, sessionID string, postL
|
||||
return login.ExternalLogoutPath(sessionID)
|
||||
}
|
||||
|
||||
func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales []language.Tag) string {
|
||||
type logoutTokenPayload struct {
|
||||
PostLogoutRedirectURI string `json:"post_logout_redirect_uri,omitempty"`
|
||||
LogoutHint string `json:"logout_hint,omitempty"`
|
||||
UILocales oidc.Locales `json:"ui_locales,omitempty"`
|
||||
}
|
||||
|
||||
func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, uiLocales oidc.Locales, signer jose.Signer) (string, error) {
|
||||
if strings.HasSuffix(logoutURI.Path, "/") && len(logoutURI.Path) > 1 {
|
||||
logoutURI.Path = strings.TrimSuffix(logoutURI.Path, "/")
|
||||
}
|
||||
@@ -386,14 +398,19 @@ func buildLoginV2LogoutURL(logoutURI *url.URL, redirectURI, logoutHint string, u
|
||||
q.Set(LoginLogoutHintParam, logoutHint)
|
||||
}
|
||||
if len(uiLocales) > 0 {
|
||||
locales := make([]string, len(uiLocales))
|
||||
for i, locale := range uiLocales {
|
||||
locales[i] = locale.String()
|
||||
q.Set(LoginUILocalesParam, uiLocales.String())
|
||||
}
|
||||
q.Set(LoginUILocalesParam, strings.Join(locales, " "))
|
||||
logoutToken, err := crypto.Sign(&logoutTokenPayload{
|
||||
PostLogoutRedirectURI: redirectURI,
|
||||
LogoutHint: logoutHint,
|
||||
UILocales: uiLocales,
|
||||
}, signer)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
q.Set(LoginLogoutTokenParam, logoutToken)
|
||||
logoutURI.RawQuery = q.Encode()
|
||||
return logoutURI.String()
|
||||
return logoutURI.String(), nil
|
||||
}
|
||||
|
||||
// v2PostLogoutRedirectURI will take care that the post_logout_redirect_uri is correctly set for v2 logins.
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/go-jose/go-jose/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/text/language"
|
||||
@@ -18,6 +22,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI string
|
||||
logoutHint string
|
||||
uiLocales []language.Tag
|
||||
signer jose.Signer
|
||||
expectedParams map[string]string
|
||||
}{
|
||||
{
|
||||
@@ -26,6 +31,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI: "https://client/cb",
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -36,6 +42,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_hint": "[email protected]",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -46,6 +53,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"ui_locales": "en it",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -58,6 +66,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_hint": "logoutme",
|
||||
"ui_locales": "de-CH fr",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -66,6 +75,7 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
redirectURI: "https://client/cb",
|
||||
expectedParams: map[string]string{
|
||||
"post_logout_redirect": "https://client/cb",
|
||||
"logout_token": "", // presence checked separately
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -79,9 +89,10 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// When
|
||||
got := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales)
|
||||
got, err := buildLoginV2LogoutURL(logoutURI, tc.redirectURI, tc.logoutHint, tc.uiLocales, signer)
|
||||
|
||||
// Then
|
||||
require.NoError(t, err)
|
||||
gotURL, err := url.Parse(got)
|
||||
require.NoError(t, err)
|
||||
require.NotContains(t, gotURL.String(), "/logout/")
|
||||
@@ -91,8 +102,39 @@ func TestBuildLoginV2LogoutURL(t *testing.T) {
|
||||
require.Len(t, q, len(tc.expectedParams))
|
||||
|
||||
for k, v := range tc.expectedParams {
|
||||
if k == LoginLogoutTokenParam {
|
||||
assertLogoutToken(t, q.Get(k), &logoutTokenPayload{
|
||||
PostLogoutRedirectURI: tc.redirectURI,
|
||||
LogoutHint: tc.logoutHint,
|
||||
UILocales: tc.uiLocales,
|
||||
})
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, v, q.Get(k))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func assertLogoutToken(t *testing.T, token string, payload *logoutTokenPayload) {
|
||||
signature, err := jose.ParseSigned(token, []jose.SignatureAlgorithm{jose.RS256})
|
||||
require.NoError(t, err)
|
||||
logoutToken := new(logoutTokenPayload)
|
||||
err = json.Unmarshal(signature.UnsafePayloadWithoutVerification(), logoutToken)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, payload, logoutToken)
|
||||
}
|
||||
|
||||
var (
|
||||
privKey, _ = rsa.GenerateKey(rand.Reader, 2048)
|
||||
signer = func() jose.Signer {
|
||||
signer, _ := jose.NewSigner(
|
||||
jose.SigningKey{
|
||||
Algorithm: jose.RS256,
|
||||
Key: privKey,
|
||||
},
|
||||
(&jose.SignerOptions{}).WithType("JWT"),
|
||||
)
|
||||
return signer
|
||||
}()
|
||||
)
|
||||
|
||||
@@ -551,15 +551,16 @@ func TestOPStorage_TerminateSession_refresh_grant(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func buildLogoutURL(origin, logoutURLV2 string, redirectURI string, extraParams map[string]string) string {
|
||||
func buildLogoutURL(origin, logoutURLV2, redirectURI string, extraParams map[string]string) *url.URL {
|
||||
u, _ := url.Parse(origin + logoutURLV2 + redirectURI)
|
||||
q := u.Query()
|
||||
for k, v := range extraParams {
|
||||
q.Set(k, v)
|
||||
}
|
||||
q.Set("logout_token", "signed-logout-token") // placeholder
|
||||
u.RawQuery = q.Encode()
|
||||
// Append the redirect URI as a URL-escaped string
|
||||
return u.String()
|
||||
return u
|
||||
}
|
||||
|
||||
func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
@@ -567,7 +568,7 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
name string
|
||||
clientID string
|
||||
authRequestID func(t testing.TB, instance *integration.Instance, clientID, redirectURI string, scope ...string) string
|
||||
logoutURL string
|
||||
logoutURL *url.URL
|
||||
}{
|
||||
{
|
||||
name: "login header",
|
||||
@@ -614,7 +615,18 @@ func TestOPStorage_TerminateSession_empty_id_token_hint(t *testing.T) {
|
||||
|
||||
postLogoutRedirect, err := rp.EndSession(CTX, provider, "", logoutRedirectURI, "state", "hint", oidc.ParseLocales([]string{"it-IT", "en-US"}))
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, tt.logoutURL, postLogoutRedirect.String())
|
||||
|
||||
requiredQueries := tt.logoutURL.Query()
|
||||
for key, value := range requiredQueries {
|
||||
if key == "logout_token" {
|
||||
assert.NotEmpty(t, value[0], "logout_token must be present")
|
||||
continue
|
||||
}
|
||||
assert.Equal(t, value[0], postLogoutRedirect.Query().Get(key))
|
||||
}
|
||||
requiredURLWithoutQueries := *tt.logoutURL
|
||||
requiredURLWithoutQueries.RawQuery = ""
|
||||
assert.Equal(t, requiredURLWithoutQueries.String(), postLogoutRedirect.Scheme+"://"+postLogoutRedirect.Host+postLogoutRedirect.Path)
|
||||
|
||||
// userinfo must not fail until login UI terminated session
|
||||
_, err = rp.Userinfo[*oidc.UserInfo](CTX, tokens.AccessToken, tokens.TokenType, tokens.IDTokenClaims.Subject, provider)
|
||||
|
||||
@@ -1,5 +1,14 @@
|
||||
import { createGrpcTransport, GrpcTransportOptions } from "@connectrpc/connect-node";
|
||||
import { importPKCS8, SignJWT } from "jose";
|
||||
import {
|
||||
createGrpcTransport,
|
||||
GrpcTransportOptions,
|
||||
} from "@connectrpc/connect-node";
|
||||
import {
|
||||
createRemoteJWKSet,
|
||||
importPKCS8,
|
||||
jwtVerify,
|
||||
JWTPayload,
|
||||
SignJWT,
|
||||
} from "jose";
|
||||
import { NewAuthorizationBearerInterceptor } from "./interceptors.js";
|
||||
|
||||
/**
|
||||
@@ -7,10 +16,16 @@ import { NewAuthorizationBearerInterceptor } from "./interceptors.js";
|
||||
* @param token
|
||||
* @param opts
|
||||
*/
|
||||
export function createServerTransport(token: string, opts: GrpcTransportOptions) {
|
||||
export function createServerTransport(
|
||||
token: string,
|
||||
opts: GrpcTransportOptions
|
||||
) {
|
||||
return createGrpcTransport({
|
||||
...opts,
|
||||
interceptors: [...(opts.interceptors || []), NewAuthorizationBearerInterceptor(token)],
|
||||
interceptors: [
|
||||
...(opts.interceptors || []),
|
||||
NewAuthorizationBearerInterceptor(token),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -34,3 +49,36 @@ export async function newSystemToken({
|
||||
.setAudience(audience)
|
||||
.sign(await importPKCS8(key, "RS256"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify a signed JWT with the given keys endpoint.
|
||||
* @param token
|
||||
* @param keysEndpoint
|
||||
* @param options
|
||||
*/
|
||||
export async function verifyJwt<T = JWTPayload>(
|
||||
token: string,
|
||||
keysEndpoint: string,
|
||||
options?: {
|
||||
issuer?: string;
|
||||
audience?: string;
|
||||
instanceHost?: string;
|
||||
publicHost?: string;
|
||||
},
|
||||
): Promise<T & JWTPayload> {
|
||||
const headers: Record<string, string> = {};
|
||||
if (options?.instanceHost) {
|
||||
headers["x-zitadel-instance-host"] = options.instanceHost;
|
||||
}
|
||||
if (options?.publicHost) {
|
||||
headers["x-zitadel-public-host"] = options.publicHost;
|
||||
}
|
||||
const JWKS = createRemoteJWKSet(new URL(keysEndpoint), {headers: headers});
|
||||
|
||||
const { payload } = await jwtVerify(token, JWKS, {
|
||||
issuer: options?.issuer,
|
||||
audience: options?.audience,
|
||||
});
|
||||
|
||||
return payload as T & JWTPayload;
|
||||
}
|
||||
|
||||
Generated
+503
-237
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user