Skip to content

Commit e671d24

Browse files
committed
enhance environment variable validation for JWT and admin credentials
1 parent 80fa3a2 commit e671d24

File tree

5 files changed

+79
-13
lines changed

5 files changed

+79
-13
lines changed

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.8.1
1+
1.8.2

backend/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,27 @@
1+
## Environment Configuration
2+
3+
The backend requires three critical environment variables to authenticate and issue JWTs:
4+
5+
```
6+
ADMIN_USER=...
7+
ADMIN_PASS=...
8+
JWT_SECRET=...
9+
```
10+
11+
Starting with v1.8.1 the server checks these values at boot. If any of them are missing **or empty**, the backend will exit with a fatal error.
12+
13+
- `ADMIN_USER` and `ADMIN_PASS` must be non-empty strings. They define the login credentials for the admin dashboard.
14+
- `JWT_SECRET` must be non-empty and should be at least 16 characters to avoid weak tokens. Longer random strings are strongly recommended.
15+
16+
When running through Docker Compose, define these in a `.env` file and ensure the values are quoted if they contain special characters. Example:
17+
18+
```
19+
ADMIN_USER="myadmin"
20+
ADMIN_PASS="super-secret-P@ssw0rd"
21+
JWT_SECRET="change-this-to-a-long-random-string"
22+
```
23+
24+
If you rely on Synology’s GUI or other orchestrators, double-check that the variables are actually persisted and not left blank by default.
125
# Invio — Backend API
226

327
Plain, fast invoicing without enterprise bloat. Create an invoice, share a link,

backend/src/app.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { adminRoutes } from "./routes/admin.ts";
66
import { publicRoutes } from "./routes/public.ts";
77
import { authRoutes } from "./routes/auth.ts";
88
import { logChromiumAvailability } from "./utils/chromium.ts";
9-
import { ensureEnv } from "./utils/env.ts";
9+
import { ensureEnv, getAdminCredentials, getJwtSecret } from "./utils/env.ts";
1010

1111
const SECURE_HEADERS_DISABLED = (Deno.env.get("SECURE_HEADERS_DISABLED") || "").toLowerCase() === "true";
1212
const HSTS_ENABLED = (Deno.env.get("ENABLE_HSTS") || "").toLowerCase() === "true";
@@ -17,7 +17,20 @@ const app = new Hono();
1717

1818
// Check for required credentials in environment
1919
try {
20-
ensureEnv(["ADMIN_USER", "ADMIN_PASS", "JWT_SECRET"]);
20+
ensureEnv(["JWT_SECRET"]);
21+
22+
const { username: adminUsername, password: adminPassword } = getAdminCredentials();
23+
if (!adminUsername || adminUsername.trim().length === 0) {
24+
throw new Error("ADMIN_USER must not be empty");
25+
}
26+
if (!adminPassword || adminPassword.trim().length === 0) {
27+
throw new Error("ADMIN_PASS must not be empty");
28+
}
29+
30+
const secret = getJwtSecret();
31+
if (!secret || secret.trim().length === 0) {
32+
throw new Error("JWT_SECRET must not be empty");
33+
}
2134
} catch (error) {
2235
console.error(`FATAL: ${error instanceof Error ? error.message : error}`);
2336
Deno.exit(1);

backend/src/utils/env.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,23 @@ export function ensureEnv(keys: string[]): void {
4343
}
4444

4545
export function getAdminCredentials() {
46-
return {
47-
username: requireEnv("ADMIN_USER"),
48-
password: requireEnv("ADMIN_PASS"),
49-
};
46+
const username = requireEnv("ADMIN_USER").trim();
47+
const password = requireEnv("ADMIN_PASS").trim();
48+
if (username.length === 0) {
49+
throw new Error("ADMIN_USER must not be empty");
50+
}
51+
if (password.length === 0) {
52+
throw new Error("ADMIN_PASS must not be empty");
53+
}
54+
return { username, password };
5055
}
5156

5257
export function getJwtSecret(): string {
53-
return requireEnv("JWT_SECRET");
58+
const secret = requireEnv("JWT_SECRET").trim();
59+
if (secret.length === 0) {
60+
throw new Error("JWT_SECRET must not be empty");
61+
}
62+
return secret;
5463
}
5564

5665
export function isDemoMode(): boolean {

backend/src/utils/jwt.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
11
import { create, decode, verify } from "djwt";
2-
import { getJwtSecret } from "./env.ts";
2+
import { getJwtSecret, getAdminCredentials } from "./env.ts";
33

4-
async function getKey(): Promise<CryptoKey> {
5-
const secretKey = getJwtSecret();
6-
const secretBytes = new TextEncoder().encode(secretKey);
7-
if (secretBytes.length === 0) {
4+
function validateSecret(secretKey: string) {
5+
if (!secretKey || secretKey.trim().length === 0) {
86
throw new Error("JWT_SECRET must not be empty");
97
}
8+
9+
const trimmed = secretKey.trim();
10+
if (trimmed.length < 16) {
11+
console.warn("Warning: JWT_SECRET is shorter than 16 characters. Consider using a longer secret for better security.");
12+
}
13+
}
14+
15+
function validateAdminCredentials() {
16+
const { username, password } = getAdminCredentials();
17+
if (!username || username.trim().length === 0) {
18+
throw new Error("ADMIN_USER must not be empty");
19+
}
20+
if (!password || password.trim().length === 0) {
21+
throw new Error("ADMIN_PASS must not be empty");
22+
}
23+
}
24+
25+
async function getKey(): Promise<CryptoKey> {
26+
validateAdminCredentials();
27+
const secretKey = getJwtSecret();
28+
validateSecret(secretKey);
29+
const secretBytes = new TextEncoder().encode(secretKey.trim());
1030
const key = await crypto.subtle.importKey(
1131
"raw",
1232
secretBytes,

0 commit comments

Comments
 (0)