Search code examples
c#asp.net.netcookiesmaui

Save CookiesCollection to file and read from it in dot NET MAUI C#


I'm trying to build an application in MAUI that will work only in android. This application is simple and does not require any kind of special security or something else. I mainly have a problem when using HttpClient, I created a class that inherits from HttpClientHandler and one that inherits from CookieContainer.

HttpClient:

public class ApiHttpClient : HttpClient
{

    #region FIELDS

    private readonly IUriService _uriService;

    #endregion FIELDS

    #region CTORS

    public ApiHttpClient(IUriService uriService, HttpClientHandler httpClientHandler) : base(httpClientHandler)
    {
        _uriService = uriService;
    }

    #endregion CTORS

    /// <summary>
    /// 
    /// </summary>
    /// <param name="loginUserInfo"></param>
    /// <param name="cancellationToken"></param>
    /// <returns></returns>
    /// <exception cref="UnauthorizedApiException"></exception>
    /// <exception cref="BadRequestApiException"></exception>
    /// <exception cref="ArgumentException"></exception>
    public async Task SignInAsync(ILoginUserInfo loginUserInfo, CancellationToken? cancellationToken = null)
    {
        var uri = _uriService.AuthenticationSignInPath();
        var httpResponseMessage = await this.PostAsJsonAsync(uri, loginUserInfo,
            cancellationToken ?? new CancellationToken(false));

        if (httpResponseMessage.StatusCode == HttpStatusCode.OK)
        {
            return;
        }
        if(httpResponseMessage.StatusCode == HttpStatusCode.Unauthorized)
        {
            var problemDetails = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
            throw new UnauthorizedApiException(problemDetails?.Detail);
        }
        if (httpResponseMessage.StatusCode == HttpStatusCode.BadRequest)
        {
            var problemDetails = await httpResponseMessage.Content.ReadFromJsonAsync<ProblemDetails>();
            throw new BadRequestApiException(problemDetails?.Detail);
        }

        throw new ArgumentException($"Unexpected error! Status code: {httpResponseMessage.StatusCode}.", nameof(httpResponseMessage.StatusCode));
    }
}

HttpClientHandler:

public class ApiHttpClientHandler : HttpClientHandler
{

    #region FIELDS

    private readonly CookiesService.CookiesService _cookiesService;

    #endregion FIELDS

    #region CTORS

    public ApiHttpClientHandler(CookiesService.CookiesService cookiesService)
    {
        _cookiesService = cookiesService;

        UseCookies = true;
        CookieContainer = _cookiesService;
    }

    protected override HttpResponseMessage Send(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var response = base.Send(request, cancellationToken);
        _cookiesService.SaveCookies();
        return response;
    }

    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        //TODO cookies are not sent with first request
        var response = await base.SendAsync(request, cancellationToken);
        _cookiesService.SaveCookies();
        return response;
    }

    #endregion CTORS

}

CookieContainer:

public class CookiesService : CookieContainer
{

    #region FIELDS
    
    private const string COOKIES_FILE_NAME = "biscuits.js";

    private string? _cookiesFilePath;

    #endregion FIELDS

    #region CTORS

    public CookiesService()
    {
        _cookiesFilePath = Path.Combine(FileSystem.CacheDirectory, COOKIES_FILE_NAME);
        LoadCookies();
    }

    #endregion CTORS

    #region METHODS

    public void LoadCookies()
    {
        if (!File.Exists(_cookiesFilePath))
        {
            return;
        }

        var cookieFileContent = File.ReadAllText(_cookiesFilePath);
        if (string.IsNullOrWhiteSpace(cookieFileContent))
            return;
        var deserializedCookies = JsonSerializer.Deserialize<CookieCollection>(cookieFileContent);

        if (deserializedCookies != null)
            Add(deserializedCookies);

        var cookies = GetAllCookies();
    }

    public void SaveCookies()
    {
        var cookies = GetAllCookies();

        if (cookies.Count <= 0) return;

        var serializedCookies = JsonSerializer.Serialize(cookies, new JsonSerializerOptions() {WriteIndented = true});
        File.WriteAllText(_cookiesFilePath, serializedCookies);
    }

    #endregion METHODS

}

So the CookiesService is used to Save and Load cookies from a cached JSON file, when I load the cookies, they are loaded correctly but when i send a request, from my HTTP Client, to authenticate, even if I have an authentication cookie loaded into the CookiesService, it is not sent to the server because the server is mine and i have intercepted the HEADERS but the Authentication token is not sent with the request so it authenticate and responds to the app with a new authentication toke that is saved into the file after the request completed, the strangest thing is that if I run the same request twice without closing the application, it sends the Authentication token and the server responds with the Unauthorized status code that means that the user is already logged so the token is sent correctly.

