Secure ASP.NET Core application with Entra ID and Traefik

Nell’odierno panorama tecnologico, l’autenticazione degli utenti è un pilastro fondamentale per qualsiasi applicazione web. Con l’aumento della complessità delle esigenze di sicurezza e la necessità di semplificare l’accesso per gli utenti, è essenziale adottare soluzioni di autenticazione affidabili e robuste.

In questo post, esploreremo come sviluppare un’applicazione ASP.NET Core integrata con Entra ID. Vedremo perché questa integrazione è vantaggiosa e come possa semplificare notevolmente il processo di autenticazione per gli utenti.
Ma soprattutto vedremo come “farla funzionare” anche in scenari infrastrutturali più complessi.

Quando si sviluppa un’applicazione web, la gestione dell’autenticazione e dell’autorizzazione è un compito critico. Anziché reinventare la ruota e creare una soluzione di autenticazione personalizzata, integrare un servizio consolidato come Entra ID offre una serie di vantaggi:

  • Sicurezza e affidabilità: Entra ID offre funzionalità di autenticazione avanzate, garantendo la sicurezza dei dati e degli account degli utenti.
  • Risparmio di tempo e risorse: utilizzare un servizio esistente significa evitare lo sviluppo e la manutenzione di un sistema di autenticazione interno.
  • Facilità d’uso per gli utenti: Entra ID offre un’esperienza utente familiare, riducendo la frizione nell’accesso all’applicazione.
  • Scalabilità: con Entra ID, il numero, anche crescente degli utenti, non è più un nostro problema.

Ad oggi è diventato talmente semplice e ben documentato il processo di setup, sia lato Entra ID sia lato applicativo, che lascerei “parlare” link ben autorevoli per guidarvi nel processo di configurazione:

E quindi? Come al solito il problema sta nel dettaglio 🙂

Sulla macchina di sviluppo è tutto semplice, ma quando l’ambiente di produzione presenta una infrastruttura differente da quella che abbiamo in locale, potremmo riscontrare comportamenti inattesi. E questo è proprio quello che può capitare quando la nostra applicazione non è direttamente accessibile, ma le richieste vengono “veicolate” alla nostra applicazione tramite un reverse proxy.

Un reverse proxy è uno strumento essenziale per la gestione di servizi web all’interno di un’infrastruttura. La sua utilità principale risiede nella capacità di instradare le richieste provenienti dai client verso i server appropriati, offrendo diversi vantaggi.

Innanzitutto, il reverse proxy ci permette di non esporre direttamente i nostri endpoint ai client, utilizzandolo come punto di accesso unico. Questo significa che le configurazioni di rete, come il routing delle richieste e la gestione dei certificati TLS, possono essere centralizzate e controllate in modo più efficace.

Inoltre, può fungere da filtro tra i client e gli endpoint, consentendo di applicare regole di sicurezza avanzate come limitazione delle connessioni e protezione da attacchi DDoS e può essere configurato per nascondere i dettagli dell’infrastruttura interna, riducendo così la superficie di attacco per potenziali minacce.

Un’altra peculiarità importante del reverse proxy è la capacità di bilanciare il carico tra i server. Questo significa che può distribuire le richieste in ingresso in modo equo tra più server, migliorando così le prestazioni complessive del sistema e garantendo una maggiore affidabilità attraverso la ridondanza dei server.

Infine, uno dei vantaggi più significativi è la capacità di offrire l’offloading TLS. Questo significa che il reverse proxy può gestire la crittografia e la decrittografia delle connessioni TLS, liberando i server di backend da questo compito, alleggerendo il loro carico computazionale.

E questo è proprio il caso su cui vorrei concentrarmi: gestendo l’offload TLS sul reverse proxy, la richiesta che viene proxata perde lo schema originale (HTTPS) facendo fallire il processo di autenticazione, dato che il return url impostato su Entra ID non corrisponde a quello che “appare” all’applicazione.

Iniziamo a fare setup dell’infrastruttura: nel nostro caso usiamo Traefik come reverse proxy e ci “facciamo aiutare” da docker per il setup:

services:
  traefik:
    image: traefik:v2.11.1
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock"
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.api.rule=Host(`traefik.example.com`)"
      - "traefik.http.routers.api.service=api@internal"

  app:
    build: .
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.demo.entrypoints=websecure"
      - "traefik.http.routers.demo.rule=Host(`demo.example.com`)"
      - "traefik.http.routers.demo.tls=true"
      - "traefik.http.services.demo.loadbalancer.server.port=8080"

Come detto, con questa configurazione, l’autenticazione della nostra applicazione, che acceduta direttamente funzionava alla perfezione, fallisce “miseramente”.

Il problema, come anticipato, dipende dal return url impostato dall’applicazione, come si può notare anche dal link a cui l’applicazione fa redirect per attivare la procedura di login:

https://login.microsoftonline.com/.../oauth2/v2.0/authorize?client_id=...&redirect_uri=http://codiceplastico.example.com/signin-oidc&...

Per risolvere il problema dobbiamo fare in modo che il reverse proxy comunichi all’applicazione, in qualche modo, quale era il protocollo della richiesta originale (https nel nostro caso) e l’applicazione lo usi correttamente.

Tendenzialmente, i reverse proxy utilizzano il meccanismo dei Forwarded Headers per condividere queste, e altre, informazioni.
Traefik non fa eccezione e, senza alcuna modifica alla sua configurazione, le informazioni necessarie per risolvere il problema sono già disponibili lato applicativo.

Dobbiamo quindi configurare ASP.NET Core per riconoscere l’header X-Forwarded-Proto e utilizzarlo per generare l’URL corretto.
In questo caso entra in gioco il middleware ForwardedHeadersMiddleware che si occupa di leggere determinati header e aggiornare i relativi attributi dell’istanza HttpContext associata alla richiesta.
Il middleware aggiorna i valori di:

Per utilizzare questo middleware, bisogna registrare i componenti necessari al suo funzionamento nel container di ASP.NET Core

builder.Services.Configure<ForwardedHeadersOptions>(options =>
{
    options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
    options.KnownNetworks.Add(new IPNetwork(IPAddress.Any, 0)); 
    options.KnownNetworks.Add(new IPNetwork(IPAddress.IPv6Any, 0));
});

e successivamente “attivarlo”

app.UseForwardedHeaders();

ricordandosi che l’ordine è importante: questo middleware deve essere “attivato” prima di qualsiasi altro middleware.

In questo modo l’autenticazione della nostra applicazione, anche se dietro ad un reverse proxy, continua a funzionare nel modo atteso.