diff --git a/apps/login/next-env-vars.d.ts b/apps/login/next-env-vars.d.ts new file mode 100644 index 00000000..dce7ea90 --- /dev/null +++ b/apps/login/next-env-vars.d.ts @@ -0,0 +1,33 @@ +declare namespace NodeJS { + interface ProcessEnv { + /** + * The system api url + */ + AUDIENCE: string; + + /** + * The system api service user ID + */ + SYSTEM_USER_ID: string; + + /** + * The service user key + */ + SYSTEM_USER_PRIVATE_KEY: string; + + /** + * The instance url + */ + ZITADEL_API_URL: string; + + /** + * The service user id for the instance + */ + ZITADEL_USER_ID: string; + + /** + * The service user token for the instance + */ + ZITADEL_USER_TOKEN: string; + } +} diff --git a/apps/login/package.json b/apps/login/package.json index 9617300a..2dbdf66c 100644 --- a/apps/login/package.json +++ b/apps/login/package.json @@ -42,6 +42,7 @@ "@zitadel/proto": "workspace:*", "clsx": "1.2.1", "copy-to-clipboard": "^3.3.3", + "jose": "^5.3.0", "deepmerge": "^4.3.1", "moment": "^2.29.4", "next": "14.2.14", diff --git a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx index c7a5f06c..90ebbdbc 100644 --- a/apps/login/src/app/(login)/idp/[provider]/success/page.tsx +++ b/apps/login/src/app/(login)/idp/[provider]/success/page.tsx @@ -117,7 +117,7 @@ export default async function Page({ userName: idpInformation.userName, }, foundUser.userId, - ).catch((error) => { + ).catch(() => { return (
diff --git a/apps/login/src/app/(login)/idp/page.tsx b/apps/login/src/app/(login)/idp/page.tsx index ee43d558..bea7c7be 100644 --- a/apps/login/src/app/(login)/idp/page.tsx +++ b/apps/login/src/app/(login)/idp/page.tsx @@ -1,17 +1,8 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; -import { getBrandingSettings, settingsService } from "@/lib/zitadel"; -import { makeReqCtx } from "@zitadel/client/v2"; +import { getActiveIdentityProviders, getBrandingSettings } from "@/lib/zitadel"; import { getLocale, getTranslations } from "next-intl/server"; -function getIdentityProviders(orgId?: string) { - return settingsService - .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => { - return resp.identityProviders; - }); -} - export default async function Page({ searchParams, }: { @@ -23,7 +14,7 @@ export default async function Page({ const authRequestId = searchParams?.authRequestId; const organization = searchParams?.organization; - const identityProviders = await getIdentityProviders(organization); + const identityProviders = await getActiveIdentityProviders(organization); const host = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` diff --git a/apps/login/src/app/(login)/loginname/page.tsx b/apps/login/src/app/(login)/loginname/page.tsx index 928bf138..7edc2f2c 100644 --- a/apps/login/src/app/(login)/loginname/page.tsx +++ b/apps/login/src/app/(login)/loginname/page.tsx @@ -2,22 +2,13 @@ import { DynamicTheme } from "@/components/dynamic-theme"; import { SignInWithIdp } from "@/components/sign-in-with-idp"; import { UsernameForm } from "@/components/username-form"; import { + getActiveIdentityProviders, getBrandingSettings, getLegalAndSupportSettings, getLoginSettings, - settingsService, } from "@/lib/zitadel"; -import { makeReqCtx } from "@zitadel/client/v2"; import { getLocale, getTranslations } from "next-intl/server"; -function getIdentityProviders(orgId?: string) { - return settingsService - .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => { - return resp.identityProviders; - }); -} - export default async function Page({ searchParams, }: { @@ -34,7 +25,7 @@ export default async function Page({ const loginSettings = await getLoginSettings(organization); const legal = await getLegalAndSupportSettings(); - const identityProviders = await getIdentityProviders(organization); + const identityProviders = await getActiveIdentityProviders(organization); const host = process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` @@ -55,7 +46,7 @@ export default async function Page({ submit={submit} allowRegister={!!loginSettings?.allowRegister} > - {legal && identityProviders && process.env.ZITADEL_API_URL && ( + {legal && identityProviders && ( { - return resp.identityProviders; - }); + ); const idp = identityProviders.find((idp) => idp.id === idpId); diff --git a/apps/login/src/lib/api.ts b/apps/login/src/lib/api.ts new file mode 100644 index 00000000..b6df92f4 --- /dev/null +++ b/apps/login/src/lib/api.ts @@ -0,0 +1,36 @@ +import { importPKCS8, SignJWT } from "jose"; +import { getInstanceByHost } from "./zitadel"; + +export async function getInstanceUrl(host: string): Promise { + const instance = await getInstanceByHost(host); + const generatedDomain = instance.domains.find( + (domain) => domain.generated === true, + ); + + if (!generatedDomain?.domain) { + throw new Error("No generated domain found"); + } + + console.log(`host: ${host}, api: ${generatedDomain?.domain}`); + + return generatedDomain?.domain; +} + +export async function systemAPIToken() { + const audience = process.env.AUDIENCE; + const userID = process.env.SYSTEM_USER_ID; + const key = process.env.SYSTEM_USER_PRIVATE_KEY; + + const decodedToken = Buffer.from(key, "base64").toString("utf-8"); + + const token = new SignJWT({}) + .setProtectedHeader({ alg: "RS256" }) + .setIssuedAt() + .setExpirationTime("1h") + .setIssuer(userID) + .setSubject(userID) + .setAudience(audience) + .sign(await importPKCS8(decodedToken, "RS256")); + + return token; +} diff --git a/apps/login/src/lib/self.ts b/apps/login/src/lib/self.ts index 64addf84..550b3865 100644 --- a/apps/login/src/lib/self.ts +++ b/apps/login/src/lib/self.ts @@ -1,27 +1,21 @@ "use server"; -import { - createSessionServiceClient, - createUserServiceClient, -} from "@zitadel/client/v2"; +import { createUserServiceClient } from "@zitadel/client/v2"; import { createServerTransport } from "@zitadel/node"; import { getSessionCookieById } from "./cookies"; -const transport = (token: string) => +const transport = (url: string, token: string) => createServerTransport(token, { - baseUrl: process.env.ZITADEL_API_URL!, + baseUrl: url, httpVersion: "2", }); -const sessionService = (sessionId: string) => { - return getSessionCookieById({ sessionId }).then((session) => { - return createSessionServiceClient(transport(session.token)); - }); -}; - const userService = (sessionId: string) => { return getSessionCookieById({ sessionId }).then((session) => { - return createUserServiceClient(transport(session.token)); + return createUserServiceClient( + // TODO: get baseurl dynamically + transport(process.env.ZITADEL_API_URL, session.token), + ); }); }; diff --git a/apps/login/src/lib/server/loginname.ts b/apps/login/src/lib/server/loginname.ts index 299d5f95..eb63d7db 100644 --- a/apps/login/src/lib/server/loginname.ts +++ b/apps/login/src/lib/server/loginname.ts @@ -37,9 +37,7 @@ export async function sendLoginname(command: SendLoginnameCommand) { const redirectUserToSingleIDPIfAvailable = async () => { const identityProviders = await getActiveIdentityProviders( command.organization, - ).then((resp) => { - return resp.identityProviders; - }); + ); if (identityProviders.length === 1) { const host = headers().get("host"); diff --git a/apps/login/src/lib/zitadel.ts b/apps/login/src/lib/zitadel.ts index 3b032200..81f8b2e3 100644 --- a/apps/login/src/lib/zitadel.ts +++ b/apps/login/src/lib/zitadel.ts @@ -18,21 +18,26 @@ import { } from "@zitadel/proto/zitadel/user/v2/user_service_pb"; import { create, fromJson, toJson } from "@zitadel/client"; +import { createSystemServiceClient } from "@zitadel/client/v1"; import { TextQueryMethod } from "@zitadel/proto/zitadel/object/v2/object_pb"; import { CreateCallbackRequest } from "@zitadel/proto/zitadel/oidc/v2/oidc_service_pb"; +import { ListOrganizationsResponse } from "@zitadel/proto/zitadel/org/v2/org_service_pb"; import { BrandingSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/branding_settings_pb"; import { LegalAndSupportSettingsSchema } from "@zitadel/proto/zitadel/settings/v2/legal_settings_pb"; import { + IdentityProvider, IdentityProviderType, LoginSettingsSchema, } from "@zitadel/proto/zitadel/settings/v2/login_settings_pb"; import { PasswordComplexitySettingsSchema } from "@zitadel/proto/zitadel/settings/v2/password_settings_pb"; +import { GetActiveIdentityProvidersResponse } from "@zitadel/proto/zitadel/settings/v2/settings_service_pb"; import type { RedirectURLsJson } from "@zitadel/proto/zitadel/user/v2/idp_pb"; import { SearchQuery, SearchQuerySchema, } from "@zitadel/proto/zitadel/user/v2/query_pb"; import { unstable_cache } from "next/cache"; +import { systemAPIToken } from "./api"; import { PROVIDER_MAPPING } from "./idp"; const SESSION_LIFETIME_S = 3600; // TODO load from oidc settings @@ -41,28 +46,89 @@ const CACHE_REVALIDATION_INTERVAL_IN_SECONDS = process.env ? Number(process.env.CACHE_REVALIDATION_INTERVAL_IN_SECONDS) : 3600; -const transport = createServerTransport( - process.env.ZITADEL_SERVICE_USER_TOKEN!, - { - baseUrl: process.env.ZITADEL_API_URL!, +// TODO: check for better typing +async function createServiceForHost(mapper: (transport: any) => any) { + // const host = headers().get("X-Forwarded-Host"); + // if (!host) { + // throw new Error("No host header found!"); + // } + + // let instanceUrl; + // try { + // instanceUrl = await getInstanceUrl(host); + // } catch (error) { + // console.error( + // "Could not get instance url, fallback to ZITADEL_API_URL", + // error, + // ); + // instanceUrl = process.env.ZITADEL_API_URL; + // } + + // remove in favor of the above + const instanceUrl = process.env.ZITADEL_API_URL; + + const systemToken = await systemAPIToken(); + + const transport = createServerTransport(systemToken, { + baseUrl: instanceUrl, httpVersion: "2", - }, + }); + + return mapper(transport); +} + +const idpService = await createServiceForHost(createIdpServiceClient); +const orgService = await createServiceForHost(createOrganizationServiceClient); +export const sessionService = await createServiceForHost( + createSessionServiceClient, ); +const userService = await createServiceForHost(createUserServiceClient); +const oidcService = await createServiceForHost(createOIDCServiceClient); +const settingsService = await createServiceForHost(createSettingsServiceClient); -export const sessionService = createSessionServiceClient(transport); -export const userService = createUserServiceClient(transport); -export const oidcService = createOIDCServiceClient(transport); -export const idpService = createIdpServiceClient(transport); -export const orgService = createOrganizationServiceClient(transport); +const systemService = async () => { + const systemToken = await systemAPIToken(); -export const settingsService = createSettingsServiceClient(transport); + const transport = createServerTransport(systemToken, { + baseUrl: process.env.ZITADEL_API_URL, + httpVersion: "2", + }); + + return createSystemServiceClient(transport); +}; + +export async function getInstanceByHost(host: string) { + return (await systemService()) + .listInstances( + { + queries: [ + { + query: { + case: "domainQuery", + value: { + domains: [host], + }, + }, + }, + ], + }, + {}, + ) + .then((resp) => { + if (resp.result.length !== 1) { + throw new Error("Could not find instance"); + } + + return resp.result[0]; + }); +} export async function getBrandingSettings(organization?: string) { return unstable_cache( async () => { return await settingsService .getBrandingSettings({ ctx: makeReqCtx(organization) }, {}) - .then((resp) => + .then((resp: any) => resp.settings ? toJson(BrandingSettingsSchema, resp.settings) : undefined, @@ -83,7 +149,7 @@ export async function getLoginSettings(orgId?: string) { async () => { return await settingsService .getLoginSettings({ ctx: makeReqCtx(orgId) }, {}) - .then((resp) => + .then((resp: any) => resp.settings ? toJson(LoginSettingsSchema, resp.settings) : undefined, @@ -126,7 +192,7 @@ export async function registerTOTP(userId: string) { export async function getGeneralSettings() { return settingsService .getGeneralSettings({}, {}) - .then((resp) => resp.supportedLanguages); + .then((resp: any) => resp.supportedLanguages); } export async function getLegalAndSupportSettings(organization?: string) { @@ -134,7 +200,7 @@ export async function getLegalAndSupportSettings(organization?: string) { async () => { return await settingsService .getLegalAndSupportSettings({ ctx: makeReqCtx(organization) }, {}) - .then((resp) => + .then((resp: any) => resp.settings ? toJson(LegalAndSupportSettingsSchema, resp.settings) : undefined, @@ -155,7 +221,7 @@ export async function getPasswordComplexitySettings(organization?: string) { async () => { return await settingsService .getPasswordComplexitySettings({ ctx: makeReqCtx(organization) }) - .then((resp) => + .then((resp: any) => resp.settings ? toJson(PasswordComplexitySettingsSchema, resp.settings) : undefined, @@ -382,6 +448,24 @@ export async function getOrgsByDomain(domain: string) { ); } +export async function getDefaultOrg() { + return orgService + .listOrganizations( + { + queries: [ + { + query: { + case: "defaultQuery", + value: {}, + }, + }, + ], + }, + {}, + ) + .then((resp: ListOrganizationsResponse) => resp.result[0]); +} + export async function startIdentityProviderFlow({ idpId, urls, @@ -454,7 +538,7 @@ export function retrieveIDPIntent(id: string, token: string) { } export function getIDPByID(id: string) { - return idpService.getIDPByID({ id }, {}).then((resp) => resp.idp); + return idpService.getIDPByID({ id }, {}).then((resp: any) => resp.idp); } export function addIDPLink( @@ -508,16 +592,7 @@ export async function passwordReset(userId: string) { */ // TODO check for token requirements! -export async function createPasskeyRegistrationLink( - userId: string, - // token: string, -) { - // const transport = createServerTransport(token, { - // baseUrl: process.env.ZITADEL_API_URL!, - // httpVersion: "2", - // }); - - // const service = createUserServiceClient(transport); +export async function createPasskeyRegistrationLink(userId: string) { return userService.createPasskeyRegistrationLink({ userId, medium: { @@ -535,17 +610,7 @@ export async function createPasskeyRegistrationLink( */ // TODO check for token requirements! -export async function registerU2F( - userId: string, - domain: string, - // token: string, -) { - // const transport = createServerTransport(token, { - // baseUrl: process.env.ZITADEL_API_URL!, - // httpVersion: "2", - // }); - - // const service = createUserServiceClient(transport); +export async function registerU2F(userId: string, domain: string) { return userService.registerU2F({ userId, domain, @@ -564,11 +629,12 @@ export async function verifyU2FRegistration( return userService.verifyU2FRegistration(request, {}); } -export async function getActiveIdentityProviders(orgId?: string) { - return settingsService.getActiveIdentityProviders( - { ctx: makeReqCtx(orgId) }, - {}, - ); +export async function getActiveIdentityProviders( + orgId?: string, +): Promise { + return settingsService + .getActiveIdentityProviders({ ctx: makeReqCtx(orgId) }, {}) + .then((resp: GetActiveIdentityProvidersResponse) => resp.identityProviders); } /** diff --git a/apps/login/src/middleware.ts b/apps/login/src/middleware.ts index 28be3cbd..f8f91997 100644 --- a/apps/login/src/middleware.ts +++ b/apps/login/src/middleware.ts @@ -9,28 +9,34 @@ export const config = { ], }; -const INSTANCE = process.env.ZITADEL_API_URL; -const SERVICE_USER_ID = process.env.ZITADEL_SERVICE_USER_ID as string; +export async function middleware(request: NextRequest) { + // escape proxy if the environment is + if ( + !process.env.ZITADEL_API_URL || + !process.env.ZITADEL_USER_ID || + !process.env.ZITADEL_USER_TOKEN + ) { + return NextResponse.next(); + } + + const INSTANCE_URL = process.env.ZITADEL_API_URL; + const instanceHost = `${INSTANCE_URL}`.replace("https://", ""); -export function middleware(request: NextRequest) { const requestHeaders = new Headers(request.headers); - requestHeaders.set("x-zitadel-login-client", SERVICE_USER_ID); + requestHeaders.set("x-zitadel-login-client", process.env.ZITADEL_USER_ID); // this is a workaround for the next.js server not forwarding the host header // requestHeaders.set("x-zitadel-forwarded", `host="${request.nextUrl.host}"`); requestHeaders.set("x-zitadel-public-host", `${request.nextUrl.host}`); // this is a workaround for the next.js server not forwarding the host header - requestHeaders.set( - "x-zitadel-instance-host", - `${INSTANCE}`.replace("https://", ""), - ); + requestHeaders.set("x-zitadel-instance-host", instanceHost); const responseHeaders = new Headers(); responseHeaders.set("Access-Control-Allow-Origin", "*"); responseHeaders.set("Access-Control-Allow-Headers", "*"); - request.nextUrl.href = `${INSTANCE}${request.nextUrl.pathname}${request.nextUrl.search}`; + request.nextUrl.href = `${INSTANCE_URL}${request.nextUrl.pathname}${request.nextUrl.search}`; return NextResponse.rewrite(request.nextUrl, { request: { headers: requestHeaders, diff --git a/apps/login/tsconfig.json b/apps/login/tsconfig.json index a1efe752..c855c432 100755 --- a/apps/login/tsconfig.json +++ b/apps/login/tsconfig.json @@ -2,6 +2,7 @@ "extends": "@zitadel/tsconfig/nextjs.json", "compilerOptions": { "jsx": "preserve", + "target": "es2022", "baseUrl": ".", "paths": { "@/*": ["./src/*"] diff --git a/packages/zitadel-node/src/index.ts b/packages/zitadel-node/src/index.ts index e614d044..ef6539cc 100644 --- a/packages/zitadel-node/src/index.ts +++ b/packages/zitadel-node/src/index.ts @@ -19,8 +19,8 @@ export async function newSystemToken() { .setProtectedHeader({ alg: "RS256" }) .setIssuedAt() .setExpirationTime("1h") - .setIssuer(process.env.ZITADEL_SYSTEM_API_USERID ?? "") - .setSubject(process.env.ZITADEL_SYSTEM_API_USERID ?? "") - .setAudience(process.env.ZITADEL_ISSUER ?? "") - .sign(await importPKCS8(process.env.ZITADEL_SYSTEM_API_KEY ?? "", "RS256")); + .setIssuer(process.env.SYSTEM_USER_ID ?? "") + .setSubject(process.env.SYSTEM_USER_ID ?? "") + .setAudience(process.env.AUDIENCE ?? "") + .sign(await importPKCS8(process.env.SYSTEM_USER_PRIVATE_KEY ?? "", "RS256")); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a2f4ff9..821b4059 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -80,6 +80,9 @@ importers: deepmerge: specifier: ^4.3.1 version: 4.3.1 + jose: + specifier: ^5.3.0 + version: 5.8.0 moment: specifier: ^2.29.4 version: 2.30.1 diff --git a/turbo.json b/turbo.json index 7d38ae4c..1f0975b3 100644 --- a/turbo.json +++ b/turbo.json @@ -4,14 +4,12 @@ "globalDependencies": ["**/.env.*local"], "globalEnv": [ "DEBUG", + "AUDIENCE", + "SYSTEM_USER_ID", + "SYSTEM_USER_PRIVATE_KEY", "ZITADEL_API_URL", - "ZITADEL_SERVICE_USER_ID", - "ZITADEL_SERVICE_USER_TOKEN", - "ZITADEL_SYSTEM_API_URL", - "ZITADEL_SYSTEM_API_USERID", - "ZITADEL_SYSTEM_API_KEY", - "ZITADEL_ISSUER", - "ZITADEL_ADMIN_TOKEN", + "ZITADEL_USER_ID", + "ZITADEL_USER_TOKEN", "CACHE_REVALIDATION_INTERVAL_IN_SECONDS", "VERCEL_URL" ],