In conclusion I'm asking why if i load the cookies from the file, and i've checked if the cookies are loaded correctly before sending the request, the authentication cookie is not sent.

Little update: I tried injecting the same cookie with same name, value, path and domain before sending the request, this way it works but i have to create it by hands. If i have the authentication token cookie saved into the file and i load it back and then insert by hands a cookie with meaningless value and name but correct domain and path, the debugger results view of the IEnumerator of cookies shows that there are two cookies but the server receives only one of them, the one created by hands and not loaded from the file.

Another Little update: I tested the fact that only cookies loaded from the file are not sent, I tried injecting a fake cookie that was sent at the moment of the injection, received from the server and then saved into the file but at the restart of the application, after having read the cookies file and have loaded them (the fake cookie was loaded alongside with the authentication token from the file), without injecting a new fake cookie, none of the cookies loaded from the file are sent to the server, neither the token nor the fake cookie. Now I just have to find out why and how to avoid this behaviour. For now I have tried this but it doesn't work:

CookiesService:

public void LoadCookies()
{
    if (!File.Exists(_cookiesFilePath))
    {
        return;
    }
    var cookieFileContent = File.ReadAllText(_cookiesFilePath);
    if (string.IsNullOrWhiteSpace(cookieFileContent))
        return;
    var deserializedCookies = JsonSerializer.Deserialize<CookieCollection>(cookieFileContent);

    if (deserializedCookies != null)
        foreach(var cookie in deserializedCookies.ToArray()) 
            Add(new Cookie()
            {
                Name = cookie.Name,
                Value = cookie.Value,
                Expires = cookie.Expires,
                Path = cookie.Path,
                Comment = cookie.Comment,
                HttpOnly = cookie.HttpOnly,
                Expired = cookie.Expired,
                CommentUri = cookie.CommentUri,
                Discard = cookie.Discard,
                Domain = cookie.Domain,
                Port = cookie.Port,
                Secure = cookie.Secure,
                Version = cookie.Version
            });
}

I even tried using reflection to see the differences between cookies sent by the server and the ones stored and there is no valuable difference.

Another Update: I found a workaround to make it work, there is a property that is making the HttpClientHandler not to send the cookie (I'll do further debug to understand which one is), for now the working code is inside the CookiesService:

CookiesService:

public void LoadCookies()
{
    if (!File.Exists(_cookiesFilePath))
        return;

    var cookieFileContent = File.ReadAllText(_cookiesFilePath);
    if (string.IsNullOrWhiteSpace(cookieFileContent))
        return;
    var deserializedCookies = JsonSerializer.Deserialize<CookieCollection>(cookieFileContent);

     if (deserializedCookies != null)
        foreach(var cookie in deserializedCookies.ToArray())
        {
            var cookieCopy = new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain);
            cookieCopy.HttpOnly = true;
            Add(cookieCopy);
        }
}

Solution

  • I found the solution, if anyone has an optimized way of doing it please post an answer.

    So after debugging, I found out that the property that is not making the cookie to be sent is the Port property, I don't understand why but its the only property that does that, so my new working code is:

    public void LoadCookies()
    {
        if (!File.Exists(_cookiesFilePath))
        {
            return;
        }
    
        var cookieFileContent = File.ReadAllText(_cookiesFilePath);
        if (string.IsNullOrWhiteSpace(cookieFileContent))
            return;
        var deserializedCookies = JsonSerializer.Deserialize<CookieCollection>(cookieFileContent);
    
        if (deserializedCookies != null)
            foreach(var cookie in deserializedCookies.ToArray())
            {
                var cookieCopy = new Cookie(cookie.Name, cookie.Value, cookie.Path, cookie.Domain)
                {
                    HttpOnly = cookie.HttpOnly,
                    Comment = cookie.Comment,
                    CommentUri = cookie.CommentUri,
                    Discard = cookie.Discard,
                    Expired = cookie.Expired,
                    Expires = cookie.Expires,
                    Secure = cookie.Secure,
                    Version = cookie.Version,
                    /* If set, this property is not making the
                     * HttpClientHandler to send the cookie loaded from the cookies file */
                    /* Port = cookie.Port,*/
                };
                Add(cookieCopy);
            }
    }
    

    Update: I opened an issue to the MAUI github repo and they found out that is a framework problem related to serializing and deserializing the Port property of the Cookie.

    You can find my issue and another issue related to it at this links: https://github.com/dotnet/runtime/issues/90031

    https://github.com/dotnet/runtime/issues/70227

    It is planned to be fixed in future dot NET updates.