Vertalingen: "English" |

Een multi dev container setup voor OAuth 2.0 authenticatie

Delen op:

Deze blog is door een LLM vertaald vanuit het engels.

Introductie

De laatste paar dagen had ik wat problemen met Docker in mijn lokale ontwikkelomgeving, dus ben ik overgestapt op Orb Stack. Tijdens het opnieuw opzetten van mijn devcontainer zag ik wat mogelijkheden voor verbetering en dacht ik: waarom niet delen? In deze gids leer je hoe je een veilige lokale ontwikkelomgeving opzet die OAuth 2.0 authenticatie correct afhandelt, terwijl je ontwikkelworkflow soepel blijft lopen.

Wat heb je nodig?

Voor we beginnen, zorg dat je het volgende hebt geïnstalleerd:

  • Visual Studio Code met de Dev Containers extensie
  • Docker Desktop of een vergelijkbaar alternatief
  • OpenSSL (voor het genereren van self-signed certificaten)
  • Git

De setup en de uitdaging

Ik werk voornamelijk aan onze portal waar alle pagina's/routes authenticatie nodig hebben via onze Identity server. Zonder een geldig token zijn de pagina's niet toegankelijk en kun je de API niet bevragen voor content. We gebruiken hiervoor OAuth 2, en hoewel er andere aanpakken mogelijk zijn, voelde het niet goed om achterdeurtjes te maken voor lokale ontwikkeling. Door dezelfde authenticatie- en autorisatiestructuur in je lokale ontwikkelomgeving te implementeren, word je nooit verrast als de flow in productie niet werkt zoals verwacht.

Dus als ik aan een pagina binnen de portal werk, moet ik als ontwikkelaar ook eerst authenticeren en autorisatie krijgen, net zoals in productie.

Toen ik met dit project begon, heb ik verschillende packages op PyPI bekeken die zouden kunnen helpen met authenticatie, maar de meeste misten goede PKCE-integratie of waren specifiek gemaakt voor platforms zoals Okta en Entra. Dus besloot ik: de beste manier om te leren en echt te begrijpen waar je mee werkt, is om het zelf te maken. Dit betekende dat ik mijn lokale ontwikkelomgeving moest nabootsen zoals deze in productie zou draaien.

Onze portal is gebouwd op het Python Flask framework, dus we gebruiken een devcontainer met Python. We hosten de applicatie in Azure App Services, die onze certificaten en inkomend verkeer afhandelt. Dit betekent dat we ons geen zorgen hoeven te maken over het vernieuwen van certificaten of het afhandelen van HTTPS-verkeer.

Dit is een belangrijke overweging - ik vond de ingebouwde HTTPS-implementatie van Flask niet overtuigend genoeg om te gebruiken. Daarom koos ik ervoor om Flask op HTTP te draaien en op een hoge poort. En omdat we Azure App Service in productie het routeren en de certificaten laten afhandelen, bleef ik met een uitdaging zitten in de lokale omgeving: ik had een container nodig die de certificaten afhandelt en kan worden gebruikt als callback URL voor authenticatie.

Dit was noodzakelijk omdat, als ik een route wil testen die achter een beveiligde route zit (een route waarvoor een gebruiker geauthenticeerd moet zijn), we de OAuth2 met PKCE flow moeten doorlopen.

OAuth2 met PKCE flow

Voordat we in de devcontainer duiken, eerst wat essentiële informatie over OAuth. OAuth 2.0 met PKCE is een autorisatieframework dat applicaties in staat stelt om veilig beperkte toegang te krijgen tot gebruikersaccounts op externe services. Zo werkt het:

  1. Een applicatie genereert een unieke code verifier en bijbehorende code challenge
  2. Bij het doorsturen van gebruikers naar de identity provider voegt je app deze challenge toe, wat bescherming biedt tegen interceptie-aanvallen
  3. Nadat gebruikers zich hebben geauthenticeerd op de authenticatieserver en toestemming hebben gegeven, stuurt de identity provider terug met een autorisatiecode
  4. De applicatie wisselt deze code samen met de originele code verifier uit voor een access token via een beveiligde backend request

Deze PKCE-verbetering voorkomt dat kwaadwillenden de autorisatiecode kunnen onderscheppen en gebruiken, wat het bijzonder geschikt maakt voor native en single-page applicaties. Het resulterende access token kan vervolgens worden gebruikt om geauthenticeerde API-aanroepen te doen namens de gebruiker.

