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

Regel nummer én: Aldri lagre passord i klartekst!

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

- 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.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):

Forteller nettleseren å 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 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_id

Refresh 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

✏️Eksempel: Komplett autentiseringssystem

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


BegrepForklaring
HashingEnveis funksjon for passordlagring
SaltTilfeldig verdi som styrker 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.