You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Xero自定义集成Node SDK请求报403错误,求排查方案

Xero自定义集成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,提出两个问题:

  1. 若需要tenant ID,如何获取并在代码中配置到Xero客户端?
  2. 若不需要,还有哪些可能的配置问题?

代码片段

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:其他可能的配置问题

  1. Client Credentials模式权限验证

    • 确认你的Xero应用已与目标组织(Demo Company或正式组织)建立连接。在开发者门户的应用详情页,检查"Connected organisations"列表是否包含目标组织。
    • 确保应用的权限范围(scopes)已被目标组织授权,即使token包含scopes,若组织未授权该应用访问,也会返回403。
  2. Token有效性

    • 检查token是否过期,虽然你注释了过期检查逻辑,但过期的token会导致认证失败。
    • 确认获取token时使用的scopes与XeroClient初始化时的scopes完全一致,避免权限不匹配。
  3. 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);
      

内容的提问来源于stack exchange,提问作者Marko Vidalis

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.21 19:54:52