Blazor WebAssembly 与 AzureAD

Blazor WebAssembly with AzureAD

我需要让我正在开发的 WebAssembly 应用程序的用户通过 AzureAD 登录。 我正在使用回购 Microsoft.Identity.Web(和 UI)。

这是我目前所做的:

appsetting.json

 "AzureAd": {
    "Instance": "https://login.microsoftonline.com",
    "Domain": "xxx",
    "TenantId": "xxx",
    "ClientId": "xxx",
    "CallbackPath": "/signin-oidc",
    "SignedOutCallbackPath ": "/signout-callback-oidc",

    "ClientSecret": "xxx"
  },

服务器端startup.cs

public class Startup
{
    public IConfiguration Configuration { get; }
    public Startup(IConfiguration configuration)
    {
        Configuration = configuration;
    }

    // This method gets called by the runtime. Use this method to add services to the container.
    // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddMvc();
        services.AddResponseCompression(opts =>
        {
            opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
                new[] { "application/octet-stream" });
        });

        // Sign-in users with the Microsoft identity platform
        //services.AddSignIn(Configuration);
        services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
                .AddSignIn("AzureAD", Configuration, options => Configuration.Bind("AzureAD", options));

        // Looks like I need this to have the login UI
        services.AddControllersWithViews(options =>
        {
            var policy = new AuthorizationPolicyBuilder()
                .RequireAuthenticatedUser()
                .Build();
            options.Filters.Add(new AuthorizeFilter(policy));
        }).AddMicrosoftIdentityUI();
        services.AddRazorPages();
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        app.UseResponseCompression();

        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseBlazorDebugging();
        }

        app.UseCors();

        app.UseStaticFiles();
        app.UseClientSideBlazorFiles<Client.Program>();

        app.UseRouting();
        app.UseAuthentication();
        app.UseAuthorization();

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapDefaultControllerRoute();
            endpoints.MapFallbackToClientSideBlazor<Client.Program>("index.html");
        });
    }
}

UserController.cs

[ApiController]
[Route("api/[controller]")]
public class UserController : ControllerBase
{
    [HttpGet]
    public AppUser Get()
    {
        AppUser toReturn = new AppUser();
        if (this.User.Identity.IsAuthenticated)
        {
            toReturn.UserName = this.User.Identity.Name;
        }
        else
        {
            toReturn.UserName = ""; // Not logged in
        }
        return toReturn;
    }
}

客户program.cs

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);

        // Use our CustomAuthenticationProvider as the 
        // AuthenticationStateProvider
        builder.Services.AddScoped<AuthenticationStateProvider, CustomAuthenticationProvider>();

        // Add Authentication support
        builder.Services.AddOptions();
        builder.Services.AddAuthorizationCore();

        builder.RootComponents.Add<App>("app");
        await builder.Build().RunAsync();
    }
}

public class CustomAuthenticationProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    public CustomAuthenticationProvider(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }
    public override async Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        ClaimsPrincipal user;

        // Call the GetUser method to get the status
        // This only sets things like the AuthorizeView
        // and the AuthenticationState CascadingParameter
        var result = await _httpClient.GetJsonAsync<AppUser>("api/user");

        // Was a UserName returned?
        if (result.UserName != "")
        {
            // Create a ClaimsPrincipal for the user
            var identity = new ClaimsIdentity(new[]
            {
               new Claim(ClaimTypes.Name, result.UserName),
            }, "AzureAdAuth");
            user = new ClaimsPrincipal(identity);
        }
        else
        {
            user = new ClaimsPrincipal(); // Not logged in
        }
        return await Task.FromResult(new AuthenticationState(user));
    }
}

App.razor

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>

Login.razor

<AuthorizeView>
    <Authorized>
        <h6>Hello, @context.User.Identity.Name! </h6>
        <a href="MicrosoftIdentity/Account/SignOut">[Log out]</a>
    </Authorized>
    <NotAuthorized>
        <a href="MicrosoftIdentity/Account/SignIn">[Log in]</a>
    </NotAuthorized>
</AuthorizeView>

我手动输入路由就可以登录https://localhost:xxxxx/MicrosoftIdentity/Account/SignIn。当用户登录时,一切都按预期进行。

当用户未登录时出现以下异常,blazor 尝试加载布局的 <AuthorizeView /> 部分:

Access to fetch at 'https://login.microsoftonline.com/xxx/oauth2/v2.0/authorize?client_id=xxx&redirect_uri=https%3A%2F%2Flocalhost%3A44316%2Fsignin-oidc&response_type=code%20id_token&scope=openid%20profile%20offline_access%xxx%2Fuser_impersonation&response_mode=form_post&nonce=xxx&x-client-SKU=ID_NETSTANDARD2_0&x-client-ver=5.5.0.0' (redirected from 'https://localhost:44316/api/user') from origin 'https://localhost:44316' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

我正在尝试按照 here 的说明在服务器端启用 CORS,但我还没有成功。 我错过了什么?

https://devblogs.microsoft.com/aspnet/blazor-webassembly-3-2-0-preview-2-release-now-available/

Blazor WebAssembly 预览版 2 附带了一个开箱即用的工作模板。