Ett dataintrång är sällan slutet på historien. Det som avgör om det blir en katastrof eller en fotnot är hur lösenorden låg lagrade när angriparen kom in. Enligt Microsofts Digital Defense Report 2025 var 97 procent av alla observerade identitetsattacker password-spray-attacker, där automatiserade verktyg testar vanliga eller läckta lösenord mot tusentals konton i följd. Om din databas lagrar lösenord som plaintext eller med en svag hashfunktion tar det angriparen minuter att knäcka dem. Med rätt algoritm, rätt parametrar och rätt implementation tar samma jobb århundraden.

Den här guiden går igenom hur du hashar lösenord korrekt i Node.js, från grundläggande bcrypt-implementation till Argon2id, som OWASP rekommenderar som förstahandsval 2026. Du får kod du kan klistra in direkt, en migreringsstrategi för gamla system som fortfarande kör MD5 eller osaltad SHA-256, och en färdig demo-app du kan klona och bygga vidare på. Vi riktar oss till utvecklare som bygger eller underhåller inloggningssystem i Sverige och Norden, men koden fungerar oavsett var dina användare sitter.

Kontexten är inte akademisk. Enligt Yubicos Global State of Authentication Report 2025 uppger bara 48 procent av organisationerna att de kör flerfaktorsautentisering på samtliga sina applikationer, vilket betyder att lösenordet fortfarande är den enda spärren för mer än hälften av alla inloggningar. Antivirus Insiders sammanställning av 2025 års data visar att den globala arbetsplatsadoptionen av MFA nådde 70 procent, upp från 66 procent året innan, men det lämnar fortfarande en stor andel konton som enbart skyddas av ett lösenord och den hash det lagras med. Är den hashen svag spelar det ingen roll hur bra resten av arkitekturen är.

Hashning vs kryptering: varför AES inte löser problemet

Ett vanligt nybörjarmisstag är att kryptera lösenord med AES istället för att hasha dem. Skillnaden är avgörande. Kryptering är reversibel: har du nyckeln kan du få tillbaka originaltexten. Hashning är envägs: en hashfunktion som bcrypt eller Argon2id tar ett lösenord och producerar ett fast långt värde som inte går att räkna tillbaka till originalet. Din applikation behöver aldrig känna till användarens riktiga lösenord efter att kontot skapats, den behöver bara kunna verifiera att ett inskickat lösenord matchar den lagrade hashen.

Om du krypterar istället för att hasha finns nyckeln någonstans i din infrastruktur, och den som får tag i nyckeln vid ett intrång får tillbaka alla lösenord i klartext på en gång. Med korrekt hashning måste angriparen knäcka varje lösenord individuellt, vilket med Argon2id och rätt parametrar kan ta veckor eller år per lösenord beroende på hur komplext det är. Det är därför NIST SP 800-63B och OWASP:s Password Storage Cheat Sheet båda pekar ut saltad, långsam hashning som standarden för lösenordslagring, inte kryptering.

Så knäcker angripare svaga hashade lösenord

För att förstå varför algoritmvalet spelar roll behöver du förstå hur en attack faktiskt går till. Om lösenord lagras med en snabb, osaltad hashfunktion som MD5 eller SHA-256 kan en angripare med konsumenthårdvara i form av en modern GPU testa flera miljarder kombinationer i sekunden. Det gör att korta eller vanliga lösenord faller inom sekunder, ofta med hjälp av färdiga rainbow tables, stora förberäknade tabeller som mappar hashvärden direkt till klartextlösenord.

Salt stoppar rainbow table-attacker eftersom varje lösenord får ett unikt tillägg innan hashningen, vilket gör att en förberäknad tabell blir värdelös. Men salt gör ingenting åt hastigheten. Där kommer arbetskrävande algoritmer in. bcrypt och Argon2id är medvetet långsamma, med en konfigurerbar kostnad, vilket sänker en angripares testhastighet från miljarder per sekund till hundratals eller tusentals per sekund beroende på hårdvara och parametrar. Argon2id lägger dessutom till ett minneskrav, vilket är den viktigaste anledningen till att den motstår GPU-kluster bättre än bcrypt: GPU:er är byggda för att vara många och parallella, inte för att ha mycket minne per beräkningsenhet.

Förkunskaper: verktyg och versioner du behöver

Innan du börjar, se till att du har följande på plats. Vi utgår från Node.js 24, som är Active LTS under 2026 (Node 22 är kvar i underhållsläge och Node 26 blir LTS i oktober 2026), men koden fungerar även på Node 22.

  • Node.js 24 LTS eller senare (kör node -v för att kontrollera)
  • npm 10 eller senare, installeras med Node.js
  • bcrypt-paketet, aktuell version 6.0.0 från kelektiv/node.bcrypt.js
  • argon2-paketet från ranisalt/node-argon2 (kontrollera senaste versionen med npm view argon2 version innan du installerar, eftersom versionsnumret ändras oftare än bcrypts)
  • Express 4 eller 5 för webbservern i exemplen
  • En databas att lagra hasharna i, vi använder SQLite i demo-appen för enkelhetens skull men koden fungerar identiskt mot Postgres eller MySQL
  • Grundläggande kunskap i async/await och REST-API:er

