After successful login (JWT-based authentication stored in protectedLocalStorage
), I want to redirect to ReturnUrl
if exists.
When the ReturnUrl
-destination page- has the Authorize
attribute & GetAuthenticationStateAsync()
is executed, protectedLocalStorage
becomes null
after NavigationManager.NavigateTo()
is executed.
This is the relevant code for login page
await protectedLocalStorage.SetAsync("authToken", token);
NavigationManager.NavigateTo(ReturnUrl ?? "");
And I'm trying to get protectedLocalStorage
at GetAuthenticationStateAsync()
like this
ProtectedBrowserStorageResult<string> result;
try
{
result = await protectedLocalStorage.GetAsync<string>("authToken"); //protectedLocalStorage is null after the redirect
}
catch
{
result = new();
}
var anonymous = new ClaimsPrincipal(new ClaimsIdentity());
if (!result.Success) //Since protectedLocalStorage is null I get redirected back to login page.
{
return new AuthenticationState(anonymous);
}
I disabled the prerender at App.razor & it's working fine except when trying to redirect to an authorized component
<Routes @rendermode="@RenderModeForPage"/>
@code {
[CascadingParameter] private HttpContext HttpContext { get; set; } = default!;
private IComponentRenderMode? RenderModeForPage => HttpContext.Request.Path.StartsWithSegments("/Account/Login") ? null : new InteractiveServerRenderMode(prerender: false);
//I added this because when login page's render mode is interactive server, it keeps reloading indefinitely
}
The login page keeps reloading as mentioned in the above comment due to this code in AccountLayout
protected override void OnParametersSet()
{
if (HttpContext is null)
{
// If this code runs, we're currently rendering in interactive mode, so there is no HttpContext.
// The identity pages need to set cookies, so they require an HttpContext. To achieve this we
// must transition back from interactive mode to a server-rendered page.
NavigationManager.Refresh(forceReload: true);
}
}
If I navigate to the authorized component normally by clicking on a link or typing the url -after login- it's working properly. This is only happening after using NavigationManager.NavigateTo()
function.
It looks like the NavigateTo()
function calls GetAuthenticationStateAsync()
too early in the Blazor life cycle that the protectedLocalStorage
is not ready yet.
I think the issue is due to login page render mode, so how to make login page's render mode interactive server & stop the reloading or how to access protectedLocalStorage
properly while it's static rendering?
I managed to achieve what I want but I consider this a workaround more than a robust solution. This seems like a known issue.
Firstly, I updated the code at App.razor to the following
private IComponentRenderMode? RenderModeForPage => new InteractiveServerRenderMode(prerender: false);
//all pages will run in interactive server
Now, the login page will keep reloading indefinitely, so I updated the AccountLayout.razor like this in order to control the continuous reload
@if (HttpContext is null && !IsInteractive)
{
<p>@localizer["Loading"]</p>
}
else
{
@Body
}
@code {
[CascadingParameter] private HttpContext? HttpContext { get; set; }
private bool IsInteractive
{
get
{
return NavigationManager.Uri.Contains("interactive=true") || NavigationManager.Uri.Contains("interactive%3Dtrue");
}
}
protected override void OnParametersSet()
{
if (HttpContext is null && !IsInteractive)
{
NavigationManager.NavigateTo($"{NavigationManager.Uri}?interactive=true", forceLoad: true);
}
}
}
By adding a query string, I'm able to stop the reload.
Now, I thought I would be able to access the protectedLocalStorage
after the successful login but I still find it equals null
at GetAuthenticationStateAsync()
.
So, I added a new razor component RedirectComponent.razor & redirected from login.razor to it, then redirect from RedirectComponent.razor to the target ReturnUrl
OnAfterRenderAsync
@page "/RedirectComponent"
@inject NavigationManager NavigationManager
@inject IStringLocalizer<Resource> localizer
<p>@localizer["Loading"]</p>
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var uri = new Uri(NavigationManager.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
var returnUrl = query["ReturnUrl"];
StateHasChanged();
NavigationManager.NavigateTo(returnUrl ?? "Account/Login", replace: true); //to prevent the page from registering in the browser history
}
}
}
This is login.razor code after successful login
var uri = new Uri(NavigationManager.Uri);
var query = System.Web.HttpUtility.ParseQueryString(uri.Query);
ReturnUrl = query["ReturnUrl"];
ReturnUrl = !string.IsNullOrEmpty(ReturnUrl) && ReturnUrl.Contains("?") ? ReturnUrl.Split("?")[0] : ReturnUrl;
StateHasChanged();
NavigationManager.NavigateTo("RedirectComponent?ReturnUrl=" + ReturnUrl ?? "", forceLoad: true);
Now, it's working as intended but I'm not satisfied with all these workarounds. I searched a lot but couldn't find a straightforward solution even though this is a common case when using a third-party API to authenticate.