asp.net-coreauthenticationcookiesblazor-webassembly

LoginAsync Always Returns False in Blazor WebAssembly with ASP.NET Core 9 Cookie Authentication


I'm building a full-stack app using Blazor WebAssembly and ASP.NET Core 9 Web API. For authentication, I've transitioned from localStorage token management to HttpOnly cookie-based authentication. When I call LoginAsync from the client, my API successfully returns 200 OK, and the cookie (accessToken) is correctly set in the browser (verified via DevTools). However, LoginAsync on the client side returns false, which prevents the UI from updating to the authenticated state.

My Login Page is

 <div class="login-page">
        <div class="login-box">
            <div class="login-header">
                <h1>Hoş Geldiniz</h1>
            </div>
    
            <EditForm Model="_model" OnSubmit="HandleLogin" class="login-form">
                <DataAnnotationsValidator />
                <ValidationSummary />
    
                <div class="form-group">
                    <label>Kullanıcı Adı</label>
                    <InputText @bind-Value="_model.UserName" class="form-control" placeholder="kullanici_adi" />
                    <ValidationMessage For="() => _model.UserName" />
                </div>
    
                <div class="form-group">
                    <label>Şifre</label>
                    <InputText type="password" @bind-Value="_model.Password" class="form-control" placeholder="••••••••" />
                    <ValidationMessage For="() => _model.Password" />
                </div>
    
                @if (!string.IsNullOrWhiteSpace(_error))
                {
                    <div class="alert alert-danger login-error">
                        <i class="bi bi-exclamation-triangle-fill"></i> @_error
                    </div>
                }
    
                <button type="submit" disabled="@_isBusy" class="login-button">
                    @if (_isBusy)
                    {
                        <span class="spinner"></span>
                    }
                    Giriş Yap
                </button>
    
                <div class="register-link">
                    Hesabınız yok mu? <a href="/create-user">Kayıt Ol</a>
                </div>
            </EditForm>
        </div>
    </div>
    @code {
        private LoginDto _model = new();
        private bool _isBusy;
        private string? _error;
    
        private HttpClient _apiClient;
    
        protected override void OnInitialized()
        {
            _apiClient = HttpClientFactory.CreateClient("Testify.API");
        }
    
        private async Task HandleLogin()
        {
            _isBusy = true;
            _error = null;
    
            try
            {
                var response = await _apiClient.PostAsJsonAsync("api/auth/login", _model);
    
                if (response.IsSuccessStatusCode)
                {
                    NotifyAuthStateChanged();
                    NavigationManager.NavigateTo("/", forceLoad: true);
                }
                else
                {
                    _error = await response.Content.ReadAsStringAsync();
                    if (string.IsNullOrWhiteSpace(_error))
                    {
                        _error = "Kullanıcı adı veya şifre hatalı";
                    }
                }
            }
            catch (Exception ex)
            {
                Console.Error.WriteLine($"Login error: {ex}");
                _error = "Giriş sırasında bir hata oluştu";
            }
            finally
            {
                _isBusy = false;
            }
        }
    
        private void NotifyAuthStateChanged()
        {
            if (QuizAuthStateProvider is QuizAuthStateProvider quizAuth)
            {
                quizAuth.NotifyStateChanged();
            }
        }
    }

QuizAuthStateProvider is

public class QuizAuthStateProvider : AuthenticationStateProvider
{
    private readonly HttpClient _httpClient;
    private ClaimsPrincipal _currentUser = new(new ClaimsIdentity());

    public QuizAuthStateProvider(HttpClient httpClient)
    {
        _httpClient = httpClient;
    }

    public override Task<AuthenticationState> GetAuthenticationStateAsync()
    {
        return Task.FromResult(new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())));
    }

    public async Task<bool> LoginAsync(string username, string password)
    {
        // Cookie otomatik olarak tarayıcıya set edilecek
        var response = await _httpClient.PostAsJsonAsync("/api/auth/login", new { username, password });

        if (response.IsSuccessStatusCode)
        {
            // Sunucudan gelen cookie ile artık kimlik doğrulandı
            _currentUser = new ClaimsPrincipal(new ClaimsIdentity(
                new[] { new Claim(ClaimTypes.Name, username) },
                "Cookies"));

            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
            return true;
        }

        return false;
    }

    public async Task LogoutAsync()
    {
        await _httpClient.PostAsync("/api/auth/logout", null);
        _currentUser = new ClaimsPrincipal(new ClaimsIdentity());
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }

    public void NotifyStateChanged()
    {
        NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
    }
}

