Symmetrisk/asymmetrisk kryptering, hashing og autentisering.
Kryptering og autentisering er grunnleggende byggesteiner i moderne IT-sikkerhet. I dette kapittelet skal vi se på hvordan vi praktisk implementerer sikker lagring av passord, bruker HTTPS, og håndterer autentiseringstokens.
Vi går fra teori til praktisk kode, med fokus på biblioteker og teknikker du vil møte som utvikler.
Når en bruker registrerer seg eller endrer passord, skal du hashe passordet før det lagres i databasen. En hash-funksjon er enveisendes - du kan ikke gjenskape det originale passordet fra hashen.
Hva er forskjellen på hashing og kryptering?
- Kryptering: Toveis, kan dekrypteres med nøkkel
- Hashing: Enveis, kan ikke reverseres
Dårlige tilnærminger (IKKE bruk disse):
# 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()Hvorfor er dette usikkert?
1. For raske: Angripere kan teste milliarder av passord per sekund
2. Rainbow tables: Forhåndsgenererte tabeller med hash → passord
3. Ingen salt: Identiske passord gir identiske hasher
Riktig 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!")Hvorfor bcrypt er sikkert:
1. Treg: Tar bevisst lang tid (konfigurerbar "work factor")
2. Salt inkludert: Hver hash er unik selv for identiske passord
3. Adaptive: Kan øke vanskelighetsgrad etter hvert som datamaskiner blir raskere
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 avlyttes
- HTTPS: Kryptert med TLS/SSL
Hva HTTPS beskytter mot:
1. Avlytting: Ingen kan lese dataene underveis
2. Manipulering: Ingen kan endre dataene
3. Identitetsforfalskning: Verifiserer at du snakker med riktig server
Hvordan HTTPS fungerer:
1. Handshake: Klient og server blir enige om krypteringsmetode
2. Sertifikat-validering: Server beviser identitet med digitalt sertifikat
3. Nøkkelutveksling: Genererer symmetriske nøkler for sesjon
4. Kryptert kommunikasjon: All data krypteres 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-sertifikater. 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):
Forteller nettleseren å 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 webapplikasjoner og API-er brukes ofte tokens for autentisering i stedet for sesjoner.
Session-basert vs Token-basert:
Session-basert (tradisjonell):
- Server lagrer sesjonsinformasjon
- Cookie med sesjons-ID sendes til klient
- Server slår opp sesjonsdata for hver forespørsel
Token-basert (moderne):
- All informasjon i selve tokenet
- Ingen server-side sesjon nødvendig
- Fungerer bra i distribuerte systemer og API-er
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 brukes ofte to tokens:
- Access token: Kort levetid (15 min), brukes for API-kall
- Refresh token: Lang levetid (30 dager), brukes 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 en standard for å gi applikasjoner tilgang til brukerdata uten å dele passord. Du har sikkert sett "Logg inn med Google" eller "Logg inn med Facebook".
Hvordan OAuth fungerer:
1. Bruker klikker "Logg inn med Google"
2. Din app omdirigerer til Google
3. Bruker logger inn på Google og godkjenner tilgang
4. Google omdirigerer tilbake med autorisasjonskode
5. Din app bytter kode mot access token
6. Din app bruker token til å hente brukerinfo fra Google
Fordeler:
- Bruker slipper å lage nytt passord
- Din app lagrer ikke passord
- Bruker kan tilbakekalle tilgang når som helst
- Mindre ansvar for deg som utvikler
OAuth-flyt (forenklet):
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 sikkerhetshensyn med OAuth:
1. Bruk HTTPS: Alltid i produksjon
2. Valider state parameter: Beskyttelse mot CSRF
3. Lagre ikke access tokens i localStorage: Bruk httpOnly cookies
4. Begrens scope: Be kun om de tilgangene du trenger
Her er et 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 (f.eks. bcrypt), aldri klartekst.
- Usikre algoritmer: MD5 og SHA-1 bør ikke brukes.
- Cost-faktor: høyere gir tregere og sikrere hashing.
- HTTPS og sertifikater: sikrer kommunikasjon (Certbot).
- Tokens og OAuth: trygg autentisering.
Noekkelbegreper
| Begrep | Forklaring |
|---|---|
| Hashing | Enveis funksjon for passordlagring |
| Salt | Tilfeldig verdi som styrker 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.