Delen via


Externe eindpunten verifiëren

Externe eindpunten in Microsoft Fabric workloads maken gebruik van een speciaal verificatiemechanisme dat zowel gebruikerscontext als toepassingsidentiteit biedt. In dit artikel wordt uitgelegd hoe u verificatie implementeert voor externe eindpunten die taken, levenscyclusmeldingen en andere back-endbewerkingen verwerken.

Overzicht van verificatiestromen

Fabric-werklasten omvatten drie primaire authenticatiestromen:

  1. Fabric naar Workload - Fabric roept uw externe eindpunt aan met de indeling SubjectAndAppToken
  2. Workload naar Fabric : uw workload roept Fabric-API's aan met SubjectAndAppToken- of Bearer-tokens
  3. Workload Frontend to Backend - uw Frontend roept uw Backend aan met Bearer-tokens

Dit artikel is gericht op het verifiëren van aanvragen van Fabric naar uw workload en het uitvoeren van geverifieerde aanroepen naar Fabric.

Indeling SubjectAndAppToken

Wanneer Fabric uw externe eindpunt aanroept (voor taken, levenscyclusmeldingen of andere bewerkingen), wordt er een dubbele token-authenticatie-indeling genaamd SubjectAndAppToken gebruikt.

Structuur van autorisatieheader

SubjectAndAppToken1.0 subjectToken="<user-delegated-token>", appToken="<app-only-token>"

Deze indeling bevat twee afzonderlijke tokens:

  • subjectToken - Een gedelegeerd token dat de gebruiker vertegenwoordigt namens wie de bewerking wordt uitgevoerd
  • appToken - Een alleen-app-token van de Fabric-toepassing, waarmee wordt aangetoond dat de aanvraag afkomstig is van Fabric

Belangrijk

De subjectToken is niet altijd aanwezig. Het kan ontbreken in scenario's zoals:

  • Service-principalbewerkingen : wanneer een service-principal communiceert met openbare Fabric-API's zonder gebruikerscontext
  • Systeembewerkingen - Bewerkingen zoals het verwijderen van items waarbij geen enkele gebruiker rechtstreeks betrokken is
  • Geautomatiseerde werkstromen - CI/CD-pijplijnen of geplande bewerkingen die worden uitgevoerd zonder tussenkomst van de gebruiker

Uw externe eindpunt moet zijn ontworpen om beide gevallen af te handelen: aanvragen met gebruikerscontext (subjectToken aanwezig) en aanvragen zonder gebruikerscontext (subjectToken afwezig).

Waarom Dual Tokens?

De dual-token-benadering biedt drie belangrijke voordelen:

  1. Validatie : controleer of de aanvraag afkomstig is van Fabric door het appToken te valideren
  2. Gebruikerscontext : het subjectToken biedt gebruikerscontext voor de actie die wordt uitgevoerd
  3. Inter-Service Communication - Gebruik het subjectToken om on-Behalf-Of -tokens (OBO) te verkrijgen voor het aanroepen van andere services met gebruikerscontext

SubjectAndAppToken parseren

Uw externe eindpunt moet de autorisatieheader parseren om beide tokens te extraheren.

Voorbeeld van 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
  };
}

Gebruiksvoorbeeld

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)');
}

Tokenvalidatie

Beide tokens moeten worden gevalideerd om de echtheid en de juiste claims te garanderen.

App-tokenvalidatie

Het appToken is een alleen-app-token van Fabric. Valideren:

  • Handtekeningtoken - Controleren op basis van Azure AD-ondertekeningssleutels
  • Levensduur van token - Controleer claims nbf (niet vóór) en exp (verlooptijd)
  • Doelgroep - moet overeenkomen met de app-registratie van uw workload
  • Issuer - Moet afkomstig zijn van Azure AD met de juiste tenant
  • idtyp claim - Moet zijn voor tokens die alleen voor apps zijn "app"
  • Geen scp claim : alleen-app-tokens hebben geen bereikclaims
  • Tenant-id : moet overeenkomen met de tenant-id van uw uitgever

Voorbeeld van appToken claims:

{
  "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"
}

Validatie van onderwerptoken

Het subjectToken is een door de gebruiker gedelegeerd token. Valideren:

  • Handtekeningtoken - Controleren op basis van Azure AD-ondertekeningssleutels
  • Levensduur van token - Het controleren van nbf en exp claims
  • Doelgroep : moet overeenkomen met de app-registratie van uw workload
  • Issuer - Moet afkomstig zijn van Azure AD met de juiste tenant
  • scp claim - Moet omvang bevatten "FabricWorkloadControl"
  • Geen idtyp claim - Gedelegeerde tokens hebben deze claim niet
  • appid moet overeenkomen - Moet hetzelfde zijn als in appToken

Voorbeeld van subjectTokenclaims:

{
  "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"
}

Voorbeeld van javaScript-tokenvalidatie

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;
}

Verificatie-middleware

Implementeer middleware om binnenkomende aanvragen van Fabric te verifiëren.

Verificatiestroom voltooien

/**
 * 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;
  }
}

Middleware gebruiken

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...
});

Tokenuitwisseling - Namens-Flow (OBO)

Als u Fabric-API's wilt aanroepen of Toegang wilt krijgen tot OneLake vanaf uw externe eindpunt, moet u het onderwerpToken van de gebruiker uitwisselen voor resourcespecifieke tokens met behulp van de OAuth 2.0 On-Behalf-Of-stroom.

Token Exchange Service

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();
  });
}

Token Exchange gebruiken

// 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' });
  }
}

Fabric-API's aanroepen

Als u Fabric Workload Control API's wilt aanroepen, moet u een SubjectAndAppToken maken met zowel OBO- als S2S-tokens.

Service-to-Service (S2S)-token

Naast het OBO-token hebt u ook een app-only S2S-token nodig voor het appToken-onderdeel.

/**
 * 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);
}

Samengesteld token bouwen voor Fabric-API's

/**
 * 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}"`;
}

Voorbeeld van fabric-API's aanroepen

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;
}

Volledig voorbeeld: Taakuitvoering met verificatie

Hier volgt een volledig voorbeeld van een taakuitvoeringseindpunt met verificatie:

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
    });
  }
});

Langlopende bewerkingen

Voor langlopende bewerkingen, zoals taken, kunnen tokens verlopen voordat ze zijn voltooid. Vernieuwingslogica voor tokens implementeren:

// 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;
}

Zie Langlopende OBO-processen voor meer informatie over het verwerken van langlopende OBO-processen.

Aanbevolen beveiligingsprocedures

Tokenopslag

  • Nooit voltooide tokens registreren- alleen de laatste vier tekens vastleggen voor foutopsporing
  • Tokens veilig opslaan in het geheugen
  • Tokens uit het geheugen wissen na gebruik
  • Sla tokens niet op in schijf of databases

Tokenvalidatie

  • Altijd het appToken valideren in de indeling SubjectAndAppToken
  • Valideer het subjectToken alleen indien aanwezig
  • Tokenhandtekeningen controleren op basis van Azure AD-sleutels
  • Controleer de verloop- en niet-vóór-tijdstippen van tokens.
  • Verlener-, doelgroep- en tenantclaims valideren
  • Controleer de vereiste claims (scp, idtyp, enzovoort)

Handelen met ontbrekende gebruikerscontext

  • Uw externe eindpunt ontwerpen om zowel door de gebruiker gedelegeerde als alleen-app-scenario's af te handelen
  • hasSubjectContext authContext inchecken voordat u toegang hebt tot gebruikersspecifieke eigenschappen
  • Voor bewerkingen waarvoor gebruikerscontext is vereist, stelt u deze in requireSubjectToken: true middlewareopties in
  • Voor verwijderbewerkingen of systeembewerkingen stelt u het ontbreken van een subjectToken toe.
  • Documenteer welke bewerkingen alleen-app-context ondersteunen en waarvoor gebruikerscontext is vereist

Omgevingsconfiguratie

// 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}`);
  }
});

Wanneer tokenuitwisseling mislukt vanwege ontbrekende toestemming of scopes, moet uw back-end deze foutmeldingen doorgeven aan de front-end of gebruikersinterface om de gebruiker om toestemming te vragen. Veelvoorkomende fouten met betrekking tot toestemming zijn:

  • AADSTS65001 - Gebruiker of beheerder heeft niet toestemming gegeven om de toepassing te gebruiken
  • AADSTS65005 - voor de toepassing zijn specifieke machtigingen vereist waarvoor geen toestemming is gegeven
/**
 * 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;
}

Wanneer uw front-end een ConsentRequired fout ontvangt, moet deze de gebruiker omleiden naar de toestemmings-URL:

// 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;
  }
}

Opmerking

Het Fabric-workloadontwikkelingsvoorbeeld bevat volledige voorbeelden van het afhandelen en doorgeven van toestemmingsfouten aan de frontend UX. Zie de verificatie-middleware en front-endfoutafhandeling van het voorbeeld voor implementaties die gereed zijn voor productie.

Aanvullende foutscenario's

// 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;
}

Troubleshooting

Veelvoorkomende verificatieproblemen

Fout: 'Ongeldige tokenindeling'

  • Controleren of de autorisatieheader begint met SubjectAndAppToken1.0
  • Controleer of zowel subjectToken als appToken aanwezig zijn
  • Zorg ervoor dat tokens correct worden vermeld in de koptekst

Fout: 'Tokenvalidatie is mislukt'

  • Controleer of BACKEND_APPID overeenkomt met uw app-registratie
  • Controleer of BACKEND_AUDIENCE juist is geconfigureerd
  • Controleren of tokens niet zijn verlopen
  • Controleren of de verlener overeenkomt met uw Azure AD-tenant

Fout: Tokenuitwisseling is mislukt: AADSTS65001

  • Gebruikerstoestemming is vereist voor het aangevraagde bereik
  • Zorg ervoor dat uw app-registratie over de benodigde API-machtigingen beschikt
  • Controleren of beheerderstoestemming is verleend indien nodig

Fout: 'App-token niet uit Fabric'

  • Controleer of de claim van appid de appToken overeenkomt met de app-id van Fabric
  • Controleer of u test in de juiste omgeving (dev/production)