AuthLoggingHandler is

public class AuthLoggingHandler : DelegatingHandler
{
    private readonly ILogger<AuthLoggingHandler> _logger;
    private readonly IJSRuntime _jsRuntime;

    // Sadece DI ile alınan parametreler, HttpMessageHandler kaldırıldı
    public AuthLoggingHandler(IJSRuntime jsRuntime, ILogger<AuthLoggingHandler> logger)
    {
        _jsRuntime = jsRuntime ?? throw new ArgumentNullException(nameof(jsRuntime));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _logger.LogDebug("Initiating request to {Method} {Uri}",
            request.Method, request.RequestUri);

        var response = await base.SendAsync(request, cancellationToken);

        _logger.LogDebug("Received {StatusCode} response from {Uri}",
            response.StatusCode, request.RequestUri);

        return response;
    }
}

Credentials.js is

// POST isteği, çerezlerle birlikte
window.postWithCredentials = async function (url, body) {
    try {
        const response = await fetch(url, {
            method: "POST",
            credentials: "include",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body)
        });
        if (!response.ok) throw new Error("HTTP " + response.status);
        return await response.json();
    } catch (err) {
        console.error("postWithCredentials error:", err);
        throw err;
    }
};

// Çerezleri döndürür
window.getCookies = function () {
    return document.cookie;
};

// GET isteği, çerezlerle birlikte
window.getWithCredentials = async function (url) {
    try {
        const response = await fetch(url, {
            method: "GET",
            credentials: "include"
        });
        if (!response.ok) throw new Error("HTTP " + response.status);
        return await response.json();
    } catch (err) {
        console.error("getWithCredentials error:", err);
        throw err;
    }
};

// Sadece giriş yapıldıysa me fonksiyonunu çağır
window.callMeIfLoggedIn = async function (meUrl) {
    if (localStorage.getItem("isLoggedIn") === "true") {
        try {
            const user = await window.getWithCredentials(meUrl);
            // Kullanıcı bilgilerini işle
            console.log("Kullanıcı:", user);
        } catch (err) {
            // Hata yönetimi
            console.warn("Me endpoint hatası:", err);
        }
    } else {
        // Giriş yapılmamış
        console.log("Kullanıcı giriş yapmamış.");
    }
};

CORS are already done and working correctly.

    public static void ConfigureApplicationCookie(this IServiceCollection services)
{
    services.ConfigureApplicationCookie(options =>
    {
        options.Cookie.HttpOnly = true; // HttpOnly cookie olarak ayarla
        options.ExpireTimeSpan = TimeSpan.FromDays(7); // 7 gün geçerli olsun
        options.SlidingExpiration = true; // Kaydırmalı geçerlilik süresi
        options.Cookie.SecurePolicy = CookieSecurePolicy.Always; // HTTPS zorunlu
        options.Cookie.SameSite = SameSiteMode.None; // SameSite özelliğini sıkı yap
    });
}

Solution

  • This isn’t a limitation of cookie-based authentication. However, it’s more likely a configuration nuance when using Blazor WASM.

    Your issue is most likely due to the default behavior of Blazor WebAssembly’s HttpClient, which by default does not include cookies/credentials with CORS. You said that your login endpoint relies on cookies for authentication, then you must explicitly set the HttpClient’s underlying handler’s IncludeCredentials property to true.
    check your Program.cs and add the following code to configure IncludeCredentials on Your HttpClient Handler (if you haven't done it already):

    
    builder.Services.AddScoped(sp =>     new HttpClient(new BrowserHttpMessageHandler     {         // Ensure credentials (cookies) are sent with each request         IncludeCredentials = true     })     {         BaseAddress = new Uri("https://your-api-domain.com")     }); 
    

    try it and let me know if this resolve your issue