如何使用 Azure AD B2C 保护 Blazor Wasm 应用程序访问的 Azure Functions?

How to secure an Azure Function accessed by a Blazor Wasm app with Azure AD B2C?

场景:我有一个使用 B2C 身份验证保护的 Blazor wasm 应用程序,它需要调用 HTTP 触发的 Azure 函数。保护该 Azure 函数的最佳方法是什么,以便只有 Blazor 应用 and/or 经过身份验证的用户才能调用该函数?

到目前为止,我知道如何使用 B2C 保护 Blazor 应用程序(显然很愚蠢!)并且我还能够将 B2C 身份验证添加到 Azure 函数并通过验证 jwt 令牌来保护调用。但我不清楚这两个部分应该如何结合在一起。

我应该在 B2C 租户的 Azure 函数的应用程序注册中公开一个 API 吗?如果是这样,Blazor 应用如何对 Azure 函数进行经过身份验证的调用?

或者我只是通过 Azure 函数调用的 http 请求 headers 从 Blazor 应用程序发送 jwt 令牌,然后在函数内手动验证该令牌?

我最近阅读了很多关于这个主题的不同帖子,但我仍然无法弄清楚实现它的最佳解决方案是什么。

任何help/cues将不胜感激。

谢谢!

ps:我对使用 Azure API 管理不感兴趣,因为它对于一个非常简单的应用程序解决方案来说有点贵。

如果要调用Azure AD B2C投射的Azure功能,请参考以下步骤

  • 为 Azure 函数配置 Azure AD B2C

    1. 创建 Azure B2C 应用。

      网络App/API:是

      允许隐式流:是

    2. 在 B2C 应用程序中设置回复 URL:https://{function app url}/.auth/login/aad/callback

    3. 在 B2C 应用程序中设置应用程序 ID URL : https://{tennat}/{prefix}

    4. 记下 B2C 应用程序 ID。

    5. 定义 API 范围。转到 B2C 应用程序 => 已发布的范围

    6. 获取您的 B2C 用户 flows/policy 的元数据 URL。记下这个 URL.

      可从运行用户流页面获取。

      格式如https://{tenant}.b2clogin.com/{tenant}.onmicrosoft.com/v2.0/.well-known/openid-configuration?p={policy}.

    7. 转到您的功能=> 身份验证/授权.

    8. 设置以下

      • 应用服务身份验证:开启
      • 未通过身份验证时采取的操作:使用 Azure AD 登录
      • 身份验证提供程序:Azure AAD
      • 管理模式:高级
      • 客户端 ID:{第 4 步中的应用程序 ID}
      • 发行人URL:{URL 来自第 6 步}
      • 允许的受众:{第 3 步中的应用程序 ID URL}
  • 创建客户端应用程序 详情请参考here.

  • 在 Azure 函数中配置 CORS 策略

  • 配置应用程序

  1. 创建
     dotnet new blazorwasm -au IndividualB2C --aad-b2c-instance "{AAD B2C INSTANCE}" --client-id "{CLIENT ID}" --domain "{TENANT DOMAIN}" -o {APP NAME} -ssp "{SIGN UP OR SIGN IN POLICY}"
    
  2. 代码
  • 创建自定义 AuthorizationMessageHandler class

    using Microsoft.AspNetCore.Components;
    using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
    
    public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler
    {
        public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, 
            NavigationManager navigationManager)
            : base(provider, navigationManager)
        {
            ConfigureHandler(
                authorizedUrls: new[] { "<your function app url>" },
                scopes: new[] { "<the function app  API scope your define>" });
        }
    }
  • Program.cs中添加如下代码。
public static async Task Main(string[] args)
        {
            var builder = WebAssemblyHostBuilder.CreateDefault(args);
            builder.RootComponents.Add<App>("app");
             // register CustomAuthorizationMessageHandler 
            builder.Services.AddScoped<CustomAuthorizationMessageHandler>();
            // configure httpclient
            // call the following code please add packageMicrosoft.Extensions.Http 3.1.0
            builder.Services.AddHttpClient("ServerAPI", client =>
              client.BaseAddress = new Uri("<your function app url>"))
                    .AddHttpMessageHandler<CustomAuthorizationMessageHandler>();
            // register the httpclient
            builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>()
             .CreateClient("ServerAPI"));
            // configure Azure AD auth
            builder.Services.AddMsalAuthentication(options =>
            {
                builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
                options.ProviderOptions.DefaultAccessTokenScopes.Add("<the function app  API scope your define>");


            });

            await builder.Build().RunAsync();
        }
  • 调用API
 @page "/fetchdata"
        @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
        @inject HttpClient Http

        <h1>Call Azure Function</h1>

         <p>This component demonstrates fetching data from the server.</p>

         <p>Result: @forecasts</p>

          <button class="btn btn-primary" @onclick="CallFun">Call Function</button>

          @code {
              private string forecasts;

              protected async Task CallFun()
              {
                forecasts = await Http.GetStringAsync("api/http");
              }


           }