Ga naar inhoud

Beveiliging & privacy

Beveiliging en privacy zijn voor I See You geen sluitstuk maar uitgangspunt. De app vraagt mensen om een korte opname van zichzelf voor te leggen aan onbekende opiniegevers; die opname mag nooit ergens blijven hangen en mag nooit aan derden lekken. Deze pagina beschrijft de doelarchitectuur voor het greenfield-project (backend en frontend vanaf 0). Het oude prototype dient uitsluitend als referentie/uitgangspunt; waar de doelarchitectuur afwijkt of een verbeterpunt aanpakt, is dat expliciet benoemd.

Zie ook: Domeinmodel, Termen, Backend & API en Anonimiteit & retentie.

Anonimiteit is de kernbelofte van I See You. Het ontwerp vertaalt die belofte in harde, afdwingbare regels in plaats van in instellingen die je kunt vergeten aan te zetten:

  • Dataminimalisatie. Van een niet-geregistreerde maker bewaren we alleen een AnoniemeGebruiker: een vluchtig, minimaal profiel (leeftijd, gender, achtergrond) zonder herleidbare identiteit. Geen echte naam, geen tracking. Gebruikers kiezen een fictieve naam; een geregistreerde gebruiker kan optioneel een e-mailadres opgeven, uitsluitend voor wachtwoordherstel — zonder e-mail is er geen herstel, en een anonieme gebruiker heeft sowieso geen herstel.
  • Geen locatieveld, geen tracking. (Per 2026-07-11m, ADR-037) is er geen Land-veld meer — I See You wordt per land als aparte site gehost (NL eerst). Er is dus geen GeoIP, geen automatische detectie en geen permissieprompt; de optionele regio is gewoon een keuze uit een Keuzelijst. Er wordt geen Geolocation-API / GPS-prompt gebruikt. Zie Gebruiker & Toegang.
  • Anoniem voorleggen. Een Vraag wordt uitsluitend aan de gematchte opiniegevers getoond (een via Beheer instelbaar aantal), nooit aan derden. De maker is voor de opiniegever anoniem.
  • Verwijderen is de standaard, niet de uitzondering. Opnames worden altijd verwijderd, ongeacht het gebruikerstype. Het Retentiebeleid is een eersteklas domeinregel uit de context Moderatie & Retentie en draait als een achtergronddienst die altijd actief is. Na afronding blijft een opname nog een begrensd resultaat-retentievenster bestaan zodat alleen de maker het resultaat + de opname kan terugzien; daarna verdwijnt zij gegarandeerd — automatisch, of eerder wanneer de maker zelf verwijdert — niet als optionele opruiming.
  • Geen upload. Media komt uitsluitend van de live camera (video van maximaal 10 seconden (mag korter) óf één foto). Er is geen route om een bestaand bestand uit de galerij te uploaden, wat hergebruik van oude of niet-eigen beelden tegengaat. Zie Media & opname.
  • Beperkte communicatie. E-mail wordt alleen ingezet op twee optionele, door de gebruiker zelf gekozen momenten, beide los van elkaar: het account-e-mailadres uitsluitend voor wachtwoordherstel, en een per vraag optioneel achtergelaten adres (notificatieEmail) voor één uitslag-seintje met alleen een link. Beide adressen zijn nooit voor tracking; het notificatie-adres wordt bovendien direct na de verzendpoging gewist. Betaalcommunicatie loopt via de payment provider (zie hieronder).
  • Anoniem richting andere gebruikers; betaaldata bij de PSP (vastgesteld per 2026-07-08b; gebouwd met een gesimuleerde dummy-iDeal — een echte PSP volgt). Met betaald advies komt er geld in het systeem: een iDeal-betaling is niet anoniem richting de betaalprovider/bank. De belofte is daarom geherformuleerd als anonimiteit richting andere gebruikers. Bank-/kaartgegevens leven uitsluitend bij de PSP; het platform bewaart alleen de transactiereferentie + saldo (dataminimalisatie), gekoppeld aan de fictieve gebruikersnaam. De PSP-webhook wordt geverifieerd (signature/secret) en idempotent verwerkt. KYC-noot: bij een uitbetaling is een bankrekening nodig en gelden ken-je-klant-verplichtingen (via de PSP); dat raakt de anonimiteit nogmaals en is expliciet onderdeel van de verificatie van het voorstel.
  • Proactieve tekstmoderatie. Alle door gebruikers ingetypte vrije tekst — de toelichting van de maker en de reactie van de reviewer — wordt vóór opslaan door de tekstmoderatie (IModeratieDienst, OpenAI Moderation API) gecontroleerd; afgekeurde tekst wordt geweigerd. Zie Backend & API.

