Middleware
Middleware utilities bieden server-side functionaliteit voor authenticatie, app version checking, en role-based authorization binnen de VEMAP applicatie.
Design
Section titled “Design”Het middleware systeem is gebaseerd op:
- Request-level authenticatie via session cookies
- App version checking voor automatische cache clearing bij updates
- Public path handling voor onbeveiligde routes
- Role-based authorization voor toegangscontrole
- Automatic redirects naar login bij ontbrekende sessies
- Token refresh voor automatische token vernieuwing
- Error handling voor malformed session data
Gebruik
Section titled “Gebruik”Public paths configuratie
Section titled “Public paths configuratie”De publieke routes zijn gedefinieerd in lib/utils/constants.js:
export const PUBLIC_ROUTES = [ "/login", "/unauthorized", "/login/wachtwoord-vergeten", "/login/wachtwoord-reset", "/login/nieuw-wachtwoord", "/robots.txt", "/api/auth/set-session", "/api/auth/refresh", "/api/auth/logout", "/api/auth/login", "/api/auth/challenge",];Authorization routes
Section titled “Authorization routes”Role-based toegangscontrole is geconfigureerd per rol:
export const AUTHORIZED_ROUTES = { ROLE_ADMIN: ["/", "/gebruikersbeheer", "/klantomgevingen", "/support"], ROLE_ORGANISATION: ["/", "/gebruikersbeheer", "/projectbeheer/*"], ROLE_MANAGER: ["/", "/gebruikersbeheer", "/projectbeheer/*"], ROLE_STAKEHOLDER: ["/", "/mijn-afsluitingen/*"],};Routes met * ondersteunen wildcard matching (bijv. /projectbeheer/* matcht /projectbeheer/123).
API referentie
Section titled “API referentie”Middleware functies
Section titled “Middleware functies”| Functie | Beschrijving | Parameters | Return Type |
|---|---|---|---|
authenticateOnRequest() | Authenticate request en set user context | context, publicPaths, next | Promise<Response> |
checkAppVersion() | Check app version cookie en clear bij mismatch | context | boolean |
isPathAuthorized() | Check of path is toegestaan voor gebruiker rol | path, authorizedPatterns | boolean |
isAccessTokenValid() | Check of access token nog geldig is | sessionData | boolean |
Parameters
Section titled “Parameters”authenticateOnRequest(context, publicPaths, next)
Section titled “authenticateOnRequest(context, publicPaths, next)”- context: Astro context object met cookies en URL
- publicPaths: Array van publieke routes die geen authenticatie vereisen
- next: Next middleware functie
Return values:
- Public paths: Direct door naar volgende middleware
- Valid session: Set
context.locals.useren ga door - Invalid/missing session: Redirect naar
/login - Token refresh nodig: Automatisch refresh en ga door
checkAppVersion(context)
Section titled “checkAppVersion(context)”- context: Astro context object met cookies
Return values:
- false: Cookie bestaat en matcht huidige versie, of geen cookie (wordt gezet)
- true: Cookie mismatch gedetecteerd, auth cookies worden gecleared
isPathAuthorized(path, authorizedPatterns)
Section titled “isPathAuthorized(path, authorizedPatterns)”- path: Het pad om te checken (bijv.
/projectbeheer/123) - authorizedPatterns: Array van toegestane patterns (bijv.
["/", "/projectbeheer/*"])
Return values:
- true: Path is toegestaan voor de rol
- false: Path is niet toegestaan
Pattern matching:
- Exact match:
"/"matcht alleen/ - Wildcard match:
"/projectbeheer/*"matcht/projectbeheer/123,/projectbeheer/abc, etc.
Implementatie details
Section titled “Implementatie details”Middleware flow
Section titled “Middleware flow”Request ↓1. Check app version ├─ Geen cookie → Zet cookie, ga door ├─ Cookie matcht → Ga door └─ Cookie mismatch → Clear auth cookies, redirect naar /login ↓2. Authenticate request ├─ Public path → Ga door ├─ Geen refresh token → Redirect naar /login ├─ Token refresh nodig → Refresh token, ga door └─ Valid session → Set context.locals.user, ga door ↓3. Execute route handler (next()) ↓4. Set app_version cookie op response (voor Edge Functions) ↓5. Set cache headers (voor beveiligde routes) ↓6. Check role-based authorization ├─ Geen user → Ga door (al gehandeld in stap 2) ├─ Path toegestaan → Ga door └─ Path niet toegestaan → Redirect naar /unauthorized ↓ResponseSession handling
Section titled “Session handling”// Session cookie parsingconst sessionCookie = context.cookies.get("user_session");const sessionData = JSON.parse(sessionCookie.value);context.locals.user = sessionData;Token refresh logic
Section titled “Token refresh logic”De middleware refresh automatisch access tokens wanneer:
- Geen access token aanwezig
- Token verloopt binnen 120 seconden
- Token is verlopen
const needsRefresh = !accessToken || (accessToken && secondsUntilExpiry(accessToken) < 120);if (needsRefresh) { const newAccessToken = await refreshAccessToken(context); if (!newAccessToken) { // Refresh failed, redirect naar login return context.redirect("/login"); }}App version checking
Section titled “App version checking”Bij elke request wordt gecontroleerd of de app_version cookie matcht met de huidige app versie:
const appVersionCookie = context.cookies.get("app_version")?.value;if (appVersionCookie !== appVersion) { // Version mismatch: clear alle auth cookies clearAuthCookies(context); // Redirect naar login om nieuwe sessie te starten return context.redirect("/login");}Dit zorgt ervoor dat gebruikers automatisch worden uitgelogd na een app update, wat voorkomt dat oude sessies problemen veroorzaken.
Versie beheer:
De app versie wordt beheerd via scripts/bump-version.js. Zie Deployment documentatie voor details over hoe je de versie update.
Error scenarios
Section titled “Error scenarios”- Missing session cookie → Redirect naar
/login - Invalid JSON in session → Redirect naar
/login - Public path access → Direct door naar volgende middleware
- Token refresh failed → Clear cookies, redirect naar
/login - Unauthorized path → Redirect naar
/unauthorized - App version mismatch → Clear auth cookies, redirect naar
/login
Security considerations
Section titled “Security considerations”- Cookie validation gebeurt op elke request
- Session parsing errors worden gelogd maar niet geëxposeerd
- Public paths zijn expliciet gedefinieerd voor veiligheid
- Automatic redirects voorkomen unauthorized access
- Role-based authorization voorkomt toegang tot onbevoegde routes
- Token refresh gebeurt automatisch en transparant
- Cache headers worden gezet voor beveiligde routes om caching te voorkomen
- App version checking zorgt voor automatische sessie clearing bij updates
Cookie Security & XSS bescherming
Section titled “Cookie Security & XSS bescherming”De VEMAP applicatie gebruikt een gelaagd cookie security model om gevoelige tokens te beschermen tegen XSS (Cross-Site Scripting) aanvallen.
Cookie overzicht
Section titled “Cookie overzicht”| Cookie | httpOnly | Secure | SameSite | Beschrijving |
|---|---|---|---|---|
access_token | ✅ (prod/dev) | ✅ (prod/dev) | lax | JWT access token voor API authenticatie |
refresh_token | ✅ (altijd) | ✅ (prod/dev) | lax | Long-lived token voor token refresh |
user_session | ❌ | ✅ (prod/dev) | lax | Gebruikersgegevens (naam, email, rollen) |
app_version | ❌ | ✅ (prod/dev) | lax | App version voor cache clearing |
httpOnly configuratie
Section titled “httpOnly configuratie”De omgeving wordt automatisch gedetecteerd via Astro’s import.meta.env.DEV:
// Automatische omgevingsdetectieconst currentEnv = import.meta.env.DEV ? "local" : "dev";// → "local" bij npm run dev// → "dev" bij Netlify deployment
// Access token: httpOnly in productie, niet in lokale ontwikkelingexport const getAuthCookieHttpOnly = () => { return currentEnv !== "local";};
// Refresh token: altijd httpOnlysetRefreshToken(refreshToken, cookies); // httpOnly: trueWaarom user_session NIET httpOnly is
Section titled “Waarom user_session NIET httpOnly is”De user_session cookie bevat niet-gevoelige gebruikersgegevens (naam, email, rollen) en is bewust niet httpOnly omdat:
- Alpine.js client-side toegang - UI componenten hebben toegang nodig tot gebruikersrollen voor conditional rendering
- Geen gevoelige tokens - Bevat geen access/refresh tokens, alleen display data
- Server-side validatie - Alle API calls worden gevalideerd met de httpOnly
access_token
XSS bescherming strategie
Section titled “XSS bescherming strategie”┌─────────────────────────────────────────────────────────────┐│ Browser (Client) │├─────────────────────────────────────────────────────────────┤│ access_token → httpOnly, niet toegankelijk via JS ││ refresh_token → httpOnly, niet toegankelijk via JS ││ user_session → Leesbaar voor Alpine.js UI logic │└─────────────────────────────────────────────────────────────┘ │ ▼┌─────────────────────────────────────────────────────────────┐│ Server (Astro SSR) │├─────────────────────────────────────────────────────────────┤│ Middleware leest httpOnly cookies voor authenticatie ││ API calls gebruiken access_token uit httpOnly cookie │└─────────────────────────────────────────────────────────────┘Voordelen:
- Zelfs bij een XSS aanval kan een aanvaller geen access/refresh tokens stelen
- Gebruikersgegevens in
user_sessionzijn niet bruikbaar voor API authenticatie - Tokens worden alleen server-side gelezen en doorgestuurd naar de backend
Best practices
Section titled “Best practices”- Definieer public paths expliciet in
PUBLIC_ROUTESconstant - Definieer authorization routes per rol in
AUTHORIZED_ROUTES - Handle parsing errors gracefully met redirects
- Log authentication errors voor debugging (console.error)
- Test edge cases zoals malformed cookies, expired tokens, en version mismatches
- Gebruik context.locals voor user data in components
- Test token refresh scenarios (expired tokens, network failures)
- Test role-based authorization voor alle rollen en routes
- Zorg voor correcte cache headers op beveiligde routes
- Update app version bij elke deploy om sessie clearing te triggeren
Netlify Edge Functions
Section titled “Netlify Edge Functions”De middleware draait als Edge Function in productie (edgeMiddleware: true). Dit betekent:
- Cookies moeten expliciet op response headers worden gezet voor Edge Functions
- Domain attributen (
.vemap.nl) worden gebruikt voor cross-subdomain cookie sharing - Secure flag is automatisch enabled in productie
- SameSite=Lax is voldoende voor same-site subdomains
Cookie setting in Edge Functions
Section titled “Cookie setting in Edge Functions”// Zet cookie via context.cookies (voor Astro)context.cookies.set("app_version", appVersion, cookieOptions);
// Zet cookie ook expliciet op response headers (voor Edge Functions)const cookieString = `app_version=${appVersion}; Path=${ cookieOptions.path}; Max-Age=${cookieOptions.maxAge}; ${ cookieOptions.secure ? "Secure; " : ""}SameSite=${cookieOptions.sameSite}${ cookieOptions.domain ? `; Domain=${cookieOptions.domain}` : ""}`;response.headers.append("Set-Cookie", cookieString);