Stai ricevendo l’errore AAD ID3035 quando la tua app Node.js prova a leggere le liste di SharePoint via REST dopo aver ottenuto correttamente un access token? Qui trovi cause, verifiche e soluzioni pronte all’uso, con esempi di codice e una checklist finale per sbloccare la chiamata.
Scenario e sintomi
Un’applicazione server‑side in Node.js recupera senza problemi un token con client credentials, ma la chiamata HTTP a https://<tenant>.sharepoint.com/_api/web/lists fallisce con risposta di Azure Active Directory che contiene l’identificatore ID3035: The request was not valid or is malformed. Il problema è tipicamente legato a parametri OAuth non coerenti con la risorsa che stai interrogando, a permessi applicativi non concessi o a intestazioni/URL non allineati all’API scelta.
Perché accade
L’errore segnala che il token presentato a SharePoint Online non è adatto alla risorsa o che la richiesta risulta malformata. Nella pratica, le cause più comuni sono le seguenti:
| Possibile causa | Dettagli | 
|---|---|
| Token emesso con parametri incompatibili con SharePoint REST | L’endpoint di autorizzazione scelto e il parametro inviato (resource oppure scope) devono riflettere la risorsa reale.SharePoint REST accetta due varianti: classica con endpoint /oauth2/token e body che include resource=https://<tenant>.sharepoint.com/;moderna con endpoint /oauth2/v2.0/token e body che include scope=https://<tenant>.sharepoint.com/.default.Qualsiasi altro valore, incompleto o malformato, porta a ID3035.  | 
| Permessi applicativi non concessi | Alle app app‑only servono permessi di tipo Application, non Delegated. Per elencare le liste sono richiesti Sites.Read.All o Sites.ReadWrite.All e un Grant admin consent a livello tenant. Se manca la concessione, il token non includerà i ruoli necessari e l’API rifiuterà la richiesta. | 
| Intestazioni o endpoint non corretti | La richiesta deve contenere Authorization: Bearer <token> e una preferenza di formato, ad esempio Accept: application/json;odata=nometadata.L’URL deve puntare all’host di SharePoint corretto. Errori di battitura, hostname errato o percorso API sbagliato generano risposte di errore che spesso vengono mappate come ID3035 sul lato AAD.  | 
Strategie di correzione
In base ai vincoli del tuo progetto puoi scegliere una delle seguenti strade.
| Opzione | Quando adottarla | Modifiche essenziali | 
|---|---|---|
| Usare Microsoft Graph | Quando non dipendi da API legacy e vuoi un modello unificato | Token con endpoint moderno (/oauth2/v2.0/token) e scope=https://graph.microsoft.com/.default.API: https://graph.microsoft.com/v1.0/sites/{site-id}/lists o percorso con alias sito. | 
| Restare su SharePoint REST con endpoint classico | Quando serve massima compatibilità con script e componenti esistenti | Token URL: https://login.microsoftonline.com/{tenant}/oauth2/tokenBody: granttype=clientcredentials&clientid=...&clientsecret=...&resource=https://{tenant}.sharepoint.com/API: https://{tenant}.sharepoint.com/_api/web/lists | 
| SharePoint REST con endpoint moderno | Quando vuoi mantenere l’endpoint moderno ma chiamare direttamente SharePoint REST | Token URL: https://login.microsoftonline.com/{tenant}/oauth2/v2.0/tokenBody: granttype=clientcredentials&clientid=...&clientsecret=...&scope=https://{tenant}.sharepoint.com/.defaultAPI: https://{tenant}.sharepoint.com/_api/web/lists | 
Preparazione nell’area di gestione
Prima di scrivere una riga di codice, assicurati che la registrazione dell’app sia a posto:
- Crea o individua la registrazione dell’app in Entra ID con Account in questa organizzazione se la userai solo nel tuo tenant.
 - Configura un segreto client oppure, preferibilmente, un certificato per l’autenticazione app‑only.
 - Aggiungi permessi Application: 
Sites.Read.AlloSites.ReadWrite.All. Per il principio del minimo privilegio valutaSites.Selectedcon assegnazione per sito. - Esegui Grant admin consent in modo che i ruoli compaiano nel token.
 
Implementazione con Microsoft Graph
Questa è la strada consigliata quando non sei vincolato a API legacy. Con Graph ottieni un modello coerente, documentazione unificata e possibilità di usare Sites.Selected per restringere l’accesso.
Token moderno con ambito predefinito
import { Buffer } from 'node:buffer';
// Variabili di ambiente consigliate
const tenantId = process.env.TENANT_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
async function getGraphToken() {
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
const body = new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
granttype: 'clientcredentials',
scope: '[https://graph.microsoft.com/.default](https://graph.microsoft.com/.default)'
});
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token Graph non ottenuto: ${res.status} ${text}`);
}
return res.json(); // { accesstoken, expiresin, token_type }
}
Richiamo della lista tramite percorso del sito
Se conosci il percorso del sito, puoi risolvere l’identificatore e poi elencare le liste:
async function getSiteIdByPath(hostname, sitePath, accessToken) {
  // Esempio: hostname = "contoso.sharepoint.com", sitePath = "/sites/Root"
  const url = `https://graph.microsoft.com/v1.0/sites/${hostname}:${sitePath}`;
  const res = await fetch(url, {
    headers: { Authorization: `Bearer ${accessToken}` }
  });
  if (!res.ok) {
    const t = await res.text();
    throw new Error(`Impossibile recuperare siteId: ${res.status} ${t}`);
  }
  const data = await res.json();
  return data.id; // formato: {host},{spsite-id},{spweb-id}
}
async function listListsWithGraph(hostname, sitePath) {
const { access_token } = await getGraphToken();
const siteId = await getSiteIdByPath(hostname, sitePath, access_token);
const url = `https://graph.microsoft.com/v1.0/sites/${siteId}/lists?$top=500`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/json'
}
});
if (!res.ok) {
const t = await res.text();
throw new Error(`Errore Graph: ${res.status} ${t}`);
}
const data = await res.json();
return data.value.map(l => ({ id: l.id, name: l.name, list: l.list }));
}
// Esempio di uso
listListsWithGraph('contoso.sharepoint.com', '/sites/Root')
.then(console.log)
.catch(console.error);
Note sui permessi con Graph
Sites.Read.Allconsente lettura di raccolte e liste;Sites.ReadWrite.Allconsente anche scrittura.- Per minimizzare la superficie d’accesso, valuta 
Sites.Selectede assegna l’accesso solo ai siti necessari tramite configurazione amministrativa. 
Implementazione con SharePoint REST
Se devi rimanere su REST nativo di SharePoint, assicurati che il token sia emesso per la risorsa SharePoint e non per Graph. Ecco due varianti compatibili.
Token classico con parametro resource
const tenantId = process.env.TENANT_ID;
const clientId = process.env.CLIENT_ID;
const clientSecret = process.env.CLIENT_SECRET;
const spHost = process.env.SP_HOST; // es. "contoso.sharepoint.com"
async function getSharePointTokenLegacy() {
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/token`;
const body = new URLSearchParams({
granttype: 'clientcredentials',
client_id: clientId,
client_secret: clientSecret,
resource: `https://${spHost}/`
});
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token SharePoint non ottenuto: ${res.status} ${text}`);
}
return res.json(); // { accesstoken, tokentype, expires_in, ... }
}
async function listSharePointListsLegacy() {
const { access_token } = await getSharePointTokenLegacy();
const url = `https://${spHost}/_api/web/lists?$top=500`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/json;odata=nometadata'
}
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Errore SharePoint REST: ${res.status} ${text}`);
}
const data = await res.json();
return data.value.map(l => ({ id: l.Id, title: l.Title, hidden: l.Hidden }));
}
listSharePointListsLegacy()
.then(console.log)
.catch(console.error);
Token moderno con ambito predefinito della risorsa SharePoint
async function getSharePointTokenModern() {
  const url = `https://login.microsoftonline.com/${process.env.TENANT_ID}/oauth2/v2.0/token`;
  const body = new URLSearchParams({
    clientid: process.env.CLIENTID,
    clientsecret: process.env.CLIENTSECRET,
    granttype: 'clientcredentials',
    scope: `https://${process.env.SP_HOST}/.default`
  });
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Token SharePoint v2 non ottenuto: ${res.status} ${text}`);
}
return res.json();
}
async function listSharePointListsModern() {
const { access_token } = await getSharePointTokenModern();
const url = `https://${process.env.SPHOST}/api/web/lists`;
const res = await fetch(url, {
headers: {
Authorization: `Bearer ${access_token}`,
Accept: 'application/json;odata=nometadata'
}
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Errore SharePoint REST: ${res.status} ${text}`);
}
const data = await res.json();
return data.value;
}
Diagnostica rapida del token
Se ancora incontri il rifiuto con ID3035, verifica in autonomia il contenuto del token. Decodificando l’header e il payload puoi capire subito se stai passando alla risorsa giusta.
- Decodifica Base64URL del token senza convalida della firma, solo a scopo diagnostico.
 - Controlla il claim 
aud: per SharePoint deve corrispondere al principale di SharePoint Online; per Graph deve corrispondere al principale di Microsoft Graph. - Controlla 
rolesper permessi applicativi (es.Sites.Read.All) oppurescpper permessi delegati; con client credentials ti aspettiroles. - Verifica 
iss,tid,exp,nbfper scarti temporali. 
function decodeSegment(seg) {
  seg = seg.replace(/-/g, '+').replace(/_/g, '/');
  const pad = seg.length % 4;
  if (pad) seg += '='.repeat(4 - pad);
  return JSON.parse(Buffer.from(seg, 'base64').toString('utf8'));
}
function dumpJwt(token) {
  const [h, p] = token.split('.');
  const header = decodeSegment(h);
  const payload = decodeSegment(p);
  console.log('HEADER', header);
  console.log('PAYLOAD', payload);
}
// Esempio d’uso:
// const { access_token } = await getSharePointTokenLegacy();
// dumpJwt(access_token);
Spunti di lettura del payload:
- aud: per Graph si riferisce al principale di Graph, per SharePoint al principale di SharePoint.
 - roles: verifica che includa 
Sites.Read.AlloSites.ReadWrite.All. - appid: è l’identificatore della tua app; utile per confermare che stai usando le credenziali attese.
 - app_displayname e tid: utili per tracciare tenant e app.
 
Intestazioni e formato risposta
SharePoint REST supporta odata=verbose, odata=fullmetadata e odata=nometadata. Per prestazioni e semplicità, odata=nometadata è spesso la scelta migliore. In GET non serve Content-Type nel request, ma non è un problema includerlo in POST/PUT/PATCH. Per elencare le liste:
GET /_api/web/lists HTTP/1.1
Host: <tenant>.sharepoint.com
Authorization: Bearer <token>
Accept: application/json;odata=nometadata
Verifiche sui permessi
- Assicurati che i permessi siano di tipo Application, non Delegated.
 - Con Graph, 
Sites.Read.AlleSites.ReadWrite.Allsono i ruoli chiave. - Con SharePoint REST, i permessi app‑only si riflettono negli stessi ruoli a livello di risorsa SharePoint.
 - Se usi Sites.Selected, ricorda che l’app non vede nulla finché non assegni l’accesso ai singoli siti; in assenza di assegnazione, potresti ricevere rifiuti che somigliano a errori di malauth.
 
Errori ricorrenti e come evitarli
- Mescolare resource e scope: l’endpoint classico usa 
resource, quello moderno usascopecon.default. Non invertire. - Hostname errato: il valore dopo 
https://nel parametroresource/scopedeve essere esattamente l’host del tuo tenant SharePoint. - Permessi non consentiti: averli aggiunti non basta, è indispensabile la concessione amministrativa.
 - Orario del server fuori sincronia: differenze di pochi minuti possono rendere il token non ancora valido o scaduto; sincronizza l’orologio.
 - Cache token non aggiornata: se cambi i permessi, ottieni un nuovo token per vederli riflessi nel claim 
roles. - Uso di endpoint misti: ottenere un token per Graph e chiamare SharePoint REST o il contrario causa rifiuto della risorsa.
 
Ricette pratiche pronte all’uso
Snippet di richiesta token classico
const url = `https://login.microsoftonline.com/${tenantId}/oauth2/token`;
const body =
  `granttype=clientcredentials&client_id=${clientId}` +
  `&client_secret=${clientSecret}` +
  `&resource=${encodeURIComponent('https://myurl.sharepoint.com/')}`;
Snippet con Graph
const body =
  `clientid=${clientId}&clientsecret=${clientSecret}` +
  `&granttype=clientcredentials` +
  `&scope=${encodeURIComponent('https://graph.microsoft.com/.default')}`;
const apiEndpoint =
  'https://graph.microsoft.com/v1.0/sites/myurl.sharepoint.com:/sites/Root:/lists';
Esempi con curl per il collaudo
Per escludere problemi di codice, testa i flussi con curl.
Token per Graph
curl -sS -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "clientid=$CLIENTID&clientsecret=$CLIENTSECRET&granttype=clientcredentials&scope=https%3A%2F%2Fgraph.microsoft.com%2F.default" \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/v2.0/token"
Token per SharePoint REST classico
curl -sS -X POST \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "clientid=$CLIENTID&clientsecret=$CLIENTSECRET&granttype=clientcredentials&resource=https%3A%2F%2F$SP_HOST%2F" \
  "https://login.microsoftonline.com/$TENANT_ID/oauth2/token"
Chiamata di elenco liste
curl -sS \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json;odata=nometadata" \
  "https://$SPHOST/api/web/lists?$top=500"
Boilerplate riutilizzabile con gestione errori
Un piccolo wrapper Node.js per ridurre gli scarti con ID3035 grazie a messaggi di diagnostica chiari.
class HttpError extends Error {
  constructor(res, body) {
    super(`HTTP ${res.status}: ${body?.error?.message || body || 'Errore'}`);
    this.status = res.status;
    this.body = body;
  }
}
async function postForm(url, form) {
  const res = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams(form)
  });
  const text = await res.text();
  if (!res.ok) throw new HttpError(res, safeJson(text));
  return safeJson(text);
}
function safeJson(text) {
  try { return JSON.parse(text); } catch { return { raw: text }; }
}
async function fetchJson(url, token, headers = {}) {
  const res = await fetch(url, {
    headers: {
      Authorization: `Bearer ${token}`,
      Accept: 'application/json;odata=nometadata',
      ...headers
    }
  });
  const text = await res.text();
  if (!res.ok) throw new HttpError(res, safeJson(text));
  return safeJson(text);
}
// Flusso SharePoint REST con endpoint moderno
async function listSPOListsModern({ tenantId, clientId, clientSecret, spHost }) {
  const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
  const token = await postForm(tokenUrl, {
    client_id: clientId,
    client_secret: clientSecret,
    granttype: 'clientcredentials',
    scope: `https://${spHost}/.default`
  });
  const lists = await fetchJson(`https://${spHost}/api/web/lists?$top=500`, token.accesstoken);
  return lists.value;
}
Domande frequenti
Perché con client credentials vedo claim roles e non scp?
Perché i permessi sono di tipo Application; con flussi delegati troveresti scp con gli scopes concessi all’utente.
Posso limitare l’accesso al solo sito interessato?
Sì, con l’approccio a ruoli selettivi di sito. In tal caso l’app riceve il ruolo globale ma l’accesso ai siti va assegnato esplicitamente; in assenza di assegnazione otterrai rifiuti di accesso anche se il token sembra valido.
Serve un digest per le richieste GET su SharePoint REST?
No. Il request digest serve per operazioni che modificano lo stato; per GET di solito basta il bearer token e l’Accept corretto.
Posso usare msal node anziché fetch manuale?
Certo. msal node semplifica la gestione di cache, reti e rotazione token. L’essenziale resta comunque la coerenza tra endpoint e parametri (resource o scope).
Consigli per produzione
- Preferisci certificati a segreti dove possibile; riduci il rischio di compromissione.
 - Gestisci la cache del token e il renewal proattivo prima della scadenza.
 - Implementa retry esponenziale su errori di rete e codici temporanei.
 - Aggiungi telemetria per tracciare il percorso del token e l’identificatore del sito chiamato.
 - Valuta Graph come front door uniforme; se rimani su SharePoint REST, documenta in modo chiaro il motivo e il perimetro.
 
Checklist finale operativa
- Permessi: assegna 
Sites.Read.AlloSites.ReadWrite.Alldi tipo Application e completa il Grant admin consent. - Token: usa l’endpoint e i parametri coerenti con la risorsa scelta:
- SharePoint REST classico: 
/oauth2/tokenconresource=https://<tenant>.sharepoint.com/ - SharePoint REST moderno: 
/oauth2/v2.0/tokenconscope=https://<tenant>.sharepoint.com/.default - Graph: 
/oauth2/v2.0/tokenconscope=https://graph.microsoft.com/.default 
 - SharePoint REST classico: 
 - Intestazioni: 
Authorization: Bearer <token> Accept: application/json;odata=nometadata - URL: verifica l’host (
contoso.sharepoint.comcome esempio) e il percorso (/_api/web/listsper REST o endpoint Graph appropriato). 
Appendice di mappatura tra scelte e parametri
| Scelta | Endpoint token | Parametro chiave | Valore corretto | Endpoint API | 
|---|---|---|---|---|
| SharePoint REST classico | /oauth2/token | resource | https://<tenant>.sharepoint.com/ | https://<tenant>.sharepoint.com/_api/web/lists | 
| SharePoint REST moderno | /oauth2/v2.0/token | scope | https://<tenant>.sharepoint.com/.default | https://<tenant>.sharepoint.com/_api/web/lists | 
| Microsoft Graph | /oauth2/v2.0/token | scope | https://graph.microsoft.com/.default | https://graph.microsoft.com/v1.0/sites/{site-id}/lists | 
In sintesi
Il codice Node.js non è necessariamente il problema: l’errore nasce quasi sempre da una discrasia fra token ed endpoint di destinazione. Allineando endpoint di emissione e parametri (resource oppure scope con .default), verificando i permessi Application con admin consent e impostando correttamente le intestazioni di richiesta, la chiamata a SharePoint REST o a Microsoft Graph risponderà con l’elenco delle liste, eliminando definitivamente l’errore ID3035.
