Add AUTH_DEV_AUTOLOGIN to switch off the login on development instances
With AUTH_DEV_AUTOLOGIN=<username>, every /api request without a valid session runs as that existing, active user and skips the forced password change. Unset keeps the normal login; unknown or deactivated users still get 401, real sessions take precedence, the CSRF application header stays mandatory and the API logs a warning at startup. The switch is not limited to localhost because the development instance is reached from other machines and through a reverse proxy (THREATMODEL D2/R15). It must never be set on an instance with real project data. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
4677b1685c
commit
084002bea9
11 changed files with 211 additions and 8 deletions
|
|
@ -5,3 +5,7 @@ AUTH_SESSION_HOURS=12
|
||||||
AUTH_SESSION_MAX_DAYS=7
|
AUTH_SESSION_MAX_DAYS=7
|
||||||
# Hinter HTTPS true, bei direktem HTTP-Zugriff im LAN false.
|
# Hinter HTTPS true, bei direktem HTTP-Zugriff im LAN false.
|
||||||
COOKIE_SECURE=false
|
COOKIE_SECURE=false
|
||||||
|
# NUR Entwicklungsinstanzen ohne echte Projektdaten: vorhandener, aktiver
|
||||||
|
# Benutzer, als der jede Anfrage ohne Session angemeldet ist (Login aus).
|
||||||
|
# Leer lassen auf jeder produktiven Instanz.
|
||||||
|
AUTH_DEV_AUTOLOGIN=
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,11 @@ It must support circuits, device rows, project devices, drag-and-drop restructur
|
||||||
(`src/app/layout.tsx`). Every unsafe API request also requires the explicit
|
(`src/app/layout.tsx`). Every unsafe API request also requires the explicit
|
||||||
`X-Leistungsbilanz-Request: 1` application header, including over plain HTTP.
|
`X-Leistungsbilanz-Request: 1` application header, including over plain HTTP.
|
||||||
Account settings are available to every signed-in user; user administration
|
Account settings are available to every signed-in user; user administration
|
||||||
remains admin-only. See `docs/authentication.md` for the full picture.
|
remains admin-only. Development instances only: `AUTH_DEV_AUTOLOGIN=<username>`
|
||||||
|
signs every request without a session in as that existing active user
|
||||||
|
(login off, no forced password change; THREATMODEL D2/R15, never on
|
||||||
|
instances with real project data). See `docs/authentication.md` for the full
|
||||||
|
picture.
|
||||||
|
|
||||||
The supported runtime model is Circuit-First. The former Consumer UI, API,
|
The supported runtime model is Circuit-First. The former Consumer UI, API,
|
||||||
tables and upgrade tooling are removed; do not reintroduce them.
|
tables and upgrade tooling are removed; do not reintroduce them.
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,9 @@ services:
|
||||||
AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}"
|
AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}"
|
||||||
AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}"
|
AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}"
|
||||||
AUTH_SESSION_MAX_DAYS: "${AUTH_SESSION_MAX_DAYS:-7}"
|
AUTH_SESSION_MAX_DAYS: "${AUTH_SESSION_MAX_DAYS:-7}"
|
||||||
|
# Nur Entwicklungsinstanzen: Benutzername, mit dem jede Anfrage ohne
|
||||||
|
# Session automatisch angemeldet ist (Login aus). Leer = Login aktiv.
|
||||||
|
AUTH_DEV_AUTOLOGIN: "${AUTH_DEV_AUTOLOGIN:-}"
|
||||||
COOKIE_SECURE: "${COOKIE_SECURE:-false}"
|
COOKIE_SECURE: "${COOKIE_SECURE:-false}"
|
||||||
# Nur auf dem Loopback: die API darf nicht aus dem LAN erreichbar sein.
|
# Nur auf dem Loopback: die API darf nicht aus dem LAN erreichbar sein.
|
||||||
ports:
|
ports:
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,9 @@ services:
|
||||||
AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}"
|
AUTH_BOOTSTRAP_PASSWORT: "${AUTH_BOOTSTRAP_PASSWORT:-}"
|
||||||
AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}"
|
AUTH_SESSION_HOURS: "${AUTH_SESSION_HOURS:-12}"
|
||||||
AUTH_SESSION_MAX_DAYS: "${AUTH_SESSION_MAX_DAYS:-7}"
|
AUTH_SESSION_MAX_DAYS: "${AUTH_SESSION_MAX_DAYS:-7}"
|
||||||
|
# Nur Entwicklungsinstanzen: Benutzername, mit dem jede Anfrage ohne
|
||||||
|
# Session automatisch angemeldet ist (Login aus). Leer = Login aktiv.
|
||||||
|
AUTH_DEV_AUTOLOGIN: "${AUTH_DEV_AUTOLOGIN:-}"
|
||||||
COOKIE_SECURE: "${COOKIE_SECURE:-false}"
|
COOKIE_SECURE: "${COOKIE_SECURE:-false}"
|
||||||
# Nicht auf dem Host veroeffentlicht: die API wird ueber den
|
# Nicht auf dem Host veroeffentlicht: die API wird ueber den
|
||||||
# Next.js-Rewrite im Web-Dienst erreicht.
|
# Next.js-Rewrite im Web-Dienst erreicht.
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,7 @@ Gesundheitsdaten: keine. Personenbezug: nur Benutzerkonten.
|
||||||
| R12 | Keine Nachvollziehbarkeit bei geteilten Projekten: der Projektverlauf hat `actorId`, die API setzt ihn aber nie | A4 | Änderungen keinem Nutzer zuordenbar | M | angemeldeten Nutzer (`req.authUser`) bis in `projectCommandService` durchreichen | `grep actorId src/server` leer; Spalte `project_revisions.actor_id` | **offen** |
|
| R12 | Keine Nachvollziehbarkeit bei geteilten Projekten: der Projektverlauf hat `actorId`, die API setzt ihn aber nie | A4 | Änderungen keinem Nutzer zuordenbar | M | angemeldeten Nutzer (`req.authUser`) bis in `projectCommandService` durchreichen | `grep actorId src/server` leer; Spalte `project_revisions.actor_id` | **offen** |
|
||||||
| R13 | Secrets oder echte Kundendaten im Repo | A6 | Zugangsdaten bzw. Kundendaten öffentlich | H | `.env`/`.env.*` und `docs/spec/kbp-Stromkreis.xlsx` gitignored, Testdaten synthetisch, gitleaks per pre-commit | `.gitignore`; `make secrets` | belegt |
|
| R13 | Secrets oder echte Kundendaten im Repo | A6 | Zugangsdaten bzw. Kundendaten öffentlich | H | `.env`/`.env.*` und `docs/spec/kbp-Stromkreis.xlsx` gitignored, Testdaten synthetisch, gitleaks per pre-commit | `.gitignore`; `make secrets` | belegt |
|
||||||
| R14 | Abhängigkeiten (u. a. `exceljs` parst Fremdinhalt) | A6 | Codeausführung, DoS | M | `npm audit` in `make check`, Lockfile, neue Pakete per `dependency-vetting` | `make audit`: 6 moderate ohne nicht-brechenden Fix (`drizzle-kit`/`esbuild`, `exceljs`/`uuid`), kein High/Critical | offen bis Eintrag in `RELEASE-AUSNAHMEN.md` oder Update |
|
| R14 | Abhängigkeiten (u. a. `exceljs` parst Fremdinhalt) | A6 | Codeausführung, DoS | M | `npm audit` in `make check`, Lockfile, neue Pakete per `dependency-vetting` | `make audit`: 6 moderate ohne nicht-brechenden Fix (`drizzle-kit`/`esbuild`, `exceljs`/`uuid`), kein High/Critical | offen bis Eintrag in `RELEASE-AUSNAHMEN.md` oder Update |
|
||||||
|
| R15 | Login per `AUTH_DEV_AUTOLOGIN` abgeschaltet, versehentlich auf einer Instanz mit echten Projektdaten gesetzt | A1, A2 | jeder, der die Instanz erreicht, arbeitet mit den Rechten des Auto-Login-Benutzers (bei Admin: inkl. Benutzerverwaltung) | H | Standard leer, nur ein vorhandener aktiver Benutzer wird verwendet (sonst 401), echte Sessions haben Vorrang, Warnung im API-Log beim Start, CSRF-Header bleibt Pflicht. Bewusst **nicht** auf localhost begrenzt (**D2**) | `tests/authentication.test.ts` „development auto-login“; Baseline „jede Route verlangt Login“ läuft ohne den Schalter | belegt für den Schalter selbst; Schutz der Live-Instanz hängt an deren `.env` (dort nie setzen) |
|
||||||
|
|
||||||
## Design-Entscheidungen
|
## Design-Entscheidungen
|
||||||
|
|
||||||
|
|
@ -95,6 +96,16 @@ nichts als entschieden ein.
|
||||||
- Folge: ASVS-light B1/B2 sind für diese Instanz nicht anwendbar (R3). `idorGap` in
|
- Folge: ASVS-light B1/B2 sind für diese Instanz nicht anwendbar (R3). `idorGap` in
|
||||||
`tests/security/security.config.ts` bleibt gesetzt und macht den Ist-Zustand sichtbar.
|
`tests/security/security.config.ts` bleibt gesetzt und macht den Ist-Zustand sichtbar.
|
||||||
|
|
||||||
|
**D2: Entwicklungsinstanz ohne Login (`AUTH_DEV_AUTOLOGIN`).**
|
||||||
|
- Aussage von: Julian im Chat mit einem Agenten am 30.10.2026 („Deaktiviere mal den login auf
|
||||||
|
dieser dev instanz. Der ist hier nicht nötig“; Schalter ausdrücklich „nicht auf localhost
|
||||||
|
begrenzen, da ich von anderen PCs und Netzen drauf zugreife. Eventuell auch mit
|
||||||
|
reverseproxy“). Eingetragen von diesem Agenten.
|
||||||
|
- Gilt für: lokale Entwicklungsinstanzen ohne echte Projektdaten. Nie auf cj-ki oder einer
|
||||||
|
anderen Instanz mit Kundendaten.
|
||||||
|
- Folge: R15. Auf solchen Instanzen ist der Login aus; wer die Instanz erreicht, arbeitet mit
|
||||||
|
den Rechten des konfigurierten Benutzers.
|
||||||
|
|
||||||
## Offene Entscheidungen
|
## Offene Entscheidungen
|
||||||
|
|
||||||
E1 (geteilte Projekte) ist mit D1 beantwortet.
|
E1 (geteilte Projekte) ist mit D1 beantwortet.
|
||||||
|
|
|
||||||
|
|
@ -69,6 +69,26 @@ Verknüpfung mit einer Mitarbeiter-/Personen-Stammdatentabelle.
|
||||||
Klartext durchs Netz. Für Zugriff außerhalb des LANs HTTPS über einen
|
Klartext durchs Netz. Für Zugriff außerhalb des LANs HTTPS über einen
|
||||||
Reverse-Proxy vorschalten und `COOKIE_SECURE=true` setzen.
|
Reverse-Proxy vorschalten und `COOKIE_SECURE=true` setzen.
|
||||||
|
|
||||||
|
## Entwicklungsinstanz ohne Login
|
||||||
|
|
||||||
|
`AUTH_DEV_AUTOLOGIN=<benutzername>` schaltet den Login für eine
|
||||||
|
Entwicklungsinstanz ab (Entscheidung Julian, 2026-10-30): Jede `/api`-Anfrage
|
||||||
|
ohne gültige Session läuft als dieser vorhandene, aktive Benutzer, auch der
|
||||||
|
Passwortwechsel-Zwang entfällt. Der Schalter ist bewusst nicht auf localhost
|
||||||
|
begrenzt, weil die Dev-Instanz von anderen Rechnern und über einen
|
||||||
|
Reverse-Proxy erreicht wird. Jeder, der die Instanz erreicht, hat damit die
|
||||||
|
Rechte dieses Benutzers. Leer (Standard) gilt der normale Login; ein
|
||||||
|
unbekannter oder deaktivierter Benutzer führt weiter zu 401. Echte Sessions
|
||||||
|
haben Vorrang, und die API protokolliert beim Start eine Warnung. Der
|
||||||
|
CSRF-Anwendungsheader bleibt Pflicht. Auf einer Instanz mit echten
|
||||||
|
Projektdaten darf der Schalter nie gesetzt sein.
|
||||||
|
|
||||||
|
```dotenv
|
||||||
|
AUTH_BOOTSTRAP_USERNAME=julian
|
||||||
|
AUTH_BOOTSTRAP_PASSWORT=<Startpasswort, mindestens 12 Zeichen>
|
||||||
|
AUTH_DEV_AUTOLOGIN=julian
|
||||||
|
```
|
||||||
|
|
||||||
## Bootstrap-Admin
|
## Bootstrap-Admin
|
||||||
|
|
||||||
`AUTH_BOOTSTRAP_USERNAME` / `AUTH_BOOTSTRAP_PASSWORT` (Env-Variablen, siehe
|
`AUTH_BOOTSTRAP_USERNAME` / `AUTH_BOOTSTRAP_PASSWORT` (Env-Variablen, siehe
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,13 @@ export interface AuthConfig {
|
||||||
* Unset means: no bootstrap account, the table must already have an admin.
|
* Unset means: no bootstrap account, the table must already have an admin.
|
||||||
*/
|
*/
|
||||||
bootstrap: { username: string; password: string } | null;
|
bootstrap: { username: string; password: string } | null;
|
||||||
|
/**
|
||||||
|
* Development instances only (AUTH_DEV_AUTOLOGIN): every /api request
|
||||||
|
* without a valid session runs as this existing, active user, so no login
|
||||||
|
* is needed. Unset means the normal login requirement. Never set it on an
|
||||||
|
* instance holding real project data.
|
||||||
|
*/
|
||||||
|
devAutoLogin: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AuthConfigError extends Error {}
|
export class AuthConfigError extends Error {}
|
||||||
|
|
@ -55,5 +62,6 @@ export function readAuthConfig(env: NodeJS.ProcessEnv = process.env): AuthConfig
|
||||||
bootstrap: bootstrapUsername
|
bootstrap: bootstrapUsername
|
||||||
? { username: bootstrapUsername, password: bootstrapPassword }
|
? { username: bootstrapUsername, password: bootstrapPassword }
|
||||||
: null,
|
: null,
|
||||||
|
devAutoLogin: (env.AUTH_DEV_AUTOLOGIN ?? "").trim().toLowerCase() || null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,31 @@ export async function identityFromRequest(
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Session identity, or on a development instance with AUTH_DEV_AUTOLOGIN the
|
||||||
|
* configured active user. The auto-login user skips the forced password
|
||||||
|
* change because nobody signs in interactively.
|
||||||
|
*/
|
||||||
|
export async function resolveRequestIdentity(
|
||||||
|
req: { headers: { cookie?: string | undefined } },
|
||||||
|
authConfig: Pick<AuthConfig, "devAutoLogin">,
|
||||||
|
sessions: SessionStore,
|
||||||
|
users: Pick<UserStore, "findById" | "findByUsername">,
|
||||||
|
slideExpiry = true
|
||||||
|
): Promise<AuthUser | undefined> {
|
||||||
|
const identity = await identityFromRequest(req, sessions, users, slideExpiry);
|
||||||
|
if (identity || !authConfig.devAutoLogin) return identity;
|
||||||
|
const user = await users.findByUsername(authConfig.devAutoLogin);
|
||||||
|
if (!user || !user.active) return undefined;
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
username: user.username,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role as UserRole,
|
||||||
|
mustChangePassword: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function cookieOptions(authConfig: AuthConfig, maxAgeMs: number) {
|
export function cookieOptions(authConfig: AuthConfig, maxAgeMs: number) {
|
||||||
return {
|
return {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
|
|
@ -107,7 +132,7 @@ export function requireSameOriginForWrites(): RequestHandler {
|
||||||
export function createAuthGuard(
|
export function createAuthGuard(
|
||||||
authConfig: AuthConfig,
|
authConfig: AuthConfig,
|
||||||
sessions: SessionStore,
|
sessions: SessionStore,
|
||||||
users: Pick<UserStore, "findById">,
|
users: Pick<UserStore, "findById" | "findByUsername">,
|
||||||
openPaths: readonly string[] = ["/auth/login", "/auth/status", "/auth/logout"]
|
openPaths: readonly string[] = ["/auth/login", "/auth/status", "/auth/logout"]
|
||||||
): RequestHandler {
|
): RequestHandler {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
|
@ -115,7 +140,7 @@ export function createAuthGuard(
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
identityFromRequest(req, sessions, users)
|
resolveRequestIdentity(req, authConfig, sessions, users)
|
||||||
.then((identity) => {
|
.then((identity) => {
|
||||||
if (!identity) {
|
if (!identity) {
|
||||||
res.status(401).json({ error: "Nicht angemeldet." });
|
res.status(401).json({ error: "Nicht angemeldet." });
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { authConfig, loginThrottle, sessionStore, userService } from "../composition/auth.js";
|
import { authConfig, loginThrottle, sessionStore, userService } from "../composition/auth.js";
|
||||||
import {
|
import {
|
||||||
cookieOptions,
|
cookieOptions,
|
||||||
identityFromRequest,
|
resolveRequestIdentity,
|
||||||
sessionIdFromRequest,
|
sessionIdFromRequest,
|
||||||
setSessionCookie,
|
setSessionCookie,
|
||||||
} from "../auth/auth.middleware.js";
|
} from "../auth/auth.middleware.js";
|
||||||
|
|
@ -16,7 +16,7 @@ import { SESSION_COOKIE } from "../auth/auth-config.js";
|
||||||
import { userRepository } from "../composition/application-repositories.js";
|
import { userRepository } from "../composition/application-repositories.js";
|
||||||
|
|
||||||
export async function getAuthStatus(req: Request, res: Response) {
|
export async function getAuthStatus(req: Request, res: Response) {
|
||||||
const identity = await identityFromRequest(req, sessionStore, userRepository);
|
const identity = await resolveRequestIdentity(req, authConfig, sessionStore, userRepository);
|
||||||
const sessionId = sessionIdFromRequest(req);
|
const sessionId = sessionIdFromRequest(req);
|
||||||
if (identity && sessionId) setSessionCookie(res, authConfig, sessionStore, sessionId);
|
if (identity && sessionId) setSessionCookie(res, authConfig, sessionStore, sessionId);
|
||||||
res.json({
|
res.json({
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { userService } from "./composition/auth.js";
|
import { authConfig, userService } from "./composition/auth.js";
|
||||||
import { createLogger, toErrorMeta } from "../shared/logging/logger.js";
|
import { createLogger, toErrorMeta } from "../shared/logging/logger.js";
|
||||||
import { closeDefaultDatabase } from "../db/client.js";
|
import { closeDefaultDatabase } from "../db/client.js";
|
||||||
|
|
||||||
|
|
@ -35,6 +35,11 @@ userService
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
if (authConfig.devAutoLogin) {
|
||||||
|
logger.warn("login disabled: AUTH_DEV_AUTOLOGIN signs every request in", {
|
||||||
|
username: authConfig.devAutoLogin,
|
||||||
|
});
|
||||||
|
}
|
||||||
const server = app.listen(port, () => {
|
const server = app.listen(port, () => {
|
||||||
logger.info("server started", { port });
|
logger.info("server started", { port });
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -17,8 +17,11 @@ import { hashPassword, verifyPassword } from "../src/server/auth/password.js";
|
||||||
import { SessionStore } from "../src/server/auth/sessions.js";
|
import { SessionStore } from "../src/server/auth/sessions.js";
|
||||||
import { LoginThrottle } from "../src/server/auth/login-throttle.js";
|
import { LoginThrottle } from "../src/server/auth/login-throttle.js";
|
||||||
import { readCookie } from "../src/server/auth/cookie.js";
|
import { readCookie } from "../src/server/auth/cookie.js";
|
||||||
import { requireSameOriginForWrites } from "../src/server/auth/auth.middleware.js";
|
import {
|
||||||
import type { AuthConfig } from "../src/server/auth/auth-config.js";
|
createAuthGuard,
|
||||||
|
requireSameOriginForWrites,
|
||||||
|
} from "../src/server/auth/auth.middleware.js";
|
||||||
|
import { readAuthConfig, type AuthConfig } from "../src/server/auth/auth-config.js";
|
||||||
import { createUserSchema } from "../src/shared/validation/user.schemas.js";
|
import { createUserSchema } from "../src/shared/validation/user.schemas.js";
|
||||||
import { loginSchema } from "../src/shared/validation/auth.schemas.js";
|
import { loginSchema } from "../src/shared/validation/auth.schemas.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -37,6 +40,7 @@ function createUserService() {
|
||||||
sessionMaxAgeMs: 7 * 24 * HOUR,
|
sessionMaxAgeMs: 7 * 24 * HOUR,
|
||||||
cookieSecure: false,
|
cookieSecure: false,
|
||||||
bootstrap: { username: "admin", password: "bootstrap-password-123" },
|
bootstrap: { username: "admin", password: "bootstrap-password-123" },
|
||||||
|
devAutoLogin: null,
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
repository,
|
repository,
|
||||||
|
|
@ -202,6 +206,122 @@ describe("same-origin guard for writes", () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("development auto-login", () => {
|
||||||
|
const baseConfig = {
|
||||||
|
sessionTtlMs: 12 * HOUR,
|
||||||
|
sessionMaxAgeMs: 7 * 24 * HOUR,
|
||||||
|
cookieSecure: false,
|
||||||
|
bootstrap: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Runs the /api auth guard for one request without a session cookie.
|
||||||
|
async function guard(
|
||||||
|
users: UserRepository,
|
||||||
|
devAutoLogin: string | null,
|
||||||
|
sessions = new SessionStore(HOUR, 24 * HOUR),
|
||||||
|
cookie?: string
|
||||||
|
) {
|
||||||
|
const req = {
|
||||||
|
path: "/projects",
|
||||||
|
headers: cookie ? { cookie } : {},
|
||||||
|
} as unknown as Request;
|
||||||
|
let status: number | undefined;
|
||||||
|
const res = {
|
||||||
|
status(code: number) {
|
||||||
|
status = code;
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
json() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
cookie() {
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
} as unknown as Response;
|
||||||
|
const passed = await new Promise<boolean>((resolve, reject) => {
|
||||||
|
const middleware = createAuthGuard(
|
||||||
|
{ ...baseConfig, devAutoLogin },
|
||||||
|
sessions,
|
||||||
|
users
|
||||||
|
);
|
||||||
|
middleware(req, res, (error?: unknown) =>
|
||||||
|
error ? reject(error) : resolve(true)
|
||||||
|
);
|
||||||
|
setTimeout(() => resolve(false), 200);
|
||||||
|
});
|
||||||
|
return { passed, status, authUser: req.authUser };
|
||||||
|
}
|
||||||
|
|
||||||
|
it("is off unless AUTH_DEV_AUTOLOGIN names a user", () => {
|
||||||
|
assert.equal(readAuthConfig({}).devAutoLogin, null);
|
||||||
|
assert.equal(readAuthConfig({ AUTH_DEV_AUTOLOGIN: " " }).devAutoLogin, null);
|
||||||
|
assert.equal(readAuthConfig({ AUTH_DEV_AUTOLOGIN: " Julian " }).devAutoLogin, "julian");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("signs every request without a session in as the configured active user", async () => {
|
||||||
|
const { repository, service } = createUserService();
|
||||||
|
const user = await service.createUser({
|
||||||
|
username: "julian",
|
||||||
|
name: "Julian",
|
||||||
|
role: "admin",
|
||||||
|
initialPassword: "start-passwort",
|
||||||
|
});
|
||||||
|
assert.ok(user);
|
||||||
|
|
||||||
|
const result = await guard(repository, "julian");
|
||||||
|
assert.equal(result.passed, true);
|
||||||
|
assert.equal(result.authUser?.id, user.id);
|
||||||
|
assert.equal(result.authUser?.role, "admin");
|
||||||
|
// A development instance must not stop at the forced password change.
|
||||||
|
assert.equal(result.authUser?.mustChangePassword, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("still requires a login without the switch, for unknown and for deactivated users", async () => {
|
||||||
|
const { repository, service } = createUserService();
|
||||||
|
const user = await service.createUser({
|
||||||
|
username: "julian",
|
||||||
|
name: "Julian",
|
||||||
|
role: "admin",
|
||||||
|
initialPassword: "start-passwort",
|
||||||
|
});
|
||||||
|
assert.ok(user);
|
||||||
|
assert.deepEqual(
|
||||||
|
{ passed: (await guard(repository, null)).passed, status: (await guard(repository, null)).status },
|
||||||
|
{ passed: false, status: 401 }
|
||||||
|
);
|
||||||
|
assert.equal((await guard(repository, "niemand")).status, 401);
|
||||||
|
await service.createUser({
|
||||||
|
username: "zweiter",
|
||||||
|
name: "Zweiter",
|
||||||
|
role: "admin",
|
||||||
|
initialPassword: "start-passwort",
|
||||||
|
});
|
||||||
|
await service.updateRoleAndActive(user.id, { active: false });
|
||||||
|
assert.equal((await guard(repository, "julian")).status, 401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps a real session in front of the auto-login user", async () => {
|
||||||
|
const { repository, service } = createUserService();
|
||||||
|
await service.createUser({
|
||||||
|
username: "julian",
|
||||||
|
name: "Julian",
|
||||||
|
role: "admin",
|
||||||
|
initialPassword: "start-passwort",
|
||||||
|
});
|
||||||
|
const other = await service.createUser({
|
||||||
|
username: "max",
|
||||||
|
name: "Max",
|
||||||
|
role: "user",
|
||||||
|
initialPassword: "start-passwort",
|
||||||
|
});
|
||||||
|
assert.ok(other);
|
||||||
|
const sessions = new SessionStore(HOUR, 24 * HOUR);
|
||||||
|
const sessionId = sessions.create(other.id).id;
|
||||||
|
const result = await guard(repository, "julian", sessions, `lb_session=${sessionId}`);
|
||||||
|
assert.equal(result.authUser?.username, "max");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("input validation", () => {
|
describe("input validation", () => {
|
||||||
it("rejects look-alike and whitespace usernames and normalizes case", () => {
|
it("rejects look-alike and whitespace usernames and normalizes case", () => {
|
||||||
const base = { name: "Test", role: "user", initialPassword: "start-passwort" };
|
const base = { name: "Test", role: "user", initialPassword: "start-passwort" };
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue