Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Los entornos remotos en las cargas de trabajo de Microsoft Fabric usan un mecanismo de autenticación especializado que proporciona tanto el contexto de usuario como la identidad de la aplicación. En este artículo se explica cómo implementar la autenticación para puntos de conexión remotos que controlan trabajos, notificaciones de ciclo de vida y otras operaciones de back-end.
Introducción a los flujos de autenticación
Las cargas de trabajo de Fabric implican tres flujos de autenticación principales:
- Fabric to Workload - Fabric llama al punto de conexión remoto con el formato SubjectAndAppToken
- Carga de trabajo en Fabric - la carga de trabajo hace llamadas a las APIs de Fabric con tokens de tipo SubjectAndAppToken o Bearer
- Carga de trabajo del Frontend al Backend - donde el Frontend llama al Backend con tokens Bearer
Este artículo se centra en autenticar solicitudes de Fabric hacia su carga de trabajo y en realizar llamadas autenticadas de regreso a Fabric.
Formato SubjectAndAppToken
Cuando Fabric llama al punto de conexión remoto (para trabajos, notificaciones del ciclo de vida u otras operaciones), usa un formato de autenticación de token dual denominado SubjectAndAppToken.
Estructura de encabezado de autorización
SubjectAndAppToken1.0 subjectToken="<user-delegated-token>", appToken="<app-only-token>"
Este formato incluye dos tokens distintos:
-
subjectToken: un token delegado que representa al usuario en cuyo nombre se realiza la operación. -
appToken- Un token exclusivo de la aplicación Fabric, que demuestra que la solicitud se originó en Fabric
Importante
El subjectToken no siempre está presente. Puede que falte en escenarios como:
- Operaciones de la entidad de servicio: cuando una entidad de servicio interactúa en las API públicas de Fabric sin contexto de usuario
- Operaciones del sistema: operaciones como la eliminación de elementos en las que ningún usuario está implicado directamente
- Flujos de trabajo automatizados: canalizaciones de CI/CD o operaciones programadas que se ejecutan sin interacción del usuario
El punto de conexión remoto debe diseñarse para controlar ambos casos: solicitudes con contexto de usuario (subjectToken presente) y solicitudes sin contexto de usuario (subjectToken ausente).
¿Por qué los tokens duales?
El enfoque de doble token proporciona tres ventajas clave:
- Validación : compruebe que la solicitud se originó en Fabric validando appToken.
- Contexto de usuario : subjectToken proporciona contexto de usuario para la acción que se está realizando.
- Comunicación Inter-Servicio - Utilice el subjectToken para adquirir tokens On-Behalf-Of (OBO) para invocar a otros servicios con contexto de usuario.
Análisis de SubjectAndAppToken
El punto de conexión remoto debe analizar el encabezado Authorization para extraer ambos tokens.
Ejemplo de JavaScript
/**
* Parse SubjectAndAppToken from Authorization header
* @param {string} authHeader - Authorization header value
* @returns {object|null} Parsed tokens or null if invalid format
*/
function parseSubjectAndAppToken(authHeader) {
if (!authHeader || !authHeader.startsWith('SubjectAndAppToken1.0 ')) {
return null;
}
const tokenPart = authHeader.substring('SubjectAndAppToken1.0 '.length);
const tokens = {};
const parts = tokenPart.split(',');
for (const part of parts) {
const [key, value] = part.split('=');
if (key && value) {
// Remove surrounding quotes from the token value
const cleanValue = value.trim().replace(/^"(.*)"$/, '$1');
tokens[key.trim()] = cleanValue;
}
}
return {
subjectToken: tokens.subjectToken || null,
appToken: tokens.appToken || null
};
}
Ejemplo de uso
const authHeader = req.headers['authorization'];
const tokens = parseSubjectAndAppToken(authHeader);
if (!tokens || !tokens.appToken) {
return res.status(401).json({ error: 'Invalid authorization header' });
}
// Now you have access to:
// - tokens.subjectToken (user-delegated token, may be null)
// - tokens.appToken (Fabric app token, always present)
// Check if user context is available
if (tokens.subjectToken) {
console.log('Request has user context');
} else {
console.log('Request is app-only (no user context)');
}
Validación de tokens
Ambos tokens deben validarse para garantizar la autenticidad y las declaraciones adecuadas.
Validación de tokens de aplicación
appToken es un token exclusivo de la aplicación de Fabric. Validar:
- Firma del token - Verificación contra las claves de firma de Azure AD
-
Vigencia del token - Comprobación
nbf(no antes) yexp(expiración) de reclamaciones - Audiencia: debe coincidir con el registro de aplicación de su carga de trabajo.
- Issuer: debe ser de Azure AD con el tenant correcto.
-
idtypclaim : debe ser"app"para tokens de solo aplicación -
Sin
scpdeclaración - los tokens de solo aplicación no tienen declaraciones de alcance - Id. de inquilino : debe coincidir con el identificador de inquilino del publicador.
Casos de uso de appToken de muestra:
{
"aud": "api://localdevinstance/aaaabbbb-0000-cccc-1111-dddd2222eeee/Fabric.WorkloadSample/123",
"iss": "https://sts.windows.net/12345678-77f3-4fcc-bdaa-487b920cb7ee/",
"iat": 1700047232,
"nbf": 1700047232,
"exp": 1700133932,
"appid": "11112222-bbbb-3333-cccc-4444dddd5555",
"appidacr": "2",
"idtyp": "app",
"oid": "aaaaaaaa-0000-1111-2222-bbbbbbbbbbbb",
"tid": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"ver": "1.0"
}
Validación del token del sujeto
subjectToken es un token delegado de usuario. Validar:
- firma de Token: verificar con las claves de firma de Azure AD
-
Vigencia del token - comprobación
nbfyexpdeclaraciones - Audiencia - debe coincidir con el registro de la aplicación de trabajo.
- Issuer: debe ser de Azure AD con el inquilino correcto.
-
scpreclamo - debe incluir"FabricWorkloadControl"el alcance -
Sin
idtypreclamación - los tokens delegados no tienen esta reclamación -
appiddebe coincidir : debe ser el mismo que en appToken.
Reclamaciones de subjectToken de muestra:
{
"aud": "api://localdevinstance/aaaabbbb-0000-cccc-1111-dddd2222eeee/Fabric.WorkloadSample/123",
"iss": "https://sts.windows.net/12345678-77f3-4fcc-bdaa-487b920cb7ee/",
"iat": 1700050446,
"nbf": 1700050446,
"exp": 1700054558,
"appid": "11112222-bbbb-3333-cccc-4444dddd5555",
"scp": "FabricWorkloadControl",
"name": "john doe",
"oid": "bbbbbbbb-1111-2222-3333-cccccccccccc",
"upn": "user1@contoso.com",
"tid": "bbbbcccc-1111-dddd-2222-eeee3333ffff",
"ver": "1.0"
}
Ejemplo de validación de tokens de JavaScript
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
/**
* Validate Azure AD token
* @param {string} token - JWT token to validate
* @param {object} options - Validation options
* @returns {Promise<object>} Validated token claims
*/
async function validateAadToken(token, options = {}) {
const { isAppOnly = false, tenantId = null, audience = null } = options;
// Decode token to get header and tenant
const decoded = jwt.decode(token, { complete: true });
if (!decoded) {
throw new Error('Invalid token format');
}
const { header, payload } = decoded;
const tokenTenantId = payload.tid;
// Get signing key from Azure AD
const client = jwksClient({
jwksUri: `https://login.microsoftonline.com/${tokenTenantId}/discovery/v2.0/keys`,
cache: true,
cacheMaxAge: 86400000 // 24 hours
});
const signingKey = await new Promise((resolve, reject) => {
client.getSigningKey(header.kid, (err, key) => {
if (err) reject(err);
else resolve(key.getPublicKey());
});
});
// Verify token signature and claims
const verifyOptions = {
algorithms: ['RS256'],
audience: audience,
clockTolerance: 60 // 1 minute clock skew
};
const verified = jwt.verify(token, signingKey, verifyOptions);
// Validate app-only vs delegated token
if (isAppOnly) {
if (verified.idtyp !== 'app') {
throw new Error('Expected app-only token');
}
if (verified.scp) {
throw new Error('App-only token should not have scp claim');
}
} else {
if (verified.idtyp) {
throw new Error('Expected delegated token');
}
if (!verified.scp || !verified.scp.includes('FabricWorkloadControl')) {
throw new Error('Missing required FabricWorkloadControl scope');
}
}
// Validate tenant if required
if (tenantId && verified.tid !== tenantId) {
throw new Error(`Token tenant mismatch: expected ${tenantId}, got ${verified.tid}`);
}
return verified;
}
Middleware de autenticación
Implemente middleware para autenticar las solicitudes entrantes de Fabric.
Flujo de autenticación completo
/**
* Authentication middleware that validates control plane calls from Fabric
* Handles both user-delegated (with subjectToken) and app-only (without subjectToken) scenarios
* @param {object} req - Express request object
* @param {object} res - Express response object
* @param {object} options - Authentication options
* @param {boolean} options.requireSubjectToken - Whether subject token is required (default: false)
* @returns {Promise<boolean>} True if authenticated successfully
*/
async function authenticateControlPlaneCall(req, res, options = {}) {
const { requireSubjectToken = false } = options;
try {
// Extract and parse authorization header
const authHeader = req.headers['authorization'];
if (!authHeader) {
return res.status(401).json({ error: 'Missing Authorization header' });
}
const tokens = parseSubjectAndAppToken(authHeader);
if (!tokens || !tokens.appToken) {
return res.status(401).json({ error: 'Invalid Authorization header format' });
}
// Get tenant ID from header
const tenantId = req.headers['ms-client-tenant-id'];
if (!tenantId) {
return res.status(400).json({ error: 'Missing ms-client-tenant-id header' });
}
// Get configuration
const publisherTenantId = process.env.TENANT_ID;
const audience = process.env.BACKEND_AUDIENCE;
const fabricBackendAppId = '00000009-0000-0000-c000-000000000000';
// Validate app token (from Fabric)
const appTokenClaims = await validateAadToken(tokens.appToken, {
isAppOnly: true,
audience: audience
});
// Verify app token is from Fabric
const appTokenAppId = appTokenClaims.appid || appTokenClaims.azp;
if (appTokenAppId !== fabricBackendAppId) {
return res.status(401).json({ error: 'App token not from Fabric' });
}
// Verify app token is in publisher's tenant
if (publisherTenantId && appTokenClaims.tid !== publisherTenantId) {
return res.status(401).json({ error: 'App token tenant mismatch' });
}
// Validate subject token if present
let subjectTokenClaims = null;
if (tokens.subjectToken) {
subjectTokenClaims = await validateAadToken(tokens.subjectToken, {
isAppOnly: false,
audience: audience,
tenantId: tenantId
});
// Verify subject token has same appid as app token
const subjectAppId = subjectTokenClaims.appid || subjectTokenClaims.azp;
if (subjectAppId !== appTokenAppId) {
return res.status(401).json({ error: 'Token appid mismatch' });
}
} else if (requireSubjectToken) {
// If subject token is required but missing, reject the request
return res.status(401).json({
error: 'Subject token required for this operation'
});
}
// Store authentication context in request
req.authContext = {
subjectToken: tokens.subjectToken,
appToken: tokens.appToken,
tenantId: tenantId,
hasSubjectContext: !!tokens.subjectToken,
appTokenClaims: appTokenClaims,
subjectTokenClaims: subjectTokenClaims,
userId: subjectTokenClaims?.oid || subjectTokenClaims?.sub,
userName: subjectTokenClaims?.name || subjectTokenClaims?.upn
};
return true; // Authentication successful
} catch (error) {
console.error('Authentication failed:', error.message);
res.status(401).json({ error: 'Authentication failed' });
return false;
}
}
Uso del middleware
app.post('/api/jobs/execute', async (req, res) => {
// Authenticate the request (subjectToken is optional)
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) {
return; // Response already sent by middleware
}
// Access authentication context
const { hasSubjectContext, userId, userName, tenantId, subjectToken } = req.authContext;
if (hasSubjectContext) {
console.log(`Executing job for user: ${userName} (${userId})`);
} else {
console.log('Executing job in app-only context (no user)');
}
// Execute job logic...
});
// Example: Require user context for specific operations
app.post('/api/lifecycle/create', async (req, res) => {
// For create operations, we might require user context
const authenticated = await authenticateControlPlaneCall(req, res, {
requireSubjectToken: true
});
if (!authenticated) {
return; // Response already sent by middleware
}
// User context is guaranteed to be present here
const { userId, userName } = req.authContext;
console.log(`Creating item for user: ${userName}`);
// Handle creation...
});
// Example: Allow app-only context for delete operations
app.post('/api/lifecycle/delete', async (req, res) => {
// Delete operations might not have user context
const authenticated = await authenticateControlPlaneCall(req, res, {
requireSubjectToken: false // Default, but shown for clarity
});
if (!authenticated) {
return;
}
const { hasSubjectContext, userName } = req.authContext;
if (hasSubjectContext) {
console.log(`Deleting item for user: ${userName}`);
} else {
console.log('Deleting item (system operation)');
}
// Handle deletion...
});
Intercambio de tokens: flujo de Behalf-Of (OBO)
Para llamar a las API de Fabric o acceder a OneLake desde el punto de conexión remoto, debe intercambiar el subjectToken del usuario por tokens específicos de recursos mediante el flujo On-Behalf-Of de OAuth 2.0.
Servicio de Intercambio de tokens
const https = require('https');
const { URL } = require('url');
const AAD_LOGIN_URL = 'https://login.microsoftonline.com';
const ONELAKE_SCOPE = 'https://storage.azure.com/.default';
const FABRIC_SCOPE = 'https://analysis.windows.net/powerbi/api/.default';
/**
* Get token for any scope using OBO flow
* @param {string} userToken - User's access token (subjectToken)
* @param {string} tenantId - User's tenant ID
* @param {string} scope - Target resource scope
* @returns {Promise<string>} Access token for the requested scope
* @throws {Error} Throws consent-related errors (AADSTS65001, AADSTS65005) that should be propagated to UX
*/
async function getTokenForScope(userToken, tenantId, scope) {
const clientId = process.env.BACKEND_APPID;
const clientSecret = process.env.BACKEND_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error('BACKEND_APPID and BACKEND_CLIENT_SECRET required');
}
const tokenEndpoint = `${AAD_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`;
const requestBody = new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: clientId,
client_secret: clientSecret,
assertion: userToken,
scope: scope,
requested_token_use: 'on_behalf_of'
}).toString();
const response = await makeTokenRequest(tokenEndpoint, requestBody);
if (!response.access_token) {
throw new Error('Token exchange failed: missing access_token');
}
return response.access_token;
}
/**
* Get OneLake access token
* @param {string} userToken - User's access token from authContext
* @param {string} tenantId - User's tenant ID
* @returns {Promise<string>} OneLake access token
*/
async function getOneLakeToken(userToken, tenantId) {
return getTokenForScope(userToken, tenantId, ONELAKE_SCOPE);
}
/**
* Get Fabric OBO token for calling Fabric APIs
* @param {string} userToken - User's access token
* @param {string} tenantId - User's tenant ID
* @returns {Promise<string>} Fabric OBO token
*/
async function getFabricOboToken(userToken, tenantId) {
return getTokenForScope(userToken, tenantId, FABRIC_SCOPE);
}
/**
* Make HTTPS request to token endpoint
*/
function makeTokenRequest(url, body) {
return new Promise((resolve, reject) => {
const parsedUrl = new URL(url);
const options = {
hostname: parsedUrl.hostname,
port: 443,
path: parsedUrl.pathname,
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': Buffer.byteLength(body)
}
};
const req = https.request(options, (res) => {
let responseBody = '';
res.on('data', (chunk) => { responseBody += chunk; });
res.on('end', () => {
const parsed = JSON.parse(responseBody);
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(parsed);
} else {
const error = parsed.error_description || parsed.error || `HTTP ${res.statusCode}`;
reject(new Error(error));
}
});
});
req.on('error', reject);
req.write(body);
req.end();
});
}
Uso de Token Exchange
// In your job or lifecycle notification handler
async function handleJobExecution(req, res) {
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) return;
const { hasSubjectContext, subjectToken, tenantId } = req.authContext;
try {
// Only exchange token if user context is available
if (hasSubjectContext) {
// Get OneLake token to access item data on behalf of the user
const oneLakeToken = await getOneLakeToken(subjectToken, tenantId);
// Use OneLake token to read/write data
const data = await readFromOneLake(oneLakeToken, workspaceId, itemId);
// Process data...
} else {
// Handle app-only scenario
// You may need to use different authentication or skip user-specific operations
console.log('Processing job without user context');
// Use workspace or item-level access instead
}
res.status(200).json({ status: 'completed' });
} catch (error) {
console.error('Job execution failed:', error);
res.status(500).json({ error: 'Job execution failed' });
}
}
Llamar a las API de Fabric
Para llamar a las APIs de control de carga de trabajo de Fabric, debe construir un SubjectAndAppToken utilizando tanto tokens de OBO como de S2S.
Token de servicio a servicio (S2S)
Además del token de OBO, necesita un token S2S de solo aplicación para el elemento appToken.
/**
* Get S2S (Service-to-Service) token using client credentials flow
* @param {string} tenantId - Publisher's tenant ID
* @param {string} scope - Target resource scope
* @returns {Promise<string>} S2S access token
*/
async function getS2STokenForScope(tenantId, scope) {
const clientId = process.env.BACKEND_APPID;
const clientSecret = process.env.BACKEND_CLIENT_SECRET;
if (!clientId || !clientSecret) {
throw new Error('BACKEND_APPID and BACKEND_CLIENT_SECRET required');
}
const tokenEndpoint = `${AAD_LOGIN_URL}/${tenantId}/oauth2/v2.0/token`;
const requestBody = new URLSearchParams({
grant_type: 'client_credentials',
client_id: clientId,
client_secret: clientSecret,
scope: scope
}).toString();
const response = await makeTokenRequest(tokenEndpoint, requestBody);
if (!response.access_token) {
throw new Error('S2S token acquisition failed');
}
return response.access_token;
}
/**
* Get Fabric S2S token
* @param {string} publisherTenantId - Publisher's tenant ID
* @returns {Promise<string>} Fabric S2S access token
*/
async function getFabricS2SToken(publisherTenantId) {
return getS2STokenForScope(publisherTenantId, FABRIC_SCOPE);
}
Creación de un token compuesto para las API de Fabric
/**
* Build composite token for Fabric API calls
* @param {object} authContext - Authentication context
* @returns {Promise<string>} SubjectAndAppToken formatted header value
*/
async function buildCompositeToken(authContext) {
const { subjectToken, tenantId } = authContext;
const publisherTenantId = process.env.TENANT_ID;
if (!subjectToken) {
throw new Error('Subject token is required');
}
// Exchange user's subject token for Fabric OBO token
const fabricOboToken = await getFabricOboToken(subjectToken, tenantId);
// Acquire S2S token using publisher tenant
const fabricS2SToken = await getFabricS2SToken(publisherTenantId);
// Combine into SubjectAndAppToken format
return `SubjectAndAppToken1.0 subjectToken="${fabricOboToken}", appToken="${fabricS2SToken}"`;
}
Ejemplo de llamada a API de Fabric
const axios = require('axios');
/**
* Call Fabric workload control API
*/
async function callFabricApi(authContext, workspaceId, itemId) {
// Build composite token
const authHeader = await buildCompositeToken(authContext);
// Call Fabric API
const response = await axios.get(
`https://api.fabric.microsoft.com/v1/workspaces/${workspaceId}/items/${itemId}`,
{
headers: {
'Authorization': authHeader,
'Content-Type': 'application/json'
}
}
);
return response.data;
}
Ejemplo completo: Ejecución de trabajos con autenticación
Aquí tienes un ejemplo completo de un punto de conexión de ejecución de un trabajo con autenticación.
const express = require('express');
const app = express();
app.post('/api/jobs/:jobType/instances/:instanceId', async (req, res) => {
// Authenticate the request from Fabric
const authenticated = await authenticateControlPlaneCall(req, res);
if (!authenticated) {
return; // Response already sent
}
const { jobType, instanceId } = req.params;
const { workspaceId, itemId } = req.body;
const { subjectToken, tenantId, userId } = req.authContext;
console.log(`Starting job ${jobType} for user ${userId}`);
try {
// Get OneLake token to access item data
const oneLakeToken = await getOneLakeToken(subjectToken, tenantId);
// Read data from OneLake
const itemData = await readItemData(oneLakeToken, workspaceId, itemId);
// Process the job
const result = await processJob(jobType, itemData);
// Write results back to OneLake
await writeResults(oneLakeToken, workspaceId, itemId, result);
// Return success
res.status(202).json({
status: 'InProgress',
instanceId: instanceId,
message: 'Job started successfully'
});
} catch (error) {
console.error(`Job ${instanceId} failed:`, error);
res.status(500).json({
status: 'Failed',
instanceId: instanceId,
error: error.message
});
}
});
Operaciones de larga duración
En el caso de operaciones de larga duración como trabajos, los tokens pueden expirar antes de la finalización. Implementar la lógica de actualización de tokens.
// Store refresh tokens securely
const tokenCache = new Map();
/**
* Get cached token or acquire new one
*/
async function getOrRefreshToken(userToken, tenantId, scope, cacheKey) {
const cached = tokenCache.get(cacheKey);
// Check if cached token is still valid (with 5 min buffer)
if (cached && cached.expiresAt > Date.now() + 300000) {
return cached.token;
}
// Acquire new token
const response = await getTokenForScopeWithRefresh(userToken, tenantId, scope);
// Cache the token
tokenCache.set(cacheKey, {
token: response.access_token,
expiresAt: Date.now() + (response.expires_in * 1000)
});
return response.access_token;
}
Para obtener más información sobre el control de procesos de OBO de larga duración, consulte Procesos de OBO de ejecución prolongada.
Procedimientos recomendados de seguridad
Almacenamiento de tokens
- Nunca registrar tokens completos: solo registra los últimos 4 caracteres para la depuración.
- Almacenamiento de tokens de forma segura solo en memoria
- Borrar tokens de memoria después de su uso
- No conservar tokens en el disco o las bases de datos
Validación de tokens
- Validar siempre appToken en formato SubjectAndAppToken
- Validar subjectToken solo si está presente
- Comprobación de las firmas de token con claves de AD de Azure
- Comprobación de la expiración del token y horas no anteriores
- Validación de reclamaciones de emisor, audiencia y arrendatario
- Verificar las declaraciones necesarias (scp, idtyp, etc.)
Control del contexto de usuario que falta
- Diseña tu punto de conexión remoto para gestionar tanto escenarios delegados por el usuario como aquellos exclusivos para aplicaciones.
- Verificar
hasSubjectContexten authContext antes de acceder a propiedades específicas del usuario - Para las operaciones que requieren contexto de usuario, establezca
requireSubjectToken: trueen opciones de middleware. - Para las operaciones de eliminación o las operaciones del sistema, permitir la ausencia de subjectToken.
- Documente qué operaciones admiten el contexto de solo aplicación y que requieren contexto de usuario
Configuración del entorno
// Required environment variables
const requiredEnvVars = [
'BACKEND_APPID', // Your workload's app registration ID
'BACKEND_CLIENT_SECRET', // Your app's client secret
'TENANT_ID', // Your publisher tenant ID
'BACKEND_AUDIENCE' // Expected token audience
];
// Validate on startup
requiredEnvVars.forEach(varName => {
if (!process.env[varName]) {
throw new Error(`Missing required environment variable: ${varName}`);
}
});
Control de errores y propagación de consentimiento
Cuando se produce un error en el intercambio de tokens debido a la falta de consentimiento o permisos, el backend debe propagar estos errores al front-end para solicitar consentimiento del usuario. Entre los errores comunes relacionados con el consentimiento se incluyen:
- AADSTS65001 : el usuario o el administrador no ha consentido usar la aplicación
- AADSTS65005 - La aplicación requiere permisos específicos a los que no se ha dado consentimiento
/**
* Handle token exchange with consent error propagation
* Consent errors should be returned to the frontend to trigger consent flow
*/
async function handleTokenExchangeWithConsent(req, res) {
const { subjectToken, tenantId } = req.authContext;
const clientId = process.env.BACKEND_APPID;
try {
const token = await getTokenForScope(subjectToken, tenantId, ONELAKE_SCOPE);
// Use token for OneLake operations...
} catch (error) {
// Check for consent-related errors
if (error.message.includes('AADSTS65001') || error.message.includes('AADSTS65005')) {
// Extract scope from error if available
const requiredScope = extractScopeFromError(error.message) || 'https://storage.azure.com/.default';
// Build consent URL for the frontend to redirect the user
const consentUrl = buildConsentUrl({
clientId: clientId,
tenantId: tenantId,
scope: requiredScope,
redirectUri: process.env.FRONTEND_URL || 'https://your-frontend.azurewebsites.net'
});
// Return consent required response
return res.status(403).json({
error: 'ConsentRequired',
errorCode: error.message.includes('AADSTS65001') ? 'AADSTS65001' : 'AADSTS65005',
message: 'User consent is required to access this resource',
consentUrl: consentUrl,
requiredScope: requiredScope
});
}
// Other errors should be handled normally
throw error;
}
}
/**
* Build consent URL for user authorization
* @param {object} options - Consent URL options
* @returns {string} Formatted consent URL
*/
function buildConsentUrl(options) {
const { clientId, tenantId, scope, redirectUri } = options;
const params = new URLSearchParams({
client_id: clientId,
response_type: 'code',
redirect_uri: redirectUri,
response_mode: 'query',
scope: scope,
state: 'consent_required' // Your frontend can use this to handle the redirect
});
return `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize?${params.toString()}`;
}
/**
* Extract scope from error message
*/
function extractScopeFromError(errorMessage) {
// Azure AD error messages often include the missing scope
const scopeMatch = errorMessage.match(/scope[s]?[:\s]+([^\s,]+)/);
return scopeMatch ? scopeMatch[1] : null;
}
Control de consentimiento del front-end
Cuando el front-end recibe un ConsentRequired error, debe redirigir al usuario a la dirección URL de consentimiento:
// Frontend code to handle consent errors
async function callBackendApi(endpoint, data) {
try {
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
if (response.status === 403) {
const error = await response.json();
if (error.error === 'ConsentRequired' && error.consentUrl) {
// Redirect user to consent page
window.location.href = error.consentUrl;
return;
}
}
return await response.json();
} catch (error) {
console.error('API call failed:', error);
throw error;
}
}
Nota:
El ejemplo de desarrollo de cargas de trabajo de Fabric incluye ejemplos completos de control y propagación de errores de consentimiento a la experiencia de usuario de front-end. Consulte el middleware de autenticación del ejemplo y el control de errores de front-end para implementaciones listas para producción.
Escenarios de error adicionales
// Handle various token exchange errors
try {
const token = await getTokenForScope(subjectToken, tenantId, scope);
} catch (error) {
const errorMessage = error.message;
if (errorMessage.includes('AADSTS65001') || errorMessage.includes('AADSTS65005')) {
// Consent required - propagate to frontend
return propagateConsentError(res, error, tenantId);
} else if (errorMessage.includes('AADSTS50013')) {
// Invalid assertion - token may be expired
return res.status(401).json({
error: 'InvalidToken',
message: 'The provided token is invalid or expired'
});
} else if (errorMessage.includes('AADSTS700016')) {
// Application not found in tenant
return res.status(400).json({
error: 'ApplicationNotFound',
message: 'Application is not configured in this tenant'
});
}
// Unknown error
throw error;
}
Solución de problemas
Problemas comunes de autenticación
Error: "Formato de token no válido"
- Compruebe que el encabezado Authorization comienza por
SubjectAndAppToken1.0 - Compruebe que subjectToken y appToken están presentes
- Asegúrese de que los tokens estén entrecomillados correctamente en el encabezado
Error: "Error de validación del token"
- Verifique que el BACKEND_APPID coincida con el registro de su aplicación
- Compruebe que BACKEND_AUDIENCE está configurado correctamente
- Asegurarse de que los tokens no han expirado
- Comprobación de que el emisor coincide con el inquilino de Azure AD
Error: "Error de intercambio de tokens: AADSTS65001"
- El consentimiento del usuario es necesario para el ámbito solicitado.
- Asegúrese de que el registro de la aplicación tiene los permisos de API necesarios.
- Compruebe que se ha concedido el consentimiento del administrador si es necesario
Error: "Token de aplicación no procedente de Fabric"
- Verifique que la declaración de
appidappToken coincida con el identificador de aplicación de Fabric - Compruebe que está probando en el entorno correcto (desarrollo y producción)