From 81b71d4fc45138c7fc079a652254251ddbed34e3 Mon Sep 17 00:00:00 2001 From: Yaser Moradi Date: Mon, 11 Dec 2023 16:39:33 +0100 Subject: [PATCH] fix(templates): resolve bit boilerplate middleware order issues #6286 (#6287) --- .../.template.config/template.json | 5 ++ .../Services/AppSecureJwtDataFormat.cs | 9 +++- .../Boilerplate.Server/Startup/Middlewares.cs | 51 ++++++++++++------- .../Components/Layout/NavMenu.razor.cs | 4 +- .../Pages/Identity/EditProfilePage.razor.cs | 8 +-- .../Components/Pages/NotAuthorizedPage.razor | 2 +- .../Pages/NotAuthorizedPage.razor.cs | 4 +- .../Services/AuthenticationManager.cs | 1 - .../Contracts/IPrerenderStateService.cs | 9 +++- .../Services/PrerenderStateService.cs | 15 ++++++ 10 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index cfde5227bc..931dcc6871 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -139,12 +139,16 @@ "src/Shared/Dtos/Products/**", "src/Boilerplate.Server/Controllers/Categories/**", "src/Boilerplate.Server/Controllers/Products/**", + "src/Boilerplate.Server/Controllers/Dashboard/**", "src/Boilerplate.Server/Data/Configurations/Category/**", "src/Boilerplate.Server/Data/Configurations/Product/**", "src/Boilerplate.Server/Mappers/CategoriesMapper.cs", "src/Boilerplate.Server/Mappers/ProductsMapper.cs", "src/Boilerplate.Server/Models/Categories/**", "src/Boilerplate.Server/Models/Products/**", + "src/Client/Boilerplate.Client.Core/Controllers/Categories/**", + "src/Client/Boilerplate.Client.Core/Controllers/Products/**", + "src/Client/Boilerplate.Client.Core/Controllers/Dashboard/**", "src/Client/Boilerplate.Client.Core/Components/Pages/Categories/**", "src/Client/Boilerplate.Client.Core/Components/Pages/Dashboard/**", "src/Client/Boilerplate.Client.Core/Components/Pages/Products/**"] @@ -155,6 +159,7 @@ "src/Boilerplate.Server/Controllers/Todo/**", "src/Boilerplate.Server/Mappers/TodoMapper.cs", "src/Boilerplate.Server/Models/Todo/**", + "src/Client/Boilerplate.Client.Core/Controllers/Todo/**", "src/Client/Boilerplate.Client.Core/Components/Pages/Todo/**"] } ] diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Services/AppSecureJwtDataFormat.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Services/AppSecureJwtDataFormat.cs index 7b0f0853b1..e6c8cd146a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Services/AppSecureJwtDataFormat.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Services/AppSecureJwtDataFormat.cs @@ -17,6 +17,11 @@ public class AppSecureJwtDataFormat(AppSettings appSettings, TokenValidationPara { try { + if (string.IsNullOrEmpty(protectedText)) + { + return NotSignedIn(); + } + var handler = new JwtSecurityTokenHandler(); ClaimsPrincipal? principal = handler.ValidateToken(protectedText, validationParameters, out var validToken); var validJwt = (JwtSecurityToken)validToken; @@ -26,9 +31,9 @@ public class AppSecureJwtDataFormat(AppSettings appSettings, TokenValidationPara }, IdentityConstants.BearerScheme); return data; } - catch + catch (Exception exp) { - return NotSignedIn(); + throw new UnauthorizedException(nameof(AppStrings.UnauthorizedException), exp); } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Startup/Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Startup/Middlewares.cs index e91eed2587..b78fd0839c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Startup/Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Boilerplate.Server/Startup/Middlewares.cs @@ -2,6 +2,7 @@ using System.Net; using System.Reflection; using System.Runtime.Loader; +using System.Web; using Boilerplate.Client.Core.Services; using Boilerplate.Server.Components; using HealthChecks.UI.Client; @@ -13,10 +14,26 @@ namespace Boilerplate.Server.Startup; public class Middlewares { + /// + /// https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-8.0#middleware-order + /// public static void Use(WebApplication app, IHostEnvironment env, IConfiguration configuration) { app.UseForwardedHeaders(); + if (AppRenderMode.MultilingualEnabled) + { + var supportedCultures = CultureInfoManager.SupportedCultures.Select(sc => CultureInfoManager.CreateCultureInfo(sc.code)).ToArray(); + app.UseRequestLocalization(new RequestLocalizationOptions + { + SupportedCultures = supportedCultures, + SupportedUICultures = supportedCultures, + ApplyCurrentCultureToResponseHeaders = true + }.SetDefaultCulture(CultureInfoManager.DefaultCulture.code)); + } + + app.UseExceptionHandler("/", createScopeForErrors: true); + if (env.IsDevelopment()) { app.UseWebAssemblyDebugging(); @@ -24,7 +41,6 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration else { app.UseHttpsRedirection(); - app.UseResponseCompression(); } Configure_401_403_404_Pages(app); @@ -45,23 +61,17 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration app.UseCors(options => options.WithOrigins("https://0.0.0.0" /*BlazorHybrid*/, "app://0.0.0.0" /*BlazorHybrid*/) .AllowAnyHeader().AllowAnyMethod()); - app.UseResponseCaching(); app.UseAuthentication(); app.UseAuthorization(); - app.UseAntiforgery(); - if (AppRenderMode.MultilingualEnabled) + if (env.IsDevelopment() is false) { - var supportedCultures = CultureInfoManager.SupportedCultures.Select(sc => CultureInfoManager.CreateCultureInfo(sc.code)).ToArray(); - app.UseRequestLocalization(new RequestLocalizationOptions - { - SupportedCultures = supportedCultures, - SupportedUICultures = supportedCultures, - ApplyCurrentCultureToResponseHeaders = true - }.SetDefaultCulture(CultureInfoManager.DefaultCulture.code)); + app.UseResponseCompression(); } - app.UseExceptionHandler("/", createScopeForErrors: true); + app.UseResponseCaching(); + + app.UseAntiforgery(); app.UseSwagger(); @@ -98,10 +108,15 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration } // Handle the rest of requests with blazor - app.MapRazorComponents() + var blazorApp = app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode() .AddAdditionalAssemblies(AssemblyLoadContext.Default.Assemblies.Where(asm => asm.GetName().Name?.Contains("Boilerplate") is true).Except([Assembly.GetExecutingAssembly()]).ToArray()); + + if (AppRenderMode.PrerenderEnabled is false) + { + blazorApp.AllowAnonymous(); // Server may not check authorization for pages when there's no pre rendering, let the client handle it. + } } /// @@ -114,11 +129,6 @@ public static void Use(WebApplication app, IHostEnvironment env, IConfiguration /// private static void Configure_401_403_404_Pages(WebApplication app) { - if (AppRenderMode.PrerenderEnabled is false) - { - return; - } - app.Use(async (context, next) => { if (context.Request.Path.HasValue) @@ -147,7 +157,10 @@ private static void Configure_401_403_404_Pages(WebApplication app) { bool is403 = httpContext.Response.StatusCode is 403; - httpContext.Response.Redirect($"/not-authorized?redirect-url={httpContext.Request.GetEncodedPathAndQuery()}&isForbidden={(is403 ? "true" : "false")}"); + var qs = HttpUtility.ParseQueryString(httpContext.Request.QueryString.Value ?? string.Empty); + qs.Remove("try_refreshing_token"); + var redirectUrl = UriHelper.BuildRelative(httpContext.Request.PathBase, httpContext.Request.Path, new QueryString(qs.ToString())); + httpContext.Response.Redirect($"/not-authorized?redirect-url={redirectUrl}&isForbidden={(is403 ? "true" : "false")}"); } else if (httpContext.Response.StatusCode is 404 && httpContext.GetEndpoint() is null /* Please be aware that certain endpoints, particularly those associated with web API actions, may intentionally return a 404 error. */) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavMenu.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavMenu.razor.cs index d95764329d..65b02cb295 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavMenu.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Layout/NavMenu.razor.cs @@ -89,8 +89,8 @@ protected override async Task OnInitAsync() user = await userController.GetCurrentUser(CurrentCancellationToken); - var access_token = await PrerenderStateService.GetValue($"{nameof(NavMenu)}-access_token", AuthTokenProvider.GetAccessTokenAsync); - profileImageUrlBase = $"{Configuration.GetApiServerAddress()}Attachment/GetProfileImage?access_token={access_token}&file="; + var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessTokenAsync); + profileImageUrlBase = $"{Configuration.GetApiServerAddress()}api/Attachment/GetProfileImage?access_token={access_token}&file="; SetProfileImageUrl(); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/EditProfilePage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/EditProfilePage.razor.cs index 93cdc1374e..669d54a2a5 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/EditProfilePage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/Identity/EditProfilePage.razor.cs @@ -30,11 +30,11 @@ protected override async Task OnInitAsync() { await LoadEditProfileData(); - var access_token = await PrerenderStateService.GetValue($"{nameof(EditProfilePage)}-access_token", AuthTokenProvider.GetAccessTokenAsync); + var access_token = await PrerenderStateService.GetValue(AuthTokenProvider.GetAccessTokenAsync); - profileImageUploadUrl = $"{Configuration.GetApiServerAddress()}Attachment/UploadProfileImage?access_token={access_token}"; - profileImageUrl = $"{Configuration.GetApiServerAddress()}Attachment/GetProfileImage?access_token={access_token}"; - profileImageRemoveUrl = $"Attachment/RemoveProfileImage?access_token={access_token}"; + profileImageUploadUrl = $"{Configuration.GetApiServerAddress()}api/Attachment/UploadProfileImage?access_token={access_token}"; + profileImageUrl = $"{Configuration.GetApiServerAddress()}api/Attachment/GetProfileImage?access_token={access_token}"; + profileImageRemoveUrl = $"api/Attachment/RemoveProfileImage?access_token={access_token}"; } finally { diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor index 3ef68a54eb..415cc719e3 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor @@ -4,7 +4,7 @@
-
+