De redirect terug (stap 3) is de cruciale stap. De callback URL moet bekend zijn bij de identity provider, en je lokale systeem moet luisteren en verkeer accepteren voor die specifieke hostnaam. Ik hou er niet van om regelmatig met het hosts-bestand te rommelen, dus hebben we een URL gemaakt die op het lokale systeem kan worden gebruikt (dev.{domain.tld}).

Overzicht van de auth flow:

flowchart TD
    %% Define main containers
    subgraph LocalDev["`**Local-Development-Environment**`"]
        Browser["Browser dev.domain.tld"]
        subgraph Containers["`**Docker Containers**`"]
            direction LR
            Nginx["Nginx Container Port 443 with SSL"]
            Flask["Flask App Container Port 5001"]
        end
    end
    
    subgraph Remote["`**Remote Services**`"]
        IdServer["Identity Server OAuth 2.0 + PKCE"]
        ApiService["API Services"]
    end

    %% Define connections
    Browser --> |"1.HTTPS Request"| Nginx
    Nginx --> |"2.HTTP Forward"| Flask
    Flask --> |"3.Login Redirect"| IdServer
    IdServer --> |"4.Authorization Code"| Browser
    Browser --> |"5.Auth Code"| Nginx
    Nginx --> |"6.Forward Auth"| Flask
    Flask --> |"7.Request Token"| Nginx
    Nginx --> |"8.Forward Token Request"| IdServer
    IdServer --> |"9.Access Token"| Nginx
    Nginx --> |"10.Forward Token"| Flask
    Flask --> |"11.API Request"| ApiService

    %% Styling
    classDef browser fill:#2ecc71,stroke:#27ae60,color:#fff,font-weight:bold
    classDef container fill:#2c3e50,stroke:#3498db,color:#fff,font-weight:bold
    classDef remote fill:#8e44ad,stroke:#9b59b6,color:#fff,font-weight:bold
    
    class Browser browser
    class Nginx,Flask container
    class IdServer,ApiService remote

    %% Container styling with white text
    style Containers fill:#1a1a1a,stroke:#666,stroke-width:2px,stroke-dasharray:5 5,color:#fff
    style LocalDev fill:#1a1a1a,stroke:#666,stroke-width:2px,stroke-dasharray:5 5,color:#fff
    style Remote fill:#1a1a1a,stroke:#666,stroke-width:2px,stroke-dasharray:5 5,color:#fff

    %% Link styling for better visibility
    linkStyle default stroke-width:2px,stroke:#666
  

De Configuratiebestanden

Laten we beginnen. We hebben twee containers nodig: één voor Flask-ontwikkeling en één voor nginx. Visual Studio Code wordt gekoppeld aan de Flask development devcontainer, en nginx luistert op poort 443 en routeert verkeer naar de Flask development server in de devcontainer.

Self-Signed certificaten aanmaken

Voordat we de containers opzetten, moeten we eerst self-signed certificaten genereren voor lokale HTTPS. Zo doe je dat:

 1# Maak een directory voor certificaten
 2mkdir certs
 3cd certs
 4
 5# Genereer self-signed certificaat en key
 6openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
 7  -keyout server.key -out server.crt \
 8  -subj "/CN=localhost"
 9
10# Zet de juiste permissions
11chmod 644 server.crt
12chmod 600 server.key

De Devcontainer

Voor de devcontainer heb ik ervoor gekozen om deze zo licht mogelijk te houden. Alle Python packages worden opnieuw geïnstalleerd elke keer als ik de development server start in de container. Dit zorgt ervoor dat ik lokaal altijd dezelfde packageversies draai als die geïnstalleerd worden tijdens de build bij het pushen naar de acceptatieomgeving.

1FROM mcr.microsoft.com/devcontainers/python:3.12
2
3RUN apt-get update && apt-get install npm -yy
4
5RUN mkdir /workspace
6WORKDIR /workspace
7COPY ./requirements.txt ./requirements.txt
8
9EXPOSE 5001

Nginx container

De nginx container handelt de SSL-terminatie en routing af. Hier is het nginx.conf bestand dat de routing naar de devcontainer regelt:

