feat(login): Support private key file with env (#10575)

# Which Problems Are Solved

Instead of encoding the private key as base64 and setting it in the env
as SYSTEM_USER_PRIVATE_KEY it is sometimes easier to write it to disk
and use a file path. This is for example also possible when setting up
system users in zitadel core.

# How the Problems Are Solved

Added SYSTEM_USER_PRIVATE_KEY_FILE which is preferred over
SYSTEM_USER_PRIVATE_KEY and expects a file path to a private key in PEM
format (like SYSTEM_USER_PRIVATE_KEY but not base64 encoded). To prevent
loading the private key too often it is cached.

---------

Co-authored-by: Ramon <[email protected]>
This commit is contained in:
Marcel Link
2026-03-16 14:55:58 +01:00
committed by GitHub
co-authored by Ramon
parent 965e3fa621
commit 7c0e1750f3
3 changed files with 29 additions and 11 deletions
+2
View File
@@ -4,10 +4,12 @@ declare namespace NodeJS {
[key: `${string}_AUDIENCE`]: string; // The system api url
[key: `${string}_SYSTEM_USER_ID`]: string; // The service account id
[key: `${string}_SYSTEM_USER_PRIVATE_KEY`]: string; // The service account private key
[key: `${string}_SYSTEM_USER_PRIVATE_KEY_FILE`]: string; // The service account private key file path
AUDIENCE: string; // The fallback system api url
SYSTEM_USER_ID: string; // The fallback service account id
SYSTEM_USER_PRIVATE_KEY: string; // The fallback service account private key
SYSTEM_USER_PRIVATE_KEY_FILE: string; // The fallback service account private key file path
/**
* The Zitadel API url
+21 -10
View File
@@ -1,7 +1,20 @@
import { newSystemToken } from "@zitadel/client/node";
import { readFileSync } from "fs";
import { readFile } from "fs/promises";
import { getLoginSystemUserId } from "./deployment";
// The key token is only loaded once from disk per process.
// If the file was loaded you need to restart the process to switch the key.
let keyToken: string | undefined;
async function getTokenFromFile(): Promise<string> {
if (keyToken) {
return keyToken;
}
keyToken = await readFile(process.env.SYSTEM_USER_PRIVATE_KEY_FILE, "binary");
return keyToken;
}
/**
* Creates a signed JWT token using system user credentials from environment
* variables. The SYSTEM_USER_PRIVATE_KEY is expected to be base64-encoded and
@@ -12,16 +25,14 @@ import { getLoginSystemUserId } from "./deployment";
* @throws If the underlying token signing fails.
*/
export async function systemAPIToken() {
const token = {
audience: process.env.AUDIENCE,
userID: process.env.SYSTEM_USER_ID,
token: Buffer.from(process.env.SYSTEM_USER_PRIVATE_KEY, "base64").toString("utf-8"),
};
const key = process.env.SYSTEM_USER_PRIVATE_KEY_FILE
? await getTokenFromFile()
: Buffer.from(process.env.SYSTEM_USER_PRIVATE_KEY, "base64").toString("utf-8");
return newSystemToken({
audience: token.audience,
subject: token.userID,
key: token.token,
audience: process.env.AUDIENCE,
subject: process.env.SYSTEM_USER_ID,
key,
});
}
@@ -38,7 +49,7 @@ export async function loginServiceKeyToken() {
const keyFile = process.env.ZITADEL_LOGIN_SERVICE_KEY_FILE!;
try {
const key = readFileSync(keyFile, "utf-8");
const key = await readFile(keyFile, "utf-8");
return newSystemToken({
audience: process.env.AUDIENCE || process.env.ZITADEL_API_URL,
+6 -1
View File
@@ -5,13 +5,18 @@
* - 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
* - SYSTEM_USER_PRIVATE_KEY_FILE: The private key file path 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;
return (
!!process.env.AUDIENCE &&
!!process.env.SYSTEM_USER_ID &&
(!!process.env.SYSTEM_USER_PRIVATE_KEY || !!process.env.SYSTEM_USER_PRIVATE_KEY_FILE)
);
}
/**