We kunnen doorgaan met passkeys zonder eigen domein, maar dan als experimentele Apps Script-passkey en niet als gegarandeerd stabiele productieoplossing.
Google Sites kan zelf geen WebAuthn-code uitvoeren. De Google Sites-knop moet daarom de Apps Script /exec-pagina als zelfstandig tabblad openen. De passkey mag niet binnen een Google Sites-embed worden geregistreerd.
De beveiligingsvolgorde wordt:
E-maillink succesvol gebruiken.
Dagelijkse PIN eenmaal correct invoeren.
Pas daarna verschijnt Passkey instellen.
Het apparaat controleert vingerafdruk, gezichtsscan of toestel-PIN.
De server verifieert het cryptografische resultaat.
Vervolgens kan de gebruiker met de passkey inloggen.
E-maillink + PIN blijven beschikbaar als herstelroute.
Er blijven twee beperkingen:
De passkey wordt gekoppeld aan de lange script.googleusercontent.com-origin uit jouw test.
Als Google deze sandbox-origin verandert, kan de passkey niet meer werken en moet de gebruiker terugvallen op e-maillink + PIN.
Daarom verwijderen we e-maillink en PIN niet.
Voor echte server-side verificatie hebben we nog steeds een WebAuthn-verificatieservice nodig. Apps Script heeft zelf niet de geschikte cryptografische functies om WebAuthn-handtekeningen verantwoord te controleren. Hiervoor kunnen we Cloud Run gebruiken op het gratis standaard run.app-adres; een eigen domein is daarvoor niet verplicht. Cloud Run is serverloos en hoeft niet continu actief te blijven.
De Google Sites-knop wordt dan bijvoorbeeld:
Open KlantKompas met passkey
en opent:
https://script.google.com/macros/s/.../exec
met target="_blank" of als zelfstandig venster.
De volgende verantwoorde stap is nu een origin-stabiliteitstest:
huidige browser;
incognitovenster;
andere browser;
mobiele telefoon;
opnieuw na een nieuwe Apps Script-deploy.
Als overal exact dezelfde origin verschijnt, kunnen we het passkeyprototype aan die origin koppelen. Als de origin wisselt, kunnen passkeys binnen Apps Script niet betrouwbaar worden gebruikt. Daarna bouwen we registratie pas vrij na een geldige e-maillink én PIN_VERIFY.
De volledige aangepaste en syntactisch gecontroleerde versie staat hier als tekstbestand.
Open het bestand en kopieer alles naar je bestaande Code.gs:
Volledige Code.gs openen en kopiëren
De versie bevat:
alle bestaande magic-linkfuncties;
sessies van 24 uur;
vertrouwde apparaten van 30 dagen;
pin: true in de beveiligingsstatus;
passkeys: false zolang het prototype nog niet is afgerond;
de nieuwe route voor PasskeyTest.html;
behoud van de dynamische bedrijfsnaam.
Je hoeft setupKlantKompasLogin() niet opnieuw uit te voeren. Sla Code.gs op en maak een nieuwe implementatie. Open daarna:
JOUW-NIEUWE-EXEC-URL?view=passkey-test
Bijvoorbeeld:
https://script.google.com/macros/s/IMPLEMENTATIE-ID/exec?view=passkey-test
De passkeytest is bewust zonder login bereikbaar, omdat hij uitsluitend browsermogelijkheden onderzoekt en geen gebruikers- of Sheetgegevens leest.
Voor Uw bedrijfsnaam ga je in de Sheet naar Instellingen en wijzig je de waarde bij:
Bedrijfsnaam
Bijvoorbeeld naar:
KlantKompas
of de echte naam van de ondernemer. Als de waarde ontbreekt, gebruikt het systeem automatisch KlantKompas; de tekst Uw bedrijfsnaam hoeft daarmee niet zichtbaar te blijven.
{ "testName": "KlantKompas Passkey Capability Test", "testedAt": "2026-08-17T14:15:38.825Z", "summary": { "webAuthnAvailable": true, "secureContext": true, "platformAuthenticator": true, "conditionalMediation": true, "embedded": true, "likelyStableOrigin": false, "usablePrototype": true, "productionReady": false }, "environment": { "origin": "https://n-hanzmhrz564fea7t2g6yssndlgfn7tmtuwo6jga-0lu-script.googleusercontent.com", "hostname": "n-hanzmhrz564fea7t2g6yssndlgfn7tmtuwo6jga-0lu-script.googleusercontent.com", "protocol": "https:", "pathname": "/userCodeAppPanel", "referrer": "https://n-hanzmhrz564fea7t2g6yssndlgfn7tmtuwo6jga-0lu-script.googleusercontent.com/userCodeAppPanel?createOAuthDialog=true", "embedded": true, "suggestedRpId": "n-hanzmhrz564fea7t2g6yssndlgfn7tmtuwo6jga-0lu-script.googleusercontent.com", "ancestorOrigins": [ "https://n-hanzmhrz564fea7t2g6yssndlgfn7tmtuwo6jga-0lu-script.googleusercontent.com", "https://script.google.com" ], "userAgent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", "platform": "Windows", "language": "nl", "cookieEnabled": true, "online": true }, "capabilities": { "conditionalCreate": true, "conditionalGet": true, "extension:appid": true, "extension:appidExclude": true, "extension:cmtgKey": false, "extension:credBlob": true, "extension:credProps": true, "extension:credentialProtectionPolicy": true, "extension:crossDeviceFallbackUrl": false, "extension:enforceCredentialProtectionPolicy": true, "extension:getCredBlob": true, "extension:hmacCreateSecret": true, "extension:largeBlob": true, "extension:minPinLength": true, "extension:payment": true, "extension:prf": true, "hybridTransport": false, "immediateGet": true, "passkeyPlatformAuthenticator": true, "relatedOrigins": true, "signalAllAcceptedCredentials": true, "signalCurrentUserDetails": true, "signalUnknownCredential": true, "userVerifyingPlatformAuthenticator": true }, "relatedOriginSupport": true, "checks": [ { "title": "WebAuthn API", "status": "pass", "description": "Deze browser ondersteunt de basisfuncties van WebAuthn." }, { "title": "Beveiligde HTTPS-context", "status": "pass", "description": "De browser beschouwt deze pagina als een beveiligde context." }, { "title": "Toestelverificatie", "status": "pass", "description": "Dit apparaat meldt ondersteuning voor een platform-authenticator, zoals vingerafdruk, gezichtsherkenning of toestel-PIN." }, { "title": "Passkey-autofill", "status": "pass", "description": "De browser ondersteunt voorwaardelijke passkey-aanmelding via autofill." }, { "title": "Uitvoeringsomgeving", "status": "warning", "description": "De test draait in een iframe. Dit kan invloed hebben op de beschikbare WebAuthn-origin en het Permissions Policy." }, { "title": "Vaste origin", "status": "warning", "description": "De weergegeven Google-hostname is mogelijk niet geschikt als definitieve verkoopbare KlantKompas-origin." } ] }
De test is geslaagd voor een prototype, maar bevestigt ook dat de huidige Apps Script-origin niet geschikt is voor de definitieve passkey-login.
Belangrijkste resultaten:
WebAuthn: beschikbaar
HTTPS: correct
Windows Hello/toestelverificatie: beschikbaar
Passkey-autofill: beschikbaar
Related origins: technisch ondersteund
Apps Script draait in een iframe: ja
Origin: dynamische script.googleusercontent.com-host
Productierijp: nee
De lange hostname:
n-hanzmhrz...script.googleusercontent.com
is niet een domein dat wij beheren en kan door een andere implementatie of uitvoeringscontext veranderen. Een passkey die we hier registreren, wordt cryptografisch aan die origin/RP gekoppeld. Dat is ongeschikt voor een kopieerbaar product voor verschillende ondernemers.
We moeten nu géén echte passkeys aanmaken op deze Apps Script-origin. De test heeft precies gedaan wat nodig was: aantonen dat browser en apparaat geschikt zijn, maar Apps Script niet de juiste definitieve origin biedt.
We kiezen een vaste HTTPS-adres, bijvoorbeeld:
https://klantkompas.gabeler.eu
of:
https://portaal.godogood.eu
Daarop plaatsen we:
de loginpagina;
passkeyregistratie;
passkey-authenticatie;
server-side verificatie;
veilige sessiecookies.
Apps Script en de bestaande Sheet kunnen daarna voorlopig blijven werken voor:
klanten;
berichten;
offertes en facturen;
bestellingen;
agenda;
kennisbank;
klachten;
automatisering en e-mail.
De eerstvolgende keuze is daarom: welk domein of subdomein wil je definitief voor KlantKompas gebruiken? Mijn voorkeur is klantkompas.gabeler.eu als centraal productdomein. Daarna kunnen ondernemers binnen hetzelfde systeem een eigen styling en organisatieomgeving krijgen.
Een gebruiker mag alleen een passkey registreren vanuit een bestaande, volledig gecontroleerde sessie:
Inloggen met e-maillink.
Dagelijkse PIN verifiëren.
Klikken op Passkey toevoegen.
Backend maakt een willekeurige challenge.
Backend levert registratieopties met:
rp.id = klantkompas
vaste gebruikers-ID;
residentKey: "required" of "preferred";
userVerification: "required";
bestaande credential-ID’s in excludeCredentials.
Browser voert navigator.credentials.create() uit.
Telefoon of computer vraagt om vingerafdruk, gezichtsscan of toestel-PIN.
Browser stuurt het resultaat naar de backend.
Backend controleert:
challenge;
origin;
RP ID;
attestation;
user verification;
credential-ID.
Alleen na een geldige controle wordt de publieke sleutel opgeslagen.
Gebruiker kiest Inloggen met passkey.
Backend maakt een nieuwe eenmalige challenge.
Browser roept navigator.credentials.get() aan.
Het apparaat controleert de gebruiker.
Backend ontvangt het ondertekende resultaat.
Backend controleert:
challenge is dezelfde en nog geldig;
challenge is niet eerder gebruikt;
origin is exact toegestaan;
RP ID-hash klopt;
userPresent is waar;
userVerified is waar;
handtekening klopt met de opgeslagen publieke sleutel;
counter is geldig.
Backend maakt een sessiecookie:
HttpOnly
Secure
SameSite=Lax of strenger
beperkte geldigheidsduur
KlantKompas opent zonder e-maillink en zonder dagelijkse PIN.
Google beschrijft precies dit model: de server maakt challenges, bewaart publieke sleutels en moet de registratie- en authenticatieresultaten zorgvuldig verifiëren. Google waarschuwt bovendien dat zelfgeschreven WebAuthn-verificatie beveiligingsfouten kan veroorzaken en adviseert een beproefde bibliotheek. Google Passkeys developer guide.
Na een geslaagde passkey-login laat de backend een korte KlantKompas-toegangssessie ontstaan. Apps Script mag niet alleen vertrouwen op een User_ID dat vanuit de browser wordt meegestuurd.
Een veilige koppeling gebruikt bijvoorbeeld:
Cloud Run maakt een kort geldig, ondertekend portaalbewijs.
Browser stuurt dat bewijs naar Apps Script.
Apps Script controleert handtekening, vervaldatum, organisatie en gebruiker.
Apps Script maakt vervolgens de bestaande accessToken-sessie.
PortalApi.gs blijft iedere gegevensaanroep controleren.
Nog beter voor de latere productieversie: Cloud Run benadert de Sheet rechtstreeks via de Google Sheets API met een serviceaccount. Dan hoeft een passkey-sessie niet tussen twee verschillende authenticatiesystemen te worden vertaald.
Na succesvolle invoering wordt het beleid:
Passkey-login: e-maillink en dagelijkse PIN overslaan.
Nieuwe passkey registreren: bestaande passkey opnieuw bevestigen.
Geen werkende passkey meer: e-maillink + PIN.
PIN vergeten: beveiligde e-mailreset en eventueel handmatige organisatiecontrole.
Gevoelige beheeractie: opnieuw een passkey bevestigen.
Passkey verwijderen: minimaal één andere passkey of geldige herstelroute verplicht.
Niet meteen PasskeyApi.gs in Apps Script. De juiste volgende stap is een klein zelfstandige prototype met:
PasskeyTest.html
controle op window.PublicKeyCredential;
controle op platform-authenticator;
registratieknop;
authenticatieknop;
logging van origin en browsermogelijkheden;
nog geen echte toegang tot klantgegevens.
Daarmee bepalen we eerst of de huidige /exec-omgeving überhaupt een bruikbare en stabiele WebAuthn-origin oplevert. Apps Script HTML Service draait immers verplicht in een iframe-sandbox, waardoor een geslaagde test in de /exec-omgeving nog niet automatisch geschikt is voor Google Sites of productie. Apps Script HTML Service-beperkingen.
Mijn technische advies is daarom: gebruik de Apps Script-versie voor de capability-test, maar bouw de echte passkeyverificatie op een eigen domein met Cloud Run, Firestore en een beproefde WebAuthn-bibliotheek.
een correct geïmplementeerde passkey kan uiteindelijk zowel de e-maillink als de dagelijkse KlantKompas-PIN vervangen voor normale aanmeldingen. De passkey moet dan wel userVerification: "required" gebruiken, zodat het apparaat daadwerkelijk een vingerafdruk, gezichtsscan of toestel-PIN verlangt. WebAuthn levert vervolgens een cryptografisch bewijs aan de server; biometrische gegevens verlaten het apparaat niet. W3C WebAuthn Level 3, Google Passkeys developer guide.
Situatie Beveiligingsroute
Eerste registratie
E-maillink + KlantKompas-PIN
Passkey registreren
Alleen vanuit een volledig geverifieerde sessie
Normaal opnieuw inloggen
Passkey; geen e-maillink en geen KlantKompas-PIN
Gevoelige actie
Passkey opnieuw laten bevestigen
Passkey kwijt/nieuw toestel
E-maillink + PIN als herstelroute
PIN vergeten
Nieuwe e-maillink en gecontroleerde PIN-reset
Alle passkeys kwijt
Beveiligd accountherstel door organisatie
De e-maillink en PIN worden dus niet meteen verwijderd. Ze veranderen in een herstel- en fallbackmechanisme. Google adviseert bestaande authenticatiemethoden tijdens de overgang te behouden, omdat niet iedere browser, gebruiker of omgeving passkeys volledig ondersteunt. Google Passkeys developer guide.
Passkey toevoegen en daarna tóch iedere dag de KlantKompas-PIN vragen. Dat maakt de gebruikservaring onnodig zwaar.
De passkey alleen in JavaScript “controleren”. De server moet een eenmalige challenge maken en de handtekening, origin, RP ID, challenge en sign counter controleren.
De e-maillink en PIN onmiddellijk verwijderen.
Een eigen cryptografische WebAuthn-verificatie improviseren in Apps Script.
Google Sites als definitieve passkey-origin gebruiken.
Google benadrukt dat server-side verificatie cruciaal is en raadt aan hiervoor een bestaande passkeybibliotheek of gespecialiseerde oplossing te gebruiken, omdat fouten kritieke beveiligingsproblemen kunnen veroorzaken. Google Passkeys developer guide.
Apps Script HTML Service draait verplicht in een iframe-sandbox. De toegestane sandboxrechten worden door Google bepaald. Daardoor hebben we niet dezelfde controle over origin, iframebeleid en HTTP-headers als op een eigen website. Apps Script HTML Service-beperkingen.
Daarom beschouwen we een passkey op de /exec-pagina eerst als prototype, niet direct als productiebeveiliging.
PIN-reset via een nieuwe e-maillink bouwen.
Scherm voor vertrouwde apparaten maken: bekijken, intrekken en “alle apparaten uitloggen”.
Tabbladen Passkeys en Passkey_Challenges laten aanmaken.
Een WebAuthn-geschiktheidstest op de zelfstandige /exec-pagina uitvoeren.
Passkeyregistratie testen zonder de bestaande login te vervangen.
Passkey-authenticatie en server-side verificatie toevoegen.
Testen op Android, iPhone/iPad, Windows en macOS.
Pas daarna passkey als primaire login activeren.
Voor de verkoopbare versie een eigen HTTPS-domein met een WebAuthn-backend en beproefde bibliotheek gebruiken.
Mijn advies: een geslaagde passkey-login geeft direct een normale KlantKompas-sessie en slaat de dagelijkse PIN over. De e-maillink plus PIN blijven voorlopig beschikbaar als streng bewaakte herstelroute. Dat is zowel gebruiksvriendelijk als verstandig.
Een passkey gebruikt WebAuthn. De vingerafdruk, gezichtsscan of toestel-PIN wordt door het apparaat gecontroleerd; biometrische gegevens worden niet naar KlantKompas of de Sheet gestuurd. KlantKompas ontvangt alleen een cryptografisch ondertekend bewijs. Passkeys vereisen een vaste beveiligde HTTPS-origin en een correct ingestelde Relying Party ID.
Google Passkeys developer guide, W3C WebAuthn-specificatie
Daar zit voor Apps Script een aandachtspunt: HTML-servicewebapps draaien verplicht in een beveiligde iframe-sandbox. Ingesloten gebruik via Google Sites voegt daar nog een extra iframe/origin-laag aan toe. Officiële Apps Script HTML Service-beperkingen
Daarom bouwen we passkeys zo:
Eerst testen op de Apps Script /exec-pagina als zelfstandige pagina.
Registratie en login testen op mobiel en desktop.
Niet vertrouwen op een Google Sites-embed voor de passkeyceremonie.
Als Apps Script de WebAuthn-origin onvoldoende stabiel ondersteunt, gebruiken we voor de verkoopbare versie een vaste eigen HTTPS-domain via bijvoorbeeld Firebase Hosting of Cloud Run.
Sheets en Apps Script kunnen dan nog steeds als gegevenslaag blijven functioneren.
De beste bouwvolgorde is nu:
Dagelijkse 6-cijferige PIN volledig werkend maken.
PIN-reset via nieuwe e-maillink.
Beheer van vertrouwde apparaten.
Passkey-prototype op de zelfstandige /exec-pagina.
Daarna besluiten of de definitieve passkey-login op Apps Script of een eigen domein komt.