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

通过Azure AD B2C认证获取Azure Function访问令牌异常问题

Azure AD B2C认证后获取Azure Function访问令牌失败

错误信息

MsalUiRequiredException: No account or login hint was passed to the AcquireTokenSilent call.

错误触发代码行:

// Try acquiring the access token silently
string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);

重现步骤

  • 用户通过Azure AD B2C完成认证登录流程。
  • 认证成功后,应用调用ITokenAcquisition.GetAccessTokenForUserAsync()方法尝试获取Azure Function访问令牌。
  • 抛出上述错误。

已尝试操作

  • 在Azure AD B2C应用注册的令牌配置中启用了Access Tokens和ID Tokens。
  • 已为Azure AD B2C应用授予Azure Function API的访问权限,使用作用域为api://{azure-function-client-id}/.default。
  • 尝试捕获MsalUiRequiredException并触发重新认证流程,问题仍未解决。

预期行为

用户认证后,应用可静默获取Azure Function访问令牌;若无法静默获取,则自动触发重新认证。

问题代码

option.Events.OnTokenValidated = async context => {
    var claims = context.Principal.Claims;
    var signClaimUser = new SignClaimModel
    {
        email = claims.FirstOrDefault(c => c.Type == "emails" || c.Type == "email")?.Value,
        givenName = claims.FirstOrDefault(c => c.Type == "given_name")?.Value,
        surname = claims.FirstOrDefault(c => c.Type == "family_name")?.Value,
        objectId = claims.FirstOrDefault(c => c.Type == "oid")?.Value,
        userPrincipalName = claims.FirstOrDefault(c => c.Type == "upn")?.Value,
        tenantId = claims.FirstOrDefault(c => c.Type == "tid")?.Value
    };
    // Serialize the model
    var json = JsonConvert.SerializeObject(signClaimUser);
    var content = new StringContent(json, Encoding.UTF8, "application/json");

    // Prepare HTTP client to call the Azure Function
    using var httpClient = new HttpClient();

    // Get the function URL and key from your appsettings.json
    var functionBaseUrl = builder.Configuration["AzureFunction:FunctionUrl"]?.TrimEnd('/');
    var functionKey = builder.Configuration["AzureFunction:FunctionKey"];
    var functionUrl = $"{functionBaseUrl}?code={functionKey}";

    // Retrieve ITokenAcquisition from the DI container to acquire a token
    var tokenAcquisition = context.HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();

    // The scope for the Azure Function (make sure your Azure Function API client ID is correctly set in appsettings.json)
    var azureFunctionApiClientId = builder.Configuration["AzureAdB2CAzureFunctionAPI:ClientId"];
    string[] scopes = new[] { $"api://{azureFunctionApiClientId}/.default" };

    try
    {
        // Try acquiring the access token silently
        string accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);

        // If we get a valid access token, add it to the authorization header for the request
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);

        // Call the Azure Function
        var response = await httpClient.PostAsync(functionUrl, content);

        // Optionally, handle the response (e.g., logging, error handling, etc.)
        if (!response.IsSuccessStatusCode)
        {
            var errorMessage = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Error calling Azure Function: {errorMessage}");
        }
    }
    catch (MsalUiRequiredException)
    {
        // This exception occurs if the user needs to reauthenticate and provide consent
        // Trigger the user sign-in process interactively
        var redirectUrl = context.HttpContext.Request.Scheme + "://" + context.HttpContext.Request.Host.Value + "/signin-oidc"; // Your redirect URI
        
        // Redirect the user to the sign-in page
        context.Response.Redirect(redirectUrl);
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error acquiring token or calling Azure Function: {ex.Message}");
    }
};

解决方案

核心原因

OnTokenValidated是令牌验证完成后的早期事件,此时MSAL尚未将用户会话持久化到缓存中,ITokenAcquisition无法定位到对应的用户账户,因此抛出MsalUiRequiredException。同时,该事件中直接执行重定向可能因响应生命周期逻辑冲突而失效。

修复方案

方案1:将API调用移至认证完成后的请求流程

避免在OnTokenValidated中直接处理令牌获取和API调用,改为存储用户信息到上下文,后续在Controller Action中执行:

// 在OnTokenValidated中仅存储用户信息
option.Events.OnTokenValidated = context => {
    var signClaimUser = new SignClaimModel
    {
        email = context.Principal.Claims.FirstOrDefault(c => c.Type is "emails" or "email")?.Value,
        givenName = context.Principal.Claims.FirstOrDefault(c => c.Type == "given_name")?.Value,
        surname = context.Principal.Claims.FirstOrDefault(c => c.Type == "family_name")?.Value,
        objectId = context.Principal.Claims.FirstOrDefault(c => c.Type == "oid")?.Value,
        tenantId = context.Principal.Claims.FirstOrDefault(c => c.Type == "tid")?.Value
    };
    context.HttpContext.Items["SignClaimUser"] = signClaimUser;
    return Task.CompletedTask;
};

然后在Controller中处理:

public async Task<IActionResult> PostAuthRedirect()
{
    var signClaimUser = HttpContext.Items["SignClaimUser"] as SignClaimModel;
    if (signClaimUser == null) return RedirectToAction("Index");

    var tokenAcquisition = HttpContext.RequestServices.GetRequiredService<ITokenAcquisition>();
    var clientId = Configuration["AzureAdB2CAzureFunctionAPI:ClientId"];
    var scopes = new[] { $"api://{clientId}/.default" };

    try
    {
        var accessToken = await tokenAcquisition.GetAccessTokenForUserAsync(scopes);
        using var httpClient = new HttpClient();
        httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        
        var functionUrl = $"{Configuration["AzureFunction:FunctionUrl"].TrimEnd('/')}?code={Configuration["AzureFunction:FunctionKey"]}";
        var content = new StringContent(JsonConvert.SerializeObject(signClaimUser), Encoding.UTF8, "application/json");
        var response = await httpClient.PostAsync(functionUrl, content);

        if (!response.IsSuccessStatusCode)
        {
            var errorMsg = await response.Content.ReadAsStringAsync();
            Console.WriteLine($"Function call failed: {errorMsg}");
        }
    }
    catch (MsalUiRequiredException)
    {
        return Challenge(new AuthenticationProperties { RedirectUri = "/PostAuthRedirect" });
    }
    catch (Exception ex)
    {
        Console.WriteLine($"Error: {ex.Message}");
    }

    return RedirectToAction("Home");
}

方案2:手动指定账户获取令牌(仅当必须在OnTokenValidated中执行时)

从当前ClaimsPrincipal中提取账户信息,手动传入AcquireTokenSilent:

try
{
    var account = await tokenAcquisition.GetAccountAsync(context.Principal);
    if (account == null)
    {
        context.HandleResponse();
        context.Response.Redirect("/signin-oidc");
        return;
    }
    var tokenResult = await tokenAcquisition.AcquireTokenSilent(scopes, account).ExecuteAsync();
    string accessToken = tokenResult.AccessToken;
    // 后续调用Azure Function逻辑...
}
catch (MsalUiRequiredException)
{
    context.HandleResponse();
    context.Response.Redirect($"{context.HttpContext.Request.Scheme}://{context.HttpContext.Request.Host}/signin-oidc");
}

关键注意点

  • 在OnTokenValidated中执行重定向时,必须先调用context.HandleResponse()终止当前响应处理,否则重定向指令可能被忽略。
  • 确保应用配置中已正确设置Azure AD B2C的租户ID、客户端ID、回调URI等参数,且Azure Function API的权限已被正确授予并同意。

内容的提问来源于stack exchange,提问作者Ret 2

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:20:55