A Step-by-Step Guide on how to configure Blazor WebAssembly standalone app & ASP.NET Core Identity combined with IdentityServer4 server app using gRPC-Web(Code-first) middleware.
Blazor-WASM-IdentityServer4-gRPC
dotnet new blazorwasm -au Individual -ho -o WebApp- Add a Grpc.Net.Client package reference
- Add a protobuf-net.Grpc package reference
- Create service and data contract types.
using System; using ProtoBuf; namespace WebApp.Shared { [ProtoContract] public class WeatherForecast { [ProtoMember(1)] public DateTime Date { get; set; } [ProtoMember(2)] public int TemperatureC { get; set; } [ProtoMember(3)] public string Summary { get; set; } public int TemperatureF => 32 + (int) (TemperatureC / 0.5556); } }
using System.Collections.Generic; using ProtoBuf; namespace WebApp.Shared { [ProtoContract] public class WeatherReply { [ProtoMember(1)] public IEnumerable<WeatherForecast> Forecasts { get; set; } } }
using System.Threading.Tasks; using ProtoBuf.Grpc; using ProtoBuf.Grpc.Configuration; namespace WebApp.Shared { [Service] public interface IWeatherService { [Operation] Task<WeatherReply> GetWeather(CallContext context = default); } }
- Add a Grpc.AspNetCore.Web package reference
- Add a protobuf-net.Grpc.AspNetCore package reference
- Delete
WebApp.Clientproject reference so we can hostWebApp.Client&WebApp.Serverapps independently
<ProjectReference Include="..\Client\WebApp.Client.csproj" />
- Update
launchSettings.jsonapplicationUrl ports to 5005 & 5004, we will use 5001 & 5000 ports forWebApp.Client
"applicationUrl": "https://localhost:5005;http://localhost:5004",
- Update
appsettings.jsonIdentityServer Client configuration, change Profile to SPA & addWebApp.ClientRedirectUri/LogoutUri
"IdentityServer": { "Clients": { "WebApp.Client": { "Profile": "SPA", "RedirectUri": "https://localhost:5001/authentication/login-callback", "LogoutUri": "https://localhost:5001/authentication/logout-callback" } } }
- Implement
WeatherService : IWeatherService
using System; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using ProtoBuf.Grpc; using WebApp.Shared; namespace WebApp.Server.Services { [Authorize] public class WeatherService : IWeatherService { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; public Task<WeatherReply> GetWeather(CallContext context) { var reply = new WeatherReply(); var rng = new Random(); reply.Forecasts = Enumerable.Range(1, 10).Select(index => new WeatherForecast { Date = DateTime.UtcNow.AddDays(index), TemperatureC = rng.Next(20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }); return Task.FromResult(reply); } } }
- Configure Code-first gRPC & CORS services
services.AddCodeFirstGrpc(); services.AddCors(options => options.AddPolicy("CorsPolicy", builder => { builder // WebApp.Client ApplicationUrls .WithOrigins("https://localhost:5001", "http://localhost:5000") .AllowAnyHeader() .AllowAnyMethod() // To allow a browser app to make cross-origin gRPC-Web calls .WithExposedHeaders("Grpc-Status", "Grpc-Message", "Grpc-Encoding", "Grpc-Accept-Encoding"); }));
- Configure middlewares
// Should be placed before app.UseIdentityServer(); app.UseCors("CorsPolicy"); app.UseIdentityServer(); app.UseAuthentication(); app.UseAuthorization(); // new GrpcWebOptions() {DefaultEnabled = true} configures so all services support gRPC-Web by default app.UseGrpcWeb(new GrpcWebOptions() {DefaultEnabled = true}); app.UseEndpoints(endpoints => { // Adds the code-first service endpoint endpoints.MapGrpcService<WeatherService>(); endpoints.MapRazorPages(); endpoints.MapControllers(); endpoints.MapFallbackToFile("index.html"); });
- Add a Grpc.Net.Client.Web package reference
- Implement
CustomAuthorizationMessageHandler : AuthorizationMessageHandler
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; namespace WebApp.Client.Authentication { public class CustomAuthorizationMessageHandler : AuthorizationMessageHandler { public CustomAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigation) : base( provider, navigation) { // Configures this handler to authorize outbound HTTP requests using an access token. // authorizedUrls – The base addresses of endpoint URLs to which the token will be attached ConfigureHandler(authorizedUrls: new[] {"https://localhost:5005"}); // WebApp.Server Url } } }
- Update
Program.cs
using System; using System.Net.Http; using System.Threading.Tasks; using Grpc.Net.Client; using Grpc.Net.Client.Web; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.DependencyInjection; using ProtoBuf.Grpc.Client; using WebApp.Client.Authentication; using WebApp.Shared; namespace WebApp.Client { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("#app"); // Register our custom AuthorizationMessageHandler builder.Services.AddScoped<CustomAuthorizationMessageHandler>(); builder.Services.AddHttpClient("WebApp.ServerAPI", client => { // WebApp.Server BaseAddress client.BaseAddress = new Uri("https://localhost:5005"); }) // Replace .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>() with custom .AddHttpMessageHandler<CustomAuthorizationMessageHandler>() // Add GrpcWebHandler to be able make gRPC-Web calls. .AddHttpMessageHandler(() => new GrpcWebHandler(GrpcWebMode.GrpcWeb)); // Supply HttpClient instances that include access tokens when making requests to the server project builder.Services.AddScoped(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("WebApp.ServerAPI")); // Configure WebApp.Server RemoteRegisterPath, RemoteProfilePath & ConfigurationEndpoint builder.Services.AddApiAuthorization(options => { options.AuthenticationPaths.RemoteRegisterPath = "Https://localhost:5005/Identity/Account/Register"; options.AuthenticationPaths.RemoteProfilePath = "Https://localhost:5005/Identity/Account/Manage"; options.ProviderOptions.ConfigurationEndpoint = "https://localhost:5005/_configuration/WebApp.Client"; }); builder.Services.AddSingleton(services => { // Creates our configured HttpClient var httpClient = services.GetRequiredService<IHttpClientFactory>().CreateClient("WebApp.ServerAPI"); // Creates a gRPC channel var channel = GrpcChannel.ForAddress(httpClient.BaseAddress, new GrpcChannelOptions { HttpClient = httpClient, }); // Creates a code-first client from the channel with the CreateGrpcService<IWeatherService> extension method return channel.CreateGrpcService<IWeatherService>(); }); await builder.Build().RunAsync(); } } }
- Implement
CustomRemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState>to avoid RemoteRegisterPath & RemoteProfilePath Issue
using System; using System.Threading.Tasks; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.WebAssembly.Authentication; using Microsoft.JSInterop; namespace WebApp.Client.Authentication { public class CustomRemoteAuthenticatorView : RemoteAuthenticatorViewCore<RemoteAuthenticationState> { [Inject] internal IJSRuntime JS { get; set; } [Inject] internal NavigationManager Navigation { get; set; } public CustomRemoteAuthenticatorView() => AuthenticationState = new RemoteAuthenticationState(); protected override async Task OnParametersSetAsync() { switch (Action) { case RemoteAuthenticationActions.Profile: if (ApplicationPaths.RemoteProfilePath == null) { UserProfile ??= ProfileNotSupportedFragment; } else { UserProfile ??= LoggingIn; await RedirectToProfile(); } break; case RemoteAuthenticationActions.Register: if (ApplicationPaths.RemoteRegisterPath == null) { Registering ??= RegisterNotSupportedFragment; } else { Registering ??= LoggingIn; await RedirectToRegister(); } break; default: await base.OnParametersSetAsync(); break; } } private static void ProfileNotSupportedFragment(RenderTreeBuilder builder) { builder.OpenElement(0, "p"); builder.AddContent(1, "Editing the profile is not supported."); builder.CloseElement(); } private static void RegisterNotSupportedFragment(RenderTreeBuilder builder) { builder.OpenElement(0, "p"); builder.AddContent(1, "Registration is not supported."); builder.CloseElement(); } private ValueTask RedirectToProfile() => JS.InvokeVoidAsync("location.replace", Navigation.ToAbsoluteUri(ApplicationPaths.RemoteProfilePath)); private ValueTask RedirectToRegister() { var loginUrl = Navigation.ToAbsoluteUri(ApplicationPaths.LogInPath).PathAndQuery; var registerUrl = Navigation .ToAbsoluteUri($"{ApplicationPaths.RemoteRegisterPath}?returnUrl={Uri.EscapeDataString(loginUrl)}"); return JS.InvokeVoidAsync("location.replace", registerUrl); } } }
- Update
Authentication.razorto useCustomRemoteAuthenticatorView
@page "/authentication/{action}" @using WebApp.Client.Authentication @* <RemoteAuthenticatorView Action="@Action" /> *@ <CustomRemoteAuthenticatorView Action="@Action"/> @code{ [Parameter] public string Action { get; set; } }
- Update
NavMenu.razorwrap Fetch data navlink in<AuthorizeView>to make it visible based on Authentication state
<AuthorizeView> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> </AuthorizeView>
- Update
FetchData.razorinjectIWeatherServiceinstead ofHttpClient
@attribute [Authorize] @* @inject HttpClient Http *@ @inject IWeatherService _weatherService
- Update
OnInitializedAsyncto useIWeatherService
@code { // private WeatherForecast[] forecasts; private IEnumerable<WeatherForecast> forecasts; protected override async Task OnInitializedAsync() { try { // forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast"); forecasts = (await _weatherService.GetWeather()).Forecasts; } catch (AccessTokenNotAvailableException exception) { exception.Redirect(); } } }
Add Scaffolded Identity if you want to customize Login/Register/Profile pages
dotnet run --project src/WebApp.Server/WebApp.Server.csprojdotnet run --project src/WebApp.Client/WebApp.Client.csproj