Xero自定义集成Node SDK请求报403错误,求排查方案
问题背景
正在为Xero组织账户搭建自定义集成,当前使用Demo Company开发,生产环境将使用已购买订阅的正式组织。通过xero-node SDK开发首个API功能:已使用client id和secret成功获取access token并设置到Xero客户端,但调用xero.accountingAPI.getContacts时返回403错误。
已确认权限范围(scopes)正常,解码后的access token包含accounting.contacts权限。了解到xero-tenant-id请求头,但执行xero.updateTenants()后,xero.tenants为空数组。疑惑自定义集成针对自身组织是否需要tenant ID,提出两个问题:
- 若需要tenant ID,如何获取并在代码中配置到Xero客户端?
- 若不需要,还有哪些可能的配置问题?
代码片段
const xero = new XeroClient({ clientId: process.env.XERO_CLIENT_ID, clientSecret: process.env.XERO_CLIENT_SECRET, grantType: "client_credentials", scopes: "accounting.transactions accounting.settings accounting.contacts accounting.settings.read".split(" "), }); const generateClientInXero = async (company) => { try { // await generateConsentUrl(); // Obtain access token const tokenSet = await getAccessToken(); console.log("back with access token", tokenSet); // const xero_scopes = ; // Set the token in Xero client xero.setTokenSet(tokenSet); xero.updateTenants(); console.log(xero.tenants); // // Setup client and contacts in Xero const clientContactID = await setupClientInXero(company); } catch (err) { console.error(err); } }; const getClientCredentialsToken = async () => { const url = "https://identity.xero.com/connect/token"; const client_id = process.env.XERO_CLIENT_ID; const client_secret = process.env.XERO_CLIENT_SECRET; const xero_scopes = "accounting.transactions accounting.settings accounting.contacts accounting.settings.read"; // Assuming xero_scopes is defined somewhere // Encode client_id and client_secret to Base64 const credentials = Buffer.from(`${client_id}:${client_secret}`).toString("base64"); // Request body parameters const data = new URLSearchParams(); data.append("grant_type", "client_credentials"); data.append("scope", "accounting.contacts accounting.transactions accounting.settings accounting.settings.read"); try { const response = await axios.post(url, data, { headers: { Authorization: `Basic ${credentials}`, "Content-Type": "application/x-www-form-urlencoded", }, }); return response.data; } catch (error) { console.error("Error obtaining access token:", error.response ? error.response.data : error.message); throw new Error("Failed to obtain access token"); } }; const storeAccessToken = async (accessToken) => { const tokenDocRef = firestore.collection("xeroTokens").doc("tokenSet"); await tokenDocRef.set({ ...accessToken, updatedAt: DateTime.now().toISO(), }); }; const loadAccessToken = async () => { const tokenDocRef = firestore.collection("xeroTokens").doc("tokenSet"); const tokenDoc = await tokenDocRef.get(); if (tokenDoc.exists) { const tokenData = tokenDoc.data(); console.log("Token exists"); // const currentTimestamp = new Date().getTime(); // Current time in milliseconds // const expiresAt = tokenData.expiresAt || 0; // Ensure expiresAt is defined // if (expiresAt < currentTimestamp) { // console.log("TOKEN EXPIRED - should get new one now"); // throw new Error("Xero access token has expired. Please re-authenticate."); // } return tokenData; } else { throw new Error("Xero access token not found. Please authenticate first."); } }; const getAccessToken = async () => { let accessToken; try { accessToken = await loadAccessToken(); } catch (error) { console.log("ERROR IN GET ACCESS TOKEN: ", error); accessToken = await getClientCredentialsToken(); await storeAccessToken(accessToken); } return accessToken; };
错误信息
{ "response": { "statusCode": 403, "body": { "Type": null, "Title": "Forbidden", "Status": 403, "Detail": "AuthenticationUnsuccessful", "Instance": "43e09fc9-ea0f-4679-b1d5-c907777bacf0", "Extensions": {} }, "headers": { "content-type": "application/json", "content-length": "150", "server": "nginx", "xero-correlation-id": "43e01324-ea0f-4679-b1d5-c912313132", "x-appminlimit-remaining": "9998", "expires": "Fri, 28 Jun 2024 13:33:19 GMT", "cache-control": "max-age=0, no-cache, no-store", "pragma": "no-cache", "date": "Fri, 28 Jun 2024 13:33:19 GMT", "connection": "close", "x-client-tls-ver": "tls1.3", "set-cookie": "ak_bmsc=xxxxxxxxxx....; Domain=.xero.com; Path=/; Expires=Fri, 28 Jun 2024 15:33:19 GMT; Max-Age=7200" }, "request": { "url": { "protocol": "https:", "port": 443, "host": "api.xero.com", "path": "/api.xro/2.0/Contacts" }, "headers": { "accept": "application/json", "content-type": "application/json", "user-agent": "xero-node-7.0.0", "xero-tenant-id": "[object Object]", "authorization": "Bearer xxxxxxxxxxxxxx....", "content-length": "2", "accept-encoding": "gzip, compress, deflate, br", "host": "api.xero.com" }, "method": "GET" } }, "body": { "Type": null, "Title": "Forbidden", "Status": 403, "Detail": "AuthenticationUnsuccessful", "Instance": "43e09fc9-ea0f-4679-b1d5-c912313132", "Extensions": {} } }
关于Tenant ID的必要性
无论集成是否针对自身组织,调用Xero Accounting API必须携带有效的xero-tenant-id请求头。Xero的每个组织对应唯一的Tenant ID,API需要通过这个ID确定要操作的组织资源。
问题1:获取并配置Tenant ID的方法
1. 手动获取Tenant ID
登录Xero开发者门户,进入你的应用详情页,在"Connected organisations"下找到Demo Company或正式组织,查看其Tenant ID。
2. 通过API自动获取
使用Client Credentials模式获取token后,调用GET https://api.xero.com/connections接口,该接口会返回当前token有权访问的所有组织信息,包含Tenant ID。示例代码:
const getTenantId = async (tokenSet) => { const response = await axios.get("https://api.xero.com/connections", { headers: { Authorization: `Bearer ${tokenSet.access_token}` } }); if (response.data.length > 0) { return response.data[0].tenantId; } throw new Error("No connected tenants found"); };
3. 在代码中配置到Xero客户端
获取到Tenant ID后,有两种方式配置:
- 方式一:调用API时指定tenant ID
const contacts = await xero.accountingAPI.getContacts(null, null, { headers: { "xero-tenant-id": YOUR_TENANT_ID } });
- 方式二:更新Xero客户端的tenants数组,让SDK自动携带头
const tenantId = await getTenantId(tokenSet); xero.tenants = [{ tenantId: tenantId }]; // 之后调用API时SDK会自动添加xero-tenant-id头 const contacts = await xero.accountingAPI.getContacts();
注意:你的错误信息中显示xero-tenant-id: "[object Object]",这是因为xero.tenants为空时,SDK尝试读取无效的对象导致的,必须确保xero.tenants中包含有效的tenant对象。
问题2:其他可能的配置问题
Client Credentials模式权限验证
- 确认你的Xero应用已与目标组织(Demo Company或正式组织)建立连接。在开发者门户的应用详情页,检查"Connected organisations"列表是否包含目标组织。
- 确保应用的权限范围(scopes)已被目标组织授权,即使token包含scopes,若组织未授权该应用访问,也会返回403。
Token有效性
- 检查token是否过期,虽然你注释了过期检查逻辑,但过期的token会导致认证失败。
- 确认获取token时使用的scopes与XeroClient初始化时的scopes完全一致,避免权限不匹配。
SDK使用问题
- 你手动实现了token获取逻辑,建议直接使用xero-node SDK的内置方法获取token,避免手动处理时的格式错误:
await xero.getClientCredentialsToken(); // 此时xero.tenants会自动更新 console.log(xero.tenants); xero.updateTenants()是异步方法,你当前同步调用,导致xero.tenants还未更新就打印,应该改为:await xero.updateTenants(); console.log(xero.tenants);
- 你手动实现了token获取逻辑,建议直接使用xero-node SDK的内置方法获取token,避免手动处理时的格式错误:
内容的提问来源于stack exchange,提问作者Marko Vidalis

