【Blazor Server】SignIn with custom user
Tags: dotnet, csharp, blazor
Published: 2021-02-07 12:53:15

## Intro
This time, I will try signing in from Blazor (Server) applications.

* [【.NET 5】【ASP.NET Core Identity】SignIn with custom user](https://dev.to/masanori_msl/net-5-asp-net-core-identity-signin-with-custom-user-56fe)

### Environments
* .NET Core ver.5.0.102

### Samples
* [ApproveWorkflowSample](https://github.com/masanori840816/ApproveWorkflowSample)

### My posts about Blazor Server
* [【ASP.NET Core】Try Blazor(Blazor Server)](https://dev.to/masanori_msl/asp-net-core-try-blazor-blazor-server-4j34)
* [【ASP.NET Core】【Blazor Server】Try SPA](https://dev.to/masanori_msl/asp-net-core-blazor-server-try-spa-2cd7)
* [【Blazor Server】CSS Isolation & using child components](https://dev.to/masanori_msl/blazor-server-css-isolation-using-child-components-17pn)

## SignIn
### Signing In with SignInManager(Failed)
Because Blazor can use DI, I tried SignInManager to sign in first.

#### ApplicationUserService.cs
```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
```html
@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
```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
```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
```cs
namespace ApprovementWorkflowSample.Applications.Dto
{
    public record SignInValue(string Email, string Password);
}
```

#### SignIn.razor.cs
```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
```xml
@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.

* [c# - SignIn for Blazor Server-Side app not working - Stack Overflow](https://stackoverflow.com/questions/58387683/signin-for-blazor-server-side-app-not-working)

### Use ClaimsPrincipal, ClaimsIdentity and AuthenticationState (OK)
According to the article of Stack Overflow, I add "IHostEnvironmentAuthenticationStateProvider" and changed "SignIn.razor.cs".

#### Startup.cs
```cs
...
        public void ConfigureServices(IServiceCollection services)
        {
...
            services.AddScoped<IHostEnvironmentAuthenticationStateProvider>(sp =>
                (ServerAuthenticationStateProvider) sp.GetRequiredService<AuthenticationStateProvider>()
            );
...
        }
...
```
#### SignIn.razor.cs
```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".
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/64unciixpfdxl0en1672.jpg)

## 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.

* [Blazor - Redirect non-authenticated user to login page](https://blog.vfrz.fr/blazor-redirect-non-authenticated-user/)

#### App.razor
```xml
@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
```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
* [ASP.NET Core Blazor authentication and authorization | Microsoft Docs](https://docs.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-5.0)
* [c# - SignIn for Blazor Server-Side app not working - Stack Overflow](https://stackoverflow.com/questions/58387683/signin-for-blazor-server-side-app-not-working)
* [Blazor - Redirect non-authenticated user to login page](https://blog.vfrz.fr/blazor-redirect-non-authenticated-user/)