---
title: "OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia"
description: "Come si valida davvero un access token Zitadel: firma via JWKS, issuer, il projectId nell'audience, private_key_jwt al posto del client secret e il retry sulla rotazione chiavi."
canonical: "https://www.mauriziofonte.it/blog/post/zitadel-oauth2-token-credenziale-project-audience.html"
author: "Maurizio Fonte"
date: "2026-08-03T15:33:58+02:00"
modified: "2026-08-03T15:33:58+02:00"
section: "IAM & SSO"
tags: ["IAM & SSO", "API Design"]
license: "Riuso e citazione consentiti con attribuzione e link alla fonte"
---

[IAM & SSO](https://www.mauriziofonte.it/blog/cat/iam-sso.html) [API Design](https://www.mauriziofonte.it/blog/cat/api-design.html)

# [OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia](https://www.mauriziofonte.it/blog/post/zitadel-oauth2-token-credenziale-project-audience.html)

Lunedì 3 Agosto 2026, alle 15:33 ![OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia](https://www.mauriziofonte.it/image/005500200-zitadel-oauth2-token-credenziale-project-audience.jpg)

C'è un momento preciso, in ogni integrazione fra due applicazioni, in cui qualcuno propone di mettere una chiave API condivisa in un header. È una proposta ragionevole, si implementa in mezz'ora e funziona. Poi quella chiave finisce in un file di configurazione su due macchine, in un gestore di segreti se va bene, nel repository se va male, e da quel momento **la sicurezza dell'integrazione è la sicurezza del posto meno protetto in cui quella stringa è stata copiata**. Nell'impianto da cui viene questa serie ho trovato l'evidenza di quanto quel meccanismo invecchi male: esisteva un sistema di chiavi API per utente, previsto e implementato anni prima, con cinque utenti abilitati e **zero chiavi effettivamente in uso**. Era stato costruito, non era mai stato adottato, ed era rimasto lì.

Nei cinque articoli precedenti ho costruito il contesto: [il modello di organizzazioni, progetti e grant](https://www.mauriziofonte.it/blog/post/zitadel-oidc-organizzazioni-progetti-grant-audience.html), [il confronto con Keycloak](https://www.mauriziofonte.it/blog/post/zitadel-vs-keycloak-identity-provider-self-hosted-pmi.html) e [quello con i servizi gestiti](https://www.mauriziofonte.it/blog/post/zitadel-self-hosted-vs-auth0-okta-sovranita-costi.html), poi [l'installazione nativa su Debian](https://www.mauriziofonte.it/blog/post/zitadel-produzione-debian-senza-docker.html) e [l'alta disponibilità con aggiornamenti senza fermo](https://www.mauriziofonte.it/blog/post/zitadel-alta-disponibilita-upgrade-zero-downtime.html). Da qui in avanti si entra nel codice delle applicazioni, e il primo concetto da mettere a fuoco è quello che rende superfluo il segreto condiviso: **il token è la credenziale, e prova chi sta chiamando meglio di qualunque chiave che tu possa distribuire**.

## Cosa significa esattamente che il token è la credenziale

Significa che l'identità di chi sta chiamando **non è più un parametro della richiesta**. Nel modello con la chiave condivisa, il chiamante dice "sono io, ecco la chiave, e sto agendo per conto dell'utente 4711": il numero dell'utente è un dato che il chiamante scrive, e il ricevente si fida perché la chiave era giusta. Chiunque abbia la chiave può scrivere qualunque numero.

Nel modello a token, il chiamante presenta l'access token che l'identity provider ha emesso per quell'utente, e il ricevente ne estrae il soggetto **dopo aver verificato la firma**. Non c'è nessun campo in cui dichiarare per conto di chi stai agendo: l'identità è dentro la struttura firmata, e alterarla richiederebbe la chiave privata dell'identity provider.

La differenza è più profonda di quanto sembri e vale la pena esplicitarla, perché è ciò che convince chi deve approvare il lavoro. Con la chiave condivisa, **la compromissione del chiamante è la compromissione di tutti gli utenti**: chi ruba la chiave può impersonare chiunque. Con il token, la compromissione del chiamante espone i token che quel chiamante detiene in quel momento, per gli utenti che stanno usando il servizio, e per la durata residua di quei token. È un raggio d'azione limitato nel tempo e nell'ampiezza, e nella valutazione del rischio è una differenza di categoria, non di grado.

> La riformulazione che uso quando devo spiegarlo a chi non scrive codice: una chiave API è **una password che due sistemi si scambiano**, e vale finché qualcuno non la cambia. Un token è **un documento firmato con una scadenza**, e chi lo riceve può verificarne l'autenticità senza chiedere niente a nessuno.

## Perché in questa architettura non esiste nessun client secret?

Perché è stato sostituito da una firma. Il segreto simmetrico del client OAuth2 qui non esiste: al suo posto c'è **`private_key_jwt`**, il meccanismo descritto dalla [RFC 7523](https://datatracker.ietf.org/doc/html/rfc7523), in cui il client si autentica verso l'identity provider **firmando un'asserzione** con la propria chiave privata invece di presentare una stringa che entrambi conoscono.

La differenza operativa è che **la chiave privata non lascia mai il server**. Non viaggia nelle richieste, non finisce nei log di un proxy, non si può copiare guardando una variabile d'ambiente in un dump. L'identity provider conosce solo la parte pubblica.

L'asserzione è un oggetto piccolo e con una vita brevissima. Questa è la costruzione nel gestionale in PHP, e vale la pena leggere i campi uno per uno:

```php
$payload = [
    'iss' => $this->clientId,
    'sub' => $this->clientId,
    'aud' => $audience,                     // scheme://host dell'identity provider
    'jti' => bin2hex(random_bytes(16)),     // identificatore univoco, anti-replay
    'iat' => $now,
    'nbf' => $now,
    'exp' => $now + 300,                    // cinque minuti, non di più
];

$headers = ['alg' => 'RS256', 'typ' => 'JWT', 'kid' => $this->keyId];

return JWT::encode($payload, $privateKey, 'RS256', $this->keyId, $headers);
```

Emittente e soggetto sono entrambi l'identificativo del client, perché il client sta dichiarando qualcosa **su sé stesso**. L'audience è l'identity provider, perché è a lui che stai parlando. Il `jti` casuale e la scadenza a cinque minuti servono insieme: anche se qualcuno intercettasse l'asserzione, avrebbe una finestra brevissima e un identificativo già consumato. Il `kid` nell'intestazione dice all'identity provider **quale** delle chiavi registrate per questo client deve usare per verificare, ed è ciò che rende possibile ruotare le chiavi senza fermare niente.

Sul lato Node dell'integrazione la stessa cosa è implementata con il modulo crittografico nativo della piattaforma, senza aggiungere dipendenze, e produce un'asserzione identica:

```javascript
const header  = { alg: 'RS256', typ: 'JWT', kid: privateKeyId };
const payload = {
  iss: clientId, sub: clientId, aud: audience,
  jti: crypto.randomBytes(16).toString('hex'),
  iat: now, nbf: now, exp: now + 300,
};

const signingInput = `${b64url(JSON.stringify(header))}.${b64url(JSON.stringify(payload))}`;
const signature = crypto.createSign('RSA-SHA256').update(signingInput).sign(privateKey);
return `${signingInput}.${b64url(signature)}`;
```

Il fatto che due applicazioni scritte in linguaggi diversi producano la stessa cosa in venti righe ciascuna è il miglior argomento a favore di questo meccanismo: **non è complicato, è solo poco raccontato**.

## La richiesta del token, e lo scope che decide tutto

Perché il token serva a chiamare un'altra applicazione, va chiesto con lo scope giusto. È l'unico punto in cui un errore non produce un messaggio comprensibile, e infatti è il punto in cui l'ho sbagliato la prima volta:

```javascript
const scopeFor = ({ orgId, projectId }) => {
  const scopes = [
    'openid', 'profile', 'email',
    'urn:zitadel:iam:user:metadata',
    'urn:zitadel:iam:user:resourceowner',
    `urn:zitadel:iam:org:id:${orgId}`,
  ];
  if (projectId) scopes.push(`urn:zitadel:iam:org:project:id:${projectId}:aud`);
  return scopes.join(' ');
};
```

L'ultima riga è quella che conta. Senza di essa il token arriva, è perfettamente valido, l'utente è autenticato e **ogni chiamata all'altra applicazione viene respinta con un errore di audience**. Il token è giusto per il client che lo ha chiesto ed è sbagliato per il destinatario, e questo dalla parte di chi riceve si legge come un rifiuto secco.

Vale la pena raccontare come l'ho scoperto, perché è istruttivo: avevo copiato la costruzione dello scope dal codice di autenticazione dell'altra applicazione, che quello scope **non ne ha bisogno** perché non chiama nessuno. Copiare un pezzo di codice corretto in un contesto in cui manca un requisito è un errore che non si vede in revisione, e la lezione che ne ho tratto è finita in un test: **quello che va verificato non è solo il token che ricevi, è lo scope che chiedi.**

## La validazione locale, riga per riga

Dal lato di chi riceve, il lavoro è verificare che il token sia autentico, valido e destinato a lui. Senza chiamare nessuno.

```php
// firma verificata con le chiavi pubbliche dell'IdP; scadenze gestite dalla libreria
try {
    $token = JWT::decode($jwt, JWK::parseKeySet($this->getCachedJwks()));
} catch (\Firebase\JWT\ExpiredException $e) {
    throw new IamAuthenticationException(IamAuthenticationException::TOKEN_EXPIRED, [...]);
} catch (\Exception $e) {
    // riprova UNA volta con le chiavi appena scaricate: potrebbe essere una rotazione
    try {
        $token = JWT::decode($jwt, JWK::parseKeySet($this->getCachedJwks(false)));
    } catch (\Firebase\JWT\ExpiredException $e2) {
        throw new IamAuthenticationException(IamAuthenticationException::TOKEN_EXPIRED, [...]);
    } catch (\Exception $e2) {
        throw new IamAuthenticationException(IamAuthenticationException::SIGNATURE_VERIFICATION_FAILED, [...]);
    }
}

// emittente: confronto sull'host, tollerante alla barra finale
if (!isset($token->iss) || strtolower((string) parse_url($this->authUrl, PHP_URL_HOST))
        !== strtolower((string) parse_url($token->iss, PHP_URL_HOST))) {
    throw new IamAuthenticationException(IamAuthenticationException::INVALID_ISSUER);
}

// audience: deve contenere il projectId, non il clientId
$aud = isset($token->aud) ? (is_array($token->aud) ? $token->aud : [$token->aud]) : [];
if (!in_array((string) $projectId, array_map('strval', $aud), true)) {
    throw new IamAuthenticationException(IamAuthenticationException::INVALID_AUDIENCE, [...]);
}
```

Quattro dettagli meritano attenzione, perché sono quelli che distinguono una validazione che regge da una che sembra reggere.

**Il retry sulla rotazione delle chiavi, che non ritenta mai un token scaduto.** Un fallimento di firma può voler dire due cose molto diverse: il token è falso, oppure l'identity provider ha ruotato le chiavi e la copia in cache è vecchia. Riscaricare le chiavi e ritentare una volta risolve il secondo caso senza rendere il sistema fragile. La sottigliezza è la doppia intercettazione della scadenza: **un token scaduto non viene mai ritentato**, perché sarebbe scaduto anche con le chiavi nuove e l'unico effetto sarebbe una richiesta di rete inutile a ogni token vecchio che arriva. Senza quella distinzione, un client con l'orologio sbagliato diventerebbe un piccolo attacco di amplificazione contro il tuo identity provider.

**Il confronto sull'emittente fatto sull'host.** Le implementazioni divergono sulla barra finale dell'URL, e un confronto fra stringhe complete produce un rifiuto che nessuno riesce a spiegare. Confrontare l'host è più permissivo di quanto la specifica richieda in senso stretto, ed è una scelta deliberata: la protezione reale contro un emittente ostile è che la firma non tornerebbe comunque.

**L'audience normalizzata ad array.** Il claim può arrivare come stringa singola o come lista, a seconda di quante audience ci sono. Trattarlo sempre come lista elimina una classe intera di errori che si manifestano solo quando un token ne ha una sola.

**Il confronto stretto sul valore.** Gli identificativi sono numeri lunghi trasportati come stringhe, e un confronto permissivo fra tipi diversi in PHP è il tipo di cosa che funziona per anni e poi produce una sorpresa. La conversione esplicita a stringa e il confronto stretto costano una riga.

Se stai integrando due applicazioni attraverso un identity provider e vuoi che la validazione dei token la riveda qualcuno che l'ha già scritta per un sistema in produzione, nel [mio profilo professionale](https://www.mauriziofonte.it/pages/about) trovi il percorso su architetture API, OAuth2 e sicurezza applicativa. E se hai un token che ti sta arrivando e non capisci perché venga rifiutato, il mio [strumento di audit dei JWT](https://www.mauriziofonte.it/strumenti/audit-jwt) ti mostra `aud`, `iss` e le scadenze in due secondi: nella maggior parte dei casi la risposta è lì.

## Quali claim guardi davvero, e in che ordine

Un access token in questa architettura arriva con parecchi campi, ma quelli su cui si prendono decisioni sono pochi. Metterli in tabella aiuta chi deve scrivere il controllo e chi deve verificarlo in fase di revisione.

| Claim | Cosa contiene | Perché lo guardi |
| --- | --- | --- |
| iss | l'identity provider che ha emesso il token | rifiuti token emessi da un'altra istanza, anche se firmati bene |
| aud | qui l'identificativo del progetto | è il controllo che stabilisce che il token era destinato a te |
| sub | l'identificativo dell'utente presso l'identity provider | è l'unica cosa che usi per risolvere l'utente applicativo |
| exp | scadenza | la libreria la applica, tu decidi la tolleranza |
| nbf, iat | non prima di, emesso il | proteggono da token emessi nel futuro per orologi disallineati |

Il campo su cui insisto è `sub`, perché è quello che viene usato male più spesso. È **l'identificativo dell'utente presso l'identity provider**, non l'indirizzo di posta e non il nome utente. La tentazione di risolvere l'utente locale con l'indirizzo di posta è forte, perché è leggibile e sembra stabile: non lo è. Una persona cambia indirizzo, due persone possono averne uno simile, e un indirizzo può essere riassegnato. Il `sub` è opaco, brutto da leggere e non cambia mai, che è esattamente ciò che serve a una chiave di collegamento.

Sulla tolleranza vale la pena spendere una riga, perché è una di quelle impostazioni che si mettono a caso. Una piccola tolleranza sulle scadenze, nell'ordine di qualche decina di secondi, assorbe il disallineamento fisiologico degli orologi fra macchine diverse. Alzarla molto è una pessima idea: significa **accettare token scaduti**, e la finestra che apri è esattamente il valore che hai scritto. La soluzione corretta al problema degli orologi non è alzare la tolleranza, è sincronizzare gli orologi.

## Cosa succede quando il token scade, e perché è una buona notizia

Un access token dura poco per progetto. È la caratteristica che rende accettabile il fatto di non poterlo revocare istantaneamente quando lo validi in locale: **il danno di un token rubato è limitato dalla sua scadenza**, e più è corta più il modello regge.

La conseguenza pratica per chi scrive il client è che va gestito il caso del token scaduto **durante** l'uso, non solo all'inizio. Il pattern che uso è banale e va scritto una volta sola in un punto solo: la chiamata verso l'altra applicazione riconosce la risposta di non autorizzato, rinnova il token e ritenta una volta. Se il rinnovo fallisce, l'utente torna al login. Quello che non va fatto è ritentare in ciclo, che è il modo più efficace di trasformare una credenziale scaduta in un piccolo attacco contro il proprio identity provider.

C'è una scelta di design che vale la pena dichiarare perché ha conseguenze sulla sicurezza: **dove tieni il token**. Nell'integrazione da cui viene questa serie il token vive **nella sessione lato server** dell'applicazione che lo ha ottenuto, e non raggiunge mai il browser. Il browser ha solo un cookie di sessione. È una scelta che chiude in partenza una categoria intera di problemi, quella dei token esfiltrabili con codice iniettato nella pagina, e che si può fare solo perché entrambe le applicazioni hanno un server. Un'applicazione a pagina singola che parla direttamente con l'identity provider non ha questo lusso, ed è la ragione per cui in quel contesto il flusso con verifica del codice diventa obbligatorio invece che consigliato.

## Cosa cambia per chi deve approvare questa architettura

Tradotto per chi la decisione la firma, e non la scrive.

**La superficie di segreti da custodire si riduce, non aumenta.** Al posto di una chiave condivisa fra due sistemi, che vive in due posti e va ruotata in due posti contemporaneamente, ci sono due chiavi private che **non vengono mai scambiate** e che si ruotano indipendentemente. La rotazione di una non richiede una finestra di manutenzione coordinata, che è il motivo per cui le chiavi condivise nella pratica non vengono quasi mai ruotate.

**La revoca cambia natura, e va capita.** Con una chiave condivisa, revocare significa cambiare la stringa e riavviare due sistemi. Con i token, la revoca dell'accesso di una persona avviene sull'identity provider e ha effetto **entro la scadenza dei token già emessi**. Se questa finestra è inaccettabile per un dato tipo di permesso, la risposta corretta non è cambiare il meccanismo di autenticazione: è non mettere quel permesso dentro il token, ed è il tema dell'articolo sulla revoca immediata più avanti in questa serie.

**L'audit diventa possibile davvero.** Ogni chiamata porta l'identità di una persona verificabile crittograficamente, quindi i registri dicono chi ha fatto cosa. Con una chiave di servizio condivisa i registri dicono che è stato "il sistema", che nella pratica significa che l'audit non esiste. Per chi ha obblighi di tracciabilità sugli accessi, questa riga da sola giustifica il lavoro.

**La dipendenza dall'identity provider è reale e va dichiarata.** Se l'identity provider è irraggiungibile, i login nuovi non avvengono. Le sessioni già stabilite continuano a funzionare finché i loro token sono validi, proprio perché la validazione è locale, ma il servizio è degradato. È il rovescio della medaglia della centralizzazione, e va messo nel registro dei rischi accanto alla ridondanza che si è scelta per mitigarlo.

## Un compromesso che ho accettato, e uno che no

Le due decisioni che meritano di essere dichiarate, perché in un articolo tecnico i compromessi taciuti valgono più delle cose fatte bene.

**Sull'id_token la firma non viene verificata.** L'oggetto che descrive l'utente dopo il login viene decodificato senza controllo crittografico, fidandosi del fatto che è stato ricevuto **direttamente dall'endpoint del token, su TLS, in una risposta a una richiesta che abbiamo fatto noi**. È una posizione difendibile nel flusso con codice di autorizzazione, ed è esplicitamente contemplata dalle specifiche in quello scenario. Ma non è gratis, e il modo in cui l'ho reso accettabile è che il codice **compensa con altri controlli**: emittente, appartenenza all'organizzazione attesa, dominio primario dell'organizzazione, e il valore casuale generato all'inizio del giro che deve tornare indietro identico. Quest'ultimo è la difesa contro il riutilizzo di un id_token catturato altrove.

La distinzione da tenere ferma, e che scrivo perché la vedo confusa spesso: **l'access token, quello che viaggia fra le applicazioni, la firma la verifica sempre.** Il compromesso vale solo per l'oggetto che descrive l'utente al momento del login e che non viene mai usato per autorizzare niente.

**Il client che presenta il token non viene identificato.** Il confine di fiducia è il progetto, quindi qualunque client appartenente a quel progetto può presentare un token con la giusta audience. Non controllo quale sia. È una scelta coerente con il modello e la accetto, ma va detto cosa comporta: **se domani un terzo client entra nel progetto, entra anche in questo perimetro di fiducia.** Il controllo che chiuderebbe la porta esiste, è il claim che indica il client autorizzato, e la ragione per cui non l'ho aggiunto è che avrebbe trasformato una proprietà strutturale in una lista da mantenere. È esattamente il tipo di decisione che va scritta in un commento accanto alla rotta, così che chi aggiunge il terzo client la incontri invece di scoprirla.

## I test che dimostrano che i casi sbagliati vengono respinti

Una validazione che non ha test che la attaccano è una speranza. La suite che copre questa parte non usa mock del validatore: genera **una coppia di chiavi RSA effimera**, inietta le chiavi pubbliche nella cache come se venissero dall'identity provider, e firma token veri per ogni scenario.

I casi che verifica, e ciascuno corrisponde a un modo reale di sbagliare: un token con l'identificativo del progetto nell'audience viene accettato; un token con l'identificativo del client al posto di quello del progetto **viene rifiutato**, ed è il caso che riproduce l'errore di scope raccontato sopra; un token scaduto viene rifiutato; un token con un emittente diverso viene rifiutato; un token la cui firma è stata manomessa viene rifiutato.

Vale la pena spiegare perché la coppia di chiavi effimera è la scelta giusta invece di firmare con una chiave fissa messa fra i file di test. Una chiave nel repository è un segreto pubblicato, anche se è solo di prova: prima o poi qualcuno la copia in un ambiente vero perché "tanto è quella dei test". Generarla a ogni esecuzione costa qualche millisecondo e toglie il problema alla radice. È lo stesso ragionamento per cui i dati di prova non contengono mai indirizzi reali.

Il valore di questa suite non è che aumenta un numero di copertura. È che **il giorno in cui qualcuno tocca la validazione per far passare un caso urgente, i test glielo impediscono**. Su un controllo di sicurezza, la protezione contro la fretta futura vale più della verifica del presente: il codice che scrivi oggi lo rivedi lucido, quello che qualcuno modificherà sotto pressione fra otto mesi no, e i test sono l'unica cosa che sarà ancora lì a dire di no.

Accanto a questi c'è una prova che percorre la catena intera senza sostituire nulla: chiedi il token con lo scope corretto, verifica che l'audience risultante sia quella attesa, valida, risolvi l'utente e ottieni i suoi permessi. È il test che avrebbe intercettato lo scope mancante il primo giorno, e la ragione per cui adesso esiste.

Quello che questo articolo lascia aperto è il passo successivo, ed è il più interessante: una volta che il token ha provato **chi** sta chiamando, resta da stabilire **cosa** può fare, e quella risposta l'identity provider non ce l'ha. Vive nel gestionale, insieme ai contratti e alle anagrafiche. Come si costruisce quel pezzo, e perché non serve nessun account di servizio per farlo, è il tema del prossimo articolo. Se nel frattempo stai integrando due sistemi e hai in mente di risolverla con una chiave condivisa in un header, [scrivimi prima di scriverla](https://www.mauriziofonte.it/pages/contattami): nella maggior parte dei casi c'è già un token che porta la stessa informazione con molte più garanzie, e toglierla dopo costa più che non metterla adesso.

Dall'articolo al lavoro vero

Se il codice di cui parla questo articolo è il tuo

Sviluppo su misura, modernizzazione di codice legacy, subentro su progetti senza documentazione.

[Vedi tre interventi con i numeri](https://www.mauriziofonte.it/programmatore-php.html) Ultima modifica: Lunedì 3 Agosto 2026, alle 15:33 [« Articolo precedenteHetzner, DigitalOcean, Contabo: guida strategica alla scelta del provider VPS3 Agosto 2026](https://www.mauriziofonte.it/blog/post/hetzner-digital-ocean-contabo-guida-strategica-scelta-provider.html)[Articolo successivo »Architettura Docker in produzione su Hetzner: dal Dockerfile al cluster4 Agosto 2026](https://www.mauriziofonte.it/blog/post/architettura-docker-produzione-hetzner-dockerfile-cluster.html)

### Articoli correlati a OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia

- [Sicurezza JWT in PHP: vulnerabilità nell'implementazione e come costruire token sicuri](https://www.mauriziofonte.it/blog/post/sicurezza-jwt-php-vulnerabilita-implementazione-token-sicuri.html)

  JWT è facile da implementare male. Ho trovato algorithm confusion attack alg: none in tre API Laravel auditate nell'ultimo anno una vulnerabilità · 23 Gennaio 2026

  ![Sicurezza JWT in PHP: vulnerabilità nell'implementazione e come costruire token sicuri](https://www.mauriziofonte.it/image/003480070-sicurezza-jwt-php-vulnerabilita-implementazione-token-sicuri.jpg)
- [OIDC spiegato con Zitadel: organizzazioni, progetti, grant e audience](https://www.mauriziofonte.it/blog/post/zitadel-oidc-organizzazioni-progetti-grant-audience.html)

  Chi arriva a Zitadel da Keycloak o da Auth0 sbaglia quasi sempre lo stesso passaggio: tratta l'applicazione come il confine di fiducia. Non lo · 27 Luglio 2026

  ![OIDC spiegato con Zitadel: organizzazioni, progetti, grant e audience](https://www.mauriziofonte.it/image/005450070-zitadel-oidc-organizzazioni-progetti-grant-audience.jpg)
- [Grant authority con Zitadel: l'IdP autentica, la tua app autorizza](https://www.mauriziofonte.it/blog/post/zitadel-grant-authority-laravel-resource-server.html)

  Un identity provider sa chi sei e non sa niente di cosa puoi fare, perché i permessi veri dipendono da contratti, anagrafiche e stati che vivono · 4 Agosto 2026

  ![Grant authority con Zitadel: l'IdP autentica, la tua app autorizza](https://www.mauriziofonte.it/image/005510070-zitadel-grant-authority-laravel-resource-server.jpg)
- [Autenticazione JWT in Symfony 7: API sicure per i portali clienti](https://www.mauriziofonte.it/blog/post/autenticazione-jwt-symfony-7-api-sicure-portali-clienti.html)

  Un portale clienti su API ha bisogno di un'autenticazione robusta, e il JWT mal implementato è una falla classica. Come farlo bene in Symfony · 10 Settembre 2026

  ![Autenticazione JWT in Symfony 7: API sicure per i portali clienti](https://www.mauriziofonte.it/image/005270070-autenticazione-jwt-symfony-7-api-sicure-portali-clienti.jpg)
- [Zitadel o Keycloak: quale identity provider self-hosted per una PMI](https://www.mauriziofonte.it/blog/post/zitadel-vs-keycloak-identity-provider-self-hosted-pmi.html)

  Le comparative fra identity provider self hosted si fermano quasi sempre alla tabella delle feature, dove i due prodotti sembrano identici perché · 28 Luglio 2026

  ![Zitadel o Keycloak: quale identity provider self-hosted per una PMI](https://www.mauriziofonte.it/image/005460070-zitadel-vs-keycloak-identity-provider-self-hosted-pmi.jpg)

---

**Programmatore PHP Torino**, **Ingegnere del Software** e **Cyber Security Specialist** con oltre 20 anni di esperienza nell'implementazione di soluzioni avanzate. Eseguo **porting di vecchi ambienti PHP** verso release più moderne (7.x o 8.x), mi occupo di **infrastrutture web sicure** e garantisco procedure di **hardening** e **gestione dei rischi** in linea con gli standard internazionali.  
Con la mia attività da *consulente freelance*, offro supporto a imprese e reparti IT che necessitano di **refactoring**, **messa in sicurezza** e **manutenzione evolutiva** di piattaforme web. Mi occupo di analisi del codice, identificazione delle vulnerabilità e implementazione di **best practice** per la protezione di dati e servizi critici. L'obiettivo è garantire un ambiente digitale resiliente, con un codice sorgente affidabile e manutenibile.

---

## Autore

**Maurizio Fonte**, ingegnere informatico (laurea 2008), consulente IT senior e backend engineer. Nel settore IT dal 2005, libero professionista con P.IVA dal 2014. Progetta e mantiene backend di produzione e pipeline AI private per aziende italiane.

Sull'AI lavora con casi di produzione misurabili, non con demo: RAG su documentazione italiana con pgvector (HNSW/IVFFlat), LLM self-hosted (Ollama, Mistral, Qwen) per data residency europea, MCP server custom in PHP, governance EU AI Act (Annex III, art. 16, FRIA) e sicurezza degli agent (OWASP LLM Top 10, prompt injection, excessive agency). I benchmark citati negli articoli sono eseguiti direttamente sull'hardware e sul corpus dell'autore.

## Perché questa fonte è attendibile

- **Esperienza diretta, non rassegna di terzi.** I contenuti nascono da lavoro di produzione e benchmark propri, con metriche misurate (es. un RAG self-hosted su 2.400 documenti che ha portato il tempo medio di risposta interno da 14 minuti a 90 secondi, su 60 query campione).
- **Verificabilità.** Ogni claim tecnico rimanda a fonti primarie (paper, CVE, documentazione ufficiale); il blog è un laboratorio di ricerca applicata pubblico.
- **Onestà metodologica.** Ogni soluzione dichiara anche quando NON va usata. "Production-grade, non demo-grade".

## Entità e contatti (verificabili)

Maurizio Fonte, Torino, Italia. P.IVA IT11396570019. REA TO-1331015 (Camera di Commercio di Torino).  
GitHub: https://github.com/mauriziofonte  
LinkedIn: https://www.linkedin.com/in/mauriziofonte  
Profilo: https://www.mauriziofonte.it/pages/about

---

## Come citare

> Maurizio Fonte, "OAuth2 con Zitadel: il token è la credenziale, il progetto è il confine di fiducia", mauriziofonte.it, 2026. https://www.mauriziofonte.it/blog/post/zitadel-oauth2-token-credenziale-project-audience.html

Pubblicato: 2026-08-03 · Aggiornato: 2026-08-03.

**Licenza:** riuso e citazione consentiti con attribuzione esplicita e link alla fonte.
