MSAL 节点服务和合作伙伴中心 API
MSAL Node service & The Partner Center API
tl;博士
在 非 交互式服务中使用 msal-node
,如何授权为启用 MFA 的用户 once 和永远(或很少)必须再次授权为该用户?特别是在向合作伙伴中心提出请求时 API.
更长的版本
我一直在尝试在 NodeJS 中创建与 Microsoft 合作伙伴中心交互的后端服务(即守护进程)API。但是,如果没有持续的用户交互,我找不到官方支持的方法来执行此操作。
基于most documentation I could find, services without any user interaction are meant to use the "client credentials" flow. This works great for other Azure APIs, but doesn't seem to work in the Partner Center API despite what the docs说。
有效的方法,也是我被迫做的,是以用户身份登录。但是,因为所有用户都启用了 MFA,所以我必须遵循 in these docs 概述的方法,我以用户身份手动登录并一次性交换刷新令牌。然后我将它保存到一个文件中,然后守护进程可以不断地用它交换访问令牌和更新的刷新令牌。
但看起来 msal-node
不支持仅使用开箱即用的刷新令牌。唯一接受一个作为参数的实例方法是 .acquireTokenByRefreshToken()
,它 return 一个访问令牌(参见 this sample)但 不 更新实例的内部缓存或 return 一个新的刷新令牌,因此现有的刷新令牌会过早过期。它似乎也被弃用以支持 .acquireTokenSilent()
,但该方法要求实例的内部缓存已经填充了身份验证数据。但是,我没有看到任何 public 方法来手动设置内部缓存的数据,它只会在调用 .acquireTokenByCode()
之类的方法后自动设置,这些方法接受用户登录后生成的短期令牌。同样,由于所有用户都启用了 MFA,因此每次重新启动服务时都需要用户交互。
所以有点先有鸡还是先有蛋的问题。感谢任何帮助,我一直在疯狂阅读这么多 MS 文档和代码。
The refresh token is never returned to the user in a response
因此,您无法通过这种方式获取和更新刷新令牌,这是设计使然。
作为解决方法,您可以直接调用 Azure 广告 /token
端点 API 以通过旧令牌获取新的刷新令牌,然后替换文件中的旧令牌。
详情见this doc。虽然标题是 refresh the access token
,但您可以在每个请求中获得一个新的刷新令牌:
以便您可以在刷新令牌即将过期时刷新它。
发布这个问题后,我想出了在 msal-node
中如何做到这一点。默认情况下 msal-node
仅在内存中缓存令牌,这对于不可避免地重新启动的非交互式服务来说不是很好。
幸运的是,msal-node
客户端应用程序的配置参数有一个可选的 cache.cachePlugin
属性 来定义您自己的缓存 getter/setter。如 this 文档所示(忽略“ADAL”部分),您可以将其用于 fetch/store 磁盘上的标记。有了这个,你现在可以在任何需要 new 访问令牌的时候调用 acquireTokenSilent
,它会自动为你检索 and/or 刷新一个,即使你的服务重新启动。这是一个示例模块,它具有获取缓存用户帐户的访问令牌的功能,以及首先缓存用户帐户的功能:
auth.js
const fs = require('fs');
const msal = require('@azure/msal-node');
const CLIENT_ID = "MY-CLIENT-ID";
const CLIENT_SECRET = "MY-CLIENT-SECRET";
const CACHE_PATH = "/PATH/TO/CACHE/FILE";
const SCOPES = ["MY-REQUEST-SCOPE(S)"]; // e.g. "https://api.partnercenter.microsoft.com/.default", "https://graph.microsoft.com/.default", etc.
const CCA = new msal.ConfidentialClientApplication({
auth: { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET },
cache: {
cachePlugin: {
// Read the disk for tokens
beforeCacheAccess: async (context) => context.tokenCache.deserialize(await fs.promises.readFile(CACHE_PATH, 'utf-8')),
// Write the token back to the disk if it was refreshed
afterCacheAccess: (context) => context.cacheHasChanged ? fs.promises.writeFile(CACHE_PATH, context.tokenCache.serialize()) : null
}
}
});
/**
* Add the account that generated the authCode to the disk cache.
* @param {String} authCode - The code in the query of the MS redirect after the successful one-time user login
* @param {String} redirectUri - The URL that MS redirects to after the one-time user authentication
*/
exports.addAccountToCache = async (authCode, redirectUri) => {
await CCA.acquireTokenByCode({
redirectUri: redirectUri,
scopes: ["user.read"],
code: authCode
});
};
var lastTokenRes;
/**
* Gets a new or existing access token for a user account, and automatically handles refreshing when needed.
*/
exports.getAccessToken = async () => {
// Checking token expiration ourselves may seem redundant since calling .acquireTokenSilent() will do that on its own
// anyway, but each call to it takes at least 300ms regardless of whether or not a valid token is in the cache
if (!lastTokenRes || lastTokenRes.expiresOn >= Date.now()) {
let firstAccount = await CCA.getTokenCache().getAllAccounts()[0];
lastTokenRes = await CCA.acquireTokenSilent({
account: firstAccount,
scopes: SCOPES
});
}
return lastTokenRes.accessToken;
}
缓存用户帐户需要使用该用户的授权代码调用 addAccountToCache
。所述代码是通过 here, and should only need to be done once so long as the service calls getAccessToken
at least once every 90 days. For redundancy in a production setting, I'd also create an express route to sign-in a user account and generate their auth codes like in this sample app 描述的授权代码流生成的,但也可以手动工作,因为您可以根据需要传递生成的缓存文件。
tl;博士
在 非 交互式服务中使用 msal-node
,如何授权为启用 MFA 的用户 once 和永远(或很少)必须再次授权为该用户?特别是在向合作伙伴中心提出请求时 API.
更长的版本
我一直在尝试在 NodeJS 中创建与 Microsoft 合作伙伴中心交互的后端服务(即守护进程)API。但是,如果没有持续的用户交互,我找不到官方支持的方法来执行此操作。
基于most documentation I could find, services without any user interaction are meant to use the "client credentials" flow. This works great for other Azure APIs, but doesn't seem to work in the Partner Center API despite what the docs说。
有效的方法,也是我被迫做的,是以用户身份登录。但是,因为所有用户都启用了 MFA,所以我必须遵循 in these docs 概述的方法,我以用户身份手动登录并一次性交换刷新令牌。然后我将它保存到一个文件中,然后守护进程可以不断地用它交换访问令牌和更新的刷新令牌。
但看起来 msal-node
不支持仅使用开箱即用的刷新令牌。唯一接受一个作为参数的实例方法是 .acquireTokenByRefreshToken()
,它 return 一个访问令牌(参见 this sample)但 不 更新实例的内部缓存或 return 一个新的刷新令牌,因此现有的刷新令牌会过早过期。它似乎也被弃用以支持 .acquireTokenSilent()
,但该方法要求实例的内部缓存已经填充了身份验证数据。但是,我没有看到任何 public 方法来手动设置内部缓存的数据,它只会在调用 .acquireTokenByCode()
之类的方法后自动设置,这些方法接受用户登录后生成的短期令牌。同样,由于所有用户都启用了 MFA,因此每次重新启动服务时都需要用户交互。
所以有点先有鸡还是先有蛋的问题。感谢任何帮助,我一直在疯狂阅读这么多 MS 文档和代码。
The refresh token is never returned to the user in a response
因此,您无法通过这种方式获取和更新刷新令牌,这是设计使然。
作为解决方法,您可以直接调用 Azure 广告 /token
端点 API 以通过旧令牌获取新的刷新令牌,然后替换文件中的旧令牌。
详情见this doc。虽然标题是 refresh the access token
,但您可以在每个请求中获得一个新的刷新令牌:
以便您可以在刷新令牌即将过期时刷新它。
发布这个问题后,我想出了在 msal-node
中如何做到这一点。默认情况下 msal-node
仅在内存中缓存令牌,这对于不可避免地重新启动的非交互式服务来说不是很好。
幸运的是,msal-node
客户端应用程序的配置参数有一个可选的 cache.cachePlugin
属性 来定义您自己的缓存 getter/setter。如 this 文档所示(忽略“ADAL”部分),您可以将其用于 fetch/store 磁盘上的标记。有了这个,你现在可以在任何需要 new 访问令牌的时候调用 acquireTokenSilent
,它会自动为你检索 and/or 刷新一个,即使你的服务重新启动。这是一个示例模块,它具有获取缓存用户帐户的访问令牌的功能,以及首先缓存用户帐户的功能:
auth.js
const fs = require('fs');
const msal = require('@azure/msal-node');
const CLIENT_ID = "MY-CLIENT-ID";
const CLIENT_SECRET = "MY-CLIENT-SECRET";
const CACHE_PATH = "/PATH/TO/CACHE/FILE";
const SCOPES = ["MY-REQUEST-SCOPE(S)"]; // e.g. "https://api.partnercenter.microsoft.com/.default", "https://graph.microsoft.com/.default", etc.
const CCA = new msal.ConfidentialClientApplication({
auth: { clientId: CLIENT_ID, clientSecret: CLIENT_SECRET },
cache: {
cachePlugin: {
// Read the disk for tokens
beforeCacheAccess: async (context) => context.tokenCache.deserialize(await fs.promises.readFile(CACHE_PATH, 'utf-8')),
// Write the token back to the disk if it was refreshed
afterCacheAccess: (context) => context.cacheHasChanged ? fs.promises.writeFile(CACHE_PATH, context.tokenCache.serialize()) : null
}
}
});
/**
* Add the account that generated the authCode to the disk cache.
* @param {String} authCode - The code in the query of the MS redirect after the successful one-time user login
* @param {String} redirectUri - The URL that MS redirects to after the one-time user authentication
*/
exports.addAccountToCache = async (authCode, redirectUri) => {
await CCA.acquireTokenByCode({
redirectUri: redirectUri,
scopes: ["user.read"],
code: authCode
});
};
var lastTokenRes;
/**
* Gets a new or existing access token for a user account, and automatically handles refreshing when needed.
*/
exports.getAccessToken = async () => {
// Checking token expiration ourselves may seem redundant since calling .acquireTokenSilent() will do that on its own
// anyway, but each call to it takes at least 300ms regardless of whether or not a valid token is in the cache
if (!lastTokenRes || lastTokenRes.expiresOn >= Date.now()) {
let firstAccount = await CCA.getTokenCache().getAllAccounts()[0];
lastTokenRes = await CCA.acquireTokenSilent({
account: firstAccount,
scopes: SCOPES
});
}
return lastTokenRes.accessToken;
}
缓存用户帐户需要使用该用户的授权代码调用 addAccountToCache
。所述代码是通过 here, and should only need to be done once so long as the service calls getAccessToken
at least once every 90 days. For redundancy in a production setting, I'd also create an express route to sign-in a user account and generate their auth codes like in this sample app 描述的授权代码流生成的,但也可以手动工作,因为您可以根据需要传递生成的缓存文件。