diff --git a/apps/login/next-env-vars.d.ts b/apps/login/next-env-vars.d.ts index 041e3a955..f57ab6a30 100644 --- a/apps/login/next-env-vars.d.ts +++ b/apps/login/next-env-vars.d.ts @@ -22,6 +22,20 @@ declare namespace NodeJS { */ ZITADEL_SERVICE_USER_TOKEN: string; + /** + * Path to a private key file for JWT authentication. + * When set, the login service will read the key and sign JWTs for API authentication. + * Requires ZITADEL_LOGIN_SYSTEM_USER_ID or SYSTEM_USER_ID to be set. + * AUDIENCE defaults to ZITADEL_API_URL if not explicitly set. + */ + ZITADEL_LOGIN_SERVICE_KEY_FILE?: string; + + /** + * The system user ID for login service key authentication. + * Falls back to SYSTEM_USER_ID if not set. + */ + ZITADEL_LOGIN_SYSTEM_USER_ID?: string; + /** * Optional: wheter a user must have verified email */ diff --git a/apps/login/src/lib/api.ts b/apps/login/src/lib/api.ts index 732400730..56616d659 100644 --- a/apps/login/src/lib/api.ts +++ b/apps/login/src/lib/api.ts @@ -1,5 +1,16 @@ import { newSystemToken } from "@zitadel/client/node"; +import { readFileSync } from "fs"; +import { getLoginSystemUserId } from "./deployment"; +/** + * Creates a signed JWT token using system user credentials from environment + * variables. The SYSTEM_USER_PRIVATE_KEY is expected to be base64-encoded and + * is decoded before signing. Requires AUDIENCE, SYSTEM_USER_ID, and + * SYSTEM_USER_PRIVATE_KEY to be set in the environment. + * + * @returns A signed JWT token string for authenticating API requests. + * @throws If the underlying token signing fails. + */ export async function systemAPIToken() { const token = { audience: process.env.AUDIENCE, @@ -15,3 +26,28 @@ export async function systemAPIToken() { key: token.token, }); } + +/** + * Creates a signed JWT token by reading a private key from the file path + * specified in ZITADEL_LOGIN_SERVICE_KEY_FILE. The audience is resolved from + * AUDIENCE or falls back to ZITADEL_API_URL, and the subject is resolved via + * {@link getLoginSystemUserId}. + * + * @returns A signed JWT token string for authenticating API requests. + * @throws If the key file cannot be read or the token signing fails. + */ +export async function loginServiceKeyToken() { + const keyFile = process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE!; + + try { + const key = readFileSync(keyFile, "utf-8"); + + return newSystemToken({ + audience: process.env.AUDIENCE || process.env.ZITADEL_API_URL, + subject: getLoginSystemUserId()!, + key: key, + }); + } catch (err) { + throw new Error(`Failed to read login service key file "${keyFile}": ${err instanceof Error ? err.message : err}`); + } +} diff --git a/apps/login/src/lib/deployment.test.ts b/apps/login/src/lib/deployment.test.ts index b8a7c288d..8005b763b 100644 --- a/apps/login/src/lib/deployment.test.ts +++ b/apps/login/src/lib/deployment.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test, beforeEach, afterEach } from "vitest"; -import { hasSystemUserCredentials, hasServiceUserToken } from "./deployment"; +import { hasSystemUserCredentials, hasServiceUserToken, hasLoginServiceKey, getLoginSystemUserId } from "./deployment"; describe("Deployment utilities", () => { const originalEnv = process.env; @@ -55,6 +55,61 @@ describe("Deployment utilities", () => { }); }); + describe("hasLoginServiceKey", () => { + test("should return true with ZITADEL_LOGIN_SERVICE_KEY_FILE and ZITADEL_LOGIN_SYSTEM_USER_ID", () => { + process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE = "/path/to/key.pem"; + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = "login-user"; + + expect(hasLoginServiceKey()).toBe(true); + }); + + test("should return true with ZITADEL_LOGIN_SERVICE_KEY_FILE and SYSTEM_USER_ID fallback", () => { + process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE = "/path/to/key.pem"; + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = undefined as any; + process.env.SYSTEM_USER_ID = "system-user"; + + expect(hasLoginServiceKey()).toBe(true); + }); + + test("should return false when ZITADEL_LOGIN_SERVICE_KEY_FILE is missing", () => { + process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE = undefined as any; + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = "login-user"; + + expect(hasLoginServiceKey()).toBe(false); + }); + + test("should return false when no user ID is set", () => { + process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE = "/path/to/key.pem"; + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = undefined as any; + process.env.SYSTEM_USER_ID = undefined as any; + + expect(hasLoginServiceKey()).toBe(false); + }); + }); + + describe("getLoginSystemUserId", () => { + test("should return ZITADEL_LOGIN_SYSTEM_USER_ID when set", () => { + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = "login-user"; + process.env.SYSTEM_USER_ID = "system-user"; + + expect(getLoginSystemUserId()).toBe("login-user"); + }); + + test("should return SYSTEM_USER_ID as fallback", () => { + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = undefined as any; + process.env.SYSTEM_USER_ID = "system-user"; + + expect(getLoginSystemUserId()).toBe("system-user"); + }); + + test("should return undefined when neither is set", () => { + process.env.ZITADEL_LOGIN_SYSTEM_USER_ID = undefined as any; + process.env.SYSTEM_USER_ID = undefined as any; + + expect(getLoginSystemUserId()).toBeUndefined(); + }); + }); + describe("hasServiceUserToken", () => { test("should return true when ZITADEL_SERVICE_USER_TOKEN is present", () => { process.env.ZITADEL_SERVICE_USER_TOKEN = "token123"; diff --git a/apps/login/src/lib/deployment.ts b/apps/login/src/lib/deployment.ts index a8fc329b9..85c8d4953 100644 --- a/apps/login/src/lib/deployment.ts +++ b/apps/login/src/lib/deployment.ts @@ -14,6 +14,27 @@ export function hasSystemUserCredentials(): boolean { return !!process.env.AUDIENCE && !!process.env.SYSTEM_USER_ID && !!process.env.SYSTEM_USER_PRIVATE_KEY; } +/** + * Checks if login service key file is available for JWT authentication. + * + * @returns true if ZITADEL_LOGIN_SERVICE_KEY_FILE and a user ID are present, false otherwise + */ +export function hasLoginServiceKey(): boolean { + return ( + !!process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE && + !!(process.env.ZITADEL_LOGIN_SYSTEM_USER_ID || process.env.SYSTEM_USER_ID) + ); +} + +/** + * Gets the system user ID for login service key authentication. + * + * @returns ZITADEL_LOGIN_SYSTEM_USER_ID if set, otherwise SYSTEM_USER_ID + */ +export function getLoginSystemUserId(): string | undefined { + return process.env.ZITADEL_LOGIN_SYSTEM_USER_ID || process.env.SYSTEM_USER_ID; +} + /** * Checks if service account token is available for authentication. * diff --git a/apps/login/src/lib/service.ts b/apps/login/src/lib/service.ts index fa7b0c158..8b1a98d87 100644 --- a/apps/login/src/lib/service.ts +++ b/apps/login/src/lib/service.ts @@ -6,8 +6,8 @@ import { SAMLService } from "@zitadel/proto/zitadel/saml/v2/saml_service_pb"; import { SessionService } from "@zitadel/proto/zitadel/session/v2/session_service_pb"; 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 { hasSystemUserCredentials, hasServiceUserToken } from "./deployment"; +import { systemAPIToken, loginServiceKeyToken } from "./api"; +import { hasSystemUserCredentials, hasLoginServiceKey, hasServiceUserToken } from "./deployment"; import { createServerTransport, ServiceConfig } from "./zitadel"; type ServiceClass = @@ -23,15 +23,17 @@ export async function createServiceForHost(service: T, s let token; // Determine authentication method based on available credentials - // Prefer system user JWT if available, fallback to service account token + // Priority: system user JWT > login service key > service account token if (hasSystemUserCredentials()) { token = await systemAPIToken(); + } else if (hasLoginServiceKey()) { + token = await loginServiceKeyToken(); } else if (hasServiceUserToken()) { // Use service account 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", + "No authentication credentials found. Set either system user credentials (AUDIENCE, SYSTEM_USER_ID, SYSTEM_USER_PRIVATE_KEY), login service key (ZITADEL_LOGIN_SERVICE_KEY_FILE with ZITADEL_LOGIN_SYSTEM_USER_ID or SYSTEM_USER_ID), or ZITADEL_SERVICE_USER_TOKEN", ); }