简体   繁体   中英

Blazor WASM protected with Azure Active Directory can't call protected WebAPI

im currently working on a Blazor wasm webapp which is protected with Azure Active Directory and a WebAPI which is also protected with AAD. The APP itself also calls the MSGraph API to get basic user informations.

What is currently working: The user cannot access the page if hes not authorized, if hes not authorized he gets redirected to microsoft page to login/get authorized. After accessing the page, i'm also able to get basic user information via msgraph following this tutorial: Official MS GraphAPI Tutorial and adding a GraphClientExtension class.

My WebAPI:

API Azure 配置 1 API Azure 配置 2

API Startup:

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

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            
            services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
                .AddMicrosoftIdentityWebApi(Configuration.GetSection("AzureAd"));
            services.AddCors(options =>
            {
                options.AddDefaultPolicy(builder => 
                    builder.WithOrigins("*")
                        .AllowAnyMethod()
                        .AllowAnyHeader());
            }); 
            services.AddControllers();
            services.AddSwaggerGen(c =>
            {
                c.SwaggerDoc("v1", new OpenApiInfo { Title = "X.Y.API", Version = "v1" });
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseSwagger();
                app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "X.Y.API v1"));
            }

            //app.UseHttpsRedirection();

            app.UseRouting();
            app.UseCors();
            app.UseAuthentication();
            app.UseAuthorization();
            app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
        }
    }

The controller/Endpoint I want to call:

 [Authorize]
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private static readonly string[] Summaries = new[]
    {
        "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
    };

    private readonly ILogger<WeatherForecastController> _logger;
    

    public WeatherForecastController(ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
    }

    [HttpGet]
    public IEnumerable<WeatherForecast> Get()
    {
        var rng = new Random();
        return Enumerable.Range(1, 5).Select(index => new WeatherForecast
            {
                Date = DateTime.Now.AddDays(index),
                TemperatureC = rng.Next(-20, 55),
                Summary = Summaries[rng.Next(Summaries.Length)]
            })
            .ToArray();
    }
}

AzureAD Settings

 "AzureAd": {
    "Instance": "https://login.microsoftonline.com/",
    "Domain": "<company>.onmicrosoft.com",
    "TenantId": "d3c98095-xyz",
    "ClientId": "a51b0337-xyz",
  }

My APP: Azure 应用设置 1 Azure 应用程序设置 2 Azure APP 见闻 3

Blazor Wasm:

Program.cs:

public class Program
{
    public static async Task Main(string[] args)
    {
        var builder = WebAssemblyHostBuilder.CreateDefault(args);
        builder.RootComponents.Add<App>("#app");
        AddMsIdentityAuthentication(builder);
        AddMsGraphServices(builder);
        AddHttpClients(builder);
        
        AddExternalServices(builder);
        await builder.Build().RunAsync();
    }

    private static void AddHttpClients(WebAssemblyHostBuilder builder)
    {
        builder.Services.AddHttpClient<LocalHttpClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress));
        builder.Services.AddHttpClient<ProtectedAPIHttpClient>(client =>
     {
         client.BaseAddress = new Uri("http://localhost:5000");
     });
    }

    private static void AddMsIdentityAuthentication(WebAssemblyHostBuilder builder)
    {
        builder.Services.AddMsalAuthentication(options =>
        {
            builder.Configuration.Bind("AzureAd", options.ProviderOptions.Authentication);
            options.ProviderOptions.DefaultAccessTokenScopes.Add("a51b0337-xyz/Read");
            options.ProviderOptions.LoginMode = "redirect";
            
        });
    }
    
    private static void AddMsGraphServices(WebAssemblyHostBuilder builder)
    {
        builder.Services.AddGraphClient();
    }

    private static void AddExternalServices(WebAssemblyHostBuilder builder)
    {
        builder.Services.AddTelerikBlazor();
    }
}

ProtectedAPIHttpClient

 public class ProtectedAPIHttpClient
    {
        private readonly HttpClient _http;

        public ProtectedAPIHttpClient(HttpClient http)
        {
            _http = http;
        }

        public async Task<string> GetDataTest()
        {
            try
            {
                var test = await _http.GetStringAsync("WeatherForecast");
                Console.WriteLine(test);
                return test;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                if(e is HttpRequestException ex)
                {
                   Console.WriteLine((int)ex.StatusCode); 
                }
                throw;
            }
        }
    }

App.razor:

<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
        <Found Context="routeData">
            <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
                <NotAuthorized>
                    @if (!context.User.Identity.IsAuthenticated)
                    {
                        <RedirectToLogin/>
                    }
                    else
                    {
                        <p>You are not authorized to access this resource.</p>
                    }
                </NotAuthorized>
            </AuthorizeRouteView>
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

RedirectToLogin:

@inject NavigationManager _navigation

@code {

    protected override void OnInitialized()
    {
        _navigation.NavigateTo($"authentication/login?returnUrl={Uri.EscapeDataString(_navigation.Uri)}");
    }

}

authentication:

@page "/authentication/{action}"
@attribute [AllowAnonymous]
<RemoteAuthenticatorView Action="@Action">
    <LoggingIn></LoggingIn>
    <LogInFailed></LogInFailed>
    <CompletingLoggingIn></CompletingLoggingIn>
    <LogOut></LogOut>
    <LogOutFailed></LogOutFailed>
    <LogOutSucceeded></LogOutSucceeded>
    <CompletingLogOut></CompletingLogOut>
    </RemoteAuthenticatorView>

@code{
    [Parameter]
    public string Action { get; set; }

}

AzureAD Config:

"AzureAd": {
    "Authority": "https://login.microsoftonline.com/d3c98095-xyz",
    "ClientId": "b02ce171-xyz",
    "ValidateAuthority": true
  },

I've also added @attribute [Authorize] to _Imports.cs

Ive been wrapping my head around this for days now. If I delete/comment out the API Scope "options.ProviderOptions.DefaultAccessTokenScopes" in the program.cs everything is working: The user gets forced to login and can navigate around the app (and get user information like the picture,first name etc from msgraph). However if I add the DefautAccessTokenScopes a login to the page is simply not possible. Its in some kind of login-redirect-loop. Ive also tried to use AdditionalScopesToConsent instead but its the same result. Ive followed a lot of tutorials and in most of them configuring the app/api like that should be enough for a simple protected api call example..but it seems im misunderstanding something. Does anyone have a idea or know what im doing wrong?

I checked my solution working similar to this, what I found is that App.razor should not contain NotAuthorized section und AuthorizeRouteView this should be in the MailLayout.razor or Index.razor etc. under AuthorizeView .

App.razor sample:

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

On some *.razor page:

<AuthorizeView Policy="@($"{PermissionLevel.XYZ}")">
        <Authorized>
          <p>You are able to see the page</p>
          <p> Even more content</p>
        </Authorized>
</AuthorizeView>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM