Might be trying to solve this the wrong way, but here's the situation.
I have a component designed to redirect to login if the user trying to access it isn't authenticated, and display not found if they aren't authorized for the page requested.
<AuthorizeViewWithPermissions RequiredPermission="RequiredPermission">
<Authorized>
@ChildContent
</Authorized>
<NotAuthenticated>
<LoginRedirect />
</NotAuthenticated>
<NotAuthorized>
<NotFoundRedirect />
</NotAuthorized>
</AuthorizeViewWithPermissions>
@code {
[Parameter] public RenderFragment ChildContent { get; set; }
[Parameter] public Permissions RequiredPermission { get; set; }
protected override void OnInitialized()
{
}
}
LoginRedirect is this:
public class LoginRedirect : ComponentBase
{
[Inject] protected NavigationManager NavigationManager { get; set; }
protected override void OnInitialized()
{
NavigationManager.NavigateTo("/Login", true);
}
}
internals of AuthorizeViewWithPermissions:
/// <summary>
/// Largely borrowed from the original AuthorizeView, but cut up a bit to use custom permissions and cut out a lot of stuff that isn't needed.
/// </summary>
public class AuthorizeViewWithPermissions : ComponentBase
{
private AuthenticationState _currentAuthenticationState;
private bool _isAuthorized;
private bool _isAuthenticated;
/// <summary>
/// The permission type required to display the content
/// </summary>
[Parameter] public Permissions RequiredPermission { get; set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> ChildContent { get; set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; set; }
/// <summary>
/// The content that will be displayed if the user is not authenticated.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> NotAuthenticated { get; set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// If you specify a value for this parameter, do not also specify a value for <see cref="ChildContent"/>.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> Authorized { get; set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter] public RenderFragment Authorizing { get; set; }
/// <summary>
/// The resource to which access is being controlled.
/// </summary>
[Parameter] public object Resource { get; set; }
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
// We're using the same sequence number for each of the content items here
// so that we can update existing instances if they are the same shape
if (_currentAuthenticationState == null)
{
builder.AddContent(0, Authorizing);
}
else if (_isAuthorized)
{
var authorized = Authorized ?? ChildContent;
builder.AddContent(0, authorized?.Invoke(_currentAuthenticationState));
}
else if (!_isAuthenticated)
{
builder.AddContent(0, NotAuthenticated?.Invoke(_currentAuthenticationState));
}
else
{
builder.AddContent(0, NotAuthorized?.Invoke(_currentAuthenticationState));
}
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
// with 'NotAuthorized' in other cases. Besides naming, they are equivalent. To avoid
// confusion, explicitly prevent the case where both are supplied.
if (ChildContent != null && Authorized != null)
{
throw new InvalidOperationException($"Do not specify both '{nameof(Authorized)}' and '{nameof(ChildContent)}'.");
}
if (AuthenticationState == null)
{
throw new InvalidOperationException($"Authorization requires a cascading parameter of type Task<{nameof(AuthenticationState)}>. Consider using {typeof(CascadingAuthenticationState).Name} to supply this.");
}
// First render in pending state
// If the task has already completed, this render will be skipped
_currentAuthenticationState = null;
// Then render in completed state
// Importantly, we *don't* call StateHasChanged between the following async steps,
// otherwise we'd display an incorrect UI state while waiting for IsAuthorizedAsync
_currentAuthenticationState = await AuthenticationState;
SetAuthorizedAndAuthenticated(_currentAuthenticationState.User);
}
private void SetAuthorizedAndAuthenticated(ClaimsPrincipal user)
{
var userWithData = SessionHelper.GetCurrentUser(user);
_isAuthenticated = userWithData != null;
_isAuthorized = userWithData?.Permissions.Any(p => p == RequiredPermission || p == Permissions.SuperAdmin) ?? false;
}
}
The authentication and authorization checks are working perfectly fine, but the issue is that the page OnInitializedAsync or OnParametersSetAsync fire before the LoginRedirect OnInitialized.
I'm kicking off my calls for data (to an API, that uses a token stored on the logged in user's data) in OnInitializedAsync which results in it attempting to load the data (with no auth token) instead of just redirecting. If I comment out the data call the redirect works as expected without issue, so it's just a timing/sequence of events issue.
Is there a solution to this? Should I just be changing my api client code to silently fail instead of throwing an unauthorized exception if the auth token is missing?
This is also my app.razor component:
CascadingAuthenticationState>
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)">
<NotAuthorized>
<LoginRedirect />
</NotAuthorized>
<Authorizing>
<p>Checking authorization...</p>
</Authorizing>
</AuthorizeRouteView>
</Found>
<NotFound>
<LayoutView Layout="@typeof(MainLayout)">
<p>These aren't the droids you're looking for.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>