1

I have a Blazor WASM Hosted solution (Server, Client, Models). Obviously the Client side is blazor components that are posting to the Server API Controllers. However, I am looking to have some routes be run specifically on the Server in an MVC format. Example being profile management like changing password or upgrading membership options. Things that are most secure being served on the server rather then hosted in the client. However, I am not able to get this to work. I am assuming i am missing something when it comes to setting up the MVC routing on the Server application.

Here is my Server Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            //Register the Datacontext and Connection String
            services.AddDbContext<DataContext>(options =>
                options.UseSqlServer(
                    Configuration.GetConnectionString("DefaultConnection")));

            services.AddDatabaseDeveloperPageExceptionFilter();

            //Sets up the default Asp.net core Identity Screens - Use Identity Scaffolding to override defaults
            services.AddDefaultIdentity<ApplicationUser>( options =>
                    {
                        options.SignIn.RequireConfirmedAccount = true;
                        options.Password.RequireDigit = true;
                        options.Password.RequireLowercase = true;
                        options.Password.RequireUppercase = true;
                        options.Password.RequiredUniqueChars = 0;
                        options.Password.RequireNonAlphanumeric = false;
                        options.Password.RequiredLength = 8;
                        options.User.RequireUniqueEmail = true;
                    })
                .AddRoles<IdentityRole>()
                .AddEntityFrameworkStores<DataContext>();

            //Associates the User to Context with Identity
            services.AddIdentityServer()
                .AddApiAuthorization<ApplicationUser, DataContext>( options =>
            {
                options.IdentityResources["openid"].UserClaims.Add(JwtClaimTypes.Role);
                options.ApiResources.Single().UserClaims.Add(JwtClaimTypes.Role);
            });
            JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Remove(JwtClaimTypes.Role);

            //Adds authentication handler
            services.AddAuthentication().AddIdentityServerJwt();

            services.AddHttpContextAccessor();
        

            //Register Radzen Services
            services.AddScoped<DialogService>();
            services.AddScoped<NotificationService>();
            services.AddScoped<TooltipService>();
            services.AddScoped<ContextMenuService>();

            services.AddControllersWithViews();
            services.AddRazorPages();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, DataContext dataContext)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseMigrationsEndPoint();
                app.UseWebAssemblyDebugging();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
                app.UseHsts();
            }

            //AutoMigrates data
            dataContext.Database.Migrate();

            app.UseHttpsRedirection();
            app.UseBlazorFrameworkFiles();
            app.UseStaticFiles();

            app.UseSerilogIngestion();
            app.UseSerilogRequestLogging();

            app.UseRouting();

            app.UseIdentityServer();
            app.UseAuthentication();
            app.UseAuthorization();

            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
                endpoints.MapControllers();
                endpoints.MapControllerRoute(name: "default", pattern: "{controller=Home}/{action=Index}/{id?}");
                endpoints.MapFallbackToFile("index.html");
            });
        }

Here is the Controller that I am trying to hit on the Server

[ApiController]
    [Route("[controller]")]
    public class ProfileController : Controller
    {
        private UserManager<ApplicationUser> _userManager;
        private ILogger<ProfileController> _logger;
        private readonly SignInManager<ApplicationUser> _signInManager;

        public ProfileController(ILogger<ProfileController> logger, UserManager<ApplicationUser> userManager, SignInManager<ApplicationUser> signInManager)
        {
            _userManager = userManager;
            _logger = logger;
            _signInManager = signInManager;
        }

        public IActionResult Index()
        {
            return View();
        }

        [HttpGet]
        [Route("ChangePassword")]        
        public IActionResult ChangePassword()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        [Route("ChangePassword/{Id?}")]
        public async Task<ActionResult> ChangePassword(ChangePasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            string userId = "";
            if (User.Identity.IsAuthenticated)
                userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;

            var user = await _userManager.FindByIdAsync(userId);

            var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
            if (result.Succeeded)
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return Redirect("/");
            }

            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(error.Code, error.Description);
            }

            return View(model);
        }
    }