Värt att nämna för team i Sverige och övriga EU: hur du lagrar lösenord räknas som en teknisk säkerhetsåtgärd enligt GDPR artikel 32, och tillsynsmyndigheter som svenska IMY har vid flera tillfällen pekat på bristfällig lösenordshantering som en bidragande faktor i sanktionsbeslut efter dataintrång. Att kunna visa att ni följer OWASP:s och NIST:s aktuella rekommendationer är inte bara god praxis, det är dokumentation du kan behöva visa upp om en tillsynsmyndighet frågar efter den efter en incident.

Skapa en ny mapp för projektet och initiera det.

mkdir sakra-losenord && cd sakra-losenord
npm init -y
npm install express bcrypt argon2 dotenv better-sqlite3
npm install --save-dev jest supertest

Förvänta dig output i stil med detta när installationen är klar.

added 87 packages, and audited 88 packages in 4s
14 packages are looking for funding
  run `npm fund` for details
found 0 vulnerabilities

Om du ser vulnerabilities som inte är noll, kör npm audit fix innan du fortsätter. Ett projekt som ska hantera lösenord bör aldrig starta med kända sårbarheter i beroendekedjan.

Steg 1–2: bygg registrering och inloggning med bcrypt

bcrypt är den mest beprövade lösenordshashfunktionen i Node.js-ekosystemet och ett bra startläge om du underhåller ett äldre system. Den bygger på Blowfish-chiffret och har en inbyggd “work factor” (kostnadsfaktor) som styr hur många rundor hashningen körs. Ju högre faktor, desto långsammare blir hashningen, vilket är precis vad du vill mot brute force. Skapa en fil auth-bcrypt.js med registreringslogiken.

const bcrypt = require('bcrypt');

const SALT_ROUNDS = 12;

async function registreraAnvandare(losenord) {
  if (losenord.length < 12) {
    throw new Error('Lösenordet måste vara minst 12 tecken');
  }
  const hash = await bcrypt.hash(losenord, SALT_ROUNDS);
  return hash;
}

module.exports = { registreraAnvandare };

SALT_ROUNDS på 12 innebär 2^12, alltså 4 096 rundor. Det ger runt 150–300 millisekunder per hashning på modern serverhårdvara, en nivå som är obehaglig för en angripare som testar miljontals lösenord men knappt märkbar för en enskild inloggning. Höj till 13 eller 14 om servern har gott om CPU-marginal, men mät alltid faktisk svarstid innan du skalar upp i produktion.

Verifiering vid inloggning använder bcrypt.compare, som själv läser ut saltet ur den lagrade hashen (bcrypt sparar salt och hash i samma sträng) och räknar om hashningen för att jämföra.

async function verifieraLosenord(inskickatLosenord, lagradHash) {
  const matchar = await bcrypt.compare(inskickatLosenord, lagradHash);
  return matchar;
}

// Exempel på lagrad hash:
// $2b$12$KIXQ4z8P3f5s7YV1h9wRZeqjJgU2N0tWm6oL4pC8dS1vB3nH5aXyO

Lägg märke till att bcrypt.compare tar konstant tid oavsett var i strängen jämförelsen skulle misslyckas, vilket skyddar mot timingattacker. Skriv aldrig egen jämförelselogik med === på hasharna, det öppnar för attacker som mäter svarstid för att gissa sig fram tecken för tecken.

Steg 3: varför Argon2id är OWASP:s förstahandsval 2026

bcrypt har en svaghet: den är minnessnål. Det gör den sårbar för attacker som körs på GPU:er eller specialbyggd hårdvara (ASIC), där tusentals hashningar kan köras parallellt eftersom varje beräkning kräver lite minne. Argon2 vann Password Hashing Competition 2015, en öppen tävling där kryptografer från hela världen tävlade om att bygga nästa generations lösenordshashning, och varianten Argon2id är idag OWASP:s rekommenderade förstahandsval i deras Password Storage Cheat Sheet.

Argon2id tvingar angriparen att avsätta en bestämd mängd minne per hashningsförsök, inte bara CPU-tid. Det gör GPU-baserade attacker dramatiskt dyrare, eftersom en GPU har enormt många kärnor men begränsat minne per kärna. OWASP:s cheat sheet, uppdaterad i augusti 2026, anger en minimikonfiguration på 19 456 KiB minne (cirka 19 MiB), 2 iterationer och en parallellitetsgrad på 1. Många team som har mer minnesmarginal kör en högre profil, till exempel 64 MB minne med 3 iterationer, för extra marginal mot framtida hårdvara.

