JWT_ACCESS_SECRET war zwar als Pflicht-Env-Var geprueft, aber ohne Mindestlaenge - ein kurzer/erratbarer Wert haette nicht nur das Faelschen von Access-/MFA-/SSO-Tokens fuer jeden Nutzer erlaubt, sondern auch (ueber mfa-crypto.util.ts, das den AES-Schluessel fuer gespeicherte TOTP-Secrets aus genau diesem Wert ableitet) das Entschluesseln aller gespeicherten MFA-Secrets. main.ts bricht den Start jetzt mit klarer Fehlermeldung ab, wenn JWT_ACCESS_SECRET kuerzer als 32 Zeichen ist. .env.example und das K8s-Secret-Template zeigen jetzt einen ausreichend langen Platzhalter mit Erzeugungshinweis (openssl rand -hex 32). Betrifft nur main.ts's bootstrap() - e2e-Tests bootstrappen ueber test/utils/create-test-app.ts direkt via configureApp() und durchlaufen diese Pruefung nicht, daher keine Testanpassung noetig. Co-Authored-By: Claude Sonnet 5 <[email protected]>
60 lines
2.4 KiB
TypeScript
60 lines
2.4 KiB
TypeScript
import { NestFactory } from '@nestjs/core';
|
||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
||
import { AppModule } from './app.module';
|
||
import { configureApp } from './app.config';
|
||
|
||
const REQUIRED_ENV_VARS = ['DATABASE_URL', 'JWT_ACCESS_SECRET'] as const;
|
||
// JWT_ACCESS_SECRET is the root of trust for both authentication (every
|
||
// access/MFA-challenge/SSO-completion token is signed with it) and MFA
|
||
// secret confidentiality (mfa-crypto.util.ts derives the AES key that
|
||
// encrypts stored TOTP secrets from it) - a short or guessable value here
|
||
// would let an attacker forge tokens for any user AND decrypt everyone's
|
||
// stored TOTP secret, not just guess a password. 32 chars matches the
|
||
// output of "openssl rand -hex 32" / "-base64 32", the standard way to
|
||
// generate one.
|
||
const MIN_JWT_SECRET_LENGTH = 32;
|
||
|
||
function assertRequiredEnvVars(): void {
|
||
const missing = REQUIRED_ENV_VARS.filter((name) => !process.env[name]);
|
||
if (missing.length > 0) {
|
||
throw new Error(
|
||
`Missing required environment variable(s): ${missing.join(', ')}`,
|
||
);
|
||
}
|
||
const jwtSecret = process.env.JWT_ACCESS_SECRET as string;
|
||
if (jwtSecret.length < MIN_JWT_SECRET_LENGTH) {
|
||
throw new Error(
|
||
`JWT_ACCESS_SECRET must be at least ${MIN_JWT_SECRET_LENGTH} characters (got ${jwtSecret.length}) - generate one with e.g. "openssl rand -hex 32"`,
|
||
);
|
||
}
|
||
}
|
||
|
||
async function bootstrap() {
|
||
assertRequiredEnvVars();
|
||
|
||
const app = await NestFactory.create<NestExpressApplication>(AppModule);
|
||
app.enableShutdownHooks();
|
||
|
||
configureApp(app);
|
||
|
||
// Swagger is mounted as raw Express middleware outside Nest's routing, so
|
||
// it is never covered by the global JwtAuthGuard - anyone reaching the
|
||
// host could otherwise read the full API schema (every endpoint and DTO
|
||
// shape) with no authentication at all. Off by default; an operator who
|
||
// wants it in a given environment opts in explicitly.
|
||
if (process.env.ENABLE_SWAGGER === 'true') {
|
||
const swaggerConfig = new DocumentBuilder()
|
||
.setTitle('ApiCore API')
|
||
.setDescription('Digitale Imkereiverwaltung – API-Dokumentation')
|
||
.setVersion('1.0')
|
||
.addBearerAuth()
|
||
.build();
|
||
const swaggerDocument = SwaggerModule.createDocument(app, swaggerConfig);
|
||
SwaggerModule.setup('api/v1/docs', app, swaggerDocument);
|
||
}
|
||
|
||
await app.listen(process.env.PORT ?? 3001);
|
||
}
|
||
void bootstrap();
|