Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Blazor WebAssembly standalone app & ASP.NET Core Identity combined with IdentityServer4 server app using gRPC-Web(Code-first) middleware

License

Notifications You must be signed in to change notification settings

dmamulashvili/Blazor-WASM-IdentityServer4-gRPC

Repository files navigation

Blazor-WASM-IdentityServer4-gRPC

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

Create a new Blazor WebAssembly project

dotnet new blazorwasm -au Individual -ho -o WebApp

WebApp.Shared project Configuration

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);
 }
}

WebbApp.Server project Configuration

<ProjectReference Include="..\Client\WebApp.Client.csproj" />
  • Update launchSettings.json applicationUrl ports to 5005 & 5004, we will use 5001 & 5000 ports for WebApp.Client
"applicationUrl": "https://localhost:5005;http://localhost:5004",
  • Update appsettings.json IdentityServer Client configuration, change Profile to SPA & add WebApp.Client RedirectUri/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");
});

WebbApp.Client project Configuration

  • 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();
 }
 }
}
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.razor to use CustomRemoteAuthenticatorView
@page "/authentication/{action}"
@using WebApp.Client.Authentication
@* <RemoteAuthenticatorView Action="@Action" /> *@
<CustomRemoteAuthenticatorView Action="@Action"/>
@code{
 [Parameter]
 public string Action { get; set; }
}
  • Update NavMenu.razor wrap 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.razor inject IWeatherService instead of HttpClient
@attribute [Authorize]
@* @inject HttpClient Http *@
@inject IWeatherService _weatherService
  • Update OnInitializedAsync to use IWeatherService
@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

Run source projects

dotnet run --project src/WebApp.Server/WebApp.Server.csproj
dotnet run --project src/WebApp.Client/WebApp.Client.csproj

About

Blazor WebAssembly standalone app & ASP.NET Core Identity combined with IdentityServer4 server app using gRPC-Web(Code-first) middleware

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

AltStyle によって変換されたページ (->オリジナル) /