De maker kan optioneel een notificatieEmail op zijn Vraag achterlaten om, zodra de uitslag klaar is, één seintje te krijgen. De privacy-keuzes hierbij:

  • Bewust geen inhoud in de mail. De notificatie bevat alleen een link naar het gebruikersportaal — geen scores, gemiddelden of reacties. Dat houdt gevoelige beoordelingsinhoud uit het (minder beveiligde) e-mailkanaal en is robuust tegen aflever-/spamproblemen: de mail is enkel een trigger, de site is de bron (achter de credits-poort).
  • Adres eenmalig, daarna gewist. Het adres wordt bij het zetten van beoordeeldOp gebruikt voor precies één verzendpoging en direct daarna op null gezet — of de verzending nu slaagde of niet. Er blijft geen mailinglijst of herleidbaar adres achter; het verdwijnt hoe dan ook met de vraag (retentie).
  • Los van het herstel-adres. Dit per-vraag notificatie-adres staat volledig los van het optionele account-e-mailadres voor wachtwoordherstel; ook een anonieme maker kan een notificatie-adres opgeven.
  • Fail-graceful. De verzending loopt via de poort IUitslagNotificaties op Azure Communication Services Email (app-settings Email__ConnectionString, Email__Afzender, App__PortaalUrl). Zonder configuratie of bij een fout blijft de app werken; de mail is best-effort.

I See You heeft twee soorten gebruikers met fundamenteel verschillende eisen: beheerders die met een echte identiteit en sterke beveiliging beheertaken doen, en gebruikers die juist zo anoniem mogelijk moeten blijven. Eén externe identity-provider (zoals Keycloak) zou voor beide het verkeerde gereedschap zijn: het voegt een aparte component toe die beheerd, geschaald en betaald moet worden, terwijl het anonieme model van I See You niet past in het standaard account-/e-mail-/reset-paradigma. De doelarchitectuur kiest daarom voor in-app authenticatie met twee mechanismen op één API.

Huidige implementatie (2026-07-08, ADR-031). De hieronder beschreven cookie-sessie is voor de gekozen deploytopologie niet werkbaar: de portalen (Static Web Apps) draaien cross-site t.o.v. de API (App Service), waar een SameSite-cookie niet meegestuurd wordt. De sessie wordt daarom gedragen door een HMAC-ondertekend bearer-token in de Authorization-header. De beheer-login (POST /api/beheer/login) geeft een admin-token dat elke gevoelige /api/beheer/*-route via een guard vereist; login/anonieme sessie geven een actor-token waaruit de server de actor afleidt i.p.v. een meegegeven actor te vertrouwen. Wachtwoorden gebruiken nu PBKDF2 (Argon2id blijft het doelbeeld). Zie ADR-031.

flowchart TB
    subgraph Portalen["Twee frontends, één API"]
        BP["Beheerportaal"]
        GP["Gebruikersportaal"]
    end
    subgraph Auth["Authenticatie in de API"]
        Identity["ASP.NET Core Identity<br/>(rol Beheerder, MFA mogelijk)"]
        Anon["Custom anonieme auth<br/>(Gebruiker/Sessie, Argon2id)"]
    end
    Policies["Autorisatiebeleid (policies)<br/>IsBeheerder vs. gebruiker/anoniem"]

    BP --> Identity
    GP --> Anon
    Identity --> Policies
    Anon --> Policies
    Policies --> API["Endpoints (Minimal API + SignalR)"]

Beheerders authenticeren via ASP.NET Core Identity, ingebouwd in de API (geen externe provider). Daarmee krijgen we beproefde, in-app accountbeveiliging:

  • Wachtwoorden als salted hash met een moderne, trage hashfunctie; geen plain-text waar dan ook.
  • Optioneel MFA (multi-factor authenticatie) voor beheeraccounts, passend bij de gevoeligheid van beheertaken (moderatie, stamdata, systeeminstellingen).
  • De rol Beheerder stuurt de autorisatie; account-lockout bij herhaald falen en wachtwoordbeleid komen uit Identity.

Beheerders horen bij de context Beheer en de admin-kant van Moderatie & Retentie.

Gebruikers authenticeren via een eigen, anonieme mechanisme, gebouwd rond de aggregates Gebruiker en Sessie. Bewuste keuzes om anonimiteit te borgen:

  • Fictieve naam als identiteit. Een gebruiker identificeert zich met een zelfgekozen gebruikersnaam. Een e-mailadres is optioneel en wordt — als het al wordt opgegeven — uitsluitend voor wachtwoordherstel gebruikt, nooit voor communicatie of tracking.
  • Wachtwoordherstel alleen mét e-mail. Een geregistreerde gebruiker die een e-mailadres heeft opgegeven, kan zijn wachtwoord herstellen via dat adres. Zonder e-mail is er geen herstel; een anonieme gebruiker heeft sowieso geen herstel. Zo blijft de keuze voor anonimiteit bij de gebruiker liggen.
  • Argon2id-hashing. Het wachtwoord wordt gehasht met Argon2id (modern, geheugen-hard, met salt) en bij inloggen constant-time geverifieerd. Het ruwe wachtwoord wordt nooit opgeslagen.
  • Minimale gegevens. Een niet-geregistreerde maker krijgt enkel een AnoniemeGebruiker. Een geregistreerde gebruiker houdt zijn profiel, een credit-saldo en de afgeleide waarderings-statistieken — en (voorgesteld per 2026-07-05) een euro-saldo met sterren/adviesniveau; financiële betaalgegevens (bank/kaart) staan niet in het systeem maar bij de payment provider.

Verschil met het prototype. Het prototype vergeleek wachtwoorden plain — het ingevoerde wachtwoord werd rechtstreeks getoetst aan de opgeslagen waarde, en wachtwoorden stonden onversleuteld opgeslagen (zowel voor gebruikers als voor admin-credentials). In de doelarchitectuur vervalt dit volledig: gebruikerswachtwoorden via Argon2id, beheerderswachtwoorden via ASP.NET Core Identity, beide als salted hash met constant-time-verificatie.

Een Sessie is single-device: per gebruikersnaam is er hooguit één actieve sessie. De sessie wordt gedragen door een cookie die strak is afgeschermd:

  • HttpOnly — niet leesbaar vanuit JavaScript (beperkt XSS-impact).
  • Secure — alleen over HTTPS.
  • SameSite=Strict — geen cross-site meesturen (beperkt CSRF).

Bij inloggen wordt een bestaande sessie van dezelfde naam gerevoket: de nieuwe login verdringt de oude, zodat er nooit twee actieve sessies op verschillende apparaten zijn. Dat voorkomt parallelle accounts en houdt het online-begrip — dat online kandidaten voorrang geeft bij de matching — eenduidig. De aanwezigheid/heartbeat die het online-venster voedt loopt via de realtime-laag (zie Backend & API — Realtime).

Sessie-aspectGedrag
Aantal per gebruikerMax. 1 (single-device)
Tweede loginVerdringt/revoket de bestaande sessie van dezelfde naam
Cookie-flagsHttpOnly, Secure, SameSite=Strict
InactiviteitVerlopen sessies worden periodiek opgeruimd
UitloggenSessie direct verwijderd

Autorisatie — beleid in plaats van een context-grens

Section titled “Autorisatie — beleid in plaats van een context-grens”

Er zijn twee frontends (beheerportaal en gebruikersportaal) maar één deployable API. De scheiding tussen wat een beheerder mag en wat een gebruiker mag loopt niet langs een bounded-context-grens, maar via autorisatiebeleid (policies):

  • IsBeheerder — vereist een geauthenticeerde Identity-gebruiker met de rol Beheerder. Beschermt beheer-endpoints (systeeminstellingen, keuzelijsten, gebruikersoverzicht met waarderings-statistieken, moderatie-overzichten).
  • Gebruiker / anoniem — endpoints voor het maken van een Vraag, het geven van een Opinie, het waarderen van ontvangen beoordelingen, het doen van een Melding en het beheren van het eigen profiel/saldo.

Grofweg dekt het beheerportaal de context Beheer plus de admin-kant van Moderatie & Retentie; het gebruikersportaal dekt Beoordeling, het melden en de gebruikerskant van Gebruiker & Toegang. Elk endpoint kiest expliciet een policy; niets is per ongeluk open.

Misbruikbeperking — ingebouwde .NET rate limiting

Section titled “Misbruikbeperking — ingebouwde .NET rate limiting”

De API gebruikt de ingebouwde rate limiting van ASP.NET Core (geen extern pakket). De limieten zijn afgestemd op de gevoeligheid van de actie: een Melding is veel strenger begrensd dan een gewone leesactie.

PolicyBereikDoel
apialgemene endpointsBescherming tegen overbelasting en scraping
opiniehet indienen van opiniesBegrenst het massaal indienen van scores
meldinghet doen van meldingenStrenger: voorkomt het spammen van meldingen (zie de meldingen-drempel)

Aanvullend voorkomt de meldingen-logica dubbele meldingen: dezelfde gebruiker kan dezelfde vraag niet twee keer melden. Anders dan in het prototype — waar de meldingen-limiter localhost oversloeg voor lokaal testen — gelden de limieten in de doelarchitectuur overal in productie, zonder uitzonderingen op herkomst.

MaatregelMechanisme
Versleuteld transportHTTPS/HSTS — verplichte TLS; HSTS dwingt browsers tot HTTPS af.
HerkomstcontroleCORS-allowlist — alleen de echte front-end-domeinen mogen de API aanroepen, nooit een wildcard.
ContentbeperkingKrappe CSP (Content-Security-Policy) — staat alleen de eigen origins en de media-host toe.
Payload-begrenzingBody-limieten — een kleine JSON-limiet voor state-mutaties; een aparte, hogere multipart-limiet voor media.
  • HTTPS/HSTS. Al het verkeer loopt over TLS; HSTS voorkomt downgrade naar HTTP.
  • CORS-allowlist. Alleen origins op de allowlist mogen de API aanroepen. In productie staan hier uitsluitend de echte domeinen van het beheer- en gebruikersportaal, nooit een wildcard. (Het prototype kende deze allowlist al als uitgangspunt.)
  • Krappe CSP. Het prototype had de CSP uitgeschakeld; de doelarchitectuur richt een expliciete, krappe CSP in die scripts/stijlen/connecties beperkt tot de eigen origins en de media-host.
  • Body-limieten. State-mutaties (scores, profielgegevens, reacties) zijn JSON en blijven klein, dus daarvoor geldt een kleine JSON-limiet die oversized-payload-aanvallen uitsluit. Media gaat niet via de JSON-body maar via een aparte multipart-route met een eigen, hogere grootte-limiet die past bij een opname van maximaal 10 seconden of één foto.

Het Retentiebeleid is de policy uit de context Moderatie & Retentie. Het bepaalt hoe lang gegevens bestaan en wanneer ze automatisch verdwijnen. In de doelarchitectuur draait dit als een achtergronddienst die altijd actief is (niet uitschakelbaar via een vlag). Zie ook Anonimiteit & retentie en Media & opname.

RegelWaardeEffect
Beoordeling klaarresultaat-retentievensterZodra alle opinies binnen zijn, wordt beoordeeldOp gezet; de vraag + opname blijven nog een instelbaar resultaat-retentievenster (resultaatRetentieUren, standaard 168) bestaan zodat alleen de maker het resultaat kan terugzien, en worden daarna (of eerder, door de maker zelf) verwijderd.
Meldingen-drempelstandaard 3, instelbaarBij het bereiken van de meldingen-drempel wordt de vraag direct verwijderd.
Veiligheids-timeoutinstelbaarVragen die nooit afgerond raken, worden na deze timeout alsnog opgeruimd.
Tijdelijke (temp) opnamekortEen nog niet gepromote opname wordt na korte tijd opgeruimd.

Wat er wordt verwijderd hangt af van het gebruikerstype, met één uitzondering die altijd geldt:

GegevenGeregistreerdAnoniem
Opname (media-bestand)Altijd verwijderd — uiterlijk na het resultaat-retentievenster (of eerder door de maker)Altijd verwijderd — uiterlijk na het resultaat-retentievenster (of eerder door de maker)
Profiel/accountBlijft behoudenVerwijderd (AnoniemeGebruiker opgeruimd met de vraag)
BerichtenBlijven behoudenNiet bewaard

Altijd actief. In het prototype stond de opruiming achter een feature-flag (standaard uit). In de doelarchitectuur is automatische retentie altijd actief en niet uitschakelbaar — de privacybelofte mag niet afhangen van een omgevingsvariabele. De opruimdienst verwijdert zowel het record in de database als het fysieke media-object in de object-storage.

flowchart LR
    Capture["Live camera-opname"] --> Temp["Tijdelijke opslag (temp)"]
    Temp -->|promote| Vraag["Vraag"]
    Temp -.->|na korte tijd ongebruikt| Del1["Verwijderd"]
    Vraag -.->|beoordeling klaar| Del2["Verwijderd"]
    Vraag -.->|meldingen-drempel bereikt| Del2
    Vraag -.->|veiligheids-timeout| Del2
    Retentie["Retentiedienst (altijd actief)"] -.-> Del1
    Retentie -.-> Del2

Onderstaande tabel mapt de beveiligings- en privacymaatregelen op het concern dat ze adresseren.

ConcernMaatregelDoelarchitectuur
Anonimiteit van de makerDataminimalisatie + altijd verwijderenAnoniemeGebruiker, fictieve naam, e-mail optioneel (alleen herstel); opname altijd weg — uiterlijk na het resultaat-retentievenster
Geen locatieveldGeen Land, optionele regioGeen GeoIP, geen IP-locatie, geen GPS-prompt; per land een aparte site (NL eerst) i.p.v. een Land-veld — ADR-037
Wachtwoordbeveiliging (gebruiker)Custom anonieme authArgon2id-hash, constant-time-verificatie; herstel alleen mét optioneel e-mailadres
Wachtwoordbeveiliging (beheerder)ASP.NET Core IdentitySalted hash, MFA mogelijk, lockout/wachtwoordbeleid
SessiekapingSingle-device cookie-sessieHttpOnly/Secure/SameSite=Strict; login revoket bestaande sessie
Ongeautoriseerde toegangAutorisatiebeleidIsBeheerder vs. gebruiker/anoniem per endpoint
Spam & overbelastingIngebouwde .NET rate limitingPolicies api / opinie / melding (melden strenger)
Cross-site-aanvallenCORS-allowlist + CSP + cookie-flagsAllowlist zonder wildcard, krappe CSP, SameSite=Strict
Afluisteren van verkeerTransportbeveiligingHTTPS/HSTS
Oversized payloadsBody-limietenKleine JSON-limiet; aparte hogere multipart-limiet voor media
Hergebruik van oude/niet-eigen beeldenGeen uploadUitsluitend live camera; geen galerij-upload
Ongepaste vrije tekstProactieve tekstmoderatieIModeratieDienst (OpenAI Moderation) keurt de toelichting en de reactie vóór opslaan; fail-open zonder sleutel
Misbruik van contactgegevensMinimale, doelgebonden e-mailE-mail alleen voor twee optionele, zelfgekozen doelen: wachtwoordherstel en de uitslagnotificatie (alleen een link)
Gevoelige inhoud in e-mailUitslagnotificatie zonder inhoud, adres eenmaligIUitslagNotificaties stuurt alleen een link (geen beoordelingsinhoud); notificatieEmail wordt direct na de verzendpoging gewist; fail-graceful
Betaaldata (voorgesteld)PSP-checkout (iDeal), dataminimalisatieBank-/kaartgegevens uitsluitend bij de payment provider; platform bewaart transactiereferentie + saldo; geverifieerde, idempotente webhook; anonimiteit geldt richting andere gebruikers
Uitbetaling & KYC (voorgesteld)KYC via de PSPUitbetalen vereist een bankrekening en ken-je-klant-checks (via de PSP); expliciet onderdeel van de verificatie van het voorstel 2026-07-05
Onbedoeld bewaren van dataRetentiebeleid altijd actiefAchtergronddienst die niet uitschakelbaar is

Verschillen ten opzichte van het prototype

Section titled “Verschillen ten opzichte van het prototype”
#OnderwerpPrototype (referentie)Doelarchitectuur
1Wachtwoorden gebruikerPlain opgeslagen en plain vergelekenArgon2id-hash, constant-time-verificatie
2Wachtwoorden beheerderPlain admin-credentialsASP.NET Core Identity (salted hash, MFA mogelijk)
3Identity-providern.v.t.In-app, geen Keycloak
4RetentieAchter feature-flag (standaard uit)Altijd actief, niet uitschakelbaar
5Content-Security-PolicyUitgeschakeldExpliciete, krappe CSP
6Rate-limit op localhostMeldingen-limiter sloeg localhost overLimieten gelden overal in productie

Deze maatregelen sluiten aan op het Domeinmodel (Retentiebeleid, single-device sessies, gebruikerstypen) en de Termen. Zie verder Backend & API (inclusief de persistentiekeuzes). Modelwijzigingen verschijnen in het Wijzigingslog.