Masui Masanori
Posted on February 7, 2021
Intro
This time, I will try signing in from Blazor (Server) applications.
Environments
- .NET Core ver.5.0.102
Samples
My posts about Blazor Server
- 【ASP.NET Core】Try Blazor(Blazor Server)
- 【ASP.NET Core】【Blazor Server】Try SPA
- 【Blazor Server】CSS Isolation & using child components
SignIn
Signing In with SignInManager(Failed)
Because Blazor can use DI, I tried SignInManager to sign in first.
ApplicationUserService.cs
...
public async Task<bool> SignInAsync(string email, string password)
{
var target = await applicationUsers.GetByEmailAsync(email);
if (target == null)
{
return false;
}
var result = await signInManager.PasswordSignInAsync(target, password, false, false);
return result.Succeeded;
}
...
SignIn.razor
@page "/Pages/SignIn"
<div id="background">
<div id="sign_in_frame">
<h1>Sign In</h1>
<div class="sign_in_input_container">
<input type="text" @bind="Email" class="sign_in_input @AdditionalClassName">
</div>
<div class="sign_in_input_container">
<input type="password" @bind="Password" class="sign_in_input @AdditionalClassName">
</div>
<div id="sign_in_controller_container">
<button @onclick="StartSigningIn">Sign In</button>
</div>
</div>
</div>
SignIn.razor.cs
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using ApprovementWorkflowSample.Applications;
namespace ApprovementWorkflowSample.Views
{
public partial class SignIn
{
[Inject]
public IJSRuntime? JSRuntime { get; init; }
[Inject]
public NavigationManager? Navigation { get; init; }
[Inject]
public IApplicationUserService? ApplicationUsers{get; init; }
[Parameter]
public string Email { get; set; } = "";
[Parameter]
public string Password { get; set; } = "";
[Parameter]
public string AdditionalClassName { get; set; } = "";
public async Task StartSigningIn()
{
if(string.IsNullOrEmpty(Email) ||
string.IsNullOrEmpty(Password))
{
await HandleSigningInFailedAsync("Email and Password are required");
return;
}
var result = await ApplicationUsers!.SignInAsync(Email, Password);
if(result)
{
Console.WriteLine("Navi");
Navigation!.NavigateTo("/Pages/Edit");
return;
}
AdditionalClassName = "login_failed";
await JSRuntime!.InvokeAsync<object>("Page.showAlert","Email or Password are not match");
}
private async Task HandleSigningInFailedAsync(string errorMessage)
{
AdditionalClassName = "login_failed";
await JSRuntime!.InvokeAsync<object>("Page.showAlert", errorMessage);
}
}
}
I got an exception when I tried calling "SignInAsync".
Unhandled exception rendering component: Headers are read-only, response has already started. System.InvalidOperationException: Headers are read-only,
response has already started.
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
...
Signing in from Controller(Failed)
So I tried signing in from Controller as same as signing in from JavaScript codes.
UserController.cs
...
[HttpPost]
[Route("Users/SignIn")]
public async ValueTask<bool> SignIn([FromBody]SignInValue value)
{
if(string.IsNullOrEmpty(value.Email) ||
string.IsNullOrEmpty(value.Password))
{
return false;
}
return await users.SignInAsync(value.Email, value.Password);
}
...
SignInValue.cs
namespace ApprovementWorkflowSample.Applications.Dto
{
public record SignInValue(string Email, string Password);
}
SignIn.razor.cs
using System.IO;
using System.Text;
using System.Net.Http;
using System.Threading.Tasks;
using ApprovementWorkflowSample.Applications.Dto;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using Microsoft.Extensions.Configuration;
using ApprovementWorkflowSample.Applications;
using Newtonsoft.Json;
namespace ApprovementWorkflowSample.Views
{
public partial class SignIn
{
[Inject]
public IJSRuntime? JSRuntime { get; init; }
[Inject]
public IHttpClientFactory? HttpClients { get; init; }
[Inject]
public IConfiguration? Configuration { get; init; }
[Inject]
public NavigationManager? Navigation { get; init; }
...
public async Task StartSigningIn()
{
...
var httpClient = HttpClients.CreateClient();
var signInValue = new SignInValue(Email, Password);
var context = new StringContent(JsonConvert.SerializeObject(signInValue), Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(Path.Combine(Configuration!["BaseUrl"], "Users/SignIn"), context);
if(response.IsSuccessStatusCode == false)
{
await HandleSigningInFailedAsync("Failed access");
return;
}
string resultText = await response.Content.ReadAsStringAsync();
bool.TryParse(resultText, out var result);
if(result)
{
Navigation!.NavigateTo("/Pages/Edit");
return;
}
AdditionalClassName = "login_failed";
await HandleSigningInFailedAsync("Email or Password are not match");
}
...
I didn't get any exceptions and I could get "true" as results.
But the status didn't be treated as "authenticated".
EditWorkflow.razor
@page "/Pages/Edit"
@attribute [Authorize]
<CascadingAuthenticationState>
<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity!.Name!</h1>
<p>You can only see this content if you're authorized.</p>
</Authorized>
<NotAuthorized>
<h1>Authentication Failure!</h1>
<p>You're not signed in.</p>
</NotAuthorized>
</AuthorizeView>
</CascadingAuthenticationState>
After authenticated, this page still showed "NotAuthorized" elements.
Because it was authenticated through HTTP connection. But Blazor Server application use SignalR.
So it couldn't get infomations about authentication.
Use ClaimsPrincipal, ClaimsIdentity and AuthenticationState (OK)
According to the article of Stack Overflow, I add "IHostEnvironmentAuthenticationStateProvider" and changed "SignIn.razor.cs".
Startup.cs
...
public void ConfigureServices(IServiceCollection services)
{
...
services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp =>
(ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>()
);
...
}
...
SignIn.razor.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using ApprovementWorkflowSample.Applications;
using Microsoft.AspNetCore.Identity;
using System.Security.Claims;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Authentication.Cookies;
namespace ApprovementWorkflowSample.Views
{
public partial class SignIn
{
[Inject]
public IJSRuntime? JSRuntime { get; init; }
[Inject]
public NavigationManager? Navigation { get; init; }
[Inject]
public IApplicationUserService? ApplicationUsers{get; init; }
[Inject]
public SignInManager<ApplicationUser>? SignInManager { get; init; }
[Inject]
public IHostEnvironmentAuthenticationStateProvider? HostAuthentication { get; init; }
[Inject]
public AuthenticationStateProvider? AuthenticationStateProvider{get; init; }
...
public async Task StartSigningIn()
{
...
ApplicationUser? user = await ApplicationUsers!.GetUserByEmailAsync(Email);
if(user == null)
{
await HandleSigningInFailedAsync("Email or Password are not match");
return;
}
SignInResult loginResult = await SignInManager!.CheckPasswordSignInAsync(user, Password, false);
if(loginResult.Succeeded == false)
{
await HandleSigningInFailedAsync("Email or Password are not match");
return;
}
if(loginResult.Succeeded)
{
ClaimsPrincipal principal = await SignInManager.CreateUserPrincipalAsync(user);
SignInManager.Context.User = principal;
HostAuthentication!.SetAuthenticationState(
Task.FromResult(new AuthenticationState(principal)));
// If you don't need doing anything without moving to next page, you can remove this.
AuthenticationState authState = await AuthenticationStateProvider!.GetAuthenticationStateAsync();
Navigation!.NavigateTo("/Pages/Edit");
}
}
...
Finally, I could sign in and the status was also treated as "authenticated".
Auto redirect for non-authenticated user
ASP.NET Core MVC can redirect automatically when the user isn't authenticated.
How about Blazor?
This time, I decided following this posts.
App.razor
@using Shared
<CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<RedirectToSignIn></RedirectToSignIn>
</NotAuthorized>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
RedirectToSignIn.razor.cs
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
namespace ApprovementWorkflowSample.Views
{
public partial class RedirectToSignIn
{
[CascadingParameter]
private Task<AuthenticationState>? AuthenticationStateTask { get; init; }
[Inject]
public NavigationManager? Navigation { get; init; }
protected override async Task OnInitializedAsync()
{
var authenticationState = await AuthenticationStateTask!;
if (authenticationState?.User?.Identity is null || !authenticationState.User.Identity.IsAuthenticated)
{
var returnUrl = Navigation!.ToBaseRelativePath(Navigation.Uri);
if (string.IsNullOrWhiteSpace(returnUrl))
{
Navigation.NavigateTo("Pages/SignIn", true);
}
else
{
Navigation.NavigateTo($"Pages/SignIn?returnUrl={returnUrl}", true);
}
}
}
}
}
Resources
Posted on February 7, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.