fix(auth): correct local cookie security configuration
This commit is contained in:
parent
b584e60273
commit
155fdbb16a
67 changed files with 1003 additions and 62 deletions
|
|
@ -2,32 +2,79 @@
|
|||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { LogIn } from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
import { AuthProvider, useAuth } from "@/components/auth";
|
||||
import { BrandLogo } from "@/components/brand/brand-logo";
|
||||
import { login } from "@/lib/api";
|
||||
import { login, User } from "@/lib/api";
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8)
|
||||
email: z.string().email("Bitte gib eine gueltige E-Mail-Adresse ein."),
|
||||
password: z.string().min(1, "Bitte gib dein Passwort ein.")
|
||||
});
|
||||
|
||||
type LoginForm = z.infer<typeof schema>;
|
||||
|
||||
type LoginError = { status: number; message: string };
|
||||
|
||||
function mapLoginError(error: unknown): LoginError {
|
||||
const status = typeof error === "object" && error && "status" in error ? Number((error as { status?: number }).status) : 0;
|
||||
switch (status) {
|
||||
case 401:
|
||||
return { status, message: "E-Mail-Adresse oder Passwort ist falsch." };
|
||||
case 403:
|
||||
return { status, message: "Dieses Benutzerkonto ist deaktiviert." };
|
||||
case 422:
|
||||
return { status, message: "Bitte pruefe deine Eingaben." };
|
||||
case 500:
|
||||
case 502:
|
||||
return { status, message: "Der Anmeldedienst ist momentan nicht erreichbar." };
|
||||
default:
|
||||
return { status, message: "Anmeldung fehlgeschlagen. Bitte erneut versuchen." };
|
||||
}
|
||||
}
|
||||
|
||||
function LoginPanel() {
|
||||
const router = useRouter();
|
||||
const auth = useAuth();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [toast, setToast] = useState("");
|
||||
const [success, setSuccess] = useState("");
|
||||
|
||||
const form = useForm<LoginForm>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: { email: "admin@schubamed.de", password: "" }
|
||||
defaultValues: { email: "admin@schubamed.de", password: "" },
|
||||
mode: "onSubmit"
|
||||
});
|
||||
|
||||
async function onSubmit(values: LoginForm) {
|
||||
const result = await login(values.email, values.password);
|
||||
auth.setToken(result.access_token);
|
||||
router.push("/dashboard");
|
||||
useEffect(() => {
|
||||
if (!toast) return;
|
||||
const timeout = window.setTimeout(() => setToast(""), 5000);
|
||||
return () => window.clearTimeout(timeout);
|
||||
}, [toast]);
|
||||
|
||||
async function handleSubmit(values: LoginForm) {
|
||||
setLoading(true);
|
||||
setToast("");
|
||||
setSuccess("");
|
||||
try {
|
||||
const result = await login(values.email, values.password);
|
||||
const currentUser = (await auth.refreshUser()) ?? (result.user as User);
|
||||
auth.setUser(currentUser);
|
||||
auth.setToken("authenticated");
|
||||
const target = currentUser.must_change_password ? "/profile/security" : "/dashboard";
|
||||
setSuccess("Anmeldung erfolgreich");
|
||||
window.location.replace(target);
|
||||
} catch (error) {
|
||||
const mapped = mapLoginError(error);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
console.error("Login failed", error);
|
||||
console.error("Login failed mapped", mapped.status, mapped.message);
|
||||
}
|
||||
setToast(mapped.message);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -38,7 +85,14 @@ function LoginPanel() {
|
|||
<h1 className="mt-2 text-3xl font-semibold text-text">Anmelden</h1>
|
||||
<p className="mt-3 text-sm leading-6 text-text-light">Sicherer Zugriff auf Atlas Workspace.</p>
|
||||
</div>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
void form.handleSubmit(handleSubmit)(event);
|
||||
}}
|
||||
className="space-y-5"
|
||||
noValidate
|
||||
>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-text">E-Mail</span>
|
||||
<input
|
||||
|
|
@ -46,6 +100,9 @@ function LoginPanel() {
|
|||
className="mt-2 h-12 w-full rounded-lg border border-border bg-white px-4 outline-none transition focus:border-primary"
|
||||
{...form.register("email")}
|
||||
/>
|
||||
{form.formState.errors.email && (
|
||||
<p className="mt-2 text-sm text-danger">{form.formState.errors.email.message}</p>
|
||||
)}
|
||||
</label>
|
||||
<label className="block">
|
||||
<span className="text-sm font-medium text-text">Passwort</span>
|
||||
|
|
@ -54,13 +111,19 @@ function LoginPanel() {
|
|||
className="mt-2 h-12 w-full rounded-lg border border-border bg-white px-4 outline-none transition focus:border-primary"
|
||||
{...form.register("password")}
|
||||
/>
|
||||
{form.formState.errors.password && (
|
||||
<p className="mt-2 text-sm text-danger">{form.formState.errors.password.message}</p>
|
||||
)}
|
||||
</label>
|
||||
{toast && <div className="rounded-lg border border-danger/30 bg-white px-4 py-3 text-sm text-danger shadow-soft">{toast}</div>}
|
||||
{success && <div className="rounded-lg border border-success/30 bg-white px-4 py-3 text-sm text-success shadow-soft">{success}</div>}
|
||||
<button
|
||||
type="submit"
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-5 font-semibold text-white shadow-soft transition hover:bg-primary-dark"
|
||||
disabled={loading}
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-lg bg-primary px-5 font-semibold text-white shadow-soft transition hover:bg-primary-dark disabled:cursor-not-allowed disabled:opacity-60"
|
||||
>
|
||||
<LogIn className="h-5 w-5" />
|
||||
Einloggen
|
||||
{loading ? <span className="spinner" /> : <LogIn className="h-5 w-5" />}
|
||||
<span>{loading ? "Anmeldung läuft…" : "Anmelden"}</span>
|
||||
</button>
|
||||
</form>
|
||||
</section>
|
||||
|
|
|
|||
32
validation-suite/frontend/atlas/app/api/login/route.ts
Normal file
32
validation-suite/frontend/atlas/app/api/login/route.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_NAME, buildAuthCookieOptions } from "@/lib/auth";
|
||||
|
||||
const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`;
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const body = await request.json();
|
||||
try {
|
||||
const response = await fetch(`${mercuryBase}/auth/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
const data = await response.json();
|
||||
if (!response.ok) {
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set({
|
||||
name: AUTH_COOKIE_NAME,
|
||||
value: data.access_token,
|
||||
...buildAuthCookieOptions(data.expires_in)
|
||||
});
|
||||
return NextResponse.json({ user: data.user, expires_in: data.expires_in });
|
||||
} catch (error) {
|
||||
return NextResponse.json(
|
||||
{ detail: "Der Anmeldedienst ist momentan nicht erreichbar." },
|
||||
{ status: 502 }
|
||||
);
|
||||
}
|
||||
}
|
||||
13
validation-suite/frontend/atlas/app/api/logout/route.ts
Normal file
13
validation-suite/frontend/atlas/app/api/logout/route.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_NAME, buildAuthCookieOptions } from "@/lib/auth";
|
||||
|
||||
export async function POST() {
|
||||
const cookieStore = await cookies();
|
||||
cookieStore.set({
|
||||
name: AUTH_COOKIE_NAME,
|
||||
value: "",
|
||||
...buildAuthCookieOptions(0)
|
||||
});
|
||||
return NextResponse.json({ status: "ok" });
|
||||
}
|
||||
18
validation-suite/frontend/atlas/app/api/me/route.ts
Normal file
18
validation-suite/frontend/atlas/app/api/me/route.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_NAME } from "@/lib/auth";
|
||||
|
||||
const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`;
|
||||
|
||||
export async function GET() {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ detail: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
const response = await fetch(`${mercuryBase}/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
});
|
||||
const data = await response.json();
|
||||
return NextResponse.json(data, { status: response.status });
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import { cookies } from "next/headers";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { AUTH_COOKIE_NAME } from "@/lib/auth";
|
||||
|
||||
const mercuryBase = `${process.env.MERCURY_INTERNAL_URL ?? "http://mercury-api:8000"}/api/v1`;
|
||||
|
||||
async function forward(request: NextRequest, method: string, path: string[]) {
|
||||
const cookieStore = await cookies();
|
||||
const token = cookieStore.get(AUTH_COOKIE_NAME)?.value;
|
||||
if (!token) {
|
||||
return NextResponse.json({ detail: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
const url = new URL(`${mercuryBase}/${path.join("/")}${request.nextUrl.search}`);
|
||||
const headers = new Headers(request.headers);
|
||||
headers.set("Authorization", `Bearer ${token}`);
|
||||
headers.delete("host");
|
||||
headers.delete("cookie");
|
||||
const init: RequestInit = { method, headers };
|
||||
if (method !== "GET" && method !== "HEAD") {
|
||||
init.body = await request.text();
|
||||
}
|
||||
const response = await fetch(url, init);
|
||||
const contentType = response.headers.get("content-type") ?? "";
|
||||
if (contentType.includes("application/json")) {
|
||||
return NextResponse.json(await response.json(), { status: response.status });
|
||||
}
|
||||
return new NextResponse(response.body, { status: response.status, headers: { "content-type": contentType } });
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
return forward(request, "GET", (await params).path);
|
||||
}
|
||||
export async function POST(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
return forward(request, "POST", (await params).path);
|
||||
}
|
||||
export async function PUT(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
return forward(request, "PUT", (await params).path);
|
||||
}
|
||||
export async function DELETE(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
|
||||
return forward(request, "DELETE", (await params).path);
|
||||
}
|
||||
|
|
@ -1,15 +1,16 @@
|
|||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { createContext, useContext, useEffect, useMemo, useState } from "react";
|
||||
import { apiGet, User } from "@/lib/api";
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { User } from "@/lib/api";
|
||||
|
||||
type AuthContextValue = {
|
||||
token: string | null;
|
||||
setToken: (value: string | null) => void;
|
||||
setUser: (value: User | null) => void;
|
||||
logout: () => void;
|
||||
user: User | null;
|
||||
refreshUser: () => void;
|
||||
refreshUser: () => Promise<User | null>;
|
||||
};
|
||||
|
||||
const AuthContext = createContext<AuthContextValue | null>(null);
|
||||
|
|
@ -18,42 +19,67 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
|
|||
const router = useRouter();
|
||||
const [token, setTokenState] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<User | null>(null);
|
||||
const tokenRef = useRef<string | null>(null);
|
||||
const userRef = useRef<User | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = window.localStorage.getItem("atlas_token");
|
||||
setTokenState(stored);
|
||||
if (!stored) return;
|
||||
void apiGet<User>("/auth/me", stored).then(setUser).catch(() => setUser(null));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token) {
|
||||
setUser(null);
|
||||
return;
|
||||
}
|
||||
void apiGet<User>("/auth/me", token).then(setUser).catch(() => setUser(null));
|
||||
tokenRef.current = token;
|
||||
}, [token]);
|
||||
|
||||
useEffect(() => {
|
||||
userRef.current = user;
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetch("/api/me", { credentials: "include", cache: "no-store" })
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
return (await response.json()) as User;
|
||||
})
|
||||
.then((current) => {
|
||||
setUser(current);
|
||||
setTokenState("authenticated");
|
||||
})
|
||||
.catch(() => {
|
||||
if (!tokenRef.current && !userRef.current) {
|
||||
setUser(null);
|
||||
setTokenState(null);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const value = useMemo<AuthContextValue>(() => {
|
||||
const setToken = (next: string | null) => {
|
||||
setTokenState(next);
|
||||
if (next) {
|
||||
window.localStorage.setItem("atlas_token", next);
|
||||
} else {
|
||||
window.localStorage.removeItem("atlas_token");
|
||||
}
|
||||
};
|
||||
return {
|
||||
token,
|
||||
setToken,
|
||||
setUser,
|
||||
logout: () => {
|
||||
setToken(null);
|
||||
void fetch("/api/logout", { method: "POST", credentials: "include" });
|
||||
router.push("/login");
|
||||
},
|
||||
user,
|
||||
refreshUser: () => {
|
||||
if (token) {
|
||||
void apiGet<User>("/auth/me", token).then(setUser).catch(() => setUser(null));
|
||||
refreshUser: async () => {
|
||||
try {
|
||||
const response = await fetch("/api/me", { credentials: "include", cache: "no-store" });
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
const current = (await response.json()) as User;
|
||||
setUser(current);
|
||||
setTokenState("authenticated");
|
||||
return current;
|
||||
} catch {
|
||||
if (!userRef.current) {
|
||||
setUser(null);
|
||||
setTokenState(null);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
export const API_BASE = process.env.NEXT_PUBLIC_API_BASE_URL ?? "/api/v1";
|
||||
export const API_BASE = "/api/v1";
|
||||
|
||||
export type Entity = {
|
||||
id: string;
|
||||
|
|
@ -121,21 +121,25 @@ export type Paginated<T> = {
|
|||
};
|
||||
|
||||
export async function login(email: string, password: string) {
|
||||
const response = await fetch(`${API_BASE}/auth/login`, {
|
||||
const response = await fetch(`/api/login`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ email, password })
|
||||
body: JSON.stringify({ email, password }),
|
||||
credentials: "include"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Anmeldung fehlgeschlagen");
|
||||
const detail = await response.text();
|
||||
const error = new Error(detail || `HTTP ${response.status}`) as Error & { status?: number };
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
return response.json() as Promise<{ access_token: string; token_type: string }>;
|
||||
return response.json() as Promise<{ user: User; expires_in: number }>;
|
||||
}
|
||||
|
||||
export async function apiGet<T>(path: string, token: string): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
cache: "no-store"
|
||||
cache: "no-store",
|
||||
credentials: "include"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
|
|
@ -146,8 +150,9 @@ export async function apiGet<T>(path: string, token: string): Promise<T> {
|
|||
export async function apiSend<T>(path: string, token: string, method: "POST" | "PUT", body: unknown): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method,
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body)
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
credentials: "include"
|
||||
});
|
||||
if (!response.ok) {
|
||||
const detail = await response.text();
|
||||
|
|
@ -159,7 +164,7 @@ export async function apiSend<T>(path: string, token: string, method: "POST" | "
|
|||
export async function apiDelete(path: string, token: string): Promise<void> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
method: "DELETE",
|
||||
headers: { Authorization: `Bearer ${token}` }
|
||||
credentials: "include"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`API request failed: ${response.status}`);
|
||||
|
|
@ -169,7 +174,8 @@ export async function apiDelete(path: string, token: string): Promise<void> {
|
|||
export async function apiFetch<T>(path: string, token: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE}${path}`, {
|
||||
...init,
|
||||
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", ...(init?.headers ?? {}) }
|
||||
headers: { "Content-Type": "application/json", ...(init?.headers ?? {}) },
|
||||
credentials: "include"
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(await response.text());
|
||||
|
|
|
|||
13
validation-suite/frontend/atlas/lib/auth.ts
Normal file
13
validation-suite/frontend/atlas/lib/auth.ts
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
export const AUTH_COOKIE_NAME = process.env.AUTH_COOKIE_NAME ?? "atlas_access_token";
|
||||
export const AUTH_COOKIE_SECURE = process.env.AUTH_COOKIE_SECURE?.trim().toLowerCase() === "true";
|
||||
export const AUTH_COOKIE_SAMESITE = (process.env.AUTH_COOKIE_SAMESITE ?? "lax").trim().toLowerCase();
|
||||
|
||||
export function buildAuthCookieOptions(maxAge: number) {
|
||||
return {
|
||||
httpOnly: true as const,
|
||||
secure: AUTH_COOKIE_SECURE,
|
||||
sameSite: AUTH_COOKIE_SAMESITE as "lax" | "strict" | "none",
|
||||
path: "/" as const,
|
||||
maxAge
|
||||
};
|
||||
}
|
||||
60
validation-suite/frontend/atlas/middleware.ts
Normal file
60
validation-suite/frontend/atlas/middleware.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { NextResponse } from "next/server";
|
||||
import type { NextRequest } from "next/server";
|
||||
import { AUTH_COOKIE_NAME } from "@/lib/auth";
|
||||
|
||||
const protectedPrefixes = [
|
||||
"/dashboard",
|
||||
"/customers",
|
||||
"/locations",
|
||||
"/contacts",
|
||||
"/devices",
|
||||
"/equipment",
|
||||
"/validations",
|
||||
"/users",
|
||||
"/profile"
|
||||
];
|
||||
|
||||
async function loadCurrentUser(request: NextRequest) {
|
||||
const response = await fetch(new URL("/api/me", request.url), {
|
||||
headers: { cookie: request.headers.get("cookie") ?? "" },
|
||||
cache: "no-store"
|
||||
});
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
return (await response.json()) as { must_change_password?: boolean };
|
||||
}
|
||||
|
||||
export async function middleware(request: NextRequest) {
|
||||
const token = request.cookies.get(AUTH_COOKIE_NAME)?.value;
|
||||
const isProtected = protectedPrefixes.some(
|
||||
(prefix) => request.nextUrl.pathname === prefix || request.nextUrl.pathname.startsWith(`${prefix}/`)
|
||||
);
|
||||
|
||||
if (!token) {
|
||||
if (isProtected) {
|
||||
return NextResponse.redirect(new URL("/login", request.url));
|
||||
}
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
const currentUser = await loadCurrentUser(request);
|
||||
const requiresPasswordChange = Boolean(currentUser?.must_change_password);
|
||||
const isLogin = request.nextUrl.pathname === "/login";
|
||||
const isPasswordProfile = request.nextUrl.pathname === "/profile/security";
|
||||
|
||||
if (isLogin) {
|
||||
const target = requiresPasswordChange ? "/profile/security" : "/dashboard";
|
||||
return NextResponse.redirect(new URL(target, request.url));
|
||||
}
|
||||
|
||||
if (requiresPasswordChange && isProtected && !isPasswordProfile) {
|
||||
return NextResponse.redirect(new URL("/profile/security", request.url));
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
export const config = {
|
||||
matcher: ["/dashboard/:path*", "/customers/:path*", "/locations/:path*", "/contacts/:path*", "/devices/:path*", "/equipment/:path*", "/validations/:path*", "/users/:path*", "/profile/:path*", "/login"]
|
||||
};
|
||||
|
|
@ -6,7 +6,8 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"test:auth": "node --test tests/auth-cookie.test.mjs"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.10.0",
|
||||
|
|
@ -31,4 +32,3 @@
|
|||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
42
validation-suite/frontend/atlas/tests/auth-cookie.test.mjs
Normal file
42
validation-suite/frontend/atlas/tests/auth-cookie.test.mjs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
function parseCookieSecure(value) {
|
||||
return value?.trim().toLowerCase() === "true";
|
||||
}
|
||||
|
||||
test("AUTH_COOKIE_SECURE=false yields secure false", async () => {
|
||||
assert.equal(parseCookieSecure("false"), false);
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
secure: parseCookieSecure("false"),
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 3600
|
||||
};
|
||||
assert.deepEqual(options, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 3600
|
||||
});
|
||||
});
|
||||
|
||||
test("AUTH_COOKIE_SECURE=true yields secure true", async () => {
|
||||
assert.equal(parseCookieSecure("true"), true);
|
||||
const options = {
|
||||
httpOnly: true,
|
||||
secure: parseCookieSecure("true"),
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 0
|
||||
};
|
||||
assert.deepEqual(options, {
|
||||
httpOnly: true,
|
||||
secure: true,
|
||||
sameSite: "lax",
|
||||
path: "/",
|
||||
maxAge: 0
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue