Error ID3035 al acceder a listas de SharePoint con Node.js: causas y solución paso a paso

Si tu aplicación Node.js obtiene un token válido con el flujo client credentials pero la llamada a /_api/web/lists de SharePoint responde con ID3035: The request was not valid or is malformed, el problema casi siempre está en un parámetro de autenticación mal alineado con el recurso al que llamas. En esta guía extensa aprenderás a:

  • Entender por qué se genera el error ID3035.
  • Solicitar y validar un token de aplicación correcto para SharePoint o Microsoft Graph.
  • Configurar permisos en Entra ID (Azure AD) con seguridad de producción.
  • Consumir listas desde SharePoint REST y desde Graph, con ejemplos completos en node‑fetch.
  • Aplicar un checklist de depuración que elimina el 90 % de los fallos de autenticación en menos de 5 minutos.
Índice

Comprender el error ID3035

ID3035 es un error genérico emitido por el STS de Entra ID cuando el scope o resource indicado en el token no coincide con el host que recibe la petición. El servidor detecta la discrepancia y descarta la solicitud sin llegar a validar permisos a nivel de lista o sitio.

Patrones de error más frecuentes

SíntomaCausa raíz habitual
Mensaje “Audience missing or invalid” en el cuerpo JSONEl token se emitió para https://graph.microsoft.com y se usa contra mytenant.sharepoint.com, o viceversa.
Código HTTP 401 + ID3035Falta de permiso Sites.Read.All o Sites.ReadWrite.All en la aplicación.
HTTP 403 después de cambiar los scopesConsentimiento de administrador aún no concedido o propagación de permisos incompleta (hasta 15 min).

Flujo de autenticación Client‑Credentials paso a paso

En entorno servidor a servidor (S2S) el flujo client credentials delega las acciones en la service principal, sin intervención de usuario:

  1. Registro de la aplicación en Entra ID como “Web / API”.
  2. Concesión de permisosSites.Read.All” o “Sites.ReadWrite.All” de tipo aplicación (application).
  3. Consentimiento de administrador para todo el inquilino.
  4. Solicitud del token al endpoint /oauth2/v2.0/token con scope=https://{tenant}.sharepoint.com/.default o scope=https://graph.microsoft.com/.default.
  5. Llamada a la API con encabezados de autenticación y formato de respuesta.

Tabla de acciones recomendadas

PasoAcción recomendadaMotivo
Registrar la aplicaciónConfigura tipo Web/API, añade permisos Sites.Read.All o Sites.ReadWrite.All, concede consentimiento global.El token emitido incluirá los claims adecuados para SharePoint o Graph.
Solicitar el tokenUsa el cuerpo:
clientid=…&scope=https://{tenant}.sharepoint.com/.default&clientsecret=…&granttype=clientcredentials
El scope debe apuntar al mismo host que consumirás.
Encabezados en la peticiónAuthorization: Bearer {token}
Accept: application/json;odata=nometadata
Sin Accept, SharePoint devuelve XML o error de tipo de contenido.
URL específica del sitioInvoca https://mytenant.sharepoint.com/sites/SiteName/_api/web/listsLa raíz del inquilino puede estar bloqueada o usar Multi‑Geo.
Migrar a Microsoft GraphGET /v1.0/sites/{domain}:/sites/{siteName}
Luego /lists
Un único token sirve para OneDrive, Teams y SharePoint; simplifica permisos.

Ejemplo completo con node‑fetch

import fetch from 'node-fetch';

const tenantId     = '00000000-1111-2222-3333-444444444444';
const clientId     = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee';
const clientSecret = process.env.CLIENT_SECRET;      // nunca hard‑codear
const siteUrl      = 'https://mytenant.sharepoint.com/sites/ProjectX';

async function getToken () {
  const url  = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
  const body = new URLSearchParams({
    client_id:     clientId,
    scope:         `${siteUrl}/.default`,
    client_secret: clientSecret,
    granttype:    'clientcredentials'
  });

  const res   = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body
  });

  const json  = await res.json();
  if (!res.ok) throw new Error(json.error_description);
  return json.access_token;
}

async function fetchLists () {
  const token  = await getToken();
  const apiUrl = `${siteUrl}/_api/web/lists?$select=Title,Id`;

  const res = await fetch(apiUrl, {
    method:  'GET',
    headers: {
      Authorization: `Bearer ${token}`,
      Accept:        'application/json;odata=nometadata'
    }
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`HTTP ${res.status}: ${text}`);
  }
  const data = await res.json();
  console.log(data);
}

fetchLists().catch(console.error);

Uso de Microsoft Graph para mayor flexibilidad

Graph unifica identidades y permisos. Los pasos son equivalentes salvo por el scope y las rutas:

// 1. Token con scope graph
scope = 'https://graph.microsoft.com/.default'

// 2. Resolver el sitio:
GET https://graph.microsoft.com/v1.0/sites/mytenant.sharepoint.com:/sites/ProjectX

// 3. Listas del sitio:
GET https://graph.microsoft.com/v1.0/sites/{siteId}/lists

Ventajas de Graph:

  • Mismo token para SharePoint, OneDrive y Teams.
  • Permisos granulares (ej. Sites.Selected con sitios asignados).
  • Filtros OData, $expand de columnas, y tipos enriquecidos.

Checklist de depuración express

  1. ¿El aud del token coincide con el host? Verifícalo en jwt.ms.
  2. ¿El permiso aparece en la sección roles? Si no, revisa el manifest.
  3. ¿Estás usando el scope /.default correcto? Graph y SharePoint no son intercambiables.
  4. ¿Se ha otorgado consentimiento de administrador tras añadir permisos? Compruébalo en Portal.
  5. ¿Ha pasado el tiempo de propagación (≤ 15 min)?
  6. ¿La URL apunta a la raíz /sites/{site} correcta? Confirma Multi‑Geo.
  7. ¿Incluyes encabezados Authorization y Accept? Sin ellos, SharePoint responde en XML y node‑fetch no lo parsea.
  8. ¿El reloj del servidor está sincronizado (NTP)? Tokens con iat/nbf en el futuro se invalidan.

Buenas prácticas de seguridad

Para producción:

  • Guarda clientSecret en Azure Key Vault o variables de entorno seguras.
  • Habilita Managed Identities si la app corre en Azure Functions, App Service o Container Apps.
  • Sustituye secretos por certificados RSA‑2048; renueva cada seis meses.
  • Limita el alcance con Sites.Selected y asigna sólo los sitios necesarios.
  • Activa Conditional Access con filtros de carga de trabajo.

¿Por qué “funciona en Postman” y falla en código?

Postman usa por defecto Accept: application/json;odata=verbose y envía cookies de seguimiento. Además, su environment puede contener un token con permisos delegated emitido por login interactivo. En el servidor, el flujo client credentials no incluye las claims scp, sólo roles; de ahí el error si confundes tipos de permisos.

Preguntas frecuentes

¿Puedo usar un solo token para varios sitios?

Sí. Un token de aplicación emitido para el host de la colección raíz sirve para todos los sitios del mismo dominio. Con Graph, un token global sirve para cualquier sitio del tenant.
¿Cómo limito el token a un solo sitio?

Entra ID admite el permiso Sites.Selected. Debes:

  1. Añadir Sites.Selected en la app.
  2. Conceder consentimiento de administrador.
  3. Asignar el sitio concreto mediante Set-PnPSiteAppPermission o Graph /sites/{id}/permissions.

¿Existe equivalente Graph para /_api/web/lists/getByTitle('DocLib')/items?

Sí:
GET /sites/{siteId}/lists/{listId}/items?expand=fields(select=Title,Id)

Conclusión

El error ID3035 suele resolverse alineando scope/resource, permisos y URL. Con el flujo client credentials tu service principal actúa como identidad corporativa; configurar correctamente Entra ID es el 80 % de la solución. El resto es enviar los encabezados adecuados y respetar la ruta del sitio. Si planeas extender tu integración a Teams o OneDrive, migra a Microsoft Graph y aprovecha un modelo de autenticación unificado.

Índice