feat(login): add LOGIN_SERVICE_KEY_FILE authentication support (#11572)

This commit is contained in:
Mridang Agarwalla
2026-02-24 10:14:24 +00:00
committed by GitHub
parent 839a2d3b5b
commit 54095b64b1
5 changed files with 133 additions and 5 deletions
+14
View File
@@ -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
*/
+36
View File
@@ -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}`);
}
}
+56 -1
View File
@@ -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";
+21
View File
@@ -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.
*
+6 -4
View File
@@ -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<T extends ServiceClass>(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",
);
}