Vertalingen: "English" |
Een multi dev container setup voor OAuth 2.0 authenticatie
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:
- Een applicatie genereert een unieke code verifier en bijbehorende code challenge
- Bij het doorsturen van gebruikers naar de identity provider voegt je app deze challenge toe, wat bescherming biedt tegen interceptie-aanvallen
- Nadat gebruikers zich hebben geauthenticeerd op de authenticatieserver en toestemming hebben gegeven, stuurt de identity provider terug met een autorisatiecode
- 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:
- We definiëren twee services (containers)
- Beide containers zijn verbonden met het
internalNetwork
, waardoor ze kunnen communiceren via hun servicenamen als hostnamen - De
ports
sectie exposeert interne poorten naar externe poorten - 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:
mounts
- koppelt de lokale .ssh map in de containerdockerComposeFile
- verwijst naar het docker-compose bestandservice
- vertelt VS Code met welke service (container) verbinding moet worden gemaaktshutdownAction
- stopt de containers wanneer VS Code stoptpostCreateCommand
- 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:
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
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
- Verifieer of beide containers draaien (
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.