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.