Autentisering — lagring, hastighetsbegränsning och MFA
Observera
Genomför TryHackMes eller PortSwiggers moduler om autentiseringssvagheter. Studera därefter, konceptuellt, hur en läckt lösenordsdatabas hanteras: skillnaden mellan en databas med lösenord i klartext, med omodern hashning, och med modern hashning är skillnaden mellan en katastrof, ett allvarligt problem och en hanterbar incident.
Analysera grundorsaken
Tre separata brister brukar förekomma tillsammans:
- Lösenorden lagras så att de går att återställa — klartext, reversibel kryptering, eller snabba hashfunktioner som MD5 och SHA-1 som är konstruerade för hastighet och därmed lätta att testa sig igenom i stor skala.
- Inget tak för antalet inloggningsförsök — ingenting hindrar att samma konto testas om och om igen.
- Lösenordet är enda faktorn — ett känt lösenord ger full åtkomst.
Bygg försvaret
1. Modern lösenordshashning. Använd Argon2id (förstahandsval), scrypt eller bcrypt. Dessa är medvetet långsamma och minneskrävande, vilket gör storskalig testning opraktisk. Saltet hanteras automatiskt av biblioteken.
Python:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
def registrera(losenord: str) -> str:
return ph.hash(losenord) # saltet genereras automatiskt
def verifiera(hash_i_db: str, inmatat: str) -> bool:
try:
ph.verify(hash_i_db, inmatat)
return True
except VerifyMismatchError:
return False
Vad koden gör — förklaring rad för rad
Säker lösenordshantering i Python. Hashning innebär att lösenordet omvandlas till en oläslig sträng som inte går att vända tillbaka. Vid inloggning jämförs inte lösenord mot lösenord, utan hash mot hash.
- from argon2 import PasswordHasher
- Hämtar in ett färdigt bibliotek. Du ska aldrig skriva din egen kryptokod — använd det som redan är granskat.
- ph = PasswordHasher()
- Skapar ett verktyg att hasha med och lägger det i variabeln ph.
- def registrera(losenord: str) -> str:
- Definierar en funktion som tar emot ett lösenord och lämnar tillbaka en hash. Texten efter kolonet anger att det ska vara text in och text ut.
- ph.hash(losenord)
- Gör om lösenordet till en hash. Biblioteket lägger automatiskt till ett slumptal, ett så kallat salt, så att två identiska lösenord ändå får olika hashvärden.
- try: / except VerifyMismatchError:
- Försök göra det här; om det går fel på just det här sättet, gör det andra i stället. Biblioteket signalerar fel lösenord genom att kasta ett fel, inte genom att svara nej.
- return True / return False
- Funktionen lämnar tillbaka sant eller falskt — lösenordet stämde eller inte.
Node.js:
const argon2 = require('argon2');
const hash = await argon2.hash(losenord);
const giltigt = await argon2.verify(hash, inmatat);
Vad koden gör — förklaring rad för rad
Samma sak i Node.js, på två rader. Kortheten är poängen: korrekt lösenordshantering kräver inte mycket kod när biblioteket gör jobbet.
- require('argon2')
- Node.js sätt att hämta in ett bibliotek.
- await argon2.hash(losenord)
- Skapar hashen. await behövs eftersom hashningen avsiktligt tar tid.
- argon2.verify(hash, inmatat)
- Jämför det inmatade lösenordet mot den sparade hashen och svarar sant eller falskt.
PHP (använder bcrypt eller Argon2 beroende på konfiguration):
$hash = password_hash($losenord, PASSWORD_DEFAULT);
$giltigt = password_verify($inmatat, $hash);
Vad koden gör — förklaring rad för rad
Samma sak i PHP, med funktioner som är inbyggda i språket. Inget extra bibliotek behövs.
- password_hash($losenord, PASSWORD_DEFAULT)
- Hashar lösenordet. PASSWORD_DEFAULT betyder "använd den metod PHP just nu anser bäst", vilket gör att koden följer med när rekommendationerna ändras.
- password_verify($inmatat, $hash)
- Jämför och svarar sant eller falskt. Observera ordningen: först det inmatade, sedan det sparade.
Skriv aldrig egen kryptorelaterad kod. Använd biblioteken ovan.
2. Hastighetsbegränsning. Begränsa både per konto och per IP-adress, med successivt ökande fördröjning:
MAX_FORSOK = 5
LASNINGSTID = 900 # 15 minuter
def kontrollera_sparr(anvandarnamn, ip):
nyckel_konto = f"forsok:konto:{anvandarnamn}"
nyckel_ip = f"forsok:ip:{ip}"
if redis.get(nyckel_konto) and int(redis.get(nyckel_konto)) >= MAX_FORSOK:
raise KontoTillfalligtSparrat()
if redis.get(nyckel_ip) and int(redis.get(nyckel_ip)) >= MAX_FORSOK * 10:
raise ForMangaForsokFranAdress()
def registrera_misslyckande(anvandarnamn, ip):
for nyckel in (f"forsok:konto:{anvandarnamn}", f"forsok:ip:{ip}"):
redis.incr(nyckel)
redis.expire(nyckel, LASNINGSTID)
Vad koden gör — förklaring rad för rad
Hastighetsbegränsning i Python. Koden räknar misslyckade inloggningsförsök och spärrar tillfälligt när de blir för många — både per konto och per avsändaradress.
- MAX_FORSOK = 5
- En konstant: ett värde som används på flera ställen och enkelt kan ändras på ett ställe. Versaler är en vana som signalerar att värdet inte ändras under körning.
- redis
- Ett snabbt minnesbaserat lager där räknarna sparas. Att använda en databas för detta vore onödigt långsamt.
- f"forsok:konto:{anvandarnamn}"
- En formatsträng. Bokstaven f före citattecknet gör att det som står i klammerparentes byts ut mot variabelns värde — nyckeln blir alltså unik per användare.
- raise KontoTillfalligtSparrat()
- Avbryter och signalerar ett fel uppåt i programmet. Inloggningen fortsätter inte.
- redis.incr(nyckel)
- Ökar räknaren med ett.
- redis.expire(nyckel, LASNINGSTID)
- Säger åt räknaren att radera sig själv efter angiven tid. Det är så spärren släpper automatiskt efter femton minuter.
- MAX_FORSOK * 10
- Gränsen per adress sätts tio gånger högre, eftersom många legitima användare kan dela samma adress på ett kontor.
3. Enhetliga felmeddelanden. "Fel användarnamn eller lösenord" avslöjar inte vilket av de två som var fel. "Användaren finns inte" låter en utomstående kartlägga vilka konton som existerar.
4. Flerfaktorsautentisering. TOTP (authenticator-app) eller helst WebAuthn/passkeys, som dessutom är motståndskraftiga mot nätfiske eftersom inloggningen är bunden till domänen. Erbjud återställningskoder, och behandla återställningsflödet med samma stränghet som inloggningen — det är ofta den svagaste länken.
5. Lösenordspolicy enligt modern praxis: längd framför komplexitet (minst 12 tecken), kontroll mot listor över kända läckta lösenord, och inga påtvingade regelbundna byten utan konkret misstanke. Påtvingade byten leder i praktiken till svagare, mer förutsägbara lösenord.
Verifiera
Testa i ditt labb att kontot faktiskt spärras efter angivet antal försök, att spärren släpper efter rätt tid, att felmeddelandet är identiskt oavsett om kontot finns, och att MFA-flödet inte går att hoppa över genom att gå direkt till en URL efter lösenordssteget.
Dokumentera
Skriv en kort säkerhetsgenomgång av autentiseringsflödet med en punkt per kontroll och status (uppfylld / delvis / saknas). Detta format är användbart i verkliga uppdrag.
Reflektionsfrågor: Varför är en långsam hashfunktion en fördel? Vad skyddar WebAuthn mot som TOTP inte gör?
Kunskapskontroll
Välj ett svar per fråga. Du får återkoppling direkt, och avsnittet markeras som klart när alla frågor är besvarade.
1Varför är en långsam hashfunktion som Argon2id en fördel för lösenordslagring?
2Vad är problemet med felmeddelandet "Användaren finns inte"?
3Vilken fördel har WebAuthn/passkeys framför TOTP från en authenticator-app?
Authentication — storage, rate limiting and MFA
Observe
Work through TryHackMe's or PortSwigger's authentication weakness modules. Then consider, conceptually, how a leaked password database is handled: the difference between plaintext storage, outdated hashing, and modern hashing is the difference between a catastrophe, a serious problem, and a manageable incident.
Analyze the root cause
Three separate flaws usually appear together:
- Passwords are stored recoverably — plaintext, reversible encryption, or fast hash functions like MD5 and SHA-1 that are built for speed and therefore easy to work through at scale.
- No cap on login attempts — nothing prevents the same account being tried over and over.
- The password is the only factor — a known password grants full access.
Build the defense
1. Modern password hashing. Use Argon2id (first choice), scrypt or bcrypt. These are deliberately slow and memory-hard, which makes large-scale testing impractical. Salting is handled automatically by the libraries.
Python:
from argon2 import PasswordHasher
from argon2.exceptions import VerifyMismatchError
ph = PasswordHasher()
def register(password: str) -> str:
return ph.hash(password) # salt generated automatically
def verify(stored_hash: str, supplied: str) -> bool:
try:
ph.verify(stored_hash, supplied)
return True
except VerifyMismatchError:
return False
What this code does — line by line
Safe password handling in Python. Hashing means the password is turned into an unreadable string that cannot be turned back. At login, passwords are not compared against passwords but hash against hash.
- from argon2 import PasswordHasher
- Pulls in a ready-made library. You should never write your own crypto code — use what has already been reviewed.
- ph = PasswordHasher()
- Creates a tool for hashing and puts it in the variable ph.
- def register(password: str) -> str:
- Defines a function taking a password and handing back a hash. The text after the colon states that it is text in and text out.
- ph.hash(password)
- Turns the password into a hash. The library automatically adds a random value, called a salt, so two identical passwords still produce different hashes.
- try: / except VerifyMismatchError:
- Try doing this; if it fails in this particular way, do that instead. The library signals a wrong password by raising an error, not by answering no.
- return True / return False
- The function hands back true or false — the password matched or it did not.
Node.js:
const argon2 = require('argon2');
const hash = await argon2.hash(password);
const valid = await argon2.verify(hash, supplied);
What this code does — line by line
The same thing in Node.js, in two lines. The brevity is the point: correct password handling needs little code when the library does the work.
- require('argon2')
- Node.js's way of pulling in a library.
- await argon2.hash(password)
- Creates the hash. await is needed because hashing deliberately takes time.
- argon2.verify(hash, supplied)
- Compares the supplied password against the stored hash and answers true or false.
PHP (uses bcrypt or Argon2 depending on configuration):
$hash = password_hash($password, PASSWORD_DEFAULT);
$valid = password_verify($supplied, $hash);
What this code does — line by line
The same thing in PHP, using functions built into the language. No extra library is needed.
- password_hash($password, PASSWORD_DEFAULT)
- Hashes the password. PASSWORD_DEFAULT means "use whichever method PHP currently considers best", so the code follows along as recommendations change.
- password_verify($supplied, $hash)
- Compares and answers true or false. Note the order: the supplied value first, then the stored one.
Never write your own crypto-related code. Use the libraries above.
2. Rate limiting. Limit both per account and per source address, with escalating delay:
MAX_ATTEMPTS = 5
LOCKOUT_SECONDS = 900 # 15 minutes
def check_lockout(username, ip):
account_key = f"attempts:account:{username}"
ip_key = f"attempts:ip:{ip}"
if redis.get(account_key) and int(redis.get(account_key)) >= MAX_ATTEMPTS:
raise AccountTemporarilyLocked()
if redis.get(ip_key) and int(redis.get(ip_key)) >= MAX_ATTEMPTS * 10:
raise TooManyAttemptsFromAddress()
def record_failure(username, ip):
for key in (f"attempts:account:{username}", f"attempts:ip:{ip}"):
redis.incr(key)
redis.expire(key, LOCKOUT_SECONDS)
What this code does — line by line
Rate limiting in Python. The code counts failed login attempts and temporarily locks when there are too many — both per account and per source address.
- MAX_ATTEMPTS = 5
- A constant: a value used in several places that can be changed in one. Capital letters are a convention signalling the value does not change while running.
- redis
- A fast in-memory store where the counters live. Using a database for this would be needlessly slow.
- f"attempts:account:{username}"
- A format string. The letter f before the quote makes whatever sits in braces be replaced by the variable's value — so the key is unique per user.
- raise AccountTemporarilyLocked()
- Stops and signals an error upward in the program. The login does not continue.
- redis.incr(key)
- Increases the counter by one.
- redis.expire(key, LOCKOUT_SECONDS)
- Tells the counter to delete itself after the given time. That is how the lock releases automatically after fifteen minutes.
- MAX_ATTEMPTS * 10
- The per-address limit is set ten times higher, since many legitimate users can share one address in an office.
3. Uniform error messages. "Incorrect username or password" does not reveal which of the two was wrong. "No such user" lets an outsider map which accounts exist.
4. Multi-factor authentication. TOTP (authenticator app), or preferably WebAuthn/passkeys, which are additionally phishing-resistant because the login is bound to the domain. Offer recovery codes, and treat the recovery flow with the same rigor as the login itself — it is frequently the weakest link.
5. Password policy per modern practice: length over complexity (at least 12 characters), checking against lists of known breached passwords, and no forced periodic rotation without a concrete reason. Forced rotation produces weaker, more predictable passwords in practice.
Verify
Test in your lab that the account actually locks after the configured number of attempts, that the lock releases after the correct interval, that the error message is identical whether or not the account exists, and that the MFA step cannot be skipped by navigating directly to a URL after the password step.
Document
Write a short security review of the authentication flow with one line per control and its status (met / partial / missing). This format is useful in real engagements.
Reflection: Why is a slow hash function an advantage? What does WebAuthn protect against that TOTP does not?
Knowledge check
Pick one answer per question. You get feedback immediately, and the unit is marked complete once every question is answered.
1Why is a slow hash function like Argon2id an advantage for password storage?
2What is the problem with the error message "No such user"?
3What advantage does WebAuthn/passkeys have over TOTP from an authenticator app?