我正在尝试配置 WebApp(MSAL + Razor Pages)应用程序以使用两个应用程序注册 - 一个用于前端,另一个用于后端。主要原因是,我计划将来迁移到 SPA,并希望过渡尽可能顺利(无需请求新的同意/新的应用程序注册)。我以与 SPA 应用程序类似的方式配置它们(两个注册,后端所需的所有 API 权限,在后端的“knownClientApplications”中配置的前端注册。系统具有前端注册,用于对范围 api://{backendClientId 的用户进行身份验证}/.默认范围。一切正常,同意已正确传播到后端注册。
我遇到 OBO 流程问题。我尝试将下游 API 配置为使用后端服务器的客户端 ID,但由于权限不足,对图形 API 的调用失败。以下是服务器端的配置:
services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(o =>
{
o.Instance = "https://login.microsoftonline.com/";
o.Domain = "{MyDomain}";
o.TenantId = "common";
o.ClientId = "{FrontendClientId}";
o.ClientSecret = "{FrontendClientSecret}";
o.CallbackPath = "/signin-oidc";
o.Scope.Add("api://{BackendClientId}/.default");
})
.EnableTokenAcquisitionToCallDownstreamApi(o =>
{
o.Instance = "https://login.microsoftonline.com/";
o.TenantId = "common";
o.ClientId = "{BackendClientId}";
o.ClientSecret = "{BackendClientSecret}";
})
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
.AddInMemoryTokenCaches();
我做错了什么?我想要的设置是否可行?
非常感谢您的帮助。
是的,这是可能的。而且,从安全角度来看是可取的。
在 OBO 流程中,您不应在前端的任何位置使用后端 ClientId 或 ClientSecret。您的后端应该获取前端身份的 OBO 令牌,以供后端 API 使用。
AddMicrosoftIdentityWebApp(o => ...)
设置凭证或令牌以向后端 API 进行身份验证。
EnableTokenAcquisitionToCallDownstreamApi
设置要传递到后端并由后端使用的 OBO 令牌。
因此,在这种情况下,后端将使用前端身份来调用 Graph API。因此,后端主体需要相关的“委托”图权限。前端将其凭证委托给后端。前端需要 Graph 的 app 权限,以便后端能够执行必要的操作。例如,如果前端需要读取用户名:
前端权限:类型App
,权限
User.ReadBasic.All
后端权限:类型Delegated
,权限
User.ReadBasic.All
您可以进一步完善安全性的一个示例是,所有登录到前端的用户都是 Entra 用户,并且您不使用前端令牌,而是使用登录用户的令牌。您使用 acquireTokenSilent
执行此操作,然后您的前端不需要显式图形权限,除非它正在执行用户不允许的操作。但这是另一篇文章的事了。
您可以按如下方式更新代码:services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(o =>
{
o.Instance = "https://login.microsoftonline.com/";
o.Domain = "{MyDomain}";
o.TenantId = "common";
o.ClientId = "{FrontendClientId}";
o.ClientSecret = "{FrontendClientSecret}";
o.CallbackPath = "/signin-oidc";
o.Scope.Add("api://{BackendClientId}/.default");
})
.EnableTokenAcquisitionToCallDownstreamApi(o =>
{
o.Instance = "https://login.microsoftonline.com/";
o.TenantId = "common";
o.ClientId = "{FrontendClientId}";
o.ClientSecret = "{FrontendClientSecret}";
o.Scopes = new[] { "api://{BackendClientId}/.default" }; // Scopes needed to call Azure Function API
})
.AddInMemoryTokenCaches();
.AddMicrosoftGraph(Configuration.GetSection("DownstreamApi"))
以下是下游 API 调用的令牌获取示例:
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
public YourFrontendController(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpClientFactory)
{
_tokenAcquisition = tokenAcquisition;
_httpClient = httpClientFactory.CreateClient();
}
public async Task<IActionResult> YourAction()
{
var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "api://{BackendClientId}/.default" });
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _httpClient.GetAsync("https://{YourAzureFunction}.azurewebsites.net/api/{FunctionName}");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return Ok(content);
}
return BadRequest();
}
以下是前端调用凭证验证、下游API获取OBO token的示例:
// Authenticate frontend
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Configuration["Jwt:SecretKey"]))
};
});
// Implement OBO flow
private readonly ITokenAcquisition _tokenAcquisition;
private readonly HttpClient _httpClient;
public YourAzureFunction(ITokenAcquisition tokenAcquisition, IHttpClientFactory httpClientFactory)
{
_tokenAcquisition = tokenAcquisition;
_httpClient = httpClientFactory.CreateClient();
}
// Example using the me API using the delegated token in OBO flow
[FunctionName("YourFunctionName")]
public async Task<IActionResult> Run(
[HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
ILogger log)
{
string accessToken = await _tokenAcquisition.GetTokenOnBehalfOfUserAsync(req.Headers["Authorization"]);
_httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
var response = await _httpClient.GetAsync("https://graph.microsoft.com/v1.0/me");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
return new OkObjectResult(content);
}
return new BadRequestResult();
}
这个下游函数API有点仓促,但是你应该明白了。