// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.Extensions.Logging;
using static Microsoft.AspNetCore.Internal.LinkerFlags;
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication;
/// <summary>
/// A component that handles remote authentication operations in an application.
/// </summary>
/// <typeparam name="TAuthenticationState">The user state type persisted while the operation is in progress. It must be serializable.</typeparam>
public partial class RemoteAuthenticatorViewCore<[DynamicallyAccessedMembers(JsonSerialized)] TAuthenticationState> : ComponentBase where TAuthenticationState : RemoteAuthenticationState
{
private RemoteAuthenticationApplicationPathsOptions? _applicationPaths;
private string? _action;
private string? _lastHandledAction;
private InteractiveRequestOptions? _cachedRequest;
private static readonly NavigationOptions AuthenticationNavigationOptions =
new() { ReplaceHistoryEntry = true, ForceLoad = false };
/// <summary>
/// Gets or sets the <see cref="RemoteAuthenticationActions"/> action the component needs to handle.
/// </summary>
[Parameter] public string? Action { get => _action; set => _action = value?.ToLowerInvariant(); }
/// <summary>
/// Gets or sets the <typeparamref name="TAuthenticationState"/> instance to be preserved during the authentication operation.
/// </summary>
[Parameter] public TAuthenticationState AuthenticationState { get; set; } = default!;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogIn"/> is being handled.
/// </summary>
[Parameter] public RenderFragment? LoggingIn { get; set; } = DefaultLogInFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.Register"/> is being handled.
/// </summary>
[Parameter] public RenderFragment? Registering { get; set; }
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.Profile"/> is being handled.
/// </summary>
[Parameter] public RenderFragment? UserProfile { get; set; }
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogInCallback"/> is being handled.
/// </summary>
[Parameter] public RenderFragment CompletingLoggingIn { get; set; } = DefaultLogInCallbackFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogInFailed"/> is being handled.
/// </summary>
[Parameter] public RenderFragment<string?> LogInFailed { get; set; } = DefaultLogInFailedFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOut"/> is being handled.
/// </summary>
[Parameter] public RenderFragment LogOut { get; set; } = DefaultLogOutFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutCallback"/> is being handled.
/// </summary>
[Parameter] public RenderFragment CompletingLogOut { get; set; } = DefaultLogOutCallbackFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutFailed"/> is being handled.
/// </summary>
[Parameter] public RenderFragment<string?> LogOutFailed { get; set; } = DefaultLogOutFailedFragment;
/// <summary>
/// Gets or sets a <see cref="RenderFragment"/> with the UI to display while <see cref="RemoteAuthenticationActions.LogOutSucceeded"/> is being handled.
/// </summary>
[Parameter] public RenderFragment LogOutSucceeded { get; set; } = DefaultLoggedOutFragment;
/// <summary>
/// Gets or sets an event callback that will be invoked with the stored authentication state when a log in operation succeeds.
/// </summary>
[Parameter] public EventCallback<TAuthenticationState> OnLogInSucceeded { get; set; }
/// <summary>
/// Gets or sets an event callback that will be invoked with the stored authentication state when a log out operation succeeds.
/// </summary>
[Parameter] public EventCallback<TAuthenticationState> OnLogOutSucceeded { get; set; }
/// <summary>
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/> with the paths to different authentication pages.
/// </summary>
[Parameter]
public RemoteAuthenticationApplicationPathsOptions ApplicationPaths
{
get => _applicationPaths ?? RemoteApplicationPathsProvider.ApplicationPaths;
set => _applicationPaths = value!;
}
[Inject] internal NavigationManager Navigation { get; set; } = default!;
[Inject] internal IRemoteAuthenticationService<TAuthenticationState> AuthenticationService { get; set; } = default!;
[Inject] internal IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; } = default!;
[Inject] internal AuthenticationStateProvider AuthenticationProvider { get; set; } = default!;
[Inject] internal ILogger<RemoteAuthenticatorViewCore<TAuthenticationState>> Logger { get; set; } = default!;
/// <inheritdoc />
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
switch (Action)
{
case RemoteAuthenticationActions.Profile:
builder.AddContent(0, UserProfile);
break;
case RemoteAuthenticationActions.Register:
builder.AddContent(0, Registering);
break;
case RemoteAuthenticationActions.LogIn:
builder.AddContent(0, LoggingIn);
break;
case RemoteAuthenticationActions.LogInCallback:
builder.AddContent(0, CompletingLoggingIn);
break;
case RemoteAuthenticationActions.LogInFailed:
builder.AddContent(0, LogInFailed(Navigation.HistoryEntryState));
break;
case RemoteAuthenticationActions.LogOut:
builder.AddContent(0, LogOut);
break;
case RemoteAuthenticationActions.LogOutCallback:
builder.AddContent(0, CompletingLogOut);
break;
case RemoteAuthenticationActions.LogOutFailed:
builder.AddContent(0, LogOutFailed(Navigation.HistoryEntryState));
break;
case RemoteAuthenticationActions.LogOutSucceeded:
builder.AddContent(0, LogOutSucceeded);
break;
default:
throw new InvalidOperationException($"Invalid action '{Action}'.");
}
}
/// <inheritdoc />
protected override async Task OnParametersSetAsync()
{
if (_lastHandledAction == Action)
{
// Avoid processing the same action more than once.
return;
}
_lastHandledAction = Action;
Log.ProcessingAuthenticatorAction(Logger, Action);
switch (Action)
{
case RemoteAuthenticationActions.LogIn:
await ProcessLogIn(GetReturnUrl(state: null));
break;
case RemoteAuthenticationActions.LogInCallback:
await ProcessLogInCallback();
break;
case RemoteAuthenticationActions.LogInFailed:
break;
case RemoteAuthenticationActions.Profile:
if (ApplicationPaths.RemoteProfilePath == null)
{
UserProfile ??= ProfileNotSupportedFragment;
}
else
{
UserProfile ??= LoggingIn;
RedirectToProfile();
}
break;
case RemoteAuthenticationActions.Register:
if (ApplicationPaths.RemoteRegisterPath == null)
{
Registering ??= RegisterNotSupportedFragment;
}
else
{
Registering ??= LoggingIn;
RedirectToRegister();
}
break;
case RemoteAuthenticationActions.LogOut:
await ProcessLogOut(GetReturnUrl(state: null, ApplicationPaths.LogOutSucceededPath));
break;
case RemoteAuthenticationActions.LogOutCallback:
await ProcessLogOutCallback();
break;
case RemoteAuthenticationActions.LogOutFailed:
break;
case RemoteAuthenticationActions.LogOutSucceeded:
break;
default:
throw new InvalidOperationException($"Invalid action '{Action}'.");
}
}
private async Task ProcessLogIn(string returnUrl)
{
AuthenticationState.ReturnUrl = returnUrl;
var interactiveRequest = GetCachedNavigationState();
var result = await AuthenticationService.SignInAsync(new RemoteAuthenticationContext<TAuthenticationState>
{
State = AuthenticationState,
InteractiveRequest = interactiveRequest
});
switch (result.Status)
{
case RemoteAuthenticationStatus.Redirect:
Log.LoginRequiresRedirect(Logger);
break;
case RemoteAuthenticationStatus.Success:
Log.LoginCompletedSuccessfully(Logger);
if (OnLogInSucceeded.HasDelegate)
{
Log.InvokingLoginCompletedCallback(Logger);
await OnLogInSucceeded.InvokeAsync(result.State);
}
var redirectUrl = GetReturnUrl(result.State, returnUrl);
Log.NavigatingToUrl(Logger, redirectUrl);
Navigation.NavigateTo(redirectUrl, AuthenticationNavigationOptions);
break;
case RemoteAuthenticationStatus.Failure:
Log.LoginFailed(Logger, result.ErrorMessage!);
Log.NavigatingToUrl(Logger, ApplicationPaths.LogInFailedPath);
Navigation.NavigateTo(ApplicationPaths.LogInFailedPath, AuthenticationNavigationOptions with { HistoryEntryState = result.ErrorMessage });
break;
case RemoteAuthenticationStatus.OperationCompleted:
default:
throw new InvalidOperationException($"Invalid authentication result status '{result.Status}'.");
}
}
private async Task ProcessLogInCallback()
{
var result = await AuthenticationService.CompleteSignInAsync(
new RemoteAuthenticationContext<TAuthenticationState> { Url = Navigation.Uri });
switch (result.Status)
{
case RemoteAuthenticationStatus.Redirect:
// There should not be any redirects as the only time CompleteSignInAsync finishes
// is when we are doing a redirect sign in flow.
throw new InvalidOperationException("Should not redirect.");
case RemoteAuthenticationStatus.Success:
Log.LoginRedirectCompletedSuccessfully(Logger);
if (OnLogInSucceeded.HasDelegate)
{
Log.InvokingLoginCompletedCallback(Logger);
await OnLogInSucceeded.InvokeAsync(result.State);
}
var redirectUrl = GetReturnUrl(result.State);
Log.NavigatingToUrl(Logger, redirectUrl);
Navigation.NavigateTo(redirectUrl, AuthenticationNavigationOptions);
break;
case RemoteAuthenticationStatus.OperationCompleted:
break;
case RemoteAuthenticationStatus.Failure:
Log.LoginCallbackFailed(Logger, result.ErrorMessage!);
Log.NavigatingToUrl(Logger, ApplicationPaths.LogInFailedPath);
Navigation.NavigateTo(
ApplicationPaths.LogInFailedPath,
AuthenticationNavigationOptions with { HistoryEntryState = result.ErrorMessage });
break;
default:
throw new InvalidOperationException($"Invalid authentication result status '{result.Status}'.");
}
}
private async Task ProcessLogOut(string returnUrl)
{
if (Navigation.HistoryEntryState != null && !ValidateSignOutRequestState())
{
Log.LogoutOperationInitiatedExternally(Logger);
Navigation.NavigateTo(ApplicationPaths.LogOutFailedPath, AuthenticationNavigationOptions with { HistoryEntryState = "The logout was not initiated from within the page." });
return;
}
AuthenticationState.ReturnUrl = returnUrl;
var state = await AuthenticationProvider.GetAuthenticationStateAsync();
var isauthenticated = state.User.Identity?.IsAuthenticated ?? false;
if (isauthenticated)
{
var interactiveRequest = GetCachedNavigationState();
var result = await AuthenticationService.SignOutAsync(new RemoteAuthenticationContext<TAuthenticationState>
{
State = AuthenticationState,
InteractiveRequest = interactiveRequest
});
switch (result.Status)
{
case RemoteAuthenticationStatus.Redirect:
Log.LogoutRequiresRedirect(Logger);
break;
case RemoteAuthenticationStatus.Success:
Log.LogoutCompletedSuccessfully(Logger);
if (OnLogOutSucceeded.HasDelegate)
{
Log.InvokingLogoutCompletedCallback(Logger);
await OnLogOutSucceeded.InvokeAsync(result.State);
}
Log.NavigatingToUrl(Logger, returnUrl);
Navigation.NavigateTo(returnUrl, AuthenticationNavigationOptions);
break;
case RemoteAuthenticationStatus.OperationCompleted:
break;
case RemoteAuthenticationStatus.Failure:
Log.LogoutFailed(Logger, result.ErrorMessage!);
Log.NavigatingToUrl(Logger, ApplicationPaths.LogOutFailedPath);
Navigation.NavigateTo(ApplicationPaths.LogOutFailedPath, AuthenticationNavigationOptions with { HistoryEntryState = result.ErrorMessage });
break;
default:
throw new InvalidOperationException($"Invalid authentication result status.");
}
}
else
{
Log.NavigatingToUrl(Logger, returnUrl);
Navigation.NavigateTo(returnUrl, AuthenticationNavigationOptions);
}
}
private async Task ProcessLogOutCallback()
{
var result = await AuthenticationService.CompleteSignOutAsync(new RemoteAuthenticationContext<TAuthenticationState> { Url = Navigation.Uri });
switch (result.Status)
{
case RemoteAuthenticationStatus.Redirect:
// There should not be any redirects as the only time completeAuthentication finishes
// is when we are doing a redirect sign in flow.
throw new InvalidOperationException("Should not redirect.");
case RemoteAuthenticationStatus.Success:
Log.LogoutRedirectCompletedSuccessfully(Logger);
if (OnLogOutSucceeded.HasDelegate)
{
Log.InvokingLogoutCompletedCallback(Logger);
await OnLogOutSucceeded.InvokeAsync(result.State);
}
var redirectUrl = GetReturnUrl(result.State, ApplicationPaths.LogOutSucceededPath);
Log.NavigatingToUrl(Logger, redirectUrl);
Navigation.NavigateTo(redirectUrl, AuthenticationNavigationOptions);
break;
case RemoteAuthenticationStatus.OperationCompleted:
break;
case RemoteAuthenticationStatus.Failure:
Log.LogoutCallbackFailed(Logger, result.ErrorMessage!);
Navigation.NavigateTo(ApplicationPaths.LogOutFailedPath, AuthenticationNavigationOptions with { HistoryEntryState = result.ErrorMessage });
break;
default:
throw new InvalidOperationException($"Invalid authentication result status.");
}
}
private string GetReturnUrl(TAuthenticationState? state, string? defaultReturnUrl = null)
{
if (state?.ReturnUrl != null)
{
return state.ReturnUrl;
}
var fromNavigationState = GetCachedNavigationState()?.ReturnUrl;
return fromNavigationState ?? defaultReturnUrl ?? Navigation.BaseUri;
}
private bool ValidateSignOutRequestState()
{
return GetCachedNavigationState()?.Interaction == InteractionType.SignOut;
}
private InteractiveRequestOptions? GetCachedNavigationState()
{
if (_cachedRequest != null)
{
return _cachedRequest;
}
if (string.IsNullOrEmpty(Navigation.HistoryEntryState))
{
return null;
}
_cachedRequest = InteractiveRequestOptions.FromState(Navigation.HistoryEntryState);
return _cachedRequest;
}
private void RedirectToRegister()
{
var loginUrl = Navigation.ToAbsoluteUri(ApplicationPaths.LogInPath).PathAndQuery;
var registerUrl = Navigation.ToAbsoluteUri(ApplicationPaths.RemoteRegisterPath).AbsoluteUri;
var navigationUrl = Navigation.GetUriWithQueryParameters(
registerUrl,
new Dictionary<string, object?> { ["returnUrl"] = loginUrl });
Navigation.NavigateTo(navigationUrl, AuthenticationNavigationOptions with
{
ForceLoad = true,
});
}
private void RedirectToProfile() =>
Navigation.NavigateTo(Navigation.ToAbsoluteUri(ApplicationPaths.RemoteProfilePath).AbsoluteUri, new NavigationOptions { ReplaceHistoryEntry = true, ForceLoad = true });
private static void DefaultLogInFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Checking login state...");
builder.CloseElement();
}
private static void RegisterNotSupportedFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Registration is not supported.");
builder.CloseElement();
}
private static void ProfileNotSupportedFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Editing the profile is not supported.");
builder.CloseElement();
}
private static void DefaultLogInCallbackFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Completing login...");
builder.CloseElement();
}
private static RenderFragment DefaultLogInFailedFragment(string? message)
{
return builder =>
{
builder.OpenElement(0, "p");
builder.AddContent(1, "There was an error trying to log you in: '");
builder.AddContent(2, message);
builder.AddContent(3, "'");
builder.CloseElement();
};
}
private static void DefaultLogOutFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Processing logout...");
builder.CloseElement();
}
private static void DefaultLogOutCallbackFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "Processing logout callback...");
builder.CloseElement();
}
private static RenderFragment DefaultLogOutFailedFragment(string? message)
{
return builder =>
{
builder.OpenElement(0, "p");
builder.AddContent(1, "There was an error trying to log you out: '");
builder.AddContent(2, message);
builder.AddContent(3, "'");
builder.CloseElement();
};
}
private static void DefaultLoggedOutFragment(RenderTreeBuilder builder)
{
builder.OpenElement(0, "p");
builder.AddContent(1, "You are logged out.");
builder.CloseElement();
}
}