0

I started a new Blazor project using Auto rendermode and Global location. By default it contains 2 projects: the server and the client. All the pages are on the client and I added a Login.razor page in the client project.

This Login.razor calls a WebApi in server (using HttpClient) that checks the credentials and if valid, calls HttpContext.SignInAsync. Then upon the API returning a response, redirects the user to a home page.

Here is what the API looks like:

[HttpPost("signin")]
public async Task<IActionResult> SignInAsync([FromForm] string username, [FromForm] string password)
{
    var user = await _userRepository.GetByNameAsync(username);
    if (user != null)
    {
        var passwordHash = await _userRepository.GetPasswordHashAsync(user.Id);
        if (passwordHash != null)
        {
            var passwordResult = _passwordHasher.VerifyPasswordHash(user, passwordHash, password);
            if (passwordResult == PasswordVerificationResult.Success)
            {
                var claimsIdentity = new ClaimsIdentity(GetClaims(user), CookieAuthenticationDefaults.AuthenticationScheme);
                await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimsIdentity));

                return Ok(user);
            }
        }
    }
}

And here is the method in the login page:

async Task LogInAsync()
{
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("username", LoginInfo.Username),
        new KeyValuePair<string, string>("password", LoginInfo.Password)
    });

    var response = await HttpClient.PostAsync("api/users/signin", content);
    if (response.IsSuccessStatusCode)
    {
        // Extract returnUrl from query string
        var uri = new Uri(NavigationManager.Uri);
        var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);

        string returnUrl = GetQueryStringParameter("returnUrl");

        // Redirect to the returnUrl
        NavigationManager.NavigateTo(returnUrl, true);
    }
    else
    {
        Console.WriteLine("Invalid credentials");
    }
}

Since my app is using Auto rendermode, initially, when the page loads, it is using Server rendermode. When a user clicks login, it reaches HttpContext.SignInAsync (confirmed by breakpoint) and does the redirect but the cookie isn't created so the user remains unauthorized and it just goes back to the login page. But since a redirect happened that reloaded the page and I am using Auto rendermode, by this time, WASM has already fully loaded and so the rendermode switches to WASM. When I click login button again, everything works, cookie is created in the browser. So only when Server rendermode does the cookie not get created in the browser.

I've read that it might have something to do with how Server rendermode function that it using SignalR does not set the cookie but I am not sure.

EDIT:

Well, I am able to make it work by getting the cookie from the response headers and writing it manually:

async Task LogInAsync()
{
    var content = new FormUrlEncodedContent(new[]
    {
        new KeyValuePair<string, string>("username", LoginInfo.Username),
        new KeyValuePair<string, string>("password", LoginInfo.Password)
    });

    var response = await HttpClient.PostAsync("api/users/signin", content);
    if (response.IsSuccessStatusCode)
    {
        if (response.Headers.Contains("Set-Cookie"))
        {
            var setCookieHeader = response.Headers.GetValues("Set-Cookie").FirstOrDefault();
            if (!string.IsNullOrEmpty(setCookieHeader))
            {
                // Write cookie to browser manually
                var cook = setCookieHeader.Split(";")[0];
                await JSRuntime.InvokeVoidAsync("eval", $"document.cookie = '" + cook + "';");
            }
        }

        // Extract returnUrl from query string
        var uri = new Uri(NavigationManager.Uri);
        var queryParams = System.Web.HttpUtility.ParseQueryString(uri.Query);

        string returnUrl = GetQueryStringParameter("returnUrl");

        // Redirect to the returnUrl
        NavigationManager.NavigateTo(returnUrl, true);
        //await JSRuntime.InvokeVoidAsync("eval", "window.location.href = '/';");
        }
    else
    {
        Console.WriteLine("Invalid credentials");
    }
}

but I don't know if this something that I should do.

1 Answer 1

1

Logically, the cookies get by httpclient shouldn't appear in browser. But Blazor WASM has a built-in design that if you make request to the same domain api, the httpclient will share cookie with browser. https://github.com/dotnet/aspnetcore/issues/53609#issuecomment-1910306857, https://github.com/dotnet/aspnetcore/issues/16239#issuecomment-473512419

I think your solution is right and there's no other way to do the same for httpclient in Server Mode. You could use CustomHttpHandler to copy the cookies for every HttpClient call.

    public class CustomHttpHandler : HttpClientHandler
    {
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var response = await base.SendAsync(request, cancellationToken);

            // Do some cookie extract and copy actions 
            return response;
        }
    }

And use the handler

@inject HttpClient _httpClient
@inject CustomHttpHandler _customHttpHander
...
_httpClient = new HttpClient(_customHttpHander);
Sign up to request clarification or add additional context in comments.

Comments

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.