如何从 OpenID Connect 获取 Azure billing API 的 refresh_token
结论
长期无人值守的后台任务不应依赖某个用户的 refresh_token。更合适的做法是使用 Microsoft Entra ID 的应用身份:
- 任务运行在 Azure 上时,优先使用 Managed Identity。
- 任务运行在 Azure 外部时,使用 Service Principal 和证书,通过 OAuth 2.0
client_credentialsflow 获取令牌。 - 只有 API 必须以用户身份调用时,才使用 authorization code flow,并由 MSAL 管理令牌缓存。不要自行提取、持久化或刷新
refresh_token。
如果当前只是排查 .NET OpenID Connect 应用无法取得 refresh_token 的问题,除了请求 offline_access,还要启用 SaveTokens,再从认证会话的 AuthenticationProperties 中读取令牌。不过,这种方式不适合作为长期后台服务的身份方案。
为什么看得到 refresh_token,应用中却取不到
ASP.NET Core 的 OpenID Connect handler 默认不会把所有令牌保存到认证 cookie。即使令牌端点返回了 refresh_token,应用通常也只能在认证事件中短暂访问它。后续调用 HttpContext.GetTokenAsync("refresh_token") 时,可能无法取得该令牌。
要让应用保存并读取 refresh_token,需要满足这些条件:
- 使用 authorization code flow,例如
ResponseType = "code"。 - 请求
offline_accessscope。 - 设置
SaveTokens = true。 - 应用注册允许相应的 delegated permissions。
- 用户或管理员已完成 consent。
- 身份提供方确实为该请求签发了
refresh_token。
示例:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
builder.Services
.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie("Cookies")
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
options.Authority =
"https://login.microsoftonline.com/{tenant-id}/v2.0";
options.ClientId = "{client-id}";
options.ClientSecret = "{client-secret}";
options.ResponseType = OpenIdConnectResponseType.Code;
options.UsePkce = true;
options.SaveTokens = true;
options.Scope.Add("offline_access");
// 示例:Azure Resource Manager delegated permission scope
options.Scope.Add("https://management.azure.com/user_impersonation");
});
登录完成后,可以读取认证会话中保存的令牌:
string? accessToken =
await HttpContext.GetTokenAsync("access_token");
string? refreshToken =
await HttpContext.GetTokenAsync("refresh_token");
也可以在认证事件中访问令牌端点返回的数据:
options.Events = new OpenIdConnectEvents
{
OnTokenResponseReceived = context =>
{
string? refreshToken = context.TokenEndpointResponse?.RefreshToken;
// 不建议在这里把 refresh_token 以明文写入数据库或日志。
return Task.CompletedTask;
}
};
启用 SaveTokens = true 后,令牌通常会进入认证属性,而认证属性可能被序列化到 cookie 中。令牌较大时会让 cookie 膨胀,也会增加敏感凭据的暴露风险。因此,这项配置更适合用于排查和验证问题,不是后台服务存储令牌的首选方式。
推荐方案:使用 Managed Identity
如果后台任务运行在 Azure Functions、App Service、Virtual Machines、Container Apps 或其他支持 Managed Identity 的服务上,可以为计算资源启用 system-assigned 或 user-assigned managed identity。
随后,为该身份分配所需的 Azure RBAC 角色。代码无须保存 client secret、access token 或 refresh_token:
using Azure.Core;
using Azure.Identity;
var credential = new DefaultAzureCredential();
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(
new[] { "https://management.azure.com/.default" }),
cancellationToken);
string accessToken = token.Token;
还可以把 credential 直接交给 Azure SDK 客户端,由 SDK 自动获取和更新 access token:
using Azure.Identity;
using Azure.ResourceManager;
var credential = new DefaultAzureCredential();
var armClient = new ArmClient(credential);
await foreach (var subscription in armClient.GetSubscriptions().GetAllAsync())
{
Console.WriteLine($"{subscription.Data.DisplayName}: {subscription.Data.SubscriptionId}");
}
在 Azure 中,DefaultAzureCredential 可以使用 Managed Identity。在本地开发环境中,它可以使用开发人员已登录的 Azure CLI、Visual Studio 等身份。生产环境仍需明确配置并验证实际使用的凭据来源。
Azure 外部运行:Service Principal 与证书
如果任务不在支持 Managed Identity 的 Azure 环境中,可以创建应用注册和对应的 Service Principal,使用证书执行 client_credentials flow。
使用 Azure.Identity 的示例:
using Azure.Core;
using Azure.Identity;
using System.Security.Cryptography.X509Certificates;
var certificate = new X509Certificate2(
"/secure/path/application.pfx",
"{certificate-password}");
var credential = new ClientCertificateCredential(
"{tenant-id}",
"{client-id}",
certificate);
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(
new[] { "https://management.azure.com/.default" }),
cancellationToken);
如果只能使用 client secret:
using Azure.Core;
using Azure.Identity;
var credential = new ClientSecretCredential(
"{tenant-id}",
"{client-id}",
"{client-secret}");
AccessToken token = await credential.GetTokenAsync(
new TokenRequestContext(
new[] { "https://management.azure.com/.default" }),
cancellationToken);
生产环境通常更适合使用证书,而不是长期有效的 client secret。证书私钥仍需保存在安全存储中,例如 Azure Key Vault,并制定轮换方案。
client_credentials flow 不会签发 refresh_token。access token 过期前后,服务使用自身凭据重新申请即可。
如何让应用枚举 subscriptions
Service Principal 或 Managed Identity 不会自动继承创建者、管理员或某个用户的订阅权限。必须将 Azure RBAC 角色明确授予该应用身份。
例如,可以在订阅范围内授予 Reader:
az role assignment create \
--assignee-object-id "<service-principal-object-id>" \
--assignee-principal-type ServicePrincipal \
--role "Reader" \
--scope "/subscriptions/<subscription-id>"
如果需要访问多个订阅,可以采用以下方式:
- 分别在每个 subscription scope 上授权。
- 在共同的 management group scope 上授权,让权限向下继承。
- 根据 API 要求授予权限范围更窄的自定义角色,遵循最小权限原则。
“枚举某个用户能访问的所有订阅”与”枚举某个应用身份能访问的所有订阅”采用不同的权限模型。改用应用身份后,返回的是该 Service Principal 或 Managed Identity 已获授权的订阅,不是某个管理员用户个人可见的全部订阅。
如果业务要求管理员选择允许应用访问的订阅,可以在初始配置阶段让管理员登录并完成 consent 或资源授权,之后的后台任务仍使用应用身份运行。
Billing API 还需要单独授权
能够读取 Azure Resource Manager 中的 subscription,不代表能够读取所有 billing data。Azure Billing、Cost Management 和 Consumption API 可能采用不同的授权范围和角色,例如 subscription、billing account、billing profile 或 invoice section。
应先确认实际调用的是哪类 API:
- Azure Resource Manager subscription API。
- Cost Management API。
- Consumption API。
- Azure Billing API。
- Microsoft Graph 或其他资源 API。
确认后,再在对应的资源范围内为应用身份授予所需角色。某些 billing 操作是否支持 application identity,以及支持哪些协议和角色,会因计费协议和 API 而异。如果特定接口只支持 delegated permission,才需要保留用户授权模型。
如果确实必须使用用户身份
当目标 API 只允许 delegated access,或业务语义要求”代表某个用户操作”时,建议使用 Microsoft Authentication Library(MSAL)完成 authorization code flow,并持久化 MSAL token cache,不要自行保存裸 refresh_token。
MSAL 会负责处理:
- access token 缓存。
- refresh token 使用。
- refresh token rotation。
- token endpoint 调用。
- 令牌失效和重新认证条件。
概念性配置如下:
using Microsoft.Identity.Web;
builder.Services
.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(
builder.Configuration.GetSection("AzureAd"))
.EnableTokenAcquisitionToCallDownstreamApi(
new[] { "https://management.azure.com/user_impersonation" })
.AddDistributedTokenCaches();
分布式或数据库 token cache 中的内容必须加密,并限制读取权限。不要将 access token 或 refresh token 写入普通日志、浏览器存储或未加密的数据库字段。
关于”refresh token 最长 90 天”
后台任务不应建立在固定的”90 天 refresh token 有效期”之上。Microsoft Entra ID 的令牌行为还会受到 refresh token rotation、撤销、用户状态、密码变更、Conditional Access、sign-in frequency 和租户策略等因素影响。即使令牌可以续期,也无法保证某个用户的 refresh token 永久有效。
以下事件都可能让无人值守任务突然要求用户重新登录:
- 用户被禁用或删除。
- 用户权限被撤销。
- 管理员撤销 consent。
- refresh token 被撤销。
- Conditional Access 要求重新认证或 MFA。
- 应用注册、凭据或租户策略发生变化。
因此,长期后台作业应使用 Managed Identity 或 Service Principal。只有确实需要 delegated user context 时,才应采用用户登录和 refresh token。
备注:内容仅供参考。
