Microsoft Fabric 워크로드의 원격 엔드포인트는 사용자 컨텍스트와 애플리케이션 ID를 모두 제공하는 특수 인증 메커니즘을 사용합니다. 이 문서에서는 작업, 수명 주기 알림 및 기타 백 엔드 작업을 처리하는 원격 엔드포인트에 대한 인증을 구현하는 방법을 설명합니다.
인증 흐름 개요
패브릭 워크로드에는 다음 세 가지 기본 인증 흐름이 포함됩니다.
- Fabric에서 워크로드로 - Fabric은 SubjectAndAppToken 형식으로 원격 엔드포인트를 호출합니다.
- Fabric에 대한 워크로드 - 워크로드가 SubjectAndAppToken 또는 전달자 토큰을 사용하여 Fabric API를 호출합니다.
- 워크로드 프런트 엔드에서 백 엔드 로 - 프런트 엔드가 전달자 토큰을 사용하여 백 엔드를 호출합니다.
이 문서에서는 Fabric에서 워크로드로 요청을 인증하는 방법과 인증된 호출을 Fabric으로 다시 수행하는 방법에 중점을 둡니다.
SubjectAndAppToken 형식
Fabric은 작업, 수명 주기 알림 또는 기타 작업의 경우 원격 엔드포인트를 호출할 때 SubjectAndAppToken이라는 이중 토큰 인증 형식을 사용합니다.
권한 부여 헤더 구조
SubjectAndAppToken1.0 subjectToken="<user-delegated-token>", appToken="<app-only-token>"
이 형식에는 다음과 같은 두 가지 고유 토큰이 포함됩니다.
-
subjectToken- 작업을 대신하는 사용자를 나타내는 위임된 토큰 -
appToken- 패브릭 애플리케이션의 앱 전용 토큰으로, 패브릭에서 시작된 요청을 증명합니다.
중요합니다
항상 subjectToken 있는 것은 아닙니다. 다음과 같은 시나리오에서 누락될 수 있습니다.
- 서비스 주체 작업 - 서비스 주체가 사용자 컨텍스트 없이 Fabric 공용 API와 상호 작용하는 경우
- 시스템 작업 - 사용자가 직접 관여하지 않는 항목 삭제와 같은 작업
- 자동화된 워크플로 - CI/CD 파이프라인 또는 사용자 상호 작용 없이 실행되는 예약된 작업
원격 엔드포인트는 사용자 컨텍스트가 있는 요청(subjectToken이 있음) 및 사용자 컨텍스트가 없는 요청(subjectToken 없음)의 두 경우를 모두 처리하도록 설계되어야 합니다.
이중 토큰을 사용하는 이유는 무엇인가요?
이중 토큰 접근 방식은 다음 세 가지 주요 이점을 제공합니다.
- 유효성 검사 - appToken의 유효성을 검사하여 요청이 패브릭에서 시작되었는지 확인합니다.
- 사용자 컨텍스트 - subjectToken은 수행 중인 작업에 대한 사용자 컨텍스트를 제공합니다.
- Inter-Service 통신 - subjectToken을 사용하여 OBO(On-Behalf-Of) 토큰을 획득하고, 이를 통해 사용자 컨텍스트로 다른 서비스를 호출합니다.
SubjectAndAppToken 구문 분석
원격 엔드포인트는 권한 부여 헤더를 구문 분석하여 두 토큰을 모두 추출해야 합니다.
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
};
}
사용 예제
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)');
}
토큰 유효성 검사
신뢰성과 적절한 클레임을 보장하기 위해 두 토큰의 유효성을 검사해야 합니다.
앱 토큰 유효성 검사
패브릭 appToken 의 앱 전용 토큰입니다. 유효성 검사:
- 토큰 서명 - Azure AD 서명 키에 대해 확인
-
토큰 수명 - (이전이 아님) 및
nbf(만료) 클레임 확인exp - 대상 그룹 - 워크로드의 앱 등록과 일치해야 합니다.
- Issuer - 올바른 테넌트가 있는 Azure AD에서 온 것이어야 합니다.
-
idtyp클레임 - 앱 전용 토큰에 대한 것이어야"app"합니다. -
클레임 없음
scp- 앱 전용 토큰에 범위 클레임이 없음 - 테넌트 ID - 게시자 테넌트 ID와 일치해야 합니다.
샘플 앱 토큰 클레임:
{
"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"
}
주체 토큰 유효성 검사
사용자가 subjectToken 위임한 토큰입니다. 유효성 검사:
- 토큰 서명 - Azure AD 서명 키에 대해 확인
-
토큰 수명 - 확인
nbf및exp클레임 - 대상 그룹 - 워크로드의 앱 등록과 일치해야 합니다.
- Issuer - 올바른 테넌트가 있는 Azure AD에서 온 것이어야 합니다.
-
scp클레임 - 범위를 포함해야"FabricWorkloadControl"합니다. -
클레임 없음
idtyp- 위임된 토큰에 이 클레임이 없습니다. -
appid일치해야 합니다 . appToken과 동일해야 합니다.
샘플 subjectToken 클레임:
{
"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 토큰 유효성 검사 예제
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;
}
인증 미들웨어
패브릭에서 들어오는 요청을 인증하는 미들웨어를 구현합니다.
전체 인증 흐름
/**
* 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;
}
}
미들웨어 사용
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...
});
토큰 교환 - OBO(on-Behalf-Of) 흐름
패브릭 API를 호출하거나 원격 엔드포인트에서 OneLake에 액세스하려면 OAuth 2.0 On-Behalf-Of 흐름을 사용하여 사용자의 subjectToken을 리소스별 토큰으로 교환해야 합니다.
토큰 거래소 서비스
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();
});
}
토큰 교환 사용
// 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 호출
패브릭 워크로드 제어 API를 호출하려면 OBO 및 S2S 토큰을 모두 사용하여 SubjectAndAppToken을 생성해야 합니다.
S2S(서비스 대 서비스) 토큰
OBO 토큰 외에도 appToken 부분에 대한 앱 전용 S2S 토큰이 필요합니다.
/**
* 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);
}
패브릭 API에 대한 복합 토큰 빌드
/**
* 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}"`;
}
Fabric API 호출 예제
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;
}
전체 예제: 인증을 사용한 작업 실행
다음은 인증을 사용하는 작업 실행 엔드포인트의 전체 예제입니다.
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
});
}
});
장시간 실행 작업
작업과 같은 장기 실행 작업의 경우 토큰이 완료되기 전에 만료될 수 있습니다. 토큰 새로 고침 논리 구현:
// 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;
}
장기 실행 OBO 프로세스 처리에 대한 자세한 내용은 장기 실행 OBO 프로세스를 참조하세요.
보안 모범 사례
토큰 스토리지
- 전체 토큰을 기록하지 마세요. 디버깅을 위해 마지막 4자만 기록합니다.
- 메모리에만 안전하게 토큰 저장
- 사용 후 메모리에서 토큰 지우기
- 디스크 또는 데이터베이스에 토큰 유지 안 함
토큰 유효성 검사
- 항상 SubjectAndAppToken 형식으로 appToken의 유효성을 검사합니다.
- subjectToken이 있는 경우에만 유효성 검사
- Azure AD 키에 대한 토큰 서명 확인
- 토큰 만료 및 이전 시간 확인
- 발급자, 대상 그룹 및 테넌트 클레임의 유효성 검사
- 필요한 클레임 확인(scp, idtyp 등)
누락된 사용자 컨텍스트 처리
- 사용자가 위임한 시나리오와 앱 전용 시나리오를 모두 처리하도록 원격 엔드포인트 디자인
- 사용자별 속성에 액세스하기 전에 authContext
hasSubjectContext를 확인하세요. - 사용자 컨텍스트가 필요한 작업의 경우 미들웨어 옵션에서 설정합니다
requireSubjectToken: true. - 삭제 작업 또는 시스템 작업의 경우 누락된 subjectToken을 허용합니다.
- 앱 전용 컨텍스트를 지원하는 작업 및 사용자 컨텍스트가 필요한 작업 문서화
환경 구성
// 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}`);
}
});
오류 처리 및 동의 전파
동의 또는 범위 누락으로 인해 토큰 교환이 실패하면 백 엔드에서 이러한 오류를 프런트 엔드 UX로 전파하여 사용자에게 동의를 요청해야 합니다. 일반적인 동의 관련 오류는 다음과 같습니다.
- AADSTS65001 - 사용자 또는 관리자가 애플리케이션 사용에 동의하지 않았습니다.
- AADSTS65005 - 애플리케이션에 대해 아직 동의되지 않은 특정 범위가 필요합니다.
/**
* 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;
}
프런트 엔드 동의 처리
프런트 엔드에서 ConsentRequired 오류가 발생하면 사용자를 동의 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;
}
}
메모
패브릭 워크로드 개발 샘플에는 프런트 엔드 UX에 대한 동의 오류 처리 및 전파의 전체 예제가 포함되어 있습니다. 프로덕션 준비 구현에 대한 샘플의 인증 미들웨어 및 프런트 엔드 오류 처리를 참조하세요.
추가 오류 시나리오
// 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
일반적인 인증 문제
오류: "잘못된 토큰 형식"
- 권한 부여 헤더가 로 시작하는지 확인합니다.
SubjectAndAppToken1.0 - subjectToken과 appToken이 모두 있는지 확인합니다.
- 헤더에서 토큰이 제대로 따옴표로 묶이도록 합니다.
오류: "토큰 유효성 검사 실패"
- BACKEND_APPID 앱 등록과 일치하는지 확인
- BACKEND_AUDIENCE 올바르게 구성되었는지 확인합니다.
- 토큰이 만료되지 않았는지 확인
- Azure AD 테넌트와 발급자가 일치하는지 확인하세요.
오류: "토큰 교환 실패: AADSTS65001"
- 요청된 범위에 대한 사용자 동의가 필요합니다.
- 앱 등록에 필요한 API 권한이 있는지 확인
- 필요한 경우 관리자 동의가 부여되었는지 확인
오류: "패브릭에서 발급되지 않은 앱 토큰"
- appToken의
appid클레임이 패브릭의 앱 ID와 일치하는지 확인합니다. - 올바른 환경(개발/프로덕션)에서 테스트하고 있는지 확인합니다.