Search code examples
blazorprogressive-web-appsopenid-connectwebassembly

How do I debug OIDC authentication issues in Blazor WebAssembly PWA / SPA?


I have built an OpenID Connect specification compliant Identity Provider authentication server. It is deployed and working well as an IP against relying parties, etc. My goal now is to create a Blazor WebAssembly Progressive Web App (or Single Page App depending on your preferred nomenclature) that uses my server for authentication.

Luckily for me Microsoft has officially released support for this scenario. So I built a simple reference implementation primarily following the guidance found here: https://learn.microsoft.com/en-us/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-7.0&source=recommendations&tabs=visual-studio.

Between having control over the server-side and the wonderful Developer Tools built into browsers (Chrome, Edge, etc.) I have quite a lot of helpful visibility into the system for troubleshooting.

However, I reached a point where I see my server returning a valid JWT from the token endpoint and the Blazor client fails almost silently, simply telling me "There was an error signing in".

I have tried everything I've been able to find to get some clues. I've attempted to enable additional logging, I've attempted to trace into the code. I've tried all the suggestions found on these pages:

And more. I'm pulling my hair out on this. Does anyone have any wisdom to offer on how to troubleshoot / debug into this mass of C# interoping with obfuscated JavaScript?


Solution

  • Well, I ended up finding a solution, so I thought I'd share. The approach I took was to replace the auto-generated, obfuscated AuthenticationService.js file provided by Microsoft with a copy that is under my control, which I then instrumented with all the logging I needed.

    This is essentially an aproach I had already tried using a heavy-weight process described in one of the links I included in the question above. What I figured out is that I didn't need to plug in a complicated "client assets" solution, I could simply drop the already formed JavaScript file into my project. Here are the steps for what I did:

    1. Load your PWA in the browser (I was using Edge at the time)
    2. Open the browser "Developer Tools"
    3. Select the "Sources" tab
    4. Find the "_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication" folder in "Sources"
    5. Select the "AuthenticationService.js" file in this folder
    6. Select the "Pretty print" option (a button along the bottom of the browser with "{}" on it)
    7. Copy the resulting pretty printed JavaScript
    8. In your PWA project create your own AuthenticationService.js file (I put mine in a folder called "src")
    9. In the "index.html" file of your PWA find this line: <script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
    10. Comment that line out and replace it with a line that references your copy of "AuthenticationService.js". For me it looks like this:
    <!--<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>-->
    <script src="src/AuthenticationService.js"></script>
    

    Your PWA is now using exactly the same JavaScript that it was before, but it is now under your control.

    For what it's worth, by adding lots of logging, I traced my specific problem to a quark with Microsoft's implementation that causes it to not know where to look for the returned token. The fix was simple. In my Blazor project Program.cs file I added the line options.ProviderOptions.ResponseMode = "query"; to my configuration. Now it looks something like this:

    builder.Services.AddOidcAuthentication(options => {
    
        // Configure our OIDC Identity Provider (for more information, see https://aka.ms/blazor-standalone-auth)
        options.ProviderOptions.Authority = "{AUTHORITY_URL}";
        options.ProviderOptions.ClientId = "{OIDC_CLIENT_ID}";
        options.ProviderOptions.ResponseMode = "query";
    
        . . .