AlgoritmMinneskravGPU-motståndOWASP-status 2026Bäst för
Argon2idKonfigurerbart, 19 MiB+Mycket högtRekommenderad förstahandsvalNya projekt
bcryptFast, cirka 4 KBMåttligtGodkänt alternativBefintliga system, enkel migrering
scryptKonfigurerbart, ofta 16–64 MBHögtGodkänt alternativSystem som redan kör Node.js crypto-modulen
PBKDF2Ingen minneskostnadLågtEndast om FIPS-kravRegelkrav som tvingar fram PBKDF2

Steg 4–5: implementera Argon2id i Node.js

Argon2-paketet för Node.js kompilerar mot den officiella C-referensimplementationen, vilket ger korrekt och snabb hashning utan att du behöver skriva kryptografi själv. Skapa en fil auth-argon2.js.

const argon2 = require('argon2');

const ARGON2_OPTIONS = {
  type: argon2.argon2id,
  memoryCost: 19456, // 19 MiB, OWASP:s minimiprofil 2026
  timeCost: 2,
  parallelism: 1,
};

async function hashaLosenord(losenord) {
  return argon2.hash(losenord, ARGON2_OPTIONS);
}

async function verifieraLosenord(lagradHash, inskickatLosenord) {
  try {
    return await argon2.verify(lagradHash, inskickatLosenord);
  } catch (fel) {
    // argon2.verify kastar fel om hashen är felformaterad,
    // aldrig returnera true i catch-blocket
    return false;
  }
}

module.exports = { hashaLosenord, verifieraLosenord };

Argon2 lagrar hela konfigurationen (algoritm, version, minne, iterationer, salt) i den resulterande strängen, precis som bcrypt. Det betyder att du kan höja parametrarna för nya konton utan att gamla hashar slutar fungera, argon2.verify läser alltid ut rätt parametrar ur strängen den får.

// Exempel på output från argon2.hash()
$argon2id$v=19$m=19456,t=2,p=1$UkFuZG9tU2FsdDEyMzQ$8fH3kLpQwZvXcNmB7yRtGjKdEaWs2oPl

Bygg sedan en Express-route som knyter ihop registrering och inloggning.

const express = require('express');
const { hashaLosenord, verifieraLosenord } = require('./auth-argon2');
const router = express.Router();

router.post('/registrera', async (req, res) => {
  const { epost, losenord } = req.body;
  if (!losenord || losenord.length < 12) {
    return res.status(400).json({ fel: 'Lösenordet måste vara minst 12 tecken' });
  }
  const hash = await hashaLosenord(losenord);
  // spara epost + hash i databasen här
  res.status(201).json({ status: 'skapad' });
});

router.post('/logga-in', async (req, res) => {
  const { epost, losenord } = req.body;
  const lagradHash = await hamtaHashFranDb(epost); // egen funktion
  if (!lagradHash) {
    return res.status(401).json({ fel: 'Fel e-post eller lösenord' });
  }
  const matchar = await verifieraLosenord(lagradHash, losenord);
  if (!matchar) {
    return res.status(401).json({ fel: 'Fel e-post eller lösenord' });
  }
  res.json({ status: 'inloggad' });
});

module.exports = router;

Lägg märke till att felmeddelandet är identiskt oavsett om det är e-posten eller lösenordet som är fel. Skiljer du på felmeddelandena avslöjar du för en angripare vilka e-postadresser som finns i systemet, vilket underlättar riktade attacker.

NIST 2026: längd slår komplexitet

Många system tvingar fortfarande fram krav på versaler, siffror och specialtecken, trots att NIST SP 800-63B sedan flera revisioner tillbaka rekommenderar tvärtom. Riktlinjen säger att applikationer bör tillåta långa lösenfraser (minst 8 tecken, med rekommendation om betydligt längre) istället för att tvinga fram komplexitetsregler som i praktiken får användare att skriva förutsägbara mönster som "Sommar2026!". Riktlinjen avråder också uttryckligen från periodiskt tvingad lösenordsbyte utan anledning, eftersom det leder till svagare, mer förutsägbara lösenord över tid snarare än bättre säkerhet.

Det NIST istället rekommenderar är att kontrollera nya lösenord mot listor över redan läckta lösenord vid registrering, till exempel via ett tjänste-API som Have I Been Pwned:s Pwned Passwords, som låter dig slå upp ett lösenord (via en k-anonymitetsmodell som aldrig skickar hela hashen) utan att avslöja det för tredje part. Kombinerat med en minimilängd på 12 tecken i din egen policy och Argon2id för lagringen får du en betydligt starkare grund än den klassiska kombinationen av korta lösenord och strikta teckenkrav.

scrypt som alternativ: inbyggt i Node.js crypto-modul

