fix impersonation issues
This commit is contained in:
@@ -5,6 +5,7 @@ DATABASE_URL=file:/app/prisma/dev.db
|
||||
FRONTEND_URL=http://localhost:6767
|
||||
# Keep disabled unless traffic always comes through a trusted reverse proxy.
|
||||
TRUST_PROXY=false
|
||||
AUTH_MODE=local
|
||||
JWT_SECRET=change-this-secret-in-production-min-32-chars
|
||||
|
||||
# Optional Feature Flags (all default to false for backward compatibility)
|
||||
@@ -12,3 +13,16 @@ JWT_SECRET=change-this-secret-in-production-min-32-chars
|
||||
# ENABLE_PASSWORD_RESET=false
|
||||
# ENABLE_REFRESH_TOKEN_ROTATION=false
|
||||
# ENABLE_AUDIT_LOGGING=false
|
||||
|
||||
# OIDC Configuration (required when AUTH_MODE=hybrid or AUTH_MODE=oidc_enforced)
|
||||
# OIDC_PROVIDER_NAME=Authentik
|
||||
# OIDC_ISSUER_URL=https://auth.example.com/application/o/excalidash/
|
||||
# OIDC_CLIENT_ID=your-client-id
|
||||
# OIDC_CLIENT_SECRET=your-client-secret
|
||||
# OIDC_REDIRECT_URI=https://excalidash.example.com/api/auth/oidc/callback
|
||||
# OIDC_SCOPES=openid profile email
|
||||
# OIDC_EMAIL_CLAIM=email
|
||||
# OIDC_EMAIL_VERIFIED_CLAIM=email_verified
|
||||
# OIDC_REQUIRE_EMAIL_VERIFIED=true
|
||||
# OIDC_JIT_PROVISIONING=true
|
||||
# OIDC_FIRST_USER_ADMIN=true
|
||||
|
||||
Generated
+63
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "backend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "backend",
|
||||
"version": "0.4.1",
|
||||
"version": "0.4.6",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^5.22.0",
|
||||
@@ -24,6 +24,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^2.0.2",
|
||||
"openid-client": "^5.7.1",
|
||||
"prisma": "^5.22.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"uuid": "^13.0.0",
|
||||
@@ -3314,6 +3315,15 @@
|
||||
"@pkgjs/parseargs": "^0.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jose": {
|
||||
"version": "4.15.9",
|
||||
"resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz",
|
||||
"integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "22.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-22.1.0.tgz",
|
||||
@@ -3909,6 +3919,15 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-hash": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.2.0.tgz",
|
||||
"integrity": "sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
@@ -3932,6 +3951,15 @@
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/oidc-token-hash": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.2.0.tgz",
|
||||
"integrity": "sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^10.13.0 || >=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
@@ -3953,6 +3981,33 @@
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.7.1.tgz",
|
||||
"integrity": "sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jose": "^4.15.9",
|
||||
"lru-cache": "^6.0.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"oidc-token-hash": "^5.0.3"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/panva"
|
||||
}
|
||||
},
|
||||
"node_modules/openid-client/node_modules/lru-cache": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
|
||||
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"yallist": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
@@ -5789,6 +5844,12 @@
|
||||
"node": ">=0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yn": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
"jszip": "^3.10.1",
|
||||
"ms": "^2.1.3",
|
||||
"multer": "^2.0.2",
|
||||
"openid-client": "^5.7.1",
|
||||
"prisma": "^5.22.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"uuid": "^13.0.0",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "AuthIdentity" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"userId" TEXT NOT NULL,
|
||||
"provider" TEXT NOT NULL,
|
||||
"issuer" TEXT NOT NULL,
|
||||
"subject" TEXT NOT NULL,
|
||||
"emailAtLink" TEXT NOT NULL,
|
||||
"lastLoginAt" DATETIME,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "AuthIdentity_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AuthIdentity_issuer_subject_key" ON "AuthIdentity"("issuer", "subject");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "AuthIdentity_provider_userId_key" ON "AuthIdentity"("provider", "userId");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "AuthIdentity_userId_idx" ON "AuthIdentity"("userId");
|
||||
@@ -21,6 +21,7 @@ model User {
|
||||
role String @default("USER")
|
||||
mustResetPassword Boolean @default(false)
|
||||
isActive Boolean @default(true)
|
||||
authIdentities AuthIdentity[]
|
||||
drawings Drawing[]
|
||||
collections Collection[]
|
||||
passwordResetTokens PasswordResetToken[]
|
||||
@@ -111,3 +112,20 @@ model AuditLog {
|
||||
details String? // JSON string for additional details
|
||||
createdAt DateTime @default(now())
|
||||
}
|
||||
|
||||
model AuthIdentity {
|
||||
id String @id @default(uuid())
|
||||
userId String
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
provider String
|
||||
issuer String
|
||||
subject String
|
||||
emailAtLink String
|
||||
lastLoginAt DateTime?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@unique([issuer, subject])
|
||||
@@unique([provider, userId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
+16
-1
@@ -14,6 +14,7 @@ import rateLimit, { MemoryStore } from "express-rate-limit";
|
||||
import { registerAccountRoutes } from "./auth/accountRoutes";
|
||||
import { registerAdminRoutes } from "./auth/adminRoutes";
|
||||
import { registerCoreRoutes } from "./auth/coreRoutes";
|
||||
import { registerOidcRoutes } from "./auth/oidcRoutes";
|
||||
import { prisma as defaultPrisma } from "./db/prisma";
|
||||
import {
|
||||
BOOTSTRAP_USER_ID,
|
||||
@@ -63,7 +64,9 @@ export const createAuthRouter = (deps: CreateAuthRouterDeps): express.Router =>
|
||||
|
||||
const ensureAuthEnabled = async (res: Response): Promise<boolean> => {
|
||||
const systemConfig = await ensureSystemConfig();
|
||||
if (!systemConfig.authEnabled) {
|
||||
const authEnabled =
|
||||
config.authMode !== "local" ? true : systemConfig.authEnabled;
|
||||
if (!authEnabled) {
|
||||
res.status(404).json({
|
||||
error: "Not found",
|
||||
message: "Authentication is disabled",
|
||||
@@ -368,6 +371,18 @@ export const createAuthRouter = (deps: CreateAuthRouterDeps): express.Router =>
|
||||
const getRefreshTokenExpiresAt = (): Date =>
|
||||
resolveExpiresAt(config.jwtRefreshExpiresIn, 7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
registerOidcRoutes({
|
||||
router,
|
||||
prisma,
|
||||
ensureAuthEnabled,
|
||||
sanitizeText,
|
||||
generateTokens,
|
||||
setAuthCookies,
|
||||
getRefreshTokenExpiresAt,
|
||||
isMissingRefreshTokenTableError,
|
||||
config,
|
||||
});
|
||||
|
||||
registerCoreRoutes({
|
||||
router,
|
||||
prisma,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { PrismaClient } from "../generated/client";
|
||||
import { config } from "../config";
|
||||
|
||||
export const BOOTSTRAP_USER_ID = "bootstrap-admin";
|
||||
export const DEFAULT_SYSTEM_CONFIG_ID = "default";
|
||||
@@ -30,7 +31,7 @@ export const createAuthModeService = (
|
||||
update: {},
|
||||
create: {
|
||||
id: DEFAULT_SYSTEM_CONFIG_ID,
|
||||
authEnabled: false,
|
||||
authEnabled: config.authMode !== "local",
|
||||
authOnboardingCompleted: false,
|
||||
registrationEnabled: false,
|
||||
authLoginRateLimitEnabled: true,
|
||||
@@ -41,6 +42,12 @@ export const createAuthModeService = (
|
||||
};
|
||||
|
||||
const getAuthEnabled = async (): Promise<boolean> => {
|
||||
if (config.authMode !== "local") {
|
||||
const now = Date.now();
|
||||
authEnabledCache = { value: true, fetchedAt: now };
|
||||
return true;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
if (authEnabledCache && now - authEnabledCache.fetchedAt < authEnabledTtlMs) {
|
||||
return authEnabledCache.value;
|
||||
|
||||
@@ -44,10 +44,16 @@ type RegisterCoreRoutesDeps = {
|
||||
impersonatorId?: string;
|
||||
};
|
||||
config: {
|
||||
authMode: "local" | "hybrid" | "oidc_enforced";
|
||||
jwtSecret: string;
|
||||
jwtAccessExpiresIn: string;
|
||||
enableRefreshTokenRotation: boolean;
|
||||
enableAuditLogging: boolean;
|
||||
oidc: {
|
||||
enabled: boolean;
|
||||
enforced: boolean;
|
||||
providerName: string;
|
||||
};
|
||||
};
|
||||
generateTokens: (
|
||||
userId: string,
|
||||
@@ -151,6 +157,12 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
router.post("/register", loginAttemptRateLimiter, async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!(await ensureAuthEnabled(res))) return;
|
||||
if (config.authMode === "oidc_enforced") {
|
||||
return res.status(403).json({
|
||||
error: "Forbidden",
|
||||
message: "Local registration is disabled in OIDC enforced mode",
|
||||
});
|
||||
}
|
||||
if (!requireCsrf(req, res)) return;
|
||||
const parsed = registerSchema.safeParse(req.body);
|
||||
|
||||
@@ -415,6 +427,12 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
router.post("/login", loginAttemptRateLimiter, async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (!(await ensureAuthEnabled(res))) return;
|
||||
if (config.authMode === "oidc_enforced") {
|
||||
return res.status(403).json({
|
||||
error: "Forbidden",
|
||||
message: "Local login is disabled in OIDC enforced mode",
|
||||
});
|
||||
}
|
||||
const parsed = loginSchema.safeParse(req.body);
|
||||
|
||||
if (!parsed.success) {
|
||||
@@ -835,16 +853,24 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
try {
|
||||
const systemConfig = await ensureSystemConfig();
|
||||
const onboarding = await getAuthOnboardingStatus(systemConfig);
|
||||
if (!systemConfig.authEnabled) {
|
||||
const effectiveAuthEnabled =
|
||||
config.authMode !== "local" ? true : systemConfig.authEnabled;
|
||||
const onboardingRequired = config.authMode === "local" ? onboarding.needsChoice : false;
|
||||
const onboardingMode = config.authMode === "local" ? onboarding.mode : null;
|
||||
if (!effectiveAuthEnabled) {
|
||||
return res.json({
|
||||
enabled: false,
|
||||
authenticated: false,
|
||||
authEnabled: false,
|
||||
authMode: config.authMode,
|
||||
oidcEnabled: config.oidc.enabled,
|
||||
oidcEnforced: config.oidc.enforced,
|
||||
oidcProvider: config.oidc.providerName,
|
||||
registrationEnabled: false,
|
||||
bootstrapRequired: false,
|
||||
authOnboardingRequired: onboarding.needsChoice,
|
||||
authOnboardingMode: onboarding.mode,
|
||||
authOnboardingRecommended: onboarding.needsChoice ? "enable" : null,
|
||||
authOnboardingRequired: onboardingRequired,
|
||||
authOnboardingMode: onboardingMode,
|
||||
authOnboardingRecommended: onboardingRequired ? "enable" : null,
|
||||
user: null,
|
||||
});
|
||||
}
|
||||
@@ -854,18 +880,23 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
select: { id: true, isActive: true },
|
||||
});
|
||||
const bootstrapRequired =
|
||||
!config.oidc.enforced &&
|
||||
Boolean(bootstrapUser && bootstrapUser.isActive === false) &&
|
||||
onboarding.activeUsers === 0;
|
||||
|
||||
res.json({
|
||||
enabled: true,
|
||||
authEnabled: true,
|
||||
authMode: config.authMode,
|
||||
oidcEnabled: config.oidc.enabled,
|
||||
oidcEnforced: config.oidc.enforced,
|
||||
oidcProvider: config.oidc.providerName,
|
||||
authenticated: Boolean(req.user),
|
||||
registrationEnabled: systemConfig.registrationEnabled,
|
||||
bootstrapRequired,
|
||||
authOnboardingRequired: onboarding.needsChoice,
|
||||
authOnboardingMode: onboarding.mode,
|
||||
authOnboardingRecommended: onboarding.needsChoice ? "enable" : null,
|
||||
authOnboardingRequired: onboardingRequired,
|
||||
authOnboardingMode: onboardingMode,
|
||||
authOnboardingRecommended: onboardingRequired ? "enable" : null,
|
||||
user: req.user
|
||||
? {
|
||||
id: req.user.id,
|
||||
@@ -889,6 +920,12 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
|
||||
router.post("/onboarding-choice", optionalAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (config.authMode !== "local") {
|
||||
return res.status(409).json({
|
||||
error: "Conflict",
|
||||
message: "Onboarding choice is managed by AUTH_MODE configuration",
|
||||
});
|
||||
}
|
||||
if (!requireCsrf(req, res)) return;
|
||||
const parsed = authOnboardingChoiceSchema.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
@@ -944,6 +981,12 @@ export const registerCoreRoutes = (deps: RegisterCoreRoutesDeps) => {
|
||||
|
||||
router.post("/auth-enabled", requireAuth, async (req: Request, res: Response) => {
|
||||
try {
|
||||
if (config.authMode === "oidc_enforced") {
|
||||
return res.status(409).json({
|
||||
error: "Conflict",
|
||||
message: "Authentication mode is managed by AUTH_MODE=oidc_enforced",
|
||||
});
|
||||
}
|
||||
if (!requireCsrf(req, res)) return;
|
||||
if (!req.user) {
|
||||
return res
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -12,22 +12,49 @@ interface Config {
|
||||
nodeEnv: string;
|
||||
databaseUrl?: string;
|
||||
frontendUrl?: string;
|
||||
authMode: AuthMode;
|
||||
jwtSecret: string;
|
||||
jwtAccessExpiresIn: string;
|
||||
jwtRefreshExpiresIn: string;
|
||||
rateLimitMaxRequests: number;
|
||||
csrfMaxRequests: number;
|
||||
csrfSecret: string | null;
|
||||
oidc: OidcConfig;
|
||||
// Feature flags - all default to false for backward compatibility
|
||||
enablePasswordReset: boolean;
|
||||
enableRefreshTokenRotation: boolean;
|
||||
enableAuditLogging: boolean;
|
||||
}
|
||||
|
||||
export type AuthMode = "local" | "hybrid" | "oidc_enforced";
|
||||
|
||||
interface OidcConfig {
|
||||
enabled: boolean;
|
||||
enforced: boolean;
|
||||
providerName: string;
|
||||
issuerUrl: string | null;
|
||||
clientId: string | null;
|
||||
clientSecret: string | null;
|
||||
redirectUri: string | null;
|
||||
scopes: string;
|
||||
emailClaim: string;
|
||||
emailVerifiedClaim: string;
|
||||
requireEmailVerified: boolean;
|
||||
jitProvisioning: boolean;
|
||||
firstUserAdmin: boolean;
|
||||
}
|
||||
|
||||
const getOptionalEnv = (key: string, defaultValue: string): string => {
|
||||
return process.env[key] || defaultValue;
|
||||
};
|
||||
|
||||
const getOptionalTrimmedEnv = (key: string): string | null => {
|
||||
const raw = process.env[key];
|
||||
if (!raw) return null;
|
||||
const trimmed = raw.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
};
|
||||
|
||||
const resolveJwtSecret = (nodeEnv: string): string => {
|
||||
const provided = process.env.JWT_SECRET;
|
||||
if (provided && provided.trim().length > 0) {
|
||||
@@ -99,17 +126,77 @@ const getRequiredEnvNumber = (key: string, defaultValue: number): number => {
|
||||
return parsed;
|
||||
};
|
||||
|
||||
const parseAuthMode = (rawValue: string | undefined): AuthMode => {
|
||||
const normalized = (rawValue || "local").trim().toLowerCase();
|
||||
if (normalized === "local" || normalized === "hybrid" || normalized === "oidc_enforced") {
|
||||
return normalized;
|
||||
}
|
||||
throw new Error(
|
||||
"Invalid AUTH_MODE. Expected one of: local, hybrid, oidc_enforced"
|
||||
);
|
||||
};
|
||||
|
||||
const resolveOidcConfig = (authMode: AuthMode): OidcConfig => {
|
||||
const issuerUrl = getOptionalTrimmedEnv("OIDC_ISSUER_URL");
|
||||
const clientId = getOptionalTrimmedEnv("OIDC_CLIENT_ID");
|
||||
const clientSecret = getOptionalTrimmedEnv("OIDC_CLIENT_SECRET");
|
||||
const redirectUri = getOptionalTrimmedEnv("OIDC_REDIRECT_URI");
|
||||
const requiredWhenEnabled = {
|
||||
OIDC_ISSUER_URL: issuerUrl,
|
||||
OIDC_CLIENT_ID: clientId,
|
||||
OIDC_CLIENT_SECRET: clientSecret,
|
||||
OIDC_REDIRECT_URI: redirectUri,
|
||||
};
|
||||
|
||||
const enabled = authMode !== "local";
|
||||
const missingRequired = Object.entries(requiredWhenEnabled)
|
||||
.filter(([, value]) => !value)
|
||||
.map(([key]) => key);
|
||||
if (enabled && missingRequired.length > 0) {
|
||||
throw new Error(
|
||||
`AUTH_MODE=${authMode} requires OIDC configuration. Missing: ${missingRequired.join(", ")}`
|
||||
);
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
const hasOidcVars = Object.values(requiredWhenEnabled).some((value) => Boolean(value));
|
||||
if (hasOidcVars) {
|
||||
console.warn("[config] AUTH_MODE=local; ignoring OIDC_* provider settings.");
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
enabled,
|
||||
enforced: authMode === "oidc_enforced",
|
||||
providerName: getOptionalEnv("OIDC_PROVIDER_NAME", "OIDC"),
|
||||
issuerUrl,
|
||||
clientId,
|
||||
clientSecret,
|
||||
redirectUri,
|
||||
scopes: getOptionalEnv("OIDC_SCOPES", "openid profile email"),
|
||||
emailClaim: getOptionalEnv("OIDC_EMAIL_CLAIM", "email"),
|
||||
emailVerifiedClaim: getOptionalEnv("OIDC_EMAIL_VERIFIED_CLAIM", "email_verified"),
|
||||
requireEmailVerified: getOptionalBoolean("OIDC_REQUIRE_EMAIL_VERIFIED", true),
|
||||
jitProvisioning: getOptionalBoolean("OIDC_JIT_PROVISIONING", true),
|
||||
firstUserAdmin: getOptionalBoolean("OIDC_FIRST_USER_ADMIN", true),
|
||||
};
|
||||
};
|
||||
|
||||
const resolvedAuthMode = parseAuthMode(process.env.AUTH_MODE);
|
||||
|
||||
export const config: Config = {
|
||||
port: getRequiredEnvNumber("PORT", 8000),
|
||||
nodeEnv: getOptionalEnv("NODE_ENV", "development"),
|
||||
databaseUrl: process.env.DATABASE_URL,
|
||||
frontendUrl: parseFrontendUrl(process.env.FRONTEND_URL),
|
||||
authMode: resolvedAuthMode,
|
||||
jwtSecret: resolveJwtSecret(getOptionalEnv("NODE_ENV", "development")),
|
||||
jwtAccessExpiresIn: getOptionalEnv("JWT_ACCESS_EXPIRES_IN", "15m"),
|
||||
jwtRefreshExpiresIn: getOptionalEnv("JWT_REFRESH_EXPIRES_IN", "7d"),
|
||||
rateLimitMaxRequests: getRequiredEnvNumber("RATE_LIMIT_MAX_REQUESTS", 1000),
|
||||
csrfMaxRequests: getRequiredEnvNumber("CSRF_MAX_REQUESTS", 60),
|
||||
csrfSecret: process.env.CSRF_SECRET || null,
|
||||
oidc: resolveOidcConfig(resolvedAuthMode),
|
||||
// Feature flags - disabled by default for backward compatibility
|
||||
enablePasswordReset: getOptionalBoolean("ENABLE_PASSWORD_RESET", false),
|
||||
enableRefreshTokenRotation: getOptionalBoolean("ENABLE_REFRESH_TOKEN_ROTATION", false),
|
||||
@@ -132,6 +219,9 @@ if (config.nodeEnv === "production") {
|
||||
) {
|
||||
throw new Error("JWT_SECRET must be changed from placeholder/default value in production");
|
||||
}
|
||||
if (config.oidc.enabled && config.oidc.redirectUri && !/^https:\/\//i.test(config.oidc.redirectUri)) {
|
||||
throw new Error("OIDC_REDIRECT_URI must be HTTPS in production");
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Configuration validated successfully");
|
||||
|
||||
Reference in New Issue
Block a user