Den 14 augusti 2026 loggar en genomsnittlig svensk användare in på fler tjänster än någonsin, och lösenord räcker inte längre som skydd. Microsofts egen forskning visar att flerfaktorsautentisering (MFA) sänker risken för kontokapning med 99,22 procent över hela populationen, och med 98,56 procent även när lösenordet redan har läckt. Google rapporterar liknande siffror: ett andra steg blockerar 100 procent av automatiserade botattacker, 96 procent av masspejsning och 76 procent av riktade attacker. Ändå saknar de flesta hemmabyggda inloggningssystem fortfarande stöd för tidsbaserade engångskoder, TOTP, trots att standarden funnits sedan 2011.
Den här guiden går igenom hur du bygger tvåfaktorsautentisering med TOTP i Node.js från grunden: hemlighetsgenerering, QR-koder, verifiering, återställningskoder och produktionshärdning. Vi använder Node.js 24 LTS, biblioteket otplib och qrcode, och landar i ett komplett, körbart Express-projekt. Läs gärna vår genomgång av säkerhet online för bredare kontext innan du börjar.
Guiden riktar sig till dig som redan har en fungerande inloggning i en Node.js-tjänst och vill lägga till ett andra autentiseringssteg utan att luta dig mot en tredjepartsleverantör. Samma mönster fungerar oavsett om backenden ligger bakom en React-frontend, en mobilapp eller en ren API-klient, eftersom hela flödet bygger på REST-endpoints som returnerar JSON. Tolv steg tar dig från tomt projekt till ett flöde du kan släppa i produktion, med felsökning och produktionshärdning inräknat.
Varför tvåfaktorsautentisering stoppar nästan alla kontokapningar
Credential stuffing, alltså att testa läckta användarnamn och lösenord mot nya tjänster i stor skala, är fortfarande den vanligaste inloggningsattacken 2026. Enligt Verizons 2025 Data Breach Investigations Report utgjorde credential stuffing ett medianvärde på 19 procent av alla autentiseringsförsök i loggarna hos SSO-leverantörer, vilket i praktiken betyder att ungefär vart femte inloggningsförsök på en typisk tjänst kommer från en angripare med stulna uppgifter. Akamais mätningar, citerade i flera 2026-rapporter, pekar på omkring 26 miljarder credential stuffing-försök per månad globalt, en ökning med nästan 50 procent på 18 månader. Över 24 miljarder stulna inloggningspar cirkulerar i dag i underjordiska databaser.
Det är här TOTP gör skillnad. Även om en angripare har rätt lösenord stoppas de av att de saknar den roterande sexsiffriga koden som bara finns i användarens autentiseringsapp. Framgångsgraden per inloggningsförsök i en credential stuffing-attack ligger enligt Imperva-data på ungefär 0,1 till 2 procent, men vid miljontals försök räcker det för att kompromettera tiotusentals konton om inget andra steg finns. GitLab fick själva uppleva baksidan av bristfällig 2FA-implementation i januari 2026, då sårbarheten CVE-2026-0723 gjorde det möjligt att kringgå tvåfaktorsautentisering genom förfalskade enhetssvar och därmed ta över konton på både Community- och Enterprise-utgåvorna.
Notera att TOTP inte är osårbart. Så kallade adversary-in-the-middle-kit (AiTM), som agerar som en reverse proxy mellan offret och den riktiga inloggningssidan, kan i realtid vidarebefordra både lösenord och TOTP-koder och därmed stjäla sessionen trots att användaren gjort allt rätt. I juni 2026 lyckades angripare kopplade till Dashlane-incidenten dessutom brute-forcea ett svagt implementerat 2FA-flöde och stjäla krypterade lösenordsvalv från 20 konton. Den här guiden bygger därför inte bara TOTP-verifiering, utan även rate limiting, repriskydd och en tydlig migreringsväg mot phishing-resistenta metoder som beskrivs längre ner.
För svenska och nordiska verksamheter finns det även en regulatorisk sida av saken. NIS2-direktivet, som beskrivs i vår genomgång av cybersäkerhetslagen, ställer krav på riskhantering och åtkomstkontroll för tusentals svenska företag inom kritisk infrastruktur och digital infrastruktur. Stark autentisering, inklusive flerfaktorslösningar, nämns explicit som en av de tekniska åtgärder tillsynsmyndigheter förväntar sig att se på plats. Att bygga in TOTP tidigt i en tjänsts livscykel är därför inte bara ett säkerhetsval, utan i många fall också ett efterlevnadskrav.
Så fungerar TOTP: RFC 6238 i korthet
TOTP, Time-Based One-Time Password, definieras i RFC 6238 och är en vidareutveckling av HOTP (RFC 4226), den händelsebaserade föregångaren. Skillnaden är att TOTP byter ut HOTP:s inkrementerande räknare mot ett värde som härleds från den aktuella tiden. Båda parter, servern och autentiseringsappen, delar en hemlig nyckel och räknar självständigt fram samma engångskod utifrån samma tidsintervall, utan att någon kod behöver skickas över nätverket i förväg.
De fyra byggstenarna i en TOTP-kod
En TOTP-implementation vilar på fyra parametrar: en delad hemlighet (secret), ett tidsfönster (vanligtvis 30 sekunder), en HMAC-hashfunktion och antalet siffror i koden. RFC 6238 tillåter HMAC-SHA-1, HMAC-SHA-256 och HMAC-SHA-512, men de flesta autentiseringsappar, inklusive Google Authenticator och Microsoft Authenticator, förväntar sig fortfarande SHA-1 som standard av kompatibilitetsskäl. Det är därför de flesta produktionsbibliotek, inklusive otplib, defaultar till SHA-1 trots att SHA-256 är kryptografiskt starkare.
| Parameter | Standardvärde | Definieras i |
|---|---|---|
| Tidsfönster (step) | 30 sekunder | RFC 6238, avsnitt 4 |
| Antal siffror | 6 siffror | RFC 4226, avsnitt 5.3 |
| Hash-algoritm | HMAC-SHA-1 (SHA-256/512 tillåtna) | RFC 6238, avsnitt 1.2 |
| Toleransfönster | ±1 steg (rekommenderas) | RFC 6238, avsnitt 6 |
| Hemlighetens längd | Minst 128 bitar (20 byte rekommenderas) | RFC 4226, avsnitt 4 |
NIST:s digitala identitetsriktlinjer, SP 800-63B-4, klassificerar TOTP som en giltig metod för AAL2 (Authenticator Assurance Level 2), som kräver bevis på kontroll över två distinkta autentiseringsfaktorer. AAL3, den högsta nivån, kräver däremot en hårdvarubaserad kryptografisk autentiserare med skydd mot verifierarimitation, vilket TOTP i sig inte uppfyller. Det förklarar varför myndigheter och banker i allt högre grad rör sig mot FIDO2-nycklar för sina känsligaste flöden, ett ämne vi går igenom i jämförelsen mellan YubiKey och Google Authenticator.
En sak som ofta förvirrar utvecklare är varför vissa QR-koder inte fungerar i alla appar. Orsaken är oftast att otpauth-URI:n saknar parametrar som appen förväntar sig, till exempel issuer eller algorithm, eller att en app hårdkodar antagandet om 6 siffror och SHA-1 trots att URI:n anger något annat. Håll dig till standardparametrarna om du vill maximera kompatibiliteten med så många autentiseringsappar som möjligt, och avvik bara från SHA-1 och 30-sekundersfönstret om du har full kontroll över klientappen också.
Förutsättningar och versioner
Innan du börjar, se till att du har rätt versioner installerade. Node.js-projektet uppdaterade sin releasekalender under 2026, så det är lätt att blanda ihop vilken linje som faktiskt är long-term support just nu.
| Komponent | Version i denna guide | Kommentar |
|---|---|---|
| Node.js | 24 LTS (v24.19.0, släppt 3 augusti 2026) | Aktiv LTS till oktober 2026, säkerhetsstöd till 30 april 2028 |
| npm | Medföljer Node.js 24 | Ingen separat installation krävs |
| express | Senaste 4.x | Webbramverk för routing och middleware |
| otplib | Senaste version från npm | Genererar och verifierar TOTP/HOTP enligt RFC 6238 |
| qrcode | Senaste version från npm | Renderar otpauth://-URI:er som QR-koder |
| express-rate-limit | Senaste version från npm | Begränsar antalet verifieringsförsök |
| Autentiseringsapp | Google Authenticator, Microsoft Authenticator, Authy eller Bitwarden | Används för att skanna QR-koden och testa flödet |
Du behöver även en kodredigerare, ett terminalfönster och grundläggande kunskap i JavaScript och Express. Guiden förutsätter att du redan har en fungerande inloggning med hashade lösenord. Om du inte har det på plats än, bygg först indatavalidering enligt vår genomgång av indatavalidering i Node.js och lägg på säkerhetshuvuden med Helmet.js innan du exponerar inloggningsendpoints publikt. Har du redan ett lösenordshanteringsflöde på plats men är osäker på om hashningen håller måttet, se vår genomgång av lösenordssäkerhet innan du lägger till 2FA ovanpå ett svagt fundament.
Steg 1: Skapa projektet och installera beroenden
Skapa en ny katalog och initiera ett Node.js-projekt. Installera sedan Express för routing, otplib för TOTP-logiken, qrcode för att rendera QR-koder och express-rate-limit för att begränsa brute force-försök mot verifieringsendpointen. Vi lägger även till dotenv för att hålla hemligheter som JWT-nycklar och krypteringsnycklar utanför källkoden, samt nodemon som ett utvecklingsverktyg för att slippa starta om servern manuellt vid varje ändring.
mkdir totp-2fa-node
cd totp-2fa-node
npm init -y
npm install express otplib qrcode express-rate-limit dotenv
npm install --save-dev nodemon
node --version
# ska visa v24.19.0 eller senare i 24.x-linjen
Lägg till "type": "module" i din package.json om du vill använda ES-moduler, eller behåll CommonJS med require() som i exemplen nedan. Skapa också en .env-fil för hemligheter som aldrig ska checkas in i git, och en .gitignore som exkluderar den.
Steg 2: Bygg en minimal Express-server och användarlagring
För att hålla fokus på 2FA-logiken använder vi en enkel in-memory-lagring i exemplen. I produktion ersätter du den med din riktiga databas, till exempel PostgreSQL eller MongoDB, men fälten som lagras är desamma.
// server.js
const express = require('express');
const rateLimit = require('express-rate-limit');
require('dotenv').config();
const app = express();
app.use(express.json());
// Enbart för demoändamål – ersätt med din databas i produktion
const users = new Map();
users.set('[email protected]', {
email: '[email protected]',
passwordHash: '$argon2id$...', // hashat lösenord från din befintliga inloggning
totpSecret: null,
totpEnabled: false,
recoveryCodes: [],
});
app.listen(3000, () => {
console.log('Servern kör på http://localhost:3000');
});
module.exports = { app, users };
Fältet totpSecret ska vara krypterat i vila om du lagrar det i en riktig databas, eftersom vem som helst med läsåtkomst annars kan generera giltiga koder åt vilken användare som helst.
Steg 3: Generera en TOTP-hemlighet per användare
När en användare vill aktivera 2FA genererar servern en unik hemlighet med otplib. Hemligheten kodas i Base32, vilket är vad autentiseringsappar förväntar sig.
// routes/enable2fa.js
const { authenticator } = require('otplib');
function generateTotpSecret(userEmail) {
const secret = authenticator.generateSecret(); // Base32-kodad hemlighet
const otpauthUrl = authenticator.keyuri(
userEmail,
'ShatteredDemo',
secret
);
return { secret, otpauthUrl };
}
module.exports = { generateTotpSecret };
Spara aldrig hemligheten som aktiverad direkt. Den ska markeras som “väntande” tills användaren har bevisat att appen faktiskt fungerar genom att skicka in en giltig kod, vilket vi bygger i steg 5. otplib använder Node.js inbyggda crypto.randomBytes() under huven för att generera hemligheten, vilket ger kryptografiskt säker slumpmässighet. Undvik att själv skriva en egen slumpgenerator eller använda Math.random(), som inte är kryptografiskt säker och därför kan göra hemligheten förutsägbar för en angripare som analyserar tillräckligt många genererade värden.
Steg 4: Skapa otpauth://-URI och rendera QR-koden
Autentiseringsappar läser inte hemligheten direkt, de skannar en QR-kod som kodar en otpauth://-URI. qrcode-biblioteket gör om URI:n till en base64-bild som du kan rendera direkt i webbläsaren utan filhantering.
// routes/enable2fa.js (forts.)
const QRCode = require('qrcode');
const { generateTotpSecret } = require('./totpUtils');
app.post('/api/2fa/setup', async (req, res) => {
const user = users.get(req.user.email); // från din auth-middleware
const { secret, otpauthUrl } = generateTotpSecret(user.email);
user.totpSecret = secret; // markera som "väntande" tills verifiering sker
user.totpEnabled = false;
const qrDataUrl = await QRCode.toDataURL(otpauthUrl);
res.json({ qrDataUrl, manualEntryKey: secret });
});
Skicka alltid med en manuell inmatningsnyckel som fallback. Vissa användare kan inte skanna QR-koder på grund av skärmläsare eller enhetsbegränsningar, och en ren textnyckel gör flödet tillgängligt för fler.
Steg 5: Verifiera den första koden för att aktivera 2FA
Innan 2FA aktiveras måste användaren bevisa att appen är korrekt konfigurerad genom att skicka in en giltig kod. otplib:s verify()-funktion tar automatiskt hänsyn till klockdrift genom ett toleransfönster.
const { authenticator } = require('otplib');
app.post('/api/2fa/verify-setup', (req, res) => {
const { token } = req.body;
const user = users.get(req.user.email);
if (!user.totpSecret) {
return res.status(400).json({ error: 'Ingen väntande 2FA-inställning' });
}
const isValid = authenticator.verify({
token,
secret: user.totpSecret,
});
if (!isValid) {
return res.status(401).json({ error: 'Ogiltig kod, försök igen' });
}
user.totpEnabled = true;
res.json({ success: true, message: '2FA är nu aktiverat' });
});
Med standardinställningarna accepterar otplib koder från ett steg före och ett steg efter servertiden, vilket ger en marginal på cirka 30 sekunder åt vardera hållet utan att öppna dörren för uppspelningsattacker på äldre koder. Om du vill vara striktare kan du sätta toleransfönstret till noll, men i praktiken leder det till fler supportärenden från användare vars telefonklocka ligger några sekunder fel, utan att nämnvärt höja säkerheten jämfört med standardinställningen.
Steg 6: Lägg till 2FA i inloggningsflödet
Inloggningen blir nu tvåstegs: först verifieras lösenordet, och om användaren har 2FA aktiverat returneras en tillfällig session som väntar på TOTP-koden innan en fullständig inloggningssession utfärdas.
const argon2 = require('argon2');
const jwt = require('jsonwebtoken');
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
const user = users.get(email);
if (!user || !(await argon2.verify(user.passwordHash, password))) {
return res.status(401).json({ error: 'Fel e-post eller lösenord' });
}
if (user.totpEnabled) {
const pendingToken = jwt.sign(
{ email, stage: 'pending-2fa' },
process.env.JWT_SECRET,
{ expiresIn: '5m' }
);
return res.json({ requiresTotp: true, pendingToken });
}
const sessionToken = jwt.sign({ email, stage: 'authenticated' }, process.env.JWT_SECRET, { expiresIn: '2h' });
res.json({ sessionToken });
});
Den korta livslängden på pendingToken, fem minuter i exemplet, begränsar hur länge ett stulet lösenord är användbart utan tillhörande TOTP-kod. Kombinera gärna detta flöde med OAuth 2.0 med PKCE om du bygger inloggning mot en tredjepartsklient snarare än en egen frontend.
Steg 7: Generera och lagra engångskoder för återställning
Användare tappar bort telefoner. Utan återställningskoder blir varje förlorad enhet ett supportärende där ni manuellt måste verifiera identitet, vilket i sig är en attackyta. Generera tio engångskoder vid aktivering, visa dem en gång och hasha dem innan lagring, precis som lösenord.
const crypto = require('crypto');
const argon2 = require('argon2');
async function generateRecoveryCodes(count = 10) {
const plainCodes = Array.from({ length: count }, () =>
crypto.randomBytes(5).toString('hex')
);
const hashedCodes = await Promise.all(
plainCodes.map((code) => argon2.hash(code))
);
return { plainCodes, hashedCodes }; // visa plainCodes en gång, spara hashedCodes
}
module.exports = { generateRecoveryCodes };
Varje kod ska markeras som förbrukad direkt efter användning, aldrig återanvändbar. Uppmana användaren att skriva ut eller spara koderna offline, inte som en skärmdump i molnet där de kan läcka tillsammans med resten av kontot.
Steg 8: Lägg till rate limiting och repriskydd
Utan begränsningar kan en angripare som redan har lösenordet försöka gissa sig fram till en giltig sexsiffrig kod, en rymd på bara en miljon kombinationer. express-rate-limit stoppar brute force på HTTP-nivå, men du behöver även spärra redan använda koder så att en avlyssnad kod inte går att återanvända inom sitt 30-sekundersfönster.
const rateLimit = require('express-rate-limit');
const totpLimiter = rateLimit({
windowMs: 5 * 60 * 1000,
max: 5,
message: { error: 'För många försök, vänta 5 minuter' },
});
const usedCodes = new Set(); // ersätt med Redis i produktion, med TTL 60s
app.post('/api/2fa/login-verify', totpLimiter, (req, res) => {
const { token, pendingToken } = req.body;
const { email } = jwt.verify(pendingToken, process.env.JWT_SECRET);
const user = users.get(email);
const codeKey = `${email}:${token}`;
if (usedCodes.has(codeKey)) {
return res.status(401).json({ error: 'Koden har redan använts' });
}
const isValid = authenticator.verify({ token, secret: user.totpSecret });
if (!isValid) {
return res.status(401).json({ error: 'Ogiltig kod' });
}
usedCodes.add(codeKey);
setTimeout(() => usedCodes.delete(codeKey), 60_000);
const sessionToken = jwt.sign({ email, stage: 'authenticated' }, process.env.JWT_SECRET, { expiresIn: '2h' });
res.json({ sessionToken });
});
Fem försök per fem minuter är en rimlig startpunkt, men justera efter din riskbild. Logga alltid misslyckade 2FA-försök separat från misslyckade lösenordsförsök, det är en av de tydligaste signalerna på ett riktat kontokapningsförsök. Om du ser upprepade misslyckade TOTP-försök mot samma konto från flera olika IP-adresser inom kort tid, är det ett starkare varningstecken än enstaka misslyckade försök från en och samma plats, och bör trigga en separat larmregel i din övervakning snarare än att bara räknas mot samma rate limit.
Steg 9: Låt användare inaktivera eller återställa 2FA
Att inaktivera 2FA är ett känsligt moment och ska alltid kräva färsk autentisering, både lösenord och en giltig TOTP-kod eller återställningskod, aldrig bara en sessionscookie.
app.post('/api/2fa/disable', async (req, res) => {
const { password, token } = req.body;
const user = users.get(req.user.email);
const passwordOk = await argon2.verify(user.passwordHash, password);
const totpOk = authenticator.verify({ token, secret: user.totpSecret });
if (!passwordOk || !totpOk) {
return res.status(401).json({ error: 'Verifiering misslyckades' });
}
user.totpSecret = null;
user.totpEnabled = false;
user.recoveryCodes = [];
// Skicka alltid ett e-postmeddelande vid 2FA-avstängning
res.json({ success: true });
});
Skicka ett notifieringsmejl varje gång 2FA stängs av eller ändras, oavsett om ändringen var legitim. Det är billigt att implementera och ger användaren en chans att larma om kontot har kapats. Överväg även att lägga in en kort fördröjning, till exempel 24 timmar, innan en avstängning av 2FA träder i kraft för konton med förhöjd risk, så att ett meddelande om ändringen hinner nå användaren innan angriparen kan utnyttja det öppna fönstret.
Steg 10: Testa hela flödet med en riktig autentiseringsapp
Starta servern, anropa /api/2fa/setup, och rendera qrDataUrl i en enkel HTML-sida eller spara den som en PNG lokalt för test. Skanna koden med Google Authenticator, Microsoft Authenticator, Authy eller Bitwarden och skicka in koden till /api/2fa/verify-setup.
npm run dev
# I ett andra terminalfönster:
curl -X POST http://localhost:3000/api/2fa/setup \
-H "Content-Type: application/json" \
-H "Authorization: Bearer DIN_TESTSESSION"
# Klistra in QR-datan i en webbläsare eller använd manualEntryKey
# i din autentiseringsapp, skicka sedan in koden:
curl -X POST http://localhost:3000/api/2fa/verify-setup \
-H "Content-Type: application/json" \
-d '{"token":"123456"}'
Ett lyckat svar ser ut ungefär så här: {"success":true,"message":"2FA är nu aktiverat"}. Testa även det motsatta scenariot, en felaktig eller föråldrad kod, för att bekräfta att felmeddelandet inte läcker information om varför verifieringen misslyckades.
Steg 11: Härda projektet för produktion
Innan du släpper 2FA-flödet live finns det ett antal produktionskrav som ofta glöms bort i tutorials. TOTP-hemligheten ska krypteras i databasen med en nyckel som lagras separat, till exempel i AWS KMS, HashiCorp Vault eller Azure Key Vault, aldrig i samma tabell som hemligheten själv. All trafik måste gå över HTTPS, annars skickas både lösenord och TOTP-koder i klartext över nätverket.
const crypto = require('crypto');
function encryptSecret(secret, encryptionKey) {
const iv = crypto.randomBytes(12);
const cipher = crypto.createCipheriv('aes-256-gcm', encryptionKey, iv);
const encrypted = Buffer.concat([cipher.update(secret, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
return Buffer.concat([iv, authTag, encrypted]).toString('base64');
}
Se också till att audit-logga aktivering, inaktivering och alla misslyckade verifieringsförsök av 2FA i ett separat, skrivskyddat loggflöde. Vid en säkerhetsincident är den loggen ofta det första en incidentresponsteam efterfrågar.
Planera även för nyckelrotation. Krypteringsnyckeln som skyddar TOTP-hemligheterna i databasen bör kunna bytas ut utan driftstopp, till exempel genom att stödja flera nyckelversioner samtidigt under en övergångsperiod. De flesta nyckelvalv, inklusive AWS KMS och HashiCorp Vault, stödjer detta mönster direkt genom versionerade nycklar, vilket gör att en misstänkt läcka kan åtgärdas genom att rotera nyckeln utan att tvinga alla användare att sätta upp 2FA på nytt.
Steg 12: Migrera befintliga användare stegvis
Att tvinga alla användare till 2FA över en natt skapar supportkaos. Rulla i stället ut i faser: gör 2FA obligatoriskt för admin- och betalkonton först, erbjud det som frivilligt för resten med en tydlig påminnelse, och sätt ett slutdatum när det blir obligatoriskt för alla. Mät aktiveringsgraden varje vecka, den siffran är din tydligaste indikator på hur väl utrullningen går.
function require2faForRole(role) {
const enforcedRoles = ['admin', 'billing', 'support'];
return enforcedRoles.includes(role);
}
app.use((req, res, next) => {
if (require2faForRole(req.user?.role) && !req.user?.totpEnabled) {
return res.status(403).json({ error: 'Aktivera 2FA för att fortsätta', redirect: '/settings/2fa' });
}
next();
});
Kommunicera alltid varför kravet införs. Användare som förstår hotbilden, till exempel att credential stuffing drabbar en av fem inloggningsförsök enligt Verizons data, är mer benägna att acceptera det extra steget utan att klaga i supportköerna. Förbered supportteamet på en tillfällig ökning av inkommande ärenden under utrullningens första vecka, främst kring hur QR-koder skannas och vad man gör om man byter telefon, och sätt gärna upp en enkel FAQ-sida i produkten innan kravet aktiveras för en ny grupp användare.
Vanliga fallgropar vid TOTP-implementation
De flesta buggar i egna TOTP-implementationer handlar inte om kryptografin i sig, den sköter otplib åt dig, utan om hur hemligheter hanteras runt omkring och hur flödet beter sig i gränsfall. Här är de misstag som återkommer oftast i kodgranskningar av 2FA-flöden.
- Fel tidszon på servern. TOTP bygger på Unix-tid (UTC), inte lokal tid. En server med fel systemklocka genererar koder som aldrig matchar klientens app.
- För brett toleransfönster. Att acceptera fler än ett steg före och efter ökar risken för att gamla, avlyssnade koder fortfarande fungerar.
- Hemligheten lagras i klartext. Om databasen läcker och hemligheterna inte är krypterade kan angriparen generera giltiga koder för varje konto direkt.
- Ingen begränsning på verifieringsförsök. Utan rate limiting går en sexsiffrig kod, en miljon kombinationer, att brute-forcea inom rimlig tid mot en oskyddad endpoint.
- Återställningskoder saknas eller återanvänds. Utan engångskoder blir varje borttappad telefon ett manuellt supportärende, och en kod som kan användas flera gånger är i praktiken ett permanent bakdörrslösenord.
- QR-koden loggas eller cachas. Om QR-bilden fastnar i en CDN-cache, en loggfil eller ett skärmdumpsverktyg exponeras hemligheten för vem som helst som kan läsa den filen.
- Samma hemlighet återanvänds vid nyaktivering. Om en användare stänger av och sätter på 2FA igen utan att en ny hemlighet genereras kan en tidigare läckt hemlighet fortsätta fungera. Generera alltid en helt ny
secretvid varje aktivering. - Felmeddelanden avslöjar för mycket. Att skilja på “fel lösenord” och “fel TOTP-kod” i en och samma inloggningssekvens ger en angripare gratis information om vilket steg som redan är forcerat. Använd generiska felmeddelanden i det publika API:et.
Felsökning: vanliga TOTP-problem i Node.js
När något går fel i ett TOTP-flöde är felsökningen sällan intuitiv, eftersom symptomet (“koden fungerar inte”) kan ha flera helt olika orsaker beroende på om problemet ligger hos servern, klienten eller själva implementationen. Tabellen nedan samlar de vanligaste felen vi stöter på i produktionsmiljöer, med konkreta åtgärder för varje.
| Symptom | Trolig orsak | Åtgärd |
|---|---|---|
| Koden är alltid “ogiltig”, även direkt efter QR-skanning | Serverns systemklocka går fel | Synka servertiden med NTP, kontrollera med date -u |
| Koden fungerar första gången men aldrig igen | Hemligheten skrivs om vid varje request | Kontrollera att generateSecret() bara anropas en gång per aktivering |
| QR-koden renderas som en trasig bild | otpauth-URI:n är felaktigt kodad | Logga URI:n innan den skickas till QRCode.toDataURL() och validera formatet |
| Koder fungerar i utvecklingsmiljö men inte i produktion | Olika serverklockor mellan miljöer eller containrar utan NTP | Aktivera automatisk klocksynk i containerorkestreringen |
| Vissa användare klagar på att koden alltid ligger 30 sekunder fel | Klientens telefonklocka är fel, inte servern | Öka toleransfönstret till exakt ett steg och uppmana användaren att synka telefonens klocka |
| Rate limiting triggas för snabbt vid legitima försök | För lågt max-värde i express-rate-limit | Höj gränsen försiktigt och separera limiter per användare, inte bara per IP |
| Återställningskoder verifieras aldrig som giltiga | Koderna hashas med fel algoritm vid jämförelse | Använd samma hash-funktion (argon2.verify) vid kontroll som vid generering |
Applikationen kraschar vid authenticator.verify() | Hemligheten är null eller odefinierad, till exempel vid avstängt 2FA | Kontrollera user.totpEnabled innan verifieringsanropet körs |
Som en generell tumregel: isolera alltid felet till ett av tre lager innan du börjar ändra kod. Kontrollera först servertiden med date -u, sedan hemligheten genom att logga (aldrig i produktion) vilken sträng som faktiskt skickas till authenticator.verify(), och sist klientens klocka genom att be användaren aktivera automatisk tidssynkronisering i telefonens inställningar. De flesta TOTP-buggar löser sig i något av de tre stegen.
Skriv automatiserade tester för TOTP-flödet
Ett TOTP-flöde som bara testas manuellt går sönder tyst, ofta först när en supportkö fylls med användare som inte kan logga in. Eftersom otplib exponerar samma generateSecret– och verify-funktioner som produktionskoden använder, kan du generera giltiga koder direkt i testerna utan att behöva en riktig telefon eller mocka bort kryptografin.
// __tests__/totp.test.js
const { authenticator } = require('otplib');
const { generateTotpSecret } = require('../utils/totpUtils');
describe('TOTP-flöde', () => {
test('en nygenererad kod ska vara giltig direkt', () => {
const { secret } = generateTotpSecret('[email protected]');
const token = authenticator.generate(secret);
expect(authenticator.verify({ token, secret })).toBe(true);
});
test('en felaktig kod ska avvisas', () => {
const { secret } = generateTotpSecret('[email protected]');
expect(authenticator.verify({ token: '000000', secret })).toBe(false);
});
test('en kod ska inte gälla för fel hemlighet', () => {
const first = generateTotpSecret('[email protected]');
const second = generateTotpSecret('[email protected]');
const token = authenticator.generate(first.secret);
expect(authenticator.verify({ token, secret: second.secret })).toBe(false);
});
});
Lägg testerna i din CI-pipeline så att en regression i verifieringslogiken, till exempel en oavsiktlig ändring av toleransfönstret, fångas innan koden når produktion. Testa gärna även gränsfallet där en kod skickas in exakt vid ett tidsstegs slut, eftersom det är där avrundningsfel i egna implementationer brukar synas, även om otplib hanterar det korrekt internt.
Avancerade tips för produktion
När grundflödet fungerar finns det flera lager du kan lägga till för att höja säkerheten ytterligare. Number matching, där användaren måste ange en siffra som visas på inloggningsskärmen i sin push-notis, gör det svårare för angripare att lura fram ett godkännande genom notis-spam. Device fingerprinting kan flagga inloggningar från okända enheter för extra granskning även efter godkänd 2FA.
Överväg också steg-upp-autentisering (step-up authentication) för känsliga åtgärder inuti applikationen, inte bara vid inloggning. Att ändra e-postadress, exportera personuppgifter eller genomföra en stor betalning är exempel på åtgärder där det är rimligt att kräva en färsk TOTP-kod även om användaren redan är inloggad, precis som banker gör vid Bank-ID-signering av överföringar. Rotera dessutom rekommendationen om återställningskoder årligen, och skicka en påminnelse till användare som inte har loggat in med 2FA på länge, det kan vara ett tecken på att de bytt telefon utan att flytta över sin autentiseringsapp.
Skydd mot AiTM-phishing
Eftersom TOTP kan vidarebefordras i realtid av en reverse proxy är den enda fullständiga lösningen att gå mot FIDO2/WebAuthn, där själva kryptografiska beviset är bundet till domänen och därför inte fungerar om det fångas upp av en falsk inloggningssida. Många team behåller TOTP som fallback men rullar samtidigt ut passkeys för sina mest känsliga konton, en väg som beskrivs närmare i vår jämförelse mellan hårdvarunycklar och autentiseringsappar.
Övervaka även för avvikande inloggningsmönster, som en lyckad TOTP-verifiering direkt följd av en IP-adress i ett annat land. Det är en klassisk signatur för AiTM-attacker, där lösenord och TOTP-kod stjäls samtidigt via en förfalskad inloggningssida.
SMS vs app-baserad TOTP vs hårdvarunycklar: vilken ska du välja?
Alla former av tvåfaktorsautentisering är inte lika starka. NIST:s riktlinjer avråder numera uttryckligen från SMS som primär andra faktor på grund av SIM-swap-risken, medan TOTP klassas som AAL2 och FIDO2-hårdvarunycklar som AAL3, den nivå som krävs för fullt phishing-motstånd. Valet handlar sällan om att välja en enda metod för hela användarbasen, utan om att matcha metoden mot vad kontot faktiskt skyddar.
| Metod | NIST-nivå | Phishing-motstånd | Kostnad | Lämplig för |
|---|---|---|---|---|
| SMS-kod | AAL1 (avrådd som ensam faktor) | Ingen (SIM-swap-sårbar) | Låg, kräver telefonnummer | Endast som sista utväg |
| App-baserad TOTP | AAL2 | Delvis (sårbar för AiTM-relay) | Gratis | De flesta konsumenttjänster |
| Push-notis med number matching | AAL2 | Bättre än ren push, fortfarande relaybar | Gratis | Företagstjänster med MDM |
| FIDO2/hårdvarunyckel | AAL3 | Fullständigt (domänbundet) | 200–800 kr per nyckel | Admin-, betal- och myndighetskonton |
För de flesta konsumentprodukter är app-baserad TOTP fortfarande rätt avvägning mellan säkerhet, kostnad och friktion. Spara hårdvarunycklar för konton där konsekvenserna av en kapning är allvarligast, som administratörskonton, betalningsflöden och system som hanterar personuppgifter enligt GDPR. Om din tjänst behandlar känsliga personuppgifter, till exempel hälsodata eller finansiell information, är det värt att räkna på kostnaden för hårdvarunycklar till nyckelroller redan i budgeten. Priset per nyckel är marginellt jämfört med kostnaden för ett dataintrång som drabbar personuppgifter enligt GDPR, där böterna kan uppgå till fyra procent av den globala omsättningen.
Komplett projektstruktur
Så här ser slutresultatet ut när alla steg är sammansatta i ett körbart projekt. Varje fil har ett tydligt ansvarsområde: routes hanterar HTTP-lager och endpoints, utils innehåller den återanvändbara TOTP- och krypteringslogiken utan beroenden till Express, och middleware sköter rate limiting och autentiseringskontroller som återanvänds över flera routes. Den uppdelningen gör det enkelt att skriva enhetstester mot utils-mappen isolerat, utan att behöva starta hela servern för varje testfall.
totp-2fa-node/
├── .env
├── .gitignore
├── package.json
├── server.js
├── routes/
│ ├── enable2fa.js
│ ├── login.js
│ ├── disable2fa.js
│ └── recovery.js
├── utils/
│ ├── totpUtils.js
│ ├── recoveryCodes.js
│ └── encryption.js
└── middleware/
├── rateLimiter.js
└── requireAuth.js
Med den strukturen på plats har du ett komplett 2FA-flöde: aktivering med QR-kod, verifiering vid inloggning, återställningskoder, rate limiting, repriskydd och en tydlig väg mot produktionshärdning.
Vanliga frågor
Måste jag använda otplib, eller finns det alternativ?
Nej. speakeasy och totp.js är etablerade alternativ, men otplib underhålls aktivt 2026 och stödjer både HOTP och TOTP med samma API, vilket gör det till ett tryggt förstahandsval för nya projekt. Undvik bibliotek som inte har sett en release på flera år, eftersom TOTP-relaterade CVE:er, som GitLabs bypass i januari 2026, ofta handlar om implementationsdetaljer snarare än själva algoritmen.
Kan jag använda TOTP tillsammans med lösenordslös inloggning?
Ja, men då fungerar TOTP snarare som en av flera faktorer i en bredare autentiseringskedja, till exempel i kombination med magic links eller WebAuthn, inte som ett självständigt andra steg efter ett lösenord.
Hur många återställningskoder bör jag generera?
Tio koder är branschstandard och ger användaren gott om marginal. Uppmana till att generera nya koder så snart de närmar sig slut, och ogiltigförklara alltid gamla koder när nya genereras. Visa dessutom en tydlig räknare i kontoinställningarna över hur många koder som återstår, så att användaren inte upptäcker att de tagit slut mitt i en inloggning utan tillgång till sin telefon.
Behöver jag kryptera TOTP-hemligheten om databasen redan är krypterad i vila?
Ja. Databaskryptering i vila skyddar mot stulna diskar, men inte mot en SQL-injektion eller en läckt databasanslutning. Applikationsnivå-kryptering med en separat nyckel är ett andra skyddslager som håller även om databasen exponeras direkt.
Är SMS-baserad 2FA bättre än ingen 2FA alls?
Ja, klart bättre än inget skydd, men NIST avråder från SMS som primär faktor på grund av SIM-swap-risken. Använd SMS som fallback, inte som förstahandsval, och migrera användare mot app-baserad TOTP eller hårdvarunycklar när möjligt.
Kan angripare fortfarande komma runt TOTP?
Ja, genom AiTM-phishing-kit som vidarebefordrar koden i realtid, eller genom att brute-forcea ett svagt skyddat verifieringsflöde som saknar rate limiting. Ingen enskild kontroll är ofelbar, vilket är varför lager som rate limiting, repriskydd och avvikelsedetektering behövs utöver själva TOTP-koden. Se TOTP som en kraftig höjning av kostnaden för en angripare, inte som ett ogenomträngligt skydd.
Ska jag tvinga alla användare att aktivera 2FA direkt?
Nej, en stegvis utrullning fungerar bättre i praktiken. Gör det obligatoriskt för admin- och betalkonton omedelbart, erbjud det som frivilligt för övriga med tydlig kommunikation, och sätt ett bestämt slutdatum för när det blir obligatoriskt för alla.
Vad händer om servern och klienten har olika tidszoner?
Det spelar ingen roll så länge båda parter räknar sekunder sedan Unix-epoken korrekt, eftersom TOTP är tidszonsoberoende internt. Problemet uppstår i praktiken bara när serverns systemklocka faktiskt går fel, inte för att den står inställd på en annan tidszon. Kör alltid servrar med NTP-synkronisering aktiverad, särskilt i containermiljöer där klockan annars kan glida över tid.
Relaterad läsning
- OAuth 2.0 med PKCE i Node.js: 12 steg, 30 min [2026]
- OWASP Top 10 i Node.js: 12 steg, 30 min [2026]
- Node.js Indatavalidering: 12 steg mot injektionsattacker [2026]
- Helmet.js i Node.js: 12 Steg för HTTP-säkerhetshuvuden [2026]
- YubiKey vs Google Authenticator: 0 nätfiskeattacker med hårdvara [2026]
- Lösenordssäkerhet: längd, hashning och lösenordshanterare
Källor: RFC 6238, TOTP: Time-Based One-Time Password Algorithm, NIST Authenticator Assurance Levels, Node.js, Evolving the Node.js Release Schedule, endoflife.date, Node.js Release Status, CSO Online, GitLab 2FA Login Protection Bypass och otplib på GitHub.




