Blazor.BFF.AzureB2C.Template
Blazor.BFF.AzureB2C.Template copied to clipboard
Unhandled exception when logging out after logging-in on multiple browser tabs
Hi,
First of all thanks for this fantastic resource!
I build a Blazor App using the template and can login/logout with my AD B2C tenant. When I open the App in a second tab, I am already signed in (as expected). If I now sign-out in either tab, and then click on the Direct-API link in the navbar on the other tab, I get an exception (in the Blazor Client) that looks like a malformed JSON object is being parsed.
This happens in Chrome and Safari on MacOS, but (curiously) not on Firefox, there I get redirected to sign in again (as expected).
I probably did something wrong, but I tried to stay as close to the template as possible. Anyway, I am totally baffled and stuck. Obviously I can give you access to my repository with the Blazor SPA. I can be reached by email on gmail or outlook by using firstname.lastname as name (to get the Client-Secret).
This is the exception I get:
crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100]
Unhandled exception rendering component: '<' is an invalid start of a value. Path: $ | LineNumber: 1 | BytePositionInLine: 0.
System.Text.Json.JsonException: '<' is an invalid start of a value. Path: $ | LineNumber: 1 | BytePositionInLine: 0.
---> System.Text.Json.JsonReaderException: '<' is an invalid start of a value. LineNumber: 1 | BytePositionInLine: 0.
at System.Text.Json.ThrowHelper.ThrowJsonReaderException(Utf8JsonReader& json, ExceptionResource resource, Byte nextByte, ReadOnlySpan`1 bytes)
at System.Text.Json.Utf8JsonReader.ConsumeValue(Byte marker)
at System.Text.Json.Utf8JsonReader.ReadFirstToken(Byte first)
at System.Text.Json.Utf8JsonReader.ReadSingleSegment()
at System.Text.Json.Utf8JsonReader.Read()
at System.Text.Json.Serialization.JsonConverter`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
--- End of inner exception stack trace ---
at System.Text.Json.ThrowHelper.ReThrowWithPath(ReadStack& state, JsonReaderException ex)
at System.Text.Json.Serialization.JsonConverter`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].ReadCore(Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[String[]](JsonConverter jsonConverter, Utf8JsonReader& reader, JsonSerializerOptions options, ReadStack& state)
at System.Text.Json.JsonSerializer.ReadCore[String[]](JsonReaderState& readerState, Boolean isFinalBlock, ReadOnlySpan`1 buffer, JsonSerializerOptions options, ReadStack& state, JsonConverter converterBase)
at System.Text.Json.JsonSerializer.ContinueDeserialize[String[]](ReadBufferState& bufferState, JsonReaderState& jsonReaderState, ReadStack& readStack, JsonConverter converter, JsonSerializerOptions options)
at System.Text.Json.JsonSerializer.<ReadAllAsync>d__65`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
at System.Net.Http.Json.HttpContentJsonExtensions.<ReadFromJsonAsyncCore>d__4`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
at System.Net.Http.Json.HttpClientJsonExtensions.<GetFromJsonAsyncCore>d__13`1[[System.String[], System.Private.CoreLib, Version=6.0.0.0, Culture=neutral, PublicKeyToken=7cec85d7bea7798e]].MoveNext()
at BlazorWasmAadB2cBffTest.Client.Pages.DirectApi.OnInitializedAsync()
at Microsoft.AspNetCore.Components.ComponentBase.RunInitAndSetParametersAsync()
at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle, ComponentState owningComponentState)
Thanks @hugh-maaskant
Will try to reproduce this
Hi @damienbod
Thanks for picking this up! If you want to use the App as-is, with the same Azure AD B2C directory, please send me a direct email using firstname.lastname at either gmail or outlook, and I will send you the ClientSecret
in a reply.
If there is anything else I can do to help, please let me know.
best regards, Hugh
Hi @damienbod
I finally had some more time to investigate an can now reproduce at will, also on Windows with Chrome.
To reproduce
- Start the server and log in to the auto-started browser window.
- Optional: check that both Direct API Call and Graph API Call work
- Go to Direct API Call page
- Open a new tab in the same browser window
- Click on "Login" => will automatically log you in there, as expected
- Optional: check both Direct API Call and Graph API Call work
- Log out from second browser tab
- Open the first browser tab, this is still on the Direct API call page
- Click on Graph API Call in the navigation pane => Exception
What I found
- As expected the https://localhost:5001/api/GraphApiCalls navigation from step 9. gets redirected to https://localhost:5001/Account/Login?ReturnUrl=%2Fapi%2FGraphApiCalls, which gets a 200 Status Code
- The redirected page loads, but it appears to receive the complete error page instead of the expected Graph data.
What I did
Because the Exception said '<' is an invalid start of a value
, I made the following change in OnInitializedAsync()
on the GraphApiCall.razor
page:
// apiData = await client.GetFromJsonAsync<string[]>("api/GraphApiCalls");
// Read the response body as string i.s.o. as JSON
string asString = await client.GetStringAsync("api/GraphApiCalls");
if (string.IsNullOrEmpty(asString))
{
apiData = new[] { "No data available ..." };
}
else
{
apiData = new[] { asString };
}
Now I got to see the response, which was:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <title>Blazor Azure B2C Cookie</title> <base href="/" /> <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" /> <link href="css/app.css" rel="stylesheet" /> <link href="BlazorWasmAadB2cBffTest.Client.styles.css" rel="stylesheet" /> <link href="manifest.json" rel="manifest" /> <link rel="apple-touch-icon" sizes="512x512" href="icon-512.png" /> </head> <body> <div id="app"> <div class="spinner d-flex align-items-center justify-content-center spinner"> <div class="spinner-border text-success" role="status"> <span class="sr-only">Loading...</span> </div> </div> </div> <div id="blazor-error-ui"> An unhandled exception has occurred. See browser dev tools for details. <a href="" class="reload">Reload</a> <a class="dismiss">🗙</a> </div> <script src="_framework/blazor.webassembly.js" ></script> <script src="antiForgeryToken.js" ></script> <input name="__RequestVerificationToken" type="hidden" value="CfDJ8CKAdUUR8h1JlrBgWAhpcgI2sPAHonslXh0ynV70w_ggvUNyKbtYkQcHK3xDDZp2IFO5QLuujZ4RA5p6wU4C4uygZ9EBV3DsOGl6Tlx9YmW0tlLwxjxDqhvZ7GyRr0YHwpK-f4URk-ufX521IRrMB_Y" /> </body> </html>
In other words: the Blazor Exception page.
This explains the '<' is an invalid start of a value
as happens with the original code, but it does not explain (nor do I understand) why the Blazor Exception page was returned on the redirected Graph call.
Hope this helps ...
Please let me know if I can be of further assistance :-)
rgds, Hugh
P.S. These are the screenshots of the redirect and the resulting page load with the Chrome Dev Tools' network tab open:
@hugh-maaskant
Thanks for the feedback, some small error handling in the code like you added would help I think
Would you like to do a PR? If not I will add this.
Greetings and thanks Damien
@damienbod
PR
Thanks, feel free to add if you wish, no need to file a PR for such a small thing I believe.
I Found the problem + solution !
Had a brainwave: the problem is the return URL, neither https://localhost:5001/directapi
nor https://localhost:5001/graphapicall
was registered in Azure AD B2C as a valid return URL for the App. If I do that, everything works and behaves as expected.
So it was my problem, not that of the template.
Damien, if you concur with the solution feel free to close this Issue.
Thoughts on a better solution
While adding all possible navigations within an App as valid return URLs is an option, -imho- it is very brittle. It would be easy to forget one, especially with updates and upgrades of the App. Also, there is a hard limit of 100 such URLs per App.
My initial feeling is that the solution should be in the frontend, e.g. by wrapping the Navigation Manger and catching the error there. Or possibly in the BFF backend if that is possible. I would have to think a little longer about that (wanted to get the solution mentioned here first).
If you take a look at https://docs.microsoft.com/en-us/azure/active-directory/develop/reply-url it suggest (at the very end) to use a State parameter if there are multiple subdomains.
Use a state parameter
If you have several subdomains and your scenario requires that, upon successful authentication, you redirect users to >the same page from which they started, using a state parameter might be helpful.
Maybe this could be intercepted and then used for the proper navigation.
Any suggestion welcome, meanwhile I will think about it some more / experiment. Any results will be published in this conversation thread.