@Localizer[nameof(AppStrings.ForbiddenException)]

@Localizer[nameof(AppStrings.YouAreSignInAs)] @user.GetUserName()

diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs index 03ced70bae..993a310cee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Pages/NotAuthorizedPage.razor.cs @@ -21,7 +21,7 @@ protected override async Task OnAfterFirstRenderAsync() // Following this procedure, the newly acquired access token may now include the necessary roles or claims. // To prevent infinitie redirect loop, let's append refresh_token=false to the url, so we only redirect in case no refresh_token=false is present - if (string.IsNullOrEmpty(refresh_token) is false && RedirectUrl?.Contains("refresh_token=false", StringComparison.InvariantCulture) is null or false) + if (string.IsNullOrEmpty(refresh_token) is false && RedirectUrl?.Contains("try_refreshing_token=false", StringComparison.InvariantCulture) is null or false) { await AuthenticationManager.RefreshToken(); @@ -30,7 +30,7 @@ protected override async Task OnAfterFirstRenderAsync() if (RedirectUrl is not null) { var @char = RedirectUrl.Contains('?') ? '&' : '?'; // The RedirectUrl may already include a query string. - NavigationManager.NavigateTo($"{RedirectUrl}{@char}refresh_token=false"); + NavigationManager.NavigateTo($"{RedirectUrl}{@char}try_refreshing_token=false"); } } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs index 0ccec88070..e8e83b7b07 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/AuthenticationManager.cs @@ -61,7 +61,6 @@ public override async Task GetAuthenticationStateAsync() try { var refreshTokenResponse = await identityController.Refresh(new() { RefreshToken = refresh_token }); - await StoreToken(refreshTokenResponse!); access_token = refreshTokenResponse!.AccessToken; } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPrerenderStateService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPrerenderStateService.cs index 4ef4bca93d..d0f897c997 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPrerenderStateService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/Contracts/IPrerenderStateService.cs @@ -1,4 +1,6 @@ -namespace Boilerplate.Client.Core.Services.Contracts; +using System.Runtime.CompilerServices; + +namespace Boilerplate.Client.Core.Services.Contracts; /// /// This service simplifies the process of persisting application state in Pre-Rendering mode @@ -13,5 +15,10 @@ public interface IPrerenderStateService /// one can easily use the following method () in the OnInit lifecycle method of the Blazor components or pages /// to retrieve everything that requires an async-await (like current user's info). /// + Task GetValue(Func> factory, + [CallerLineNumber] int lineNumber = 0, + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = ""); + Task GetValue(string key, Func> factory); } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PrerenderStateService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PrerenderStateService.cs index 30afb679fa..2746f63f38 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PrerenderStateService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Services/PrerenderStateService.cs @@ -1,5 +1,7 @@ //-:cnd:noEmit +using System.Runtime.CompilerServices; + namespace Boilerplate.Client.Core.Services; /// @@ -17,6 +19,19 @@ public PrerenderStateService(PersistentComponentState? persistentComponentState this.persistentComponentState = persistentComponentState; } + public async Task GetValue(Func> factory, + [CallerLineNumber] int lineNumber = 0, + [CallerMemberName] string memberName = "", + [CallerFilePath] string filePath = "") + { + if (AppRenderMode.PrerenderEnabled is false) + return await factory(); + + string key = $"{filePath.Split('\\').LastOrDefault()} {memberName} {lineNumber}"; + + return await GetValue(key, factory); + } + public async Task GetValue(string key, Func> factory) { if (AppRenderMode.PrerenderEnabled is false)