mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
feat(login): add LOGIN_SERVICE_KEY_FILE authentication support (#11572)
This commit is contained in:
Vendored
+14
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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,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",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user