asp.netasp.net-corecookiesblazorblazor-server-side

How do I save token in cookies on Blazor server


I can't save tokens (in which are returned from an external API) in cookies, I always get this error:

Error: System.InvalidOperationException: Headers are read-only, response has already started.

at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_SetCookie(StringValues value)
at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)
at FinUnsize.Pages.Account.Login.Login.SaveToken(String token) in C:\Users\Gui-Ribs\Desktop\FinUnsize_System-main\Pages\Account\Login\Login.razor:line 64
at FinUnsize.Pages.Account.Login.Login.<>c__DisplayClass9_0.b__0() in C:\Users\Gui-Ribs\Desktop\FinUnsize_System-main\Pages\Account\Login\Login.razor:line 92
at Microsoft.AspNetCore.Components.Rendering.RendererSynchronizationContextDispatcher.InvokeAsync(Action workItem)
at FinUnsize.Pages.Account.Login.Login.Entry() in C:\Users\Gui-Ribs\Desktop\FinUnsize_System-main\Pages\Account\Login\Login.razor:line 92
at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)

I created the token saving method in different ways and classes. The last way it is in the razor component, in which I have a SaveToken(string token) method that receives the token from the request response.

Code of Login.razor:

@page "/login"
@page "/user/login"

@using FinUnsize.Response

@layout SignLayout

@inject ApiResponse apiResponse
@inject IHttpContextAccessor HttpContextAccessor
@inject IJSRuntime JSRuntime
@inject NavigationManager NavigationManager

<PageTitle>Login</PageTitle>

<section class="sign__container">
    <div class="sign__image">
        <img src="./login.jpg">
    </div>
    <div class="sign__content">
        <div class="sign__main">
            <div class="sign__header">
                <div class="header__title">
                    <h1 class="title">Login</h1>
                </div>
                <div class="sign__header-btn">
                    <a><i class="fa-brands fa-google"></i>Entrar com o google</a>
                    <a><i class="fa-brands fa-github"></i>Entrar com o GitHub</a>
                </div>
                <div class="divider-lines">
                    <div class="line"></div><p>ou</p><div class="line"></div>
                </div>
            </div>
            <div class="sign__text">
                <div class="sign__payload">
                    <form @onsubmit="Entry">
                        <div>
                            <input type="text" placeholder="LOGIN" class="cvc" @bind="login" @ref="loginInput" autofocus required>
                            <input type="password" placeholder="Senha" class="titular" @bind="password" required>
                        </div>
                        @if (!string.IsNullOrEmpty(errorMessage))
                        {
                            <p class="error-message">@errorMessage</p>
                        }
                        <div class="form-bottom">
                            <button type="submit" class="@btn">EFETUAR O LOGIN</button>
                            <p>Não possui uma conta? <a href="/user/company">Faça o Cadastro</a></p>
                        </div>
                        
                    </form>
                </div>
            </div>
        </div>
    </div>
</section>

@code {
    private string login;
    private string password;
    private bool isLoading;
    private string btn;
    private string errorMessage;
    private string ACCESS_TOKEN = "accessToken";
    ElementReference loginInput;

    public void SaveToken(string token)
    {
        HttpContextAccessor.HttpContext.Response.Cookies.Append(
           ACCESS_TOKEN,
            token,
            new CookieOptions()
                {
                    Path = "/",
                    Expires = DateTime.Now.AddHours(1),
                    Secure = true
                }
            );
    }

    private async Task Entry()
    {
        isLoading = true;

        if (isLoading)
        {
            btn = "loading";
        }

        var endpoint = "user/login";
        var response = await apiResponse.Login(endpoint, login, password);
        await InvokeAsync(() => SaveToken(response));

        isLoading = false;

        if (!response.Contains("erro"))
        {
            JSRuntime.InvokeVoidAsync("alert", response);
            NavigationManager.NavigateTo("/count");
        }
        else
        {
            errorMessage = "Login ou Senha estão incorretos";
            loginInput.FocusAsync();
            btn = null;
        }
    }
}

ApiResponse class of connection to external API:

public class ApiResponse
{
    private readonly HttpClient _httpCliente;
    private readonly IHttpContextAccessor _httpContextAccessor;

    public ApiResponse(HttpClient httpClient, IHttpContextAccessor httpContextAccessor)
    {
        _httpCliente = httpClient;
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<string> Login(string endpoint, string login, string password)
    {
        var credentials = new { login = login, password = password};
        var content = Serialize(credentials);
        var response = await _httpCliente.PostAsync(endpoint, content);

        if (response.IsSuccessStatusCode)
        {
           return await response.Content.ReadAsStringAsync();
        }

        return "erro";
    }

    public StringContent Serialize(object payload) 
    {
        return new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    }
}

Program.cs:

builder.Services.AddScoped <HttpClient>(sp => new HttpClient { BaseAddress = new Uri(builder.Configuration["ApiSettings:BaseUrl"]) });

builder.Services.AddHttpContextAccessor();

The connection to the API together with the return are working perfectly, the main problem is that I am unable to save the returned token in cookies


Solution

  • Blazor sever app is using signalr(webscoke) to connect between the server and client browser. So if you just using the httpcontextaccessor to modify the cookies, it will not works, since it doesn't contain the http request between them.

    To solve this issue, you have two way, one way is using the Javascript interop or store the token in blazor local storage(I don't suggest this), use the client js library to set the cookie.

    Another way is you can create a CookieController to set the cookie and let the blazor server redirect to this cookie controller to set the cookie to your client.