enkodellc/blazorboilerplate

Auth cookies not set using SSB

c4l3b opened this issue · 10 comments

c4l3b commented

When running the app locally using SSB, I can't get the auth cookies to set. CSB is fine.
I can log in and make some authenticated requests, but when I refresh the page I am logged out.
Have tried both Mac and Windows with every browser I can find.
Neither the .AspNetCore.Identity.Application or idsrv.session cookies are being set.

Is it just me, or is anyone else experiencing this issue too?

Where are these cookies being set right now?

@c4l3b most of the time I have seen similar issues of not authenticating it is a browser cache or your appsettings.json / connection string. Please try a different browser first, especially when switching from CSB to SSB, I have some glitches with logging in that solves it. Try that first.

c4l3b commented

Thanks @enkodellc
Unfortunately I've tried every browser and combination I can.
Do you have a live SSB demo that I could try? Or just the CSB one?

@c4l3b Sorry I was busy and assumed it was a user specific issue. I tested and can recreate it. Personally I don't use SSB, only CSB. I will take a look to see what I can sort out. @MarkStega any ideas?

c4l3b commented

I did come across this in my research. 6th paragraph talks about the need for a redirect-style flow when using SSB. With how bleeding edge blazor is right now, I wasn't sure if it was still relevant or not.
https://www.oqtane.org/Resources/Blog/PostId/527/exploring-authentication-in-blazor

I am looking and see that GetUserInfo() is not getting called when the page is refreshed. I have a morning appt so will have to do a deeper dive later today.

Because the cookies are not set on the browser in SSB, when you refresh the page, the _httpClient used to call the APIs are essentially not used since it's registered as Scoped. This _httpClient gets its cookies when it calls the APIs. It works in CSB because those cookies are set in the browser. To make this work:

  1. Set the client-side cookies using a JS Interop.
  2. When the user refreshes page, read the request cookies and assign them back to the _httpClient.
  3. Remove cookies when logged out.

NOTE: You may want to be specific which cookies (.AspNetCore.Identity.Application and idsrv.session) you are setting and removing as other cookies may exist.

Set the client-side cookies using a JS Interop
For instance, I changed the Login call to:

    public async Task<ApiResponseDto> Login(LoginDto loginParameters)
    {
        ApiResponseDto resp;
        var httpRequestMessage = new HttpRequestMessage(HttpMethod.Post, GetApiUrl("api/Account/Login"));
        httpRequestMessage.Content = new StringContent(JsonConvert.SerializeObject(loginParameters));
        httpRequestMessage.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json");

        using (var response = await _httpClient.SendAsync(httpRequestMessage))
        {
            response.EnsureSuccessStatusCode();

            #if ServerSideBlazor
            if (response.Headers.TryGetValues("Set-Cookie", out var cookieEntries))
            {
                var uri = response.RequestMessage.RequestUri;
                var cookieContainer = new CookieContainer();

                foreach (var cookieEntry in cookieEntries)
                {
                    cookieContainer.SetCookies(uri, cookieEntry);
                }

                var cookies = cookieContainer.GetCookies(uri).Cast<Cookie>();

                foreach (var cookie in cookies)
                {
                   await _jsRuntime.InvokeVoidAsync("jsInterops.setCookie", cookie.ToString());
                }
            }
           #endif

            var content = await response.Content.ReadAsStringAsync();
            resp = JsonConvert.DeserializeObject<ApiResponseDto>(content);
        }

        return resp;
    }

This calls a JS function client-side that basically sets the cookie: document.cookie = cookie.

Read the request cookies and assign them back to the _httpClient
Basically, I updated the App.razor's OnInitializedAsync():

protected override async Task OnInitializedAsync()
{
    await base.OnInitializedAsync();

    _httpClient.BaseAddress = new Uri(_navigationManager.BaseUri);

   #if ServerSideBlazor
    if (_http != null && _http.HttpContext.Request.Cookies.Any())
    {
        var cks = new List<string>();

        foreach (var cookie in _http.HttpContext.Request.Cookies)
        {
            cks.Add($"{cookie.Key}={cookie.Value}");
        }

        _httpClient.DefaultRequestHeaders.Add("Cookie", String.Join(';', cks));
    }
   #endif
}

Remove Cookies on Logout
Finally, we need to remove the cookies from both the browser and _httpClient when user logs out successfully. In the AuthorizeApi, I updated:

    public async Task<ApiResponseDto> Logout()
    {
        var cookies = _httpClient.DefaultRequestHeaders.GetValues("Cookie").ToList();

        var resp = await _httpClient.PostJsonAsync<ApiResponseDto>(GetApiUrl("api/Account/Logout"), null);

        #if ServerSideBlazor
        if (resp.StatusCode == 200 && cookies.Any())
        {
            _httpClient.DefaultRequestHeaders.Remove("Cookie");

            foreach (var cookie in cookies[0].Split(';'))
            {
                var cookieParts = cookie.Split('=');
                await _jsRuntime.InvokeVoidAsync("jsInterops.removeCookie", cookieParts[0]);
            }
        }
      #endif

        return resp;
    }

and create the JS Interop function removeCookie to basically have:

   function (cookieName) {
       document.cookie = cookieName + '=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
   }

I tested this both in SSB and CSB. You'll notice that I included the #if directives as well as they are needed particularly when using the IHttpContextAccesor which doesn't exist in CSB mode.

@marcotana I appreciate the thorough. Do you or @c4l3b want to test and submit a PR?

You're welcome. I'll fork it and submit a PR.

FYI, this was resolved with the merge SSB Auth Cookies

@marcotana Thanks for the PR. Just merged and worked great!