1

We are currently in the process of creating a web app for our customers that is built with SyncFusion's Blazor components. Parts of this web app are also intended to be used as visualization within our WinForms app, which on its own, works great. In terms of authorization however, there is one big issue we are currently facing.

When a user navigates to "(our web app)/clients" in Chrome/Firefox/etc, they are greeted by Auth0's login screen, and after logging in are shown a page that has a SyncFusion control on it that shows a list of clients relevant to that user, with the data being fetched from one of our APIs. When a user opens a tab in our WinForms app that displays that same page through the use of a CefSharp browser, they are automatically logged in and the Auth0 login screen is skipped, because we already know the token (as they are required to log in when the WinForms app is started) and it is passed as a header on request.

In the WinForms App

Program.cs - Initialize Chromium when the WinForms App is launched

    private static void Main(string[] args)
    {
        //Program start up code
        ...
        ChromiumBrowser.InitChromium();
        ...
    }

ChromiumBrowser.cs - Grab the correct assemblies and set the CefSettings for the Chromium Browser

    public static class ChromiumBrowser
    {
        public static void InitChromium()
        {
            //Set resolver to load correct assembly
            ...

            LoadApp();
        }

        public static ChromiumWebBrowser GetBrowser(string url)
        {
            return new ChromiumWebBrowser(url);
        }

        [MethodImpl(MethodImplOptions.NoInlining)]
        private static void LoadApp()
        {
            var settings = new CefSettings();
            settings.CefCommandLineArgs.Add("--disable-gpu-compositing");

            //Load correct assembly for x86/x64
            ...

            Cef.Initialize(settings, false, browserProcessHandler: null);
        }

        ...

    }

UBrowser.cs - The WinForms control to display the ChromiumWebBrowser in. A custom RequestHandler is added to add a header with the authorization token.

        public UBrowser(string token)
        {
            InitializeComponent();

            _chromiumBrowser = ChromiumBrowser.GetBrowser(null);
            _chromiumBrowser.RequestHandler = new BearerAuthRequestHandler(token);
            Controls.Add(_chromiumBrowser);
            _chromiumBrowser.Dock = DockStyle.Fill;
        }

        public void NavigateTo(string url)
        {
            _chromiumBrowser.Load(url);
        }

BearerAuthRequestHandler.cs - The custom RequestHandler. OnBeforeResourceLoad is unfortunately not raised for WebSocket requests...

    public class BearerAuthRequestHandler : RequestHandler
    {
        private readonly string _token;

        public BearerAuthRequestHandler(string token)
        {
            _token = token;
        }

        protected override IResourceRequestHandler GetResourceRequestHandler(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, bool isNavigation, bool isDownload, string requestInitiator,
            ref bool disableDefaultHandling)
        {
            if (!string.IsNullOrEmpty(_token))
            {
                return new CustomResourceHandlerFactory(_token);
            }
            else return base.GetResourceRequestHandler(chromiumWebBrowser, browser, frame, request, isNavigation, isDownload, requestInitiator, ref disableDefaultHandling);
        }
    }

    internal class CustomResourceHandlerFactory : ResourceRequestHandler
    {
        private readonly string _token;

        public CustomResourceHandlerFactory(string token)
        {
            _token = token;
        }

        protected override CefReturnValue OnBeforeResourceLoad(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IRequestCallback callback)
        {
            if (!string.IsNullOrEmpty(_token))
            {
                var headers = request.Headers;
                headers["Authorization"] = $"Bearer {_token}";
                request.Headers = headers;
                return CefReturnValue.Continue;
            }
            else return base.OnBeforeResourceLoad(chromiumWebBrowser, browser, frame, request, callback);
        }
    }

In the Web App:

Client.razor - The razor page with the SyncFusion control on it. OnInitializedAsync is triggered twice due to RenderMode.ServerPrerendered. Once when the page is displayed in raw HTML while it is pre-rendering, and once when the SyncFusion control triggers the Blazor websocket request to render fully. This is by design. If the page's RenderMode is set to static, only the first OnInitializedAsync triggers, but the SyncFusion control never loads as Blazor does not get marked to run. If the page's RenderMode is set to server, the pre-rendering is not done, so the page "loads" slower for a user, and only the second OnInitializedAsync is triggered.

@page "/clients"
@attribute [Authorize]
@using Microsoft.AspNetCore.Routing
@using Microsoft.AspNetCore.Mvc.Localization
@using System.Text
@using OurWebApp.ViewModels
@using Syncfusion.Blazor.Grids
@using Syncfusion.Blazor.Navigations
@inject IApiConnector apiConnector
@inject NavigationManager navigationManager
@inject LinkGenerator linkGenerator

<div class="layout">
    <div id="list-container">
        <SfListView CssClass="e-list-template all" DataSource="@Clients">
            <ListViewFieldSettings Id="ID" Text="Name"></ListViewFieldSettings>
            <ListViewTemplates TValue="ClientViewModel">
                <Template>
                    @{
                        <div class="e-list-wrapper e-list-avatar e-list-multi-line">
                            <span class="e-avatar sf-icon-customer" style="font-size:16px; background:none;color:black"></span>
                            <span class="e-list-item-header">@context.Name</span>
                        </div>

                    }
                </Template>
            </ListViewTemplates>
        </SfListView>
    </div>
</div>

//Style
...

@code {

    IList<ClientViewModel> Clients = new IList<ClientViewModel>();

    protected async override Task OnInitializedAsync()
    {
        Clients = (await apiConnector.GetClients()).Clients.ToList() ?? new List<ClientViewModel>();
    }
}

ApiConnector.cs - This is constructed the moment a page that includes it is called. When the RenderMode is ServerPrerendered, it is also called twice (perhaps unnecessarily, but as the page is essentially reloaded by Blazor there's not much we can do about that I think). The first time, when the page is pre-rendered in HTML, the header for Authorization sent by CefSharp's OnBeforeResourceLoad is there. The second time, when the page is fully rendered by Blazor, the header is absent, and CefSharp triggers no events.

        public ApiConnector(IHttpContextAccessor context, IAuthenticationSchemeProvider authenticationSchemeProvider)
        {
            bool bearerAuthentication = context.HttpContext.Request.Headers.Any(h => h.Key.Equals("Authorization"));
            client.BaseAddress = new Uri(/*our API uri*/);
            if (bearerAuthentication)
            {
                //If a "Bearer" scheme token exists in the headers, use it for authenticating the API. Auth0 was skipped.
                client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.HttpContext.GetTokenAsync("Bearer", "access_token").Result);
            }
            else
            {
               //Use the token supplied by a standard Auth0 log-in to authenticate the API.
               client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", context.HttpContext.GetTokenAsync("access_token").Result);
            }
        }

CEF in general does not seem to have a way to handle websocket requests out of the box (https://magpcss.org/ceforum/viewtopic.php?f=6&t=16376), which is how Blazor tells the page to render itself once it is called through the SyncFusion control. Thus, the header is lost, and seemingly no events allow me to intercept this request client-side to re-supply the header. Said thread suggests a WebSocket proxy server, but CefServer is C++, not C#, and it looks to me like that's only helpful if you are the one hosting the WebSocket yourself, which isn't the case here I believe, unless I'm misunderstanding how sockets work?

Either way, my question is obvious: how do I re-supply the header to Blazor's websocket request so that our API can be authorized correctly? If that's not at all possible in any way, what could be some possible work-arounds? Anything involving saving the token or return value of the api between the initial HTML pre-rendering and the websocket Blazor rendering through some sort of service may certainly be possible, but that feels sketchy and makes it no longer stateless, which is less than ideal when dealing with api authentication tokens, no?

As an aside, this can be simulated in Chrome itself through the use of an add-on called ModHeader. Adding a token and setting the filtered page to only supply the header to "(our web app)/clients" causes the same thing to happen as described above. What's interesting here is that not filtering the page allows ModHeader to supply the headers to any request. Including Blazor's websocket request. So clearly, Chrome must have some way to create a header that is supplied no matter what so that this add-on functions the way it does, so the question is, does chromium do too? And if so, could CefSharp be extended to expose that functionality, if no workaround is deemed possible?

I wanted to sketch the full scope of what is happening in our project, but the above code is still heavily trimmed down. I hope I didn't miss anything necessary to understand the situation.

Senne
  • 21
  • 3
  • Do you need to use a header? Can you use a cookie instead? You can programatically add a cookie using http://cefsharp.github.io/api/81.3.x/html/M_CefSharp_ICookieManager_SetCookie.htm then instead of reading a header in your blazor code read the cookie value? – amaitland Jun 17 '20 at 20:10

0 Answers0