Om du av någon anledning inte vill dra in ett externt beroende, till exempel i en miljö med strikta krav på granskade beroendekedjor, finns scrypt inbyggt i Node.js egen crypto-modul sedan länge. scrypt är, precis som Argon2id, minneskrävande och därmed motståndskraftig mot GPU-attacker, men den saknar Argon2:s inbyggda hantering av sidokanalsattacker (Argon2id är specifikt designad för att balansera motstånd mot både GPU-attacker och timingbaserade sidokanalsattacker).

const crypto = require('crypto');
const { promisify } = require('util');
const scryptAsync = promisify(crypto.scrypt);

async function hashaMedScrypt(losenord) {
  const salt = crypto.randomBytes(16).toString('hex');
  const derivedKey = await scryptAsync(losenord, salt, 64, { N: 16384, r: 8, p: 1 });
  return `${salt}:${derivedKey.toString('hex')}`;
}

async function verifieraMedScrypt(losenord, lagradHash) {
  const [salt, key] = lagradHash.split(':');
  const derivedKey = await scryptAsync(losenord, salt, 64, { N: 16384, r: 8, p: 1 });
  const lagradBuffer = Buffer.from(key, 'hex');
  return crypto.timingSafeEqual(derivedKey, lagradBuffer);
}

Lägg märke till crypto.timingSafeEqual i verifieringsfunktionen. Till skillnad från bcrypt och argon2, som har konstant-tid-jämförelse inbyggd i sina bibliotek, måste du själv se till att jämförelsen inte läcker timinginformation när du bygger på Node.js råa crypto-modul. Glöm aldrig det steget om du väljer scrypt-vägen.

Steg 6: salt, pepper och hemlighetshantering

Både bcrypt och argon2 genererar ett unikt slumpmässigt salt per lösenord automatiskt, du behöver aldrig skapa det själv. Saltet gör att två användare med samma lösenord får helt olika hashar, vilket stoppar färdigberäknade rainbow table-attacker. Det du kan lägga till manuellt är en pepper, en hemlig applikationsnyckel som läggs till lösenordet innan det hashas och som lagras separat från databasen, till exempel som miljövariabel eller i en hemlighetshanterare.

// .env (läggs aldrig i versionshantering)
PASSWORD_PEPPER=byt-ut-mot-32-slumpmassiga-bytes-i-produktion

// I koden
require('dotenv').config();
const crypto = require('crypto');

function laggTillPepper(losenord) {
  const hmac = crypto.createHmac('sha256', process.env.PASSWORD_PEPPER);
  hmac.update(losenord);
  return hmac.digest('hex');
}

async function hashaMedPepper(losenord) {
  const pepprat = laggTillPepper(losenord);
  return argon2.hash(pepprat, ARGON2_OPTIONS);
}

Poängen med peppern är att den håller sig utanför databasen. Om en angripare bara stjäl din databas, utan att också komma åt applikationsservern eller hemlighetshanteraren, kan de inte knäcka hasharna även om Argon2-parametrarna är svaga. Lägg aldrig peppern i samma databas eller kodrepo som resten av applikationen, och rotera den om du misstänker att den läckt, med vetskap om att en rotation kräver att alla användare loggar in igen eller att du kör ett omhashningsjobb.

Steg 7: skydda mot brute force och timingattacker

Långsam hashning löser bara halva problemet. Utan rate limiting kan en angripare fortfarande skicka tiotusentals inloggningsförsök mot din /logga-in-endpoint från en botnätsflotta, oavsett hur dyr varje enskild hashning är. Lägg på en enkel rate limiter baserad på både IP-adress och kontots e-postadress.

const rateLimit = require('express-rate-limit');

const inloggningsBegransning = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minuter
  max: 5, // max 5 försök per IP och fönster
  message: { fel: 'För många inloggningsförsök, försök igen om 15 minuter' },
  standardHeaders: true,
  legacyHeaders: false,
});

router.post('/logga-in', inloggningsBegransning, async (req, res) => {
  // samma logik som tidigare
});

Komplettera gärna med progressiv fördröjning per konto (öka väntetiden för varje misslyckat försök) och kontolåsning efter ett högt antal fel, men var försiktig med hård kontolåsning, den kan missbrukas för att stänga ute legitima användare genom att medvetet mata in fel lösenord på deras konto. En kombination av IP-baserad rate limiting och CAPTCHA efter tre misslyckade försök brukar ge bäst balans mellan säkerhet och användarupplevelse.

Det finns också en mer radikal lösning: ta bort lösenordet ur ekvationen för de konton där det går. Okta rapporterade i sin Secure Sign-in Trends Report 2025 att andelen inloggningar med nätfiskeresistenta, lösenordslösa metoder som WebAuthn och passkeys växte med 63 procent på ett år, från cirka 8,6 till 14,0 procent av alla inloggningar bland arbetsplatsanvändare. Om du redan har byggt lösenordshashningen som beskrivs här kan du använda den som fallback samtidigt som du rullar ut passkeys som förstahandsval, vilket vi går igenom i detalj i vår guide till WebAuthn och passkeys i Node.js.

Steg 8: migrera gamla MD5- eller SHA-hashar till Argon2id

