From 11b9cee73dbafa70b2eacd80f857be0d9452d883 Mon Sep 17 00:00:00 2001 From: Jonathan Gotti Date: Mon, 21 Apr 2025 15:59:49 +0200 Subject: [PATCH] feat(template-helpers): Add more parameters to jwt helper - jwt without parameter now generate a real jwt - keep length parameter as is for backward compatibility - add secret and payload parameters - payload properties iss, iat, exp are automaticly set if not provided --- packages/server/src/templates/index.ts | 47 ++++++++++++++++++--- packages/server/src/templates/processors.ts | 32 +++++++++++--- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/packages/server/src/templates/index.ts b/packages/server/src/templates/index.ts index 6ae26418..0d0f87ce 100644 --- a/packages/server/src/templates/index.ts +++ b/packages/server/src/templates/index.ts @@ -1,4 +1,4 @@ -import { randomBytes } from "node:crypto"; +import { randomBytes, createHmac } from "node:crypto"; import { existsSync } from "node:fs"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { join } from "node:path"; @@ -9,7 +9,7 @@ import { fetchTemplateFiles } from "./github"; export interface Schema { serverIp: string; projectName: string; -} +}; export type DomainSchema = Pick & { path?: string; @@ -22,6 +22,12 @@ export interface Template { content: string; }>; domains: DomainSchema[]; +}; + +export interface GenerateJWTOptions { + length?: number; + secret?: string; + payload?: Record | undefined; } export const generateRandomDomain = ({ @@ -59,10 +65,41 @@ export const generatePassword = (quantity = 16): string => { */ export function generateBase64(bytes = 32): string { return randomBytes(bytes).toString("base64"); -} +}; -export function generateJwt(length = 256): string { - return randomBytes(length).toString("hex"); +function safeBase64(str: string): string { + return str + .replace(/=/g, "") + .replace(/\+/g, "-") + .replace(/\//g, "_"); +}; +function objToJWTBase64(obj: any): string { + return safeBase64(Buffer.from(JSON.stringify(obj), "utf8").toString("base64")); +}; + +export function generateJwt(options: GenerateJWTOptions = {}): string { + let { length, secret, payload = {} } = options; + if (length) { + return randomBytes(length).toString("hex"); + } + const encodedHeader = objToJWTBase64({ + alg: "HS256", + typ: "JWT", + }); + payload.iss || (payload.iss = "dokploy"); + payload.iat || (payload.iat = Math.floor(Date.now() / 1000)); + payload.exp || (payload.exp = Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000)); + const encodedPayload = objToJWTBase64({ + iat: Math.floor(Date.now() / 1000), + exp: Math.floor(new Date("2030-01-01T00:00:00Z").getTime() / 1000), + ...payload, + }); + secret || (secret = randomBytes(32).toString("hex")); + const signature = safeBase64(createHmac("SHA256", secret) + .update(`${encodedHeader}.${encodedPayload}`) + .digest("base64")); + + return `${encodedHeader}.${encodedPayload}.${signature}`; } /** diff --git a/packages/server/src/templates/processors.ts b/packages/server/src/templates/processors.ts index 31e7861a..e59cddf5 100644 --- a/packages/server/src/templates/processors.ts +++ b/packages/server/src/templates/processors.ts @@ -65,7 +65,7 @@ export interface Template { /** * Process a string value and replace variables */ -function processValue( +export function processValue( value: string, variables: Record, schema: Schema, @@ -84,11 +84,11 @@ function processValue( const length = Number.parseInt(varName.split(":")[1], 10) || 32; return generateBase64(length); } + if (varName.startsWith("password:")) { const length = Number.parseInt(varName.split(":")[1], 10) || 16; return generatePassword(length); } - if (varName === "password") { return generatePassword(16); } @@ -114,8 +114,30 @@ function processValue( } if (varName.startsWith("jwt:")) { - const length = Number.parseInt(varName.split(":")[1], 10) || 256; - return generateJwt(length); + const params:string[] = varName.split(":").slice(1); + if (params.length === 1 && params[0] && params[0].match(/^\d{1,3}$/)) { + return generateJwt({length: Number.parseInt(params[0], 10)}); + } + let [secret, payload] = params; + if (typeof payload === "string" && variables[payload]) { + payload = variables[payload]; + } + if (typeof payload === "string" && payload.startsWith("{") && payload.endsWith("}")) { + try { + payload = JSON.parse(payload); + } catch (e) { + // If payload is not a valid JSON, invalid it + payload = undefined; + console.error("Invalid JWT payload", e); + } + } + if (typeof payload !== 'object') { + payload = undefined; + } + return generateJwt({ + secret: secret ? (variables[secret] || secret) : undefined, + payload: payload as any + }); } if (varName === "username") { @@ -147,7 +169,7 @@ export function processVariables( ): Record { const variables: Record = {}; - // First pass: Process variables that don't depend on other variables + // First pass: Process some variables that don't depend on other variables for (const [key, value] of Object.entries(template.variables)) { if (typeof value !== "string") continue;