Del via


Autentificér fjernendepunkter

Fjernendepunkter i Microsoft Fabric-arbejdsbelastninger bruger en specialiseret autentificeringsmekanisme, der giver både brugerkontekst og applikationsidentitet. Denne artikel forklarer, hvordan man implementerer autentificering for fjernendepunkter, der håndterer jobs, livscyklusnotifikationer og andre backend-operationer.

Oversigt over autentificeringsstrømme

Fabric-arbejdsbelastninger involverer tre primære autentificeringsflows:

  1. Fabric to Workload - Fabric kalder dit fjernterminale med SubjectAndAppToken-formatet
  2. Arbejdsbelastning til Fabric - Din arbejdsbelastning kalder Fabric-API'er med SubjectAndAppToken eller Bearer-tokens
  3. Workload frontend til backend - Din frontend kalder din backend med Bearer-tokens

Denne artikel fokuserer på at autentificere forespørgsler fra Fabric til din arbejdsbyrde og foretage autentificerede kald tilbage til Fabric.

SubjectAndAppToken-format

Når Fabric kalder dit fjernterminale (til jobs, livscyklusnotifikationer eller andre operationer), bruger det et dual-token autentificeringsformat kaldet SubjectAndAppToken.

Autorisationsheaderstruktur

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

Dette format indeholder to særskilte tokens:

  • subjectToken - Et delegeret token, der repræsenterer brugeren, på hvis vegne operationen udføres
  • appToken - Et app-only token fra Fabric-applikationen, der beviser, at anmodningen stammer fra Fabric

Vigtigt!

Det er subjectToken ikke altid til stede. Den kan mangle i scenarier som:

  • Service principal-operationer - Når en service principal interagerer med Fabric offentlige API'er uden brugerkontekst
  • Systemoperationer - Operationer som sletning af elementer, hvor ingen bruger er direkte involveret
  • Automatiserede arbejdsgange - CI/CD-pipelines eller planlagte operationer, der kører uden brugerindblanding

Dit fjernterminale endpoint skal være designet til at håndtere begge tilfælde: forespørgsler med brugerkontekst (subjectToken til stede) og forespørgsler uden brugerkontekst (subjectToken fraværende).

Hvorfor Dual Tokens?

Dual-token-tilgangen giver tre nøglefordele:

  1. Validering - Verificér, at anmodningen stammer fra Fabric ved at validere appToken
  2. Brugerkontekst - subjectToken giver brugerkontekst for den handling, der udføres
  3. Inter-Service Kommunikation - Brug subjectToken til at erhverve On-Behalf-Of (OBO) tokens til at kalde andre tjenester med brugerkontekst

Parsing SubjectAndAppToken

Dit eksterne endpoint skal parse autorisationsheaderen for at udtrække begge tokens.

JavaScript-eksempel

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

Eksempel på brug

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

Tokenvalidering

Begge tokens skal valideres for at sikre ægthed og korrekte påstande.

App Token-validering

Der appToken er et app-only token fra Fabric. Bekræft:

  • Token-signatur - Verificér mod Azure AD-signeringsnøgler
  • Token-levetid - Check nbf (ikke før) og exp (udløb) krav
  • Publikum - Skal matche dit arbejdsprograms app-registrering
  • Udsteder - Skal være fra Azure AD med korrekt lejer
  • idtyp claim - Skal være "app" for app-only tokens
  • Ingen scp claim - App-only tokens har ikke scope-krav
  • Lejer-ID - Skal matche dit udgiver-lejer-ID

Eksempel på appToken-krav:

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

Validering af emnetoken

Det er subjectToken et brugerdelegeret token. Bekræft:

  • Token-signatur - Verificér mod Azure AD-signeringsnøgler
  • Token-levetid - Check nbf og exp krav
  • Publikum - Skal matche dit arbejdsprograms app-registrering
  • Udsteder - Skal være fra Azure AD med korrekt lejer
  • scp Krav - Skal inkludere "FabricWorkloadControl" omfanget
  • Ingen idtyp krav – Delegerede tokens har ikke dette krav
  • appid skal matche - Skal være det samme som i appToken

Eksempel på subjectToken-krav:

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

JavaScript-tokenvalideringseksempel

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

Autentificeringsmiddleware

Implementer middleware til at autentificere indkommende forespørgsler fra Fabric.

Fuldstændig autentificeringsflow

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

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

Tokenudveksling - På-Behalf-Of (OBO) Flow

For at kalde Fabric API'er eller få adgang til OneLake fra dit eksterne endpoint, skal du bytte brugerens subjectToken ud med ressourcespecifikke tokens ved brug af OAuth 2.0 On-Behalf-Of flowet.

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

Brug af tokenudveksling

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

Kald af Fabric API'er

For at kalde Fabric workload control API'er skal du konstruere en SubjectAndAppToken med både OBO- og S2S-tokens.

Service-til-tjeneste (S2S) token

Ud over OBO-tokenet skal du bruge et S2S-token kun til appToken-delen.

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

At bygge Composite Token til Fabric API'er

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

Eksempel på at kalde Fabric API'er

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

Komplet eksempel: Jobudførelse med autentificering

Her er et komplet eksempel på et jobudførelsesendepunkt med autentificering:

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

Long-Running Drift

For langvarige operationer som jobs kan tokens udløbe før færdiggørelsen. Implementér token-opdateringslogik:

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

For mere information om håndtering af langvarige OBO-processer, se Langvarige OBO-processer.

Sikkerhedsbedste praksis

Tokenopbevaring

  • Log aldrig komplette tokens – kun de sidste 4 tegn til fejlfinding
  • Gem tokens sikkert kun i hukommelsen
  • Slet tokens fra hukommelsen efter brug
  • Lagre ikke tokens på disk eller databaser

Tokenvalidering

  • Valider altid appToken i SubjectAndAppToken-formatet
  • Validér subjectToken kun, hvis det er til stede
  • Verificér tokensignaturer mod Azure AD-nøgler
  • Checktokens udløbstid og tidspunkter for ikke-før-token
  • Valider udsteder-, publikum- og lejerkrav
  • Verificér de nødvendige krav (scp, idtype osv.)

Håndtering af manglende brugerkontekst

  • Design dit fjernterminal, så det håndterer både brugerdelegerede og kun app-scenarier
  • Tjek hasSubjectContext i authContext, før du tilgår brugerspecifikke egenskaber
  • For operationer, der kræver brugerkontekst, sæt requireSubjectToken: true middleware-indstillinger i
  • For sletningsoperationer eller systemoperationer tillad manglende subjectToken
  • Dokumentér, hvilke operationer der understøtter kun app-kontekst, og hvilke der kræver brugerkontekst

Miljøkonfiguration

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

Når token exchange fejler på grund af manglende samtykke eller scopes, bør din backend videregive disse fejl til frontend UX for at bede brugeren om samtykke. Almindelige fejl relateret til samtykke inkluderer:

  • AADSTS65001 - Bruger eller administrator har ikke givet samtykke til at bruge applikationen
  • AADSTS65005 - Ansøgningen kræver specifikke scopes, som ikke er givet samtykke til
/**
 * 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;
}

Når dit frontend modtager en ConsentRequired fejl, bør det omdirigere brugeren til samtykke-URL'en:

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

Bemærkning

Fabric workload udviklingseksemplet indeholder komplette eksempler på håndtering af samtykkefejl og udbredelse til frontend UX. Se eksemplarets autentificeringsmiddleware og frontend-fejlhåndtering for produktionsklare implementeringer.

Yderligere fejlscenarier

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

Fejlfinding

Almindelige autentificeringsproblemer

Fejl: "Ugyldigt tokenformat"

  • Bekræfte, at autorisationsheaderen starter med SubjectAndAppToken1.0
  • Tjek at både subjectToken og appToken er til stede
  • Sørg for, at tokens er korrekt citeret i headeren

Fejl: "Tokenvalidering fejlede"

  • Bekræft BACKEND_APPID matcher din app-registrering
  • Tjek at BACKEND_AUDIENCE er korrekt konfigureret
  • Sørg for, at tokens ikke er udløbet
  • Verificér udstederen matcher din Azure AD-lejer

Fejl: "Token-udveksling mislykkedes: AADSTS65001"

  • Brugerens samtykke kræves for det anmodede omfang
  • Sørg for, at din app-registrering har de nødvendige API-tilladelser
  • Kontroller admin-samtykke, hvis det kræves

Fejl: "App-token ikke fra Fabric"

  • Bekræft appTokens appid krav matcher Fabrics app-ID
  • Tjek at du tester i det rigtige miljø (udvikling/produktion)