Det vanligaste scenariot i praktiken är inte att bygga nytt, utan att ärva ett system som lagrar lösenord med MD5, osaltad SHA-256 eller en gammal PBKDF2-konfiguration. Du kan inte hasha om lösenord du inte har i klartext, så migreringen måste ske gradvis, vid nästa inloggning för varje användare.

async function loggaInMedMigrering(epost, losenord) {
  const anvandare = await hamtaAnvandare(epost);
  if (!anvandare) return { ok: false };

  if (anvandare.hashTyp === 'md5') {
    const gammalHash = crypto.createHash('md5').update(losenord).digest('hex');
    if (gammalHash !== anvandare.losenordsHash) return { ok: false };

    // Lösenordet stämmer, hasha om med Argon2id direkt
    const nyHash = await hashaLosenord(losenord);
    await uppdateraAnvandare(epost, { losenordsHash: nyHash, hashTyp: 'argon2id' });
    return { ok: true, migrerad: true };
  }

  // redan migrerad till argon2id
  const matchar = await verifieraLosenord(anvandare.losenordsHash, losenord);
  return { ok: matchar };
}

Kör en bakgrundsrapport varje vecka som räknar hur många konton som fortfarande har hashTyp === 'md5'. När andelen sjunker under en gräns du är bekväm med, till exempel 2 procent, tvinga resterande konton att återställa lösenordet via e-post istället för att vänta ut en frivillig migrering som aldrig blir klar.

Logga varje migrering (utan att logga själva lösenordet) så att du kan visa framsteg för säkerhetsansvariga och, om det behövs, för en tillsynsmyndighet. En enkel räknare som skriver "migrerade X av Y konton denna vecka" till en dashboard räcker långt för att hålla projektet synligt och prioriterat, istället för att det tystnar bort i en backlog efter de första veckornas snabba framsteg.

Steg 9–10: testa och driftsätt din implementation

Skriv aldrig lösenordslogik utan tester, en regression här slår direkt mot säkerheten för alla användare. Här är ett grundläggande testfall med Jest.

const { hashaLosenord, verifieraLosenord } = require('../auth-argon2');

test('rätt lösenord verifieras korrekt', async () => {
  const hash = await hashaLosenord('EttMycketStarktLosenord123!');
  const resultat = await verifieraLosenord(hash, 'EttMycketStarktLosenord123!');
  expect(resultat).toBe(true);
});

test('fel lösenord avvisas', async () => {
  const hash = await hashaLosenord('EttMycketStarktLosenord123!');
  const resultat = await verifieraLosenord(hash, 'FelLosenord');
  expect(resultat).toBe(false);
});

test('felformaterad hash returnerar false, inte kastat fel', async () => {
  const resultat = await verifieraLosenord('inte-en-riktig-hash', 'vad-som-helst');
  expect(resultat).toBe(false);
});

Kör npx jest och förvänta dig output som liknar detta.

PASS  test/auth-argon2.test.js
  ✓ rätt lösenord verifieras korrekt (187 ms)
  ✓ fel lösenord avvisas (183 ms)
  ✓ felformaterad hash returnerar false, inte kastat fel (2 ms)

Test Suites: 1 passed, 1 total
Tests:       3 passed, 3 total

Vid driftsättning, lagra peppern och andra hemligheter i din plattforms hemlighetshanterare, inte i miljövariabler som checkas in i ett CI-verktyg utan kryptering. Mät genomsnittlig svarstid för /logga-in under realistisk last innan du går live, Argon2id med för höga parametrar kan skapa flaskhalsar vid samtidiga inloggningar, till exempel vid en gemensam arbetsdagsstart klockan åtta.

Kör ett lasttest mot inloggningsendpointen innan lansering, till exempel med autocannon, för att se hur många samtidiga Argon2id-hashningar servern faktiskt klarar innan svarstiderna spikar.

npx autocannon -c 20 -d 30 -m POST \
  -H "Content-Type: application/json" \
  -b '{"epost":"[email protected]","losenord":"EttMycketStarktLosenord123!"}' \
  http://localhost:3000/logga-in

# Förvänta dig output med latency-percentiler, exempel:
# Latency    Avg    p50    p97.5   p99   Max
#            210ms  195ms  340ms   410ms 620ms

Om p99-latensen blir för hög för din applikations krav, är det bättre att sänka Argon2id-parametrarna en aning och kompensera med bättre rate limiting, än att köra en profil som gör tjänsten långsam eller instabil under belastningstoppar. En hashning som tar en sekund känns knappt vid en enskild inloggning, men vid tusen samtidiga inloggningar kan den bli flaskhalsen för hela servern om du inte skalar ut beräkningen.

Steg 11–12: bygg en komplett fungerande demo-app

Sätt ihop allt i en körbar Express-app med SQLite som lagring, så att du kan testa hela flödet lokalt innan du bygger in det i ett större system.

const express = require('express');
const Database = require('better-sqlite3');
const argon2 = require('argon2');
require('dotenv').config();

const app = express();
app.use(express.json());
const db = new Database('anvandare.db');

db.exec(`CREATE TABLE IF NOT EXISTS anvandare (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  epost TEXT UNIQUE NOT NULL,
  losenordsHash TEXT NOT NULL
)`);

const ARGON2_OPTIONS = { type: argon2.argon2id, memoryCost: 19456, timeCost: 2, parallelism: 1 };

app.post('/registrera', async (req, res) => {
  const { epost, losenord } = req.body;
  if (!losenord || losenord.length < 12) {
    return res.status(400).json({ fel: 'Lösenordet måste vara minst 12 tecken' });
  }
  const hash = await argon2.hash(losenord, ARGON2_OPTIONS);
  try {
    db.prepare('INSERT INTO anvandare (epost, losenordsHash) VALUES (?, ?)').run(epost, hash);
    res.status(201).json({ status: 'skapad' });
  } catch {
    res.status(409).json({ fel: 'E-postadressen är redan registrerad' });
  }
});

app.post('/logga-in', async (req, res) => {
  const { epost, losenord } = req.body;
  const rad = db.prepare('SELECT losenordsHash FROM anvandare WHERE epost = ?').get(epost);
  if (!rad) return res.status(401).json({ fel: 'Fel e-post eller lösenord' });
  const matchar = await argon2.verify(rad.losenordsHash, losenord);
  if (!matchar) return res.status(401).json({ fel: 'Fel e-post eller lösenord' });
  res.json({ status: 'inloggad' });
});

app.listen(3000, () => console.log('Servern körs på port 3000'));

Testa flödet med curl. Registrera en användare först.

curl -X POST http://localhost:3000/registrera \
  -H "Content-Type: application/json" \
  -d '{"epost":"[email protected]","losenord":"EttMycketStarktLosenord123!"}'

# Svar: {"status":"skapad"}

curl -X POST http://localhost:3000/logga-in \
  -H "Content-Type: application/json" \
  -d '{"epost":"[email protected]","losenord":"EttMycketStarktLosenord123!"}'

# Svar: {"status":"inloggad"}

Nu har du ett komplett, testbart exempel att bygga vidare på med sessionshantering, tvåfaktorsautentisering och lösenordsåterställning. Har du redan implementerat tvåfaktorsautentisering i samma projekt kan du kombinera guiderna, vår genomgång av TOTP i Node.js visar hur du lägger till det extra steget ovanpå den här lösenordshashningen.

Efter lyckad inloggning: sessioner och nästa steg

Lösenordshashning är bara första halvan av ett komplett autentiseringsflöde. När argon2.verify eller bcrypt.compare returnerar sant behöver du utfärda något som håller användaren inloggad utan att skicka lösenordet igen vid varje request, oftast en sessionscookie eller en kort-livad JWT-token kombinerad med en refresh-token. Regenerera alltid sessions-ID:t vid inloggning, även om användaren redan hade en anonym session, för att förhindra session fixation-attacker där en angripare planterar ett känt sessions-ID innan offret loggar in.

Om du bygger ett API som andra tjänster autentiserar mot, snarare än en webbapplikation med cookies, är OAuth 2.0 med PKCE ett bättre val än att bygga en egen tokenlösning från grunden. Vi går igenom det flödet i detalj i vår guide till OAuth 2.0 med PKCE i Node.js, som bygger vidare på precis den typ av säker inloggning den här artikeln beskriver.

Vanliga misstag att undvika

De flesta säkerhetsincidenter kring lösenordshantering beror inte på att teamet valde fel algoritm från början, utan på små implementationsmisstag som smyger sig in längs vägen. Här är de vi ser oftast i kodgranskningar.

  • Att skriva egen kryptografi. Bygg aldrig egna hashfunktioner eller salteringslogik. Använd etablerade, granskade bibliotek som bcrypt och argon2, de har testats av tusentals utvecklare och akademiker under många år.
  • Att jämföra hashar med strikt likhet. Använd alltid bcrypt.compare eller argon2.verify, aldrig hash1 === hash2, eftersom det öppnar för timingattacker.
  • Att sätta för låga Argon2id-parametrar. En memoryCost under 19 456 KiB ger sämre skydd än OWASP:s minimiprofil. Mät alltid svarstid i din faktiska miljö istället för att gissa ett värde.
  • Att glömma migrera gamla hashar. Ett system som "tillfälligt" kör MD5 för gamla konton i väntan på en migrering som aldrig sker är ett permanent säkerhetshål.
  • Att logga lösenord i klartext av misstag. Kontrollera att request-loggning, felrapportering och API-övervakning (till exempel Sentry eller liknande verktyg) inte råkar spara request body med lösenordsfältet oredigerat.
  • Att lagra peppern i samma databas som hasharna. Det gör den värdelös som extra skyddslager om databasen läcker.
  • Att sätta ingen övre gräns på lösenordslängden. Utan ett tak, till exempel 128 tecken, kan en angripare skicka extremt långa strängar för att göra varje hashningsanrop onödigt tungt och därigenom belasta servern.

Felsökning: vanliga problem och lösningar

ProblemTrolig orsakLösning
npm install argon2 misslyckas med kompileringsfelSaknar byggverktyg (node-gyp, Python, C++-kompilator)Installera build-essential (Linux) eller Xcode Command Line Tools (macOS), eller använd en prebyggd binär för din Node-version
argon2.verify kastar undantag istället för att returnera falseHashen i databasen är trunkerad eller felformateradKontrollera kolumntypen i databasen, VARCHAR med för kort maxlängd klipper hashsträngen
Inloggningen blir plötsligt mycket långsam under hög belastningmemoryCost är satt för högt för antalet samtidiga requestsSänk memoryCost stegvis och mät, eller skala ut hashningen till en separat worker-process
bcrypt.hash kastar "data and salt arguments required"Lösenordet är undefined, ofta för att body-parsern inte är konfigureradKontrollera att express.json() är registrerat före dina routes
Alla lösenord verkar matcha varandra i testerSALT_ROUNDS eller ARGON2_OPTIONS delas felaktigt mellan testfall via global stateSkapa nya options-objekt per test och undvik globala mutabla variabler
Migreringsjobbet hashar om samma användare flera gångerhashTyp-flaggan uppdateras inte transaktionelltWrappa läsning, hashning och skrivning i en databastransaktion
node-gyp rebuild misslyckas på Apple SiliconFel arkitektur i cachead binär från tidigare npm-installationRensa npm-cachen med npm cache clean --force och installera om
Rate limiter blockerar legitima kontor med delad IPMånga användare bakom samma NAT-adress (vanligt på kontor)Kombinera IP-baserad och kontobaserad rate limiting istället för enbart IP
Argon2-hashar från ett annat språk (till exempel Python) verifieras inte i NodeOlika standardparametrar mellan bibliotekAnge samma type, memoryCost, timeCost och parallelism explicit i båda system

Avancerade tips för produktion

När grunderna sitter finns det finjusteringar som gör skillnad i skarpt läge. Kör hashningen i en separat worker thread eller en kö (till exempel med BullMQ) om du har hög samtidig trafik, så blockerar inte en tung Argon2id-beräkning event-loopen för andra requests. Övervaka p95- och p99-svarstider för inloggningsendpointen separat från resten av API:et, eftersom hashningskostnaden gör den till en naturlig avvikare i vanlig APM-statistik.

Sätt upp ett automatiskt omhashningssteg som körs vid varje lyckad inloggning: kontrollera om den lagrade hashens parametrar är lägre än din nuvarande standard, och hasha om med de nya värdena om så är fallet. Det gör att du kan höja säkerhetsnivån gradvis över tid i takt med att hårdvaran blir billigare, utan att tvinga fram en massmigrering. Håll ett öga på NIST:s och OWASP:s cheat sheets en gång per kvartal, rekommenderade minimivärden för Argon2id har höjts flera gånger sedan algoritmen introducerades och lär fortsätta göra det i takt med att GPU-kraft blir billigare.

För organisationer med högre säkerhetskrav, till exempel banker eller vårdgivare, är det värt att flytta peppern från en miljövariabel till en riktig nyckelhanteringstjänst (KMS) eller en hårdvarusäkerhetsmodul (HSM). Då signeras eller krypteras aldrig peppern i klartext på disk, och du kan rotera nycklar utan att röra applikationskoden. Bygg också in larm för avvikande inloggningsmönster, till exempel en plötslig ökning i antalet 401-svar från en enda IP-range, vilket ofta är det första tecknet på en pågående credential stuffing-kampanj där läckta lösenordslistor från andra tjänster testas mot ditt system.

Argon2id-profiler för olika användningsfall

Rätt Argon2id-profil beror på vad du bygger. En publik SaaS-tjänst med tusentals samtidiga inloggningar har andra begränsningar än en intern administratörsportal med ett fåtal användare. Tabellen nedan ger tre startpunkter baserade på OWASP:s riktlinjer, men mät alltid faktisk svarstid i din egen miljö innan du låser en profil i produktion.

AnvändningsfallmemoryCosttimeCostparallelismKommentar
Resursbegränsad API (serverless, delat minne)19 456 KiB (19 MiB)21OWASP:s uttryckliga minimiprofil 2026
Standard webbapplikation65 536 KiB (64 MB)31Vanlig balans mellan säkerhet och svarstid
Hög säkerhet (admin, bank, vård)131 072 KiB (128 MB)41Kräver dedikerad kapacitet eller kö för hashning

Höj aldrig parallelism-värdet reflexmässigt i tron att det ger bättre säkerhet. En högre parallelism gör hashningen snabbare på flerkärniga system, vilket i praktiken gör den svagare mot en angripare som också kör på flerkärnig hårdvara. Håll parallelism på 1 om du inte har ett specifikt, uppmätt behov av något annat.

Vanliga frågor om lösenordshashning i Node.js

Är bcrypt osäkert att använda 2026?
Nej, bcrypt räknas fortfarande som ett godkänt alternativ av OWASP. Skillnaden mot Argon2id är att bcrypt är svagare mot GPU-accelererade attacker eftersom den inte kräver mycket minne. För nya projekt rekommenderas Argon2id, men du behöver inte panikmigrera ett fungerande bcrypt-system.

Hur många SALT_ROUNDS bör jag använda i bcrypt?
12 är ett vanligt startvärde 2026 och ger runt 150–300 millisekunder per hashning på modern serverhårdvara. Testa på din faktiska serverhårdvara och höj successivt tills svarstiden börjar kännas i din applikations svarstidsbudget.

Kan jag hasha lösenord på klientsidan innan de skickas till servern?
Nej. Om du hashar i webbläsaren blir den klienthashen i praktiken det nya lösenordet, och en angripare som stjäl databasen kan logga in direkt med den stulna hashen utan att någonsin känna till originallösenordet. Skicka alltid lösenordet över TLS och hasha först på servern.

Behöver jag både salt och pepper?
Salt är obligatoriskt och sköts automatiskt av bcrypt och argon2. Pepper är ett extra frivilligt lager som skyddar mot att enbart databasen läcker, eftersom peppern lagras separat. De flesta mindre projekt klarar sig utan pepper, men det är rekommenderat för system med känsliga användardata, till exempel bank- eller sjukvårdsapplikationer.

Vad händer om jag byter Argon2id-parametrar för ett system i drift?
Befintliga hashar fortsätter fungera eftersom parametrarna lagras i hashsträngen. Nya registreringar och omhashningar vid inloggning använder de nya, högre värdena, medan gamla hashar verifieras med sina ursprungliga parametrar tills de hashas om.

Räcker det med HTTPS för att skydda lösenord under transport?
HTTPS/TLS skyddar lösenordet på vägen mellan klient och server, men skyddar inte hur det lagras. Du behöver båda delarna: TLS för transport och Argon2id eller bcrypt för lagring. Läs mer i vår genomgång av HTTPS och TLS.

Måste jag hasha om alla lösenord direkt om jag byter från bcrypt till Argon2id?
Nej, och du bör inte försöka. Låt bytet ske gradvis via samma mönster som migreringen från MD5: verifiera med den gamla algoritmen vid inloggning, hasha om med Argon2id direkt efter en lyckad verifiering, och låt processen självläka över veckor eller månader i takt med att aktiva användare loggar in.

Hur testar jag att min implementation faktiskt är säker?
Kör OWASP:s Password Storage Cheat Sheet som checklista, skriv enhetstester för både positiva och negativa fall, och överväg en extern säkerhetsgranskning innan lösningen går i produktion för ett system med känsliga användardata.

Ska jag byta från PBKDF2 till Argon2id om jag redan uppfyller ett regelkrav?
Om ett regelverk (till exempel vissa FIPS-krav) uttryckligen kräver PBKDF2 måste du stanna kvar tills kravet ändras. I alla andra fall är Argon2id att föredra eftersom den ger bättre skydd mot moderna GPU-baserade attacker.

Vad gör jag om en användare har ett extremt långt lösenord, till exempel 500 tecken?
Sätt ett rimligt max, till exempel 128 tecken, innan hashning. Det skyddar mot denial of service-försök där någon medvetet skickar enorma strängar för att göra hashningen onödigt dyr, samtidigt som gränsen är långt över vad en vanlig lösenfras behöver.

Bör jag hasha lösenord olika beroende på om kontot är admin eller vanlig användare?
Det går att köra en högre Argon2id-profil för adminkonton eftersom de utgör en mindre andel av trafiken och en högre riskprofil om de komprometteras. Se bara till att din inloggningskod läser rätt parametrar per konto istället för att anta en global konstant, annars riskerar du att verifieringen misslyckas för de konton du försökte skydda extra.

Börja med bcrypt om du redan har ett system i drift och migrera gradvis, eller starta direkt med Argon2id om du bygger nytt. Skriv testerna innan du rullar ut till produktion, mät verklig svarstid under last, och sätt upp automatisk omhashning så att säkerhetsnivån följer med när OWASP höjer sina rekommenderade minimivärden igen. Den koden du har nu, kombinerad med rate limiting och en tydlig migreringsplan för gamla hashar, täcker de flesta scenarier ett svenskt eller nordiskt utvecklingsteam stöter på.

Relaterad läsning

Källor och vidare läsning: OWASP Password Storage Cheat Sheet, NIST SP 800-63B, Argon2 (Password Hashing Competition), Node.js crypto-modulens dokumentation och node.bcrypt.js på GitHub.