| File: Services\RemoteAuthenticationService.cs | Web Access |
| Project: src\aspnetcore\src\Components\WebAssembly\WebAssembly.Authentication\src\Microsoft.AspNetCore.Components.WebAssembly.Authentication.csproj (Microsoft.AspNetCore.Components.WebAssembly.Authentication) |
// 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 System.Security.Claims; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.JSInterop; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication; /// <summary> /// The default implementation for <see cref="IRemoteAuthenticationService{TRemoteAuthenticationState}"/> that uses JS interop to authenticate the user. /// </summary> /// <typeparam name="TRemoteAuthenticationState">The state to preserve across authentication operations.</typeparam> /// <typeparam name="TAccount">The type of the <see cref="RemoteUserAccount" />.</typeparam> /// <typeparam name="TProviderOptions">The options to be passed down to the underlying JavaScript library handling the authentication operations.</typeparam> public class RemoteAuthenticationService< [DynamicallyAccessedMembers(JsonSerialized)] TRemoteAuthenticationState, [DynamicallyAccessedMembers(JsonSerialized)] TAccount, [DynamicallyAccessedMembers(JsonSerialized)] TProviderOptions> : AuthenticationStateProvider, IRemoteAuthenticationService<TRemoteAuthenticationState>, IAccessTokenProvider where TRemoteAuthenticationState : RemoteAuthenticationState where TProviderOptions : new() where TAccount : RemoteUserAccount { private static readonly TimeSpan _userCacheRefreshInterval = TimeSpan.FromSeconds(60); private bool _initialized; private readonly RemoteAuthenticationServiceJavaScriptLoggingOptions _loggingOptions; // This defaults to 1/1/1970 private DateTimeOffset _userLastCheck = DateTimeOffset.FromUnixTimeSeconds(0); private ClaimsPrincipal _cachedUser = new ClaimsPrincipal(new ClaimsIdentity()); /// <summary> /// Gets the <see cref="IJSRuntime"/> to use for performing JavaScript interop operations. /// </summary> protected IJSRuntime JsRuntime { get; } /// <summary> /// Gets the <see cref="NavigationManager"/> used to compute absolute urls. /// </summary> protected NavigationManager Navigation { get; } /// <summary> /// Gets the <see cref="AccountClaimsPrincipalFactory{TAccount}"/> to map accounts to <see cref="ClaimsPrincipal"/>. /// </summary> protected AccountClaimsPrincipalFactory<TAccount> AccountClaimsPrincipalFactory { get; } /// <summary> /// Gets the options for the underlying JavaScript library handling the authentication operations. /// </summary> protected RemoteAuthenticationOptions<TProviderOptions> Options { get; } /// <summary> /// Initializes a new instance. /// </summary> /// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param> /// <param name="options">The options to be passed down to the underlying JavaScript library handling the authentication operations.</param> /// <param name="navigation">The <see cref="NavigationManager"/> used to generate URLs.</param> /// <param name="accountClaimsPrincipalFactory">The <see cref="AccountClaimsPrincipalFactory{TAccount}"/> used to generate the <see cref="ClaimsPrincipal"/> for the user.</param> /// <param name="logger">The logger to use for login authentication operations.</param> public RemoteAuthenticationService( IJSRuntime jsRuntime, IOptionsSnapshot<RemoteAuthenticationOptions<TProviderOptions>> options, NavigationManager navigation, AccountClaimsPrincipalFactory<TAccount> accountClaimsPrincipalFactory, ILogger<RemoteAuthenticationService<TRemoteAuthenticationState, TAccount, TProviderOptions>>? logger) { JsRuntime = jsRuntime; Navigation = navigation; AccountClaimsPrincipalFactory = accountClaimsPrincipalFactory; Options = options.Value; _loggingOptions = new RemoteAuthenticationServiceJavaScriptLoggingOptions { DebugEnabled = logger?.IsEnabled(LogLevel.Debug) ?? false, TraceEnabled = logger?.IsEnabled(LogLevel.Trace) ?? false }; } /// <inheritdoc /> public override async Task<AuthenticationState> GetAuthenticationStateAsync() => new AuthenticationState(await GetUser(useCache: true)); /// <inheritdoc /> public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignInAsync( RemoteAuthenticationContext<TRemoteAuthenticationState> context) { await EnsureAuthService(); var result = await JSInvokeWithContextAsync<RemoteAuthenticationContext<TRemoteAuthenticationState>, RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context); await UpdateUserOnSuccess(result); return result; } /// <inheritdoc /> public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignInAsync( RemoteAuthenticationContext<TRemoteAuthenticationState> context) { await EnsureAuthService(); var result = await JsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignIn", context.Url); await UpdateUserOnSuccess(result); return result; } /// <inheritdoc /> public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> SignOutAsync( RemoteAuthenticationContext<TRemoteAuthenticationState> context) { await EnsureAuthService(); var result = await JSInvokeWithContextAsync<RemoteAuthenticationContext<TRemoteAuthenticationState>, RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context); await UpdateUserOnSuccess(result); return result; } /// <inheritdoc /> public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>> CompleteSignOutAsync( RemoteAuthenticationContext<TRemoteAuthenticationState> context) { await EnsureAuthService(); var result = await JsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.completeSignOut", context.Url); await UpdateUserOnSuccess(result); return result; } /// <inheritdoc /> public virtual async ValueTask<AccessTokenResult> RequestAccessToken() { await EnsureAuthService(); var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken"); return new AccessTokenResult( result.Status, result.Token, result.Status == AccessTokenResultStatus.RequiresRedirect ? Options.AuthenticationPaths.LogInPath : null, result.Status == AccessTokenResultStatus.RequiresRedirect ? new InteractiveRequestOptions { Interaction = InteractionType.GetToken, ReturnUrl = GetReturnUrl(null) } : null); } /// <inheritdoc /> [DynamicDependency(JsonSerialized, typeof(AccessToken))] [DynamicDependency(JsonSerialized, typeof(AccessTokenRequestOptions))] public virtual async ValueTask<AccessTokenResult> RequestAccessToken(AccessTokenRequestOptions options) { ArgumentNullException.ThrowIfNull(options); await EnsureAuthService(); var result = await JsRuntime.InvokeAsync<InternalAccessTokenResult>("AuthenticationService.getAccessToken", options); return new AccessTokenResult( result.Status, result.Token, result.Status == AccessTokenResultStatus.RequiresRedirect ? Options.AuthenticationPaths.LogInPath : null, result.Status == AccessTokenResultStatus.RequiresRedirect ? new InteractiveRequestOptions { Interaction = InteractionType.GetToken, ReturnUrl = GetReturnUrl(options.ReturnUrl), Scopes = options.Scopes ?? Array.Empty<string>(), } : null); } // JSRuntime.InvokeAsync does not properly annotate all arguments with DynamicallyAccessedMembersAttribute. https://github.com/dotnet/aspnetcore/issues/39839 // Calling JsRuntime.InvokeAsync directly results allows the RemoteAuthenticationContext.State getter to be trimmed. https://github.com/dotnet/aspnetcore/issues/49956 private ValueTask<TResult> JSInvokeWithContextAsync<[DynamicallyAccessedMembers(JsonSerialized)] TContext, [DynamicallyAccessedMembers(JsonSerialized)] TResult>( string identifier, TContext context) => JsRuntime.InvokeAsync<TResult>(identifier, context); private string GetReturnUrl(string? customReturnUrl) => customReturnUrl != null ? Navigation.ToAbsoluteUri(customReturnUrl).AbsoluteUri : Navigation.Uri; private async Task<ClaimsPrincipal> GetUser(bool useCache = false) { var now = DateTimeOffset.Now; if (useCache && now < _userLastCheck + _userCacheRefreshInterval) { return _cachedUser; } _cachedUser = await GetAuthenticatedUser(); _userLastCheck = now; return _cachedUser; } /// <summary> /// Gets the current authenticated used using JavaScript interop. /// </summary> /// <returns>A <see cref="Task{ClaimsPrincipal}"/>that will return the current authenticated user when completes.</returns> protected internal virtual async ValueTask<ClaimsPrincipal> GetAuthenticatedUser() { await EnsureAuthService(); var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser"); var user = await AccountClaimsPrincipalFactory.CreateUserAsync(account, Options.UserOptions); return user; } [DynamicDependency(JsonSerialized, typeof(RemoteAuthenticationServiceJavaScriptLoggingOptions))] private async ValueTask EnsureAuthService() { if (!_initialized) { await JsRuntime.InvokeVoidAsync("AuthenticationService.init", Options.ProviderOptions, _loggingOptions); _initialized = true; } } private async Task UpdateUserOnSuccess(RemoteAuthenticationResult<TRemoteAuthenticationState> result) { if (result.Status == RemoteAuthenticationStatus.Success) { var getUserTask = GetUser(); await getUserTask; UpdateUser(getUserTask); } } private void UpdateUser(Task<ClaimsPrincipal> task) { NotifyAuthenticationStateChanged(UpdateAuthenticationState(task)); static async Task<AuthenticationState> UpdateAuthenticationState(Task<ClaimsPrincipal> futureUser) => new AuthenticationState(await futureUser); } } // We need to do this as it can't be nested inside RemoteAuthenticationService because // it needs to be put in an attribute for linking purposes and that can't be an open generic type. internal class RemoteAuthenticationServiceJavaScriptLoggingOptions { public bool DebugEnabled { get; set; } public bool TraceEnabled { get; set; } } // Internal for testing purposes internal readonly struct InternalAccessTokenResult { [JsonConverter(typeof(JsonStringEnumConverter<AccessTokenResultStatus>))] public AccessTokenResultStatus Status { get; init; } public AccessToken Token { get; init; } public InternalAccessTokenResult(AccessTokenResultStatus status, AccessToken token) { Status = status; Token = token; } }