I can navigate to the ChangePassword page via the following NavLink in my Client component

<NavLink class="btn btn-outline-primary" href="Profile/ChangePassword">Change password</NavLink>

However, when I submit the form I get the following error

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.13","title":"Unsupported Media Type","status":415,"traceId":"00-4d07a26df9047d4bb56223728e7e2148-923c11953b1a754e-00"}

Here is the form being posted

@using (Html.BeginForm("ChangePassword", "Profile", FormMethod.Post, new { @class = "form-horizontal", role = "form" }))
{
    @Html.AntiForgeryToken()
    <h4>Change Password Form</h4>
    <hr />
    @Html.ValidationSummary(true, "", new { @class = "text-danger" })
    <div class="form-group">
        @Html.LabelFor(m => m.OldPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.OldPassword, new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.OldPassword, "", new { @class = "text-danger" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.NewPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.NewPassword, new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.NewPassword, "", new { @class = "text-danger" })
        </div>
    </div>
    <div class="form-group">
        @Html.LabelFor(m => m.ConfirmPassword, new { @class = "col-md-2 control-label" })
        <div class="col-md-10">
            @Html.PasswordFor(m => m.ConfirmPassword, new { @class = "form-control" })
            @Html.ValidationMessageFor(model => model.ConfirmPassword, "", new { @class = "text-danger" })
        </div>
    </div>
    <div class="form-group">
        <div class="col-md-offset-2 col-md-10">
            <input type="submit" value="Change password" class="btn btn-primary white" />
        </div>
    </div>
}

So this says to me I dont have this configured right and am just lucky that I am hitting the change password page.

Update: I added FromForm to the Post method which I shouldnt have to add, but that got it recognizing the post type.

[HttpPost]
        [ValidateAntiForgeryToken]
        [Route("ChangePassword/{Id?}")]
        public async Task<ActionResult> ChangePassword([FromForm] ChangePasswordViewModel model)
        {
            if (!ModelState.IsValid)
            {
                return View(model);
            }

            string userId = "";
            if (User.Identity.IsAuthenticated)
                userId = User.FindFirst(ClaimTypes.NameIdentifier).Value;

            var user = await _userManager.FindByIdAsync(userId);

            var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword);
            if (result.Succeeded)
            {
                await _signInManager.SignInAsync(user, isPersistent: false);
                return Redirect("/");
            }

            foreach (var error in result.Errors)
            {
                ModelState.AddModelError(error.Code, error.Description);
            }

            return View(model);
        }

However, if i enter an invalid password length, when the Validation Controls should show warnings, they dont. If i submit then i get another error message in the web browser

{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"00-8376a8e3bd02c7438e45da7337b889ce-0d0e067ffd75a944-00","errors":{"NewPassword":["The New password must be at least 6 characters long."]}}

So the MVC side of things definitely is not working correctly

2
  • Does the POST method on the controller get hit? Commented Feb 1, 2022 at 15:00
  • It doesnt, the page displays the Unsupported Media type error posted above Commented Feb 1, 2022 at 15:16

1 Answer 1

0

I believe that Blazor is trying to use current's context to reach Profile/ChangePassword and failing. You need to exit Blazor's context (like pasting the link in the URL bar) so the server can route the request to the MVC framework, not the Blazor one.

Chage your NavLink to:

<NavLink class="btn btn-outline-primary" href="javascript:;" @onclick="(() => OnChangePasswordClick)">Change password</NavLink>

Then write this code for the OnChangePasswordClick handler.

@inject NavigationManager Navigation;

void OnChangePasswordClick()
{
    Navigation.NavigateTo("Profile/ChangePassword", true)
}

The second argumento of NavigateTo tells Blazor to force browser reload.

Sign up to request clarification or add additional context in comments.

1 Comment

I dont have an issue getting to the Profile/ChangePassword. The issue i have is when on the ChangePassword page and trying to post back. Its not acting like an MVC framework application

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.