Tilbake
7.3
Kryptering og autentisering i praksis

7.3 Kryptering og autentisering i praksis

Symmetrisk/asymmetrisk kryptering, hashing og autentisering.

60 min
6 oppgaver
KrypteringHashingAutentiseringTLS
Du leser den tradisjonelle versjonen
Din fremgang i kapitlet
0 / 6 oppgaver

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.

Regel nummer éin: Aldri lagre passord i klartekst!

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 vs HTTPS:

- 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.no

Tvinge 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 response

Sikre 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_id

Refresh 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

✏️Eksempel: Komplett autentiseringssystem

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


OmgrepForklaring
HashingEinvegs funksjon for passordlagring
SaltTilfeldig verdi som styrkjer passord-hash
bcryptSikker 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.