server {
    listen 443 ssl;
    server_name localhost;

    ssl_certificate /etc/nginx/ssl/server.crt;
    ssl_certificate_key /etc/nginx/ssl/server.key;

    location / {
        proxy_pass http://app:5001;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

De app hostnaam in de proxy_pass wordt opgelost via Docker's interne DNS, die we configureren in het docker-compose bestand.

De Dockerfile voor de nginx container:

1FROM nginx:alpine
2COPY nginx.conf /etc/nginx/conf.d/default.conf
3COPY ./certs/server.crt /etc/nginx/ssl/server.crt
4COPY ./certs/server.key /etc/nginx/ssl/server.key
5ENTRYPOINT ["nginx", "-g", "daemon off;"]

docker-compose.yml

Omdat onze ontwikkelomgeving meerdere containers bevat die met elkaar moeten communiceren, helpt docker-compose.yml ons dit te realiseren:

 1version: '3.8'
 2services:
 3  app:
 4    build:
 5      context: .
 6      dockerfile: ./Dockerfile
 7    image: "flask-oauth-dev:latest"
 8    restart: on-failure
 9    volumes:
10      - .:/workspace
11    ports:
12      - "5001:5001"
13    command: sleep infinity
14    networks:
15      - internalNetwork
16  nginx:
17    build:
18      context: .
19      dockerfile: Dockerfile.nginx
20    restart: on-failure
21    ports:
22      - "443:443"
23    depends_on:
24      - app
25    networks:
26      - internalNetwork
27networks:
28  internalNetwork:

Belangrijke punten in dit compose bestand:

  1. We definiëren twee services (containers)
  2. Beide containers zijn verbonden met het internalNetwork, waardoor ze kunnen communiceren via hun servicenamen als hostnamen
  3. De ports sectie exposeert interne poorten naar externe poorten
  4. De volume mount gebruikt een relatief pad (.:/workspace) voor betere portabiliteit

devcontainer.json

Tot slot, hier is de devcontainer.json die de containers start en VS Code verbindt met de devcontainer:

 1{
 2    "name": "backend",
 3    "mounts": [
 4        "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached"
 5    ],
 6    "dockerComposeFile": "./docker-compose.yml",
 7    "service": "app",
 8    
 9    "workspaceFolder": "/workspace/",
10    
11    "shutdownAction": "stopCompose",
12    
13    "postCreateCommand": [
14        "python -m venv /workspace/.venv && source /workspace/.venv/bin/activate && pip install -r ./requirements.txt"
15    ],
16    "customizations": {
17        "vscode": {
18            "extensions": [
19            ]
20        }
21    }
22}

Belangrijke configuraties in dit bestand:

  1. mounts - koppelt de lokale .ssh map in de container
  2. dockerComposeFile - verwijst naar het docker-compose bestand
  3. service - vertelt VS Code met welke service (container) verbinding moet worden gemaakt
  4. shutdownAction - stopt de containers wanneer VS Code stopt
  5. postCreateCommand - maakt een virtual environment aan in de workspace directory en activeert deze

Probleemoplossing

Hier zijn enkele veelvoorkomende problemen die je kunt tegenkomen en hoe je ze oplost:

  1. Certificaatproblemen

    • Controleer of je certificaten correct zijn gegenereerd en de juiste rechten hebben
    • Check of de certificaatpaden in de nginx-configuratie overeenkomen met je werkelijke bestandslocaties
  2. Verbinding Geweigerd

    • Verifieer of beide containers draaien (docker-compose ps)
    • Controleer of de Flask-applicatie luistert op alle interfaces (0.0.0.0)
    • Zorg ervoor dat het interne netwerk correct is geconfigureerd
  3. OAuth Callback Problemen

    • Bevestig dat je callback URL correct is geregistreerd bij je identity provider
    • Controleer of de nginx-configuratie het callback pad correct doorstuurt

Afrondend

En daar heb je het! Een solide lokale ontwikkelsetup die je productieomgeving spiegelt qua authenticatieflow. Geen gedoe meer met certificaatbeheer of authenticatie-achterdeurtjes.

Ik gebruik deze setup nu al meer dan een maand, en het heeft mijn ontwikkelworkflow een stuk soepeler gemaakt. Het mooiste? Als je je wijzigingen naar acceptatie of productie pusht, kun je er zeker van zijn dat de authenticatieflow exact zo werkt als op je lokale machine.

Nog een paar handige tips uit mijn ervaring:

  • Houd je certificaten in een aparte map en voeg ze toe aan je .gitignore
  • Als je met een team werkt, deel dan de stappen voor het genereren van certificaten in je projectdocumentatie
  • Overweeg om een simpel script te maken om de hele omgeving in één keer op te zetten

Binnenkort deel ik mogelijk hoe we automatische token-verversing en sessiebeheer in deze setup aanpakken - dus blijf kijken als je daarin geïnteresseerd bent! Of hoe je je routes kunt beveiligen met authenticatie en autorisatie.


Reacties

comments powered by Disqus (not working in Firefox)

Ontvang maandelijks een update

* Verplichte velden