Search code examples
c#authenticationhttp-redirectblazorblazor-server-side

How to properly access ProtectedLocalStorage after login using NavigationManager.NavigateTo()


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?


Solution

  • 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.