Symmetrisk/asymmetrisk kryptering, hashing og autentisering.
Kryptering og autentisering er grunnleggjande byggjesteinar i moderne IT-tryggleik. I dette kapittelet skal vi sjå på korleis vi praktisk implementerer sikker lagring av passord, bruker HTTPS, og handterer autentiseringstokens.
Vi går frå teori til praktisk kode, med fokus på bibliotek og teknikkar du vil møte som utviklar.
Når ein brukar registrerer seg eller endrar passord, skal du hashe passordet før det blir lagra i databasen. Ein hash-funksjon er einvegs - du kan ikkje gjenskape det opphavlege passordet frå hashen.
Kva er skilnaden på hashing og kryptering?
- Kryptering: Tovegs, kan dekrypterast med nøkkel
- Hashing: Einvegs, kan ikkje reverserast
Dårlege tilnærmingar (IKKJE bruk desse):
# USIKKERT - ikke bruk MD5 eller SHA-1
import hashlib
hash = hashlib.md5(passord.encode()).hexdigest()
# USIKKERT - ikke bruk SHA-256 uten salt
hash = hashlib.sha256(passord.encode()).hexdigest()Kvifor er dette usikkert?
1. For raske: Angriparar kan teste milliardar av passord per sekund
2. Rainbow tables: Førehandsgenererte tabellar med hash → passord
3. Ingen salt: Identiske passord gir identiske hashar
Rett tilnærming: bcrypt
import bcrypt
# Registrering: Hash passord
passord = "mitt_hemmelige_passord"
salt = bcrypt.gensalt() # Generer tilfeldig salt
hash = bcrypt.hashpw(passord.encode(), salt)
# Lagre hash i database (ikke passordet!)
lagre_i_database(brukernavn, hash)
# Innlogging: Verifiser passord
innlogget_passord = "mitt_hemmelige_passord"
hash_fra_database = hent_fra_database(brukernavn)
if bcrypt.checkpw(innlogget_passord.encode(), hash_fra_database):
print("Riktig passord!")
else:
print("Feil passord!")Kvifor bcrypt er sikkert:
1. Treg: Tek medvite lang tid (konfigurerbar "work factor")
2. Salt inkludert: Kvar hash er unik sjølv for identiske passord
3. Adaptiv: Kan auke vanskegrad etter kvart som datamaskinar blir raskare
Eksempel med kostnads-faktor:
# Høyere cost = tregere, men sikrere
# Standard: 12 (2^12 iterasjoner = 4096)
# Øk med 1 for hver dobling av tid
salt = bcrypt.gensalt(rounds=14) # Tregere, sikrere
hash = bcrypt.hashpw(passord.encode(), salt)- HTTP: Kommunikasjon i klartekst, kan avlyttast
- HTTPS: Kryptert med TLS/SSL
Kva HTTPS vernar mot:
1. Avlytting: Ingen kan lese dataa undervegs
2. Manipulering: Ingen kan endre dataa
3. Identitetsforfalsking: Verifiserer at du snakkar med rett tenar
Korleis HTTPS fungerer:
1. Handshake: Klient og tenar blir samde om krypteringsmetode
2. Sertifikat-validering: Tenar beviser identitet med digitalt sertifikat
3. Nøkkelutveksling: Genererer symmetriske nøklar for sesjon
4. Kryptert kommunikasjon: All data blir kryptert med sesjonsnøkkel
Implementere HTTPS i Flask:
# For utvikling: Selvskrevet sertifikat
from flask import Flask
app = Flask(__name__)
if __name__ == '__main__':
# Genererer selvskrevet sertifikat
app.run(ssl_context='adhoc', debug=True)For produksjon: Let's Encrypt
Let's Encrypt gir gratis SSL-sertifikat. Med Certbot:
# Installer Certbot
sudo apt install certbot python3-certbot-nginx
# Få sertifikat
sudo certbot --nginx -d eksempel.noTvinge HTTPS:
from flask import Flask, redirect, request
app = Flask(__name__)
@app.before_request
def redirect_to_https():
if not request.is_secure and not app.debug:
url = request.url.replace('http://', 'https://', 1)
return redirect(url, code=301)HTTP Strict Transport Security (HSTS):
Fortel nettlesaren å ALDRI bruke HTTP, kun HTTPS:
@app.after_request
def set_hsts(response):
response.headers['Strict-Transport-Security'] = \
'max-age=31536000; includeSubDomains'
return responseSikre cookies:
# Sett cookies kun over HTTPS
response.set_cookie(
'sesjon_id',
value=sesjon_token,
secure=True, # Kun send over HTTPS
httponly=True, # Ikke tilgjengelig for JavaScript
samesite='Lax' # CSRF-beskyttelse
)For moderne webapplikasjonar og API-ar blir det ofte brukt tokens for autentisering i staden for sesjonar.
Session-basert vs Token-basert:
Session-basert (tradisjonell):
- Tenar lagrar sesjonsinformasjon
- Cookie med sesjons-ID blir sendt til klient
- Tenar slår opp sesjonsdata for kvar førespurnad
Token-basert (moderne):
- All informasjon i sjølve tokenet
- Ingen tenarside sesjon nødvendig
- Fungerer bra i distribuerte system og API-ar
Generere sikre tokens:
import secrets
# Generer kryptografisk sikker tilfeldig token
token = secrets.token_urlsafe(32) # 32 bytes = 256 bits
print(token) # Eksempel: "Xy3K9f2Lm_4Nq7Rw5Ts..."
# Lagre i database med bruker-ID
lagre_token(bruker_id, token)Bruke token for autentisering:
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route('/api/profil')
def hent_profil():
# Les token fra header
token = request.headers.get('Authorization')
if not token:
return jsonify({'error': 'Mangler token'}), 401
# Fjern "Bearer " prefix hvis det finnes
if token.startswith('Bearer '):
token = token[7:]
# Valider token
bruker = finn_bruker_fra_token(token)
if not bruker:
return jsonify({'error': 'Ugyldig token'}), 401
# Token er gyldig
return jsonify({
'brukernavn': bruker.brukernavn,
'epost': bruker.epost
})Token expiry (utløpstid):
from datetime import datetime, timedelta
def opprett_token(bruker_id):
token = secrets.token_urlsafe(32)
expires = datetime.now() + timedelta(hours=24)
lagre_token(bruker_id, token, expires)
return token
def valider_token(token):
token_data = hent_token_data(token)
if not token_data:
return None
if datetime.now() > token_data.expires:
# Token har utgått
slett_token(token)
return None
return token_data.bruker_idRefresh tokens:
For lengre økter blir det ofte brukt to tokens:
- Access token: Kort levetid (15 min), brukt for API-kall
- Refresh token: Lang levetid (30 dagar), brukt til å få ny access token
def login(brukernavn, passord):
# Valider kredentialer
bruker = verifiser_bruker(brukernavn, passord)
if not bruker:
return None
# Generer tokens
access_token = generer_access_token(bruker.id) # 15 min
refresh_token = generer_refresh_token(bruker.id) # 30 dager
return {
'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': 900 # 15 minutter i sekunder
}
@app.route('/api/refresh', methods=['POST'])
def refresh():
refresh_token = request.json.get('refresh_token')
bruker_id = valider_refresh_token(refresh_token)
if not bruker_id:
return jsonify({'error': 'Ugyldig refresh token'}), 401
# Generer ny access token
ny_access_token = generer_access_token(bruker_id)
return jsonify({
'access_token': ny_access_token,
'expires_in': 900
})OAuth 2.0 er ein standard for å gi applikasjonar tilgang til brukardata utan å dele passord. Du har sikkert sett "Logg inn med Google" eller "Logg inn med Facebook".
Korleis OAuth fungerer:
1. Brukar klikkar "Logg inn med Google"
2. Appen din omdirigerer til Google
3. Brukar loggar inn på Google og godkjenner tilgang
4. Google omdirigerer tilbake med autorisasjonskode
5. Appen din byter kode mot access token
6. Appen din bruker token til å hente brukarinfo frå Google
Fordelar:
- Brukaren slepp å lage nytt passord
- Appen din lagrar ikkje passord
- Brukaren kan tilbakekalle tilgang når som helst
- Mindre ansvar for deg som utviklar
OAuth-flyt (forenkla):
from flask import Flask, redirect, request, session
import requests
app = Flask(__name__)
app.secret_key = 'din-hemmelige-nøkkel'
# Registrer app hos Google Cloud Console
CLIENT_ID = 'din-google-client-id'
CLIENT_SECRET = 'din-google-client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'
@app.route('/login')
def login():
# Steg 1: Omdiriger til Google
google_auth_url = (
'https://accounts.google.com/o/oauth2/v2/auth'
f'?client_id={CLIENT_ID}'
f'&redirect_uri={REDIRECT_URI}'
'&response_type=code'
'&scope=openid email profile'
)
return redirect(google_auth_url)
@app.route('/callback')
def callback():
# Steg 2: Motta autorisasjonskode
code = request.args.get('code')
# Steg 3: Bytt kode mot token
token_response = requests.post(
'https://oauth2.googleapis.com/token',
data={
'code': code,
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'redirect_uri': REDIRECT_URI,
'grant_type': 'authorization_code'
}
)
token_data = token_response.json()
access_token = token_data['access_token']
# Steg 4: Bruk token til å hente brukerinfo
userinfo_response = requests.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
headers={'Authorization': f'Bearer {access_token}'}
)
bruker = userinfo_response.json()
# Lagre brukerinfo i sesjon
session['bruker'] = {
'email': bruker['email'],
'navn': bruker['name']
}
return redirect('/dashboard')Viktige tryggleiksomsyn med OAuth:
1. Bruk HTTPS: Alltid i produksjon
2. Valider state parameter: Vern mot CSRF
3. Lagre ikkje access tokens i localStorage: Bruk httpOnly cookies
4. Avgrens scope: Be kun om dei tilgangane du treng
Her er eit komplett eksempel på autentisering med bcrypt, sikre tokens og HTTPS:
from flask import Flask, request, jsonify, make_response
import bcrypt
import secrets
from datetime import datetime, timedelta
import sqlite3
app = Flask(__name__)
def get_db():
conn = sqlite3.connect('brukere.db')
conn.row_factory = sqlite3.Row
return conn
# ===== REGISTRERING =====
@app.route('/api/register', methods=['POST'])
def register():
data = request.get_json()
# Valider input
brukernavn = data.get('brukernavn', '').strip()
passord = data.get('passord', '')
if len(brukernavn) < 3:
return jsonify({'error': 'Brukernavn må være minst 3 tegn'}), 400
if len(passord) < 8:
return jsonify({'error': 'Passord må være minst 8 tegn'}), 400
# Hash passord
salt = bcrypt.gensalt()
passord_hash = bcrypt.hashpw(passord.encode(), salt)
# Lagre i database
db = get_db()
try:
db.execute(
'INSERT INTO brukere (brukernavn, passord_hash) VALUES (?, ?)',
(brukernavn, passord_hash)
)
db.commit()
except sqlite3.IntegrityError:
return jsonify({'error': 'Brukernavn allerede i bruk'}), 409
finally:
db.close()
return jsonify({'message': 'Bruker opprettet'}), 201
# ===== INNLOGGING =====
@app.route('/api/login', methods=['POST'])
def login():
data = request.get_json()
brukernavn = data.get('brukernavn')
passord = data.get('passord')
# Hent bruker fra database
db = get_db()
bruker = db.execute(
'SELECT * FROM brukere WHERE brukernavn = ?',
(brukernavn,)
).fetchone()
db.close()
# Sjekk om bruker eksisterer og passord er riktig
if not bruker or not bcrypt.checkpw(
passord.encode(),
bruker['passord_hash']
):
# Samme feilmelding for begge tilfeller (sikkerhet)
return jsonify({'error': 'Ugyldig brukernavn eller passord'}), 401
# Generer tokens
access_token = secrets.token_urlsafe(32)
refresh_token = secrets.token_urlsafe(32)
# Lagre tokens med utløpstid
db = get_db()
access_expires = datetime.now() + timedelta(minutes=15)
refresh_expires = datetime.now() + timedelta(days=30)
db.execute(
'''
INSERT INTO tokens (bruker_id, access_token, refresh_token,
access_expires, refresh_expires)
VALUES (?, ?, ?, ?, ?)
''',
(bruker['id'], access_token, refresh_token,
access_expires, refresh_expires)
)
db.commit()
db.close()
# Returner tokens
response = make_response(jsonify({
'access_token': access_token,
'refresh_token': refresh_token,
'expires_in': 900
}))
# Sett sikker cookie med refresh token
response.set_cookie(
'refresh_token',
value=refresh_token,
max_age=30*24*60*60, # 30 dager
secure=True, # Kun HTTPS
httponly=True, # Ikke JavaScript-tilgjengelig
samesite='Strict' # CSRF-beskyttelse
)
return response
# ===== BESKYTT RUTE =====
def krever_autentisering(f):
"""Decorator for å kreve gyldig token"""
def wrapper(*args, **kwargs):
auth_header = request.headers.get('Authorization')
if not auth_header or not auth_header.startswith('Bearer '):
return jsonify({'error': 'Mangler autentisering'}), 401
token = auth_header[7:] # Fjern "Bearer "
# Valider token
db = get_db()
token_data = db.execute(
'''
SELECT t.*, b.brukernavn
FROM tokens t
JOIN brukere b ON t.bruker_id = b.id
WHERE t.access_token = ? AND t.access_expires > ?
''',
(token, datetime.now())
).fetchone()
db.close()
if not token_data:
return jsonify({'error': 'Ugyldig eller utgått token'}), 401
# Legg til bruker i request
request.bruker = token_data
return f(*args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
@app.route('/api/profil')
@krever_autentisering
def profil():
return jsonify({
'brukernavn': request.bruker['brukernavn']
})
# ===== REFRESH TOKEN =====
@app.route('/api/refresh', methods=['POST'])
def refresh():
refresh_token = request.cookies.get('refresh_token')
if not refresh_token:
return jsonify({'error': 'Mangler refresh token'}), 401
# Valider refresh token
db = get_db()
token_data = db.execute(
'SELECT * FROM tokens WHERE refresh_token = ? AND refresh_expires > ?',
(refresh_token, datetime.now())
).fetchone()
if not token_data:
return jsonify({'error': 'Ugyldig refresh token'}), 401
# Generer ny access token
ny_access_token = secrets.token_urlsafe(32)
ny_expires = datetime.now() + timedelta(minutes=15)
db.execute(
'UPDATE tokens SET access_token = ?, access_expires = ? WHERE id = ?',
(ny_access_token, ny_expires, token_data['id'])
)
db.commit()
db.close()
return jsonify({
'access_token': ny_access_token,
'expires_in': 900
})
if __name__ == '__main__':
app.run(ssl_context='adhoc', debug=False)Oppsummering
I dette kapittelet har du lært:
- Passordlagring: hash med salt (t.d. bcrypt), aldri klartekst.
- Usikre algoritmar: MD5 og SHA-1 bør ikkje brukast.
- Cost-faktor: høgare gir tregare og sikrare hashing.
- HTTPS og sertifikat: sikrar kommunikasjon (Certbot).
- Tokens og OAuth: trygg autentisering.
Nøkkelomgrep
| Omgrep | Forklaring |
|---|---|
| Hashing | Einvegs funksjon for passordlagring |
| Salt | Tilfeldig verdi som styrkjer passord-hash |
| bcrypt | Sikker passord-hashing-algoritme |
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.