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 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",
}
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.