Merge pull request #20749 from dotnet/blazor-wasm-preview5
Blazor wasm preview5
This commit is contained in:
commit
17b4bc2b6d
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication.Internal
|
||||||
/// infrastructure and not subject to the same compatibility standards as public APIs.
|
/// infrastructure and not subject to the same compatibility standards as public APIs.
|
||||||
/// It may be changed or removed without notice in any release.
|
/// It may be changed or removed without notice in any release.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IRemoteAuthenticationPathsProvider
|
internal interface IRemoteAuthenticationPathsProvider
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
/// This is an internal API that supports the Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
{
|
{
|
||||||
|
|
@ -13,7 +14,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of granted scopes for the token.
|
/// Gets or sets the list of granted scopes for the token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] GrantedScopes { get; set; }
|
public IReadOnlyList<string> GrantedScopes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the expiration time of the token.
|
/// Gets the expiration time of the token.
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents the result of an authentication operation.
|
/// Represents the result of an authentication operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TState">The type of the preserved state during the authentication operation.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The type of the preserved state during the authentication operation.</typeparam>
|
||||||
public class RemoteAuthenticationResult<TState> where TState : class
|
public class RemoteAuthenticationResult<TRemoteAuthenticationState> where TRemoteAuthenticationState : RemoteAuthenticationState
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the status of the authentication operation. The status can be one of <see cref="RemoteAuthenticationStatus"/>.
|
/// Gets or sets the status of the authentication operation. The status can be one of <see cref="RemoteAuthenticationStatus"/>.
|
||||||
|
|
@ -22,6 +22,6 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the preserved state of a successful authentication operation.
|
/// Gets or sets the preserved state of a successful authentication operation.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TState State { get; set; }
|
public TRemoteAuthenticationState State { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of scopes to request when signing in.
|
/// Gets or sets the list of scopes to request when signing in.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IList<string> DefaultScopes { get; set; } = new List<string> { "openid", "profile" };
|
public IList<string> DefaultScopes { get; } = new List<string> { "openid", "profile" };
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the redirect uri for the application. The application will be redirected here after the user has completed the sign in
|
/// Gets or sets the redirect uri for the application. The application will be redirected here after the user has completed the sign in
|
||||||
|
|
|
||||||
|
|
@ -12,16 +12,16 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the provider options.
|
/// Gets or sets the provider options.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public TRemoteAuthenticationProviderOptions ProviderOptions { get; set; } = new TRemoteAuthenticationProviderOptions();
|
public TRemoteAuthenticationProviderOptions ProviderOptions { get; } = new TRemoteAuthenticationProviderOptions();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/>.
|
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RemoteAuthenticationApplicationPathsOptions AuthenticationPaths { get; set; } = new RemoteAuthenticationApplicationPathsOptions();
|
public RemoteAuthenticationApplicationPathsOptions AuthenticationPaths { get; } = new RemoteAuthenticationApplicationPathsOptions();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="RemoteAuthenticationUserOptions"/>.
|
/// Gets or sets the <see cref="RemoteAuthenticationUserOptions"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public RemoteAuthenticationUserOptions UserOptions { get; set; } = new RemoteAuthenticationUserOptions();
|
public RemoteAuthenticationUserOptions UserOptions { get; } = new RemoteAuthenticationUserOptions();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,44 +12,44 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
public static class RemoteAuthenticationBuilderExtensions
|
public static class RemoteAuthenticationBuilderExtensions
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
|
/// Replaces the existing <see cref="AccountClaimsPrincipalFactory{TAccount}"/> with the user factory defined by <typeparamref name="TAccountClaimsPrincipalFactory"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
|
||||||
/// <typeparam name="TAccount">The account type.</typeparam>
|
/// <typeparam name="TAccount">The account type.</typeparam>
|
||||||
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
|
/// <typeparam name="TAccountClaimsPrincipalFactory">The new user factory type.</typeparam>
|
||||||
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</param>
|
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</param>
|
||||||
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</returns>
|
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, TAccount}"/>.</returns>
|
||||||
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddUserFactory<TRemoteAuthenticationState, TAccount, TUserFactory>(
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> AddAccountClaimsPrincipalFactory<TRemoteAuthenticationState, TAccount, TAccountClaimsPrincipalFactory>(
|
||||||
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> builder)
|
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, TAccount> builder)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
where TAccount : RemoteUserAccount
|
where TAccount : RemoteUserAccount
|
||||||
where TUserFactory : AccountClaimsPrincipalFactory<TAccount>
|
where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<TAccount>
|
||||||
{
|
{
|
||||||
builder.Services.Replace(ServiceDescriptor.Scoped<AccountClaimsPrincipalFactory<TAccount>, TUserFactory>());
|
builder.Services.Replace(ServiceDescriptor.Scoped<AccountClaimsPrincipalFactory<TAccount>, TAccountClaimsPrincipalFactory>());
|
||||||
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replaces the existing <see cref="UserFactory{Account}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
|
/// Replaces the existing <see cref="AccountClaimsPrincipalFactory{Account}"/> with the user factory defined by <typeparamref name="TAccountClaimsPrincipalFactory"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
|
/// <typeparam name="TRemoteAuthenticationState">The remote authentication state.</typeparam>
|
||||||
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
|
/// <typeparam name="TAccountClaimsPrincipalFactory">The new user factory type.</typeparam>
|
||||||
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</param>
|
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</param>
|
||||||
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</returns>
|
/// <returns>The <see cref="IRemoteAuthenticationBuilder{TRemoteAuthenticationState, Account}"/>.</returns>
|
||||||
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TRemoteAuthenticationState, TUserFactory>(
|
public static IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> AddAccountClaimsPrincipalFactory<TRemoteAuthenticationState, TAccountClaimsPrincipalFactory>(
|
||||||
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> builder)
|
this IRemoteAuthenticationBuilder<TRemoteAuthenticationState, RemoteUserAccount> builder)
|
||||||
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
where TRemoteAuthenticationState : RemoteAuthenticationState, new()
|
||||||
where TUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddUserFactory<TRemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
|
where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddAccountClaimsPrincipalFactory<TRemoteAuthenticationState, RemoteUserAccount, TAccountClaimsPrincipalFactory>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Replaces the existing <see cref="UserFactory{TAccount}"/> with the user factory defined by <typeparamref name="TUserFactory"/>.
|
/// Replaces the existing <see cref="AccountClaimsPrincipalFactory{TAccount}"/> with the user factory defined by <typeparamref name="TAccountClaimsPrincipalFactory"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TUserFactory">The new user factory type.</typeparam>
|
/// <typeparam name="TAccountClaimsPrincipalFactory">The new user factory type.</typeparam>
|
||||||
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</param>
|
/// <param name="builder">The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</param>
|
||||||
/// <returns>The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</returns>
|
/// <returns>The <see cref="IRemoteAuthenticationBuilder{RemoteAuthenticationState, Account}"/>.</returns>
|
||||||
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddUserFactory<TUserFactory>(
|
public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddAccountClaimsPrincipalFactory<TAccountClaimsPrincipalFactory>(
|
||||||
this IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> builder)
|
this IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> builder)
|
||||||
where TUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddUserFactory<RemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
|
where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, TAccountClaimsPrincipalFactory>();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,46 +11,46 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default login path.
|
/// The default login path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LoginPath = "authentication/login";
|
public static readonly string LoginPath = "authentication/login";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default login callback path.
|
/// The default login callback path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LoginCallbackPath = "authentication/login-callback";
|
public static readonly string LoginCallbackPath = "authentication/login-callback";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default login failed path.
|
/// The default login failed path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LoginFailedPath = "authentication/login-failed";
|
public static readonly string LoginFailedPath = "authentication/login-failed";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default logout path.
|
/// The default logout path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LogoutPath = "authentication/logout";
|
public static readonly string LogoutPath = "authentication/logout";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default logout callback path.
|
/// The default logout callback path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LogoutCallbackPath = "authentication/logout-callback";
|
public static readonly string LogoutCallbackPath = "authentication/logout-callback";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default logout failed path.
|
/// The default logout failed path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LogoutFailedPath = "authentication/logout-failed";
|
public static readonly string LogoutFailedPath = "authentication/logout-failed";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default logout succeeded path.
|
/// The default logout succeeded path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string LogoutSucceededPath = "authentication/logged-out";
|
public static readonly string LogoutSucceededPath = "authentication/logged-out";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default profile path.
|
/// The default profile path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string ProfilePath = "authentication/profile";
|
public static readonly string ProfilePath = "authentication/profile";
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// The default register path.
|
/// The default register path.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public const string RegisterPath = "authentication/register";
|
public static readonly string RegisterPath = "authentication/register";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -88,34 +88,34 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IJSRuntime"/> to use for performin JavaScript interop.
|
/// Gets or sets the <see cref="IJSRuntime"/> to use for performin JavaScript interop.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Inject] public IJSRuntime JS { get; set; }
|
[Inject] internal IJSRuntime JS { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="NavigationManager"/> to use for redirecting the browser.
|
/// Gets or sets the <see cref="NavigationManager"/> to use for redirecting the browser.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Inject] public NavigationManager Navigation { get; set; }
|
[Inject] internal NavigationManager Navigation { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="IRemoteAuthenticationService{TRemoteAuthenticationState}"/> to use for handling the underlying authentication protocol.
|
/// Gets or sets the <see cref="IRemoteAuthenticationService{TRemoteAuthenticationState}"/> to use for handling the underlying authentication protocol.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Inject] public IRemoteAuthenticationService<TAuthenticationState> AuthenticationService { get; set; }
|
[Inject] internal IRemoteAuthenticationService<TAuthenticationState> AuthenticationService { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a default <see cref="IRemoteAuthenticationPathsProvider"/> to use as fallback if an <see cref="ApplicationPaths"/> has not been explicitly specified.
|
/// Gets or sets a default <see cref="IRemoteAuthenticationPathsProvider"/> to use as fallback if an <see cref="ApplicationPaths"/> has not been explicitly specified.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
#pragma warning disable PUB0001 // Pubternal type in public API
|
#pragma warning disable PUB0001 // Pubternal type in public API
|
||||||
[Inject] public IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; }
|
[Inject] internal IRemoteAuthenticationPathsProvider RemoteApplicationPathsProvider { get; set; }
|
||||||
#pragma warning restore PUB0001 // Pubternal type in public API
|
#pragma warning restore PUB0001 // Pubternal type in public API
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Inject] public AuthenticationStateProvider AuthenticationProvider { get; set; }
|
[Inject] internal AuthenticationStateProvider AuthenticationProvider { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
/// Gets or sets a default <see cref="AuthenticationStateProvider"/> with the current user.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Inject] public SignOutSessionStateManager SignOutManager { get; set; }
|
[Inject] internal SignOutSessionStateManager SignOutManager { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/> with the paths to different authentication pages.
|
/// Gets or sets the <see cref="RemoteAuthenticationApplicationPathsOptions"/> with the paths to different authentication pages.
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the list of scopes to request for the token.
|
/// Gets or sets the list of scopes to request for the token.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] Scopes { get; set; }
|
public IEnumerable<string> Scopes { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a specific return url to use for returning the user back to the application if it needs to be
|
/// Gets or sets a specific return url to use for returning the user back to the application if it needs to be
|
||||||
|
|
|
||||||
|
|
@ -11,32 +11,29 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
public class AccessTokenResult
|
public class AccessTokenResult
|
||||||
{
|
{
|
||||||
private readonly AccessToken _token;
|
private readonly AccessToken _token;
|
||||||
private readonly NavigationManager _navigation;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of <see cref="AccessTokenResult"/>.
|
/// Initializes a new instance of <see cref="AccessTokenResult"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="status">The status of the result.</param>
|
/// <param name="status">The status of the result.</param>
|
||||||
/// <param name="token">The <see cref="AccessToken"/> in case it was successful.</param>
|
/// <param name="token">The <see cref="AccessToken"/> in case it was successful.</param>
|
||||||
/// <param name="navigation">The <see cref="NavigationManager"/> to perform redirects.</param>
|
|
||||||
/// <param name="redirectUrl">The redirect uri to go to for provisioning the token.</param>
|
/// <param name="redirectUrl">The redirect uri to go to for provisioning the token.</param>
|
||||||
public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, NavigationManager navigation, string redirectUrl)
|
public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, string redirectUrl)
|
||||||
{
|
{
|
||||||
Status = status;
|
Status = status;
|
||||||
_token = token;
|
_token = token;
|
||||||
_navigation = navigation;
|
|
||||||
RedirectUrl = redirectUrl;
|
RedirectUrl = redirectUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the status of the current operation. See <see cref="AccessTokenResultStatus"/> for a list of statuses.
|
/// Gets or sets the status of the current operation. See <see cref="AccessTokenResultStatus"/> for a list of statuses.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public AccessTokenResultStatus Status { get; set; }
|
public AccessTokenResultStatus Status { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets the URL to redirect to if <see cref="Status"/> is <see cref="AccessTokenResultStatus.RequiresRedirect"/>.
|
/// Gets or sets the URL to redirect to if <see cref="Status"/> is <see cref="AccessTokenResultStatus.RequiresRedirect"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string RedirectUrl { get; set; }
|
public string RedirectUrl { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines whether the token request was successful and makes the <see cref="AccessToken"/> available for use when it is.
|
/// Determines whether the token request was successful and makes the <see cref="AccessToken"/> available for use when it is.
|
||||||
|
|
@ -56,27 +53,5 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether the token request was successful and makes the <see cref="AccessToken"/> available for use when it is.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="accessToken">The <see cref="AccessToken"/> if the request was successful.</param>
|
|
||||||
/// <param name="redirect">Whether or not to redirect automatically when failing to provision a token.</param>
|
|
||||||
/// <returns><c>true</c> when the token request is successful; <c>false</c> otherwise.</returns>
|
|
||||||
public bool TryGetToken(out AccessToken accessToken, bool redirect)
|
|
||||||
{
|
|
||||||
if (TryGetToken(out accessToken))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (redirect)
|
|
||||||
{
|
|
||||||
_navigation.NavigateTo(RedirectUrl);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,124 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="DelegatingHandler"/> that attaches access tokens to outgoing <see cref="HttpResponseMessage"/> instances.
|
||||||
|
/// Access tokens will only be added when the request URI is within one of the base addresses configured using
|
||||||
|
/// <see cref="ConfigureHandler(IEnumerable{string}, IEnumerable{string}, string)"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class AuthorizationMessageHandler : DelegatingHandler
|
||||||
|
{
|
||||||
|
private readonly IAccessTokenProvider _provider;
|
||||||
|
private readonly NavigationManager _navigation;
|
||||||
|
private AccessToken _lastToken;
|
||||||
|
private AuthenticationHeaderValue _cachedHeader;
|
||||||
|
private Uri[] _authorizedUris;
|
||||||
|
private AccessTokenRequestOptions _tokenOptions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="AuthorizationMessageHandler"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for provisioning tokens.</param>
|
||||||
|
/// <param name="navigation">The <see cref="NavigationManager"/> to use for performing redirections.</param>
|
||||||
|
public AuthorizationMessageHandler(
|
||||||
|
IAccessTokenProvider provider,
|
||||||
|
NavigationManager navigation)
|
||||||
|
{
|
||||||
|
_provider = provider;
|
||||||
|
_navigation = navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.Now;
|
||||||
|
if (_authorizedUris == null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException($"The '{nameof(AuthorizationMessageHandler)}' is not configured. " +
|
||||||
|
$"Call '{nameof(AuthorizationMessageHandler.ConfigureHandler)}' and provide a list of endpoint urls to attach the token to.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_authorizedUris.Any(uri => uri.IsBaseOf(request.RequestUri)))
|
||||||
|
{
|
||||||
|
if (_lastToken == null || now >= _lastToken.Expires.AddMinutes(-5))
|
||||||
|
{
|
||||||
|
var tokenResult = _tokenOptions != null ?
|
||||||
|
await _provider.RequestAccessToken(_tokenOptions) :
|
||||||
|
await _provider.RequestAccessToken();
|
||||||
|
|
||||||
|
if (tokenResult.TryGetToken(out var token))
|
||||||
|
{
|
||||||
|
_lastToken = token;
|
||||||
|
_cachedHeader = new AuthenticationHeaderValue("Bearer", _lastToken.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new AccessTokenNotAvailableException(_navigation, tokenResult, _tokenOptions?.Scopes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't try to handle 401s and retry the request with a new token automatically since that would mean we need to copy the request
|
||||||
|
// headers and buffer the body and we expect that the user instead handles the 401s. (Also, we can't really handle all 401s as we might
|
||||||
|
// not be able to provision a token without user interaction).
|
||||||
|
request.Headers.Authorization = _cachedHeader;
|
||||||
|
}
|
||||||
|
|
||||||
|
return await base.SendAsync(request, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configures this handler to authorize outbound HTTP requests using an access token. The access token is only attached if only attached if at least one of
|
||||||
|
/// <paramref name="authorizedUrls" /> is a base of <see cref="HttpRequestMessage.RequestUri" />.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="authorizedUrls">The base addresses of endpoint URLs to which the token will be attached.</param>
|
||||||
|
/// <param name="scopes">The list of scopes to use when requesting an access token.</param>
|
||||||
|
/// <param name="returnUrl">The return URL to use in case there is an issue provisioning the token and a redirection to the
|
||||||
|
/// identity provider is necessary.
|
||||||
|
/// </param>
|
||||||
|
/// <returns>This <see cref="AuthorizationMessageHandler"/>.</returns>
|
||||||
|
public AuthorizationMessageHandler ConfigureHandler(
|
||||||
|
IEnumerable<string> authorizedUrls,
|
||||||
|
IEnumerable<string> scopes = null,
|
||||||
|
string returnUrl = null)
|
||||||
|
{
|
||||||
|
if (_authorizedUris != null)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Handler already configured.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (authorizedUrls == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(authorizedUrls));
|
||||||
|
}
|
||||||
|
|
||||||
|
var uris = authorizedUrls.Select(uri => new Uri(uri, UriKind.Absolute)).ToArray();
|
||||||
|
if (uris.Length == 0)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("At least one URL must be configured.", nameof(authorizedUrls));
|
||||||
|
}
|
||||||
|
|
||||||
|
_authorizedUris = uris;
|
||||||
|
var scopesList = scopes?.ToArray();
|
||||||
|
if (scopesList != null || returnUrl != null)
|
||||||
|
{
|
||||||
|
_tokenOptions = new AccessTokenRequestOptions
|
||||||
|
{
|
||||||
|
Scopes = scopesList,
|
||||||
|
ReturnUrl = returnUrl
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Net.Http;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="DelegatingHandler"/> that attaches access tokens to outgoing <see cref="HttpResponseMessage"/> instances.
|
||||||
|
/// Access tokens will only be added when the request URI is within the application's base URI.
|
||||||
|
/// </summary>
|
||||||
|
public class BaseAddressAuthorizationMessageHandler : AuthorizationMessageHandler
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of <see cref="BaseAddressAuthorizationMessageHandler"/>.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="provider">The <see cref="IAccessTokenProvider"/> to use for requesting tokens.</param>
|
||||||
|
/// <param name="navigationManager">The <see cref="NavigationManager"/> used to compute the base address.</param>
|
||||||
|
public BaseAddressAuthorizationMessageHandler(IAccessTokenProvider provider, NavigationManager navigationManager)
|
||||||
|
: base(provider, navigationManager)
|
||||||
|
{
|
||||||
|
ConfigureHandler(new[] { navigationManager.BaseUri });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="Exception"/> that is thrown when an <see cref="AuthorizationMessageHandler"/> instance
|
||||||
|
/// is not able to provision an access token.
|
||||||
|
/// </summary>
|
||||||
|
public class AccessTokenNotAvailableException : Exception
|
||||||
|
{
|
||||||
|
private readonly NavigationManager _navigation;
|
||||||
|
private readonly AccessTokenResult _tokenResult;
|
||||||
|
|
||||||
|
public AccessTokenNotAvailableException(
|
||||||
|
NavigationManager navigation,
|
||||||
|
AccessTokenResult tokenResult,
|
||||||
|
IEnumerable<string> scopes)
|
||||||
|
: base(message: "Unable to provision an access token for the requested scopes: " +
|
||||||
|
scopes != null ? $"'{string.Join(", ", scopes ?? Array.Empty<string>())}'" : "(default scopes)")
|
||||||
|
{
|
||||||
|
_tokenResult = tokenResult;
|
||||||
|
_navigation = navigation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Redirect() => _navigation.NavigateTo(_tokenResult.RedirectUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -43,9 +43,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
protected NavigationManager Navigation { get; }
|
protected NavigationManager Navigation { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="UserFactory{TAccount}"/> to map accounts to <see cref="ClaimsPrincipal"/>.
|
/// Gets the <see cref="AccountClaimsPrincipalFactory{TAccount}"/> to map accounts to <see cref="ClaimsPrincipal"/>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
protected AccountClaimsPrincipalFactory<TAccount> UserFactory { get; }
|
protected AccountClaimsPrincipalFactory<TAccount> AccountClaimsPrincipalFactory { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the options for the underlying JavaScript library handling the authentication operations.
|
/// Gets the options for the underlying JavaScript library handling the authentication operations.
|
||||||
|
|
@ -58,16 +58,16 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
/// <param name="jsRuntime">The <see cref="IJSRuntime"/> to use for performing JavaScript interop operations.</param>
|
/// <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="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="navigation">The <see cref="NavigationManager"/> used to generate URLs.</param>
|
||||||
/// <param name="userFactory">The <see cref="UserFactory{TAccount}"/> used to generate the <see cref="ClaimsPrincipal"/> for the user.</param>
|
/// <param name="accountClaimsPrincipalFactory">The <see cref="AccountClaimsPrincipalFactory{TAccount}"/> used to generate the <see cref="ClaimsPrincipal"/> for the user.</param>
|
||||||
public RemoteAuthenticationService(
|
public RemoteAuthenticationService(
|
||||||
IJSRuntime jsRuntime,
|
IJSRuntime jsRuntime,
|
||||||
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
IOptions<RemoteAuthenticationOptions<TProviderOptions>> options,
|
||||||
NavigationManager navigation,
|
NavigationManager navigation,
|
||||||
AccountClaimsPrincipalFactory<TAccount> userFactory)
|
AccountClaimsPrincipalFactory<TAccount> accountClaimsPrincipalFactory)
|
||||||
{
|
{
|
||||||
JsRuntime = jsRuntime;
|
JsRuntime = jsRuntime;
|
||||||
Navigation = navigation;
|
Navigation = navigation;
|
||||||
UserFactory = userFactory;
|
AccountClaimsPrincipalFactory = accountClaimsPrincipalFactory;
|
||||||
Options = options.Value;
|
Options = options.Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
result.RedirectUrl = redirectUrl.ToString();
|
result.RedirectUrl = redirectUrl.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AccessTokenResult(parsedStatus, result.Token, Navigation, result.RedirectUrl);
|
return new AccessTokenResult(parsedStatus, result.Token, result.RedirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|
@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
result.RedirectUrl = redirectUrl.ToString();
|
result.RedirectUrl = redirectUrl.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new AccessTokenResult(parsedStatus, result.Token, Navigation, result.RedirectUrl);
|
return new AccessTokenResult(parsedStatus, result.Token, result.RedirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Uri GetRedirectUrl(string customReturnUrl)
|
private Uri GetRedirectUrl(string customReturnUrl)
|
||||||
|
|
@ -217,7 +217,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
{
|
{
|
||||||
await EnsureAuthService();
|
await EnsureAuthService();
|
||||||
var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser");
|
var account = await JsRuntime.InvokeAsync<TAccount>("AuthenticationService.getUser");
|
||||||
var user = await UserFactory.CreateUserAsync(account, Options.UserOptions);
|
var user = await AccountClaimsPrincipalFactory.CreateUserAsync(account, Options.UserOptions);
|
||||||
|
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ namespace Microsoft.Extensions.DependencyInjection
|
||||||
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
services.TryAddTransient<BaseAddressAuthorizationMessageHandler>();
|
||||||
|
services.TryAddTransient<AuthorizationMessageHandler>();
|
||||||
|
|
||||||
services.TryAddScoped(sp =>
|
services.TryAddScoped(sp =>
|
||||||
{
|
{
|
||||||
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
|
return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,208 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Moq;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
{
|
||||||
|
public class AuthorizationMessageHandlerTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task Throws_IfTheListOfAllowedUrlsIsNotConfigured()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var expectedMessage = "The 'AuthorizationMessageHandler' is not configured. " +
|
||||||
|
"Call 'ConfigureHandler' and provide a list of endpoint urls to attach the token to.";
|
||||||
|
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
// Act & Assert
|
||||||
|
|
||||||
|
var exception = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||||
|
() => new HttpClient(handler).GetAsync("https://www.example.com"));
|
||||||
|
|
||||||
|
Assert.Equal(expectedMessage, exception.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DoesNotAttachTokenToRequest_IfNotPresentInListOfAllowedUrls()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(new[] { "https://localhost:5001" });
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://www.example.com");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
tokenProvider.VerifyNoOtherCalls();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestsTokenWithDefaultScopes_WhenNoTokenIsAvailable()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
tokenProvider.Setup(tp => tp.RequestAccessToken())
|
||||||
|
.Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
|
||||||
|
new AccessToken
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddHours(1),
|
||||||
|
GrantedScopes = new string[] { "All" },
|
||||||
|
Value = "asdf"
|
||||||
|
},
|
||||||
|
"https://www.example.com")));
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(new[] { "https://localhost:5001" });
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CachesExistingTokenWhenPossible()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
tokenProvider.Setup(tp => tp.RequestAccessToken())
|
||||||
|
.Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
|
||||||
|
new AccessToken
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddHours(1),
|
||||||
|
GrantedScopes = new string[] { "All" },
|
||||||
|
Value = "asdf"
|
||||||
|
},
|
||||||
|
"https://www.example.com")));
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(new[] { "https://localhost:5001" });
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
response.RequestMessage = null;
|
||||||
|
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Single(tokenProvider.Invocations);
|
||||||
|
Assert.Equal("asdf", response.RequestMessage.Headers.Authorization.Parameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestNewTokenWhenCurrentTokenIsAboutToExpire()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
tokenProvider.Setup(tp => tp.RequestAccessToken())
|
||||||
|
.Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
|
||||||
|
new AccessToken
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddMinutes(3),
|
||||||
|
GrantedScopes = new string[] { "All" },
|
||||||
|
Value = "asdf"
|
||||||
|
},
|
||||||
|
"https://www.example.com")));
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(new[] { "https://localhost:5001" });
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
response.RequestMessage = null;
|
||||||
|
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(2, tokenProvider.Invocations.Count);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ThrowsWhenItCanNotProvisionANewToken()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
tokenProvider.Setup(tp => tp.RequestAccessToken())
|
||||||
|
.Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.RequiresRedirect,
|
||||||
|
null,
|
||||||
|
"https://www.example.com")));
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(new[] { "https://localhost:5001" });
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act & assert
|
||||||
|
var exception = await Assert.ThrowsAsync<AccessTokenNotAvailableException>(() => new HttpClient(handler).GetAsync("https://localhost:5001/weather"));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UsesCustomScopesAndReturnUrlWhenProvided()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var tokenProvider = new Mock<IAccessTokenProvider>();
|
||||||
|
tokenProvider.Setup(tp => tp.RequestAccessToken(It.IsAny<AccessTokenRequestOptions>()))
|
||||||
|
.Returns(new ValueTask<AccessTokenResult>(new AccessTokenResult(AccessTokenResultStatus.Success,
|
||||||
|
new AccessToken
|
||||||
|
{
|
||||||
|
Expires = DateTime.Now.AddMinutes(3),
|
||||||
|
GrantedScopes = new string[] { "All" },
|
||||||
|
Value = "asdf"
|
||||||
|
},
|
||||||
|
"https://www.example.com/return")));
|
||||||
|
|
||||||
|
var handler = new AuthorizationMessageHandler(tokenProvider.Object, Mock.Of<NavigationManager>());
|
||||||
|
handler.ConfigureHandler(
|
||||||
|
new[] { "https://localhost:5001" },
|
||||||
|
scopes: new[] { "example.read", "example.write" },
|
||||||
|
returnUrl: "https://www.example.com/return");
|
||||||
|
|
||||||
|
var response = new HttpResponseMessage(HttpStatusCode.OK);
|
||||||
|
handler.InnerHandler = new TestMessageHandler(response);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
_ = await new HttpClient(handler).GetAsync("https://localhost:5001/weather");
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(1, tokenProvider.Invocations.Count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class TestMessageHandler : HttpMessageHandler
|
||||||
|
{
|
||||||
|
private readonly HttpResponseMessage _response;
|
||||||
|
|
||||||
|
public TestMessageHandler(HttpResponseMessage response) => _response = response;
|
||||||
|
|
||||||
|
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
_response.RequestMessage = request;
|
||||||
|
return Task.FromResult(_response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -406,7 +406,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager(),
|
new TestNavigationManager(),
|
||||||
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
new TestAccountClaimsPrincipalFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var account = new CoolRoleAccount
|
var account = new CoolRoleAccount
|
||||||
{
|
{
|
||||||
|
|
@ -442,7 +442,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
testJsRuntime,
|
testJsRuntime,
|
||||||
options,
|
options,
|
||||||
new TestNavigationManager(),
|
new TestNavigationManager(),
|
||||||
new TestUserFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
new TestAccountClaimsPrincipalFactory(Mock.Of<IAccessTokenProviderAccessor>()));
|
||||||
|
|
||||||
var account = new CoolRoleAccount
|
var account = new CoolRoleAccount
|
||||||
{
|
{
|
||||||
|
|
@ -481,39 +481,30 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
|
|
||||||
private static IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> CreateOptions(string scopeClaim = null)
|
private static IOptions<RemoteAuthenticationOptions<OidcProviderOptions>> CreateOptions(string scopeClaim = null)
|
||||||
{
|
{
|
||||||
return Options.Create(
|
var options = new RemoteAuthenticationOptions<OidcProviderOptions>();
|
||||||
new RemoteAuthenticationOptions<OidcProviderOptions>()
|
|
||||||
{
|
options.AuthenticationPaths.LogInPath = "login";
|
||||||
AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
options.AuthenticationPaths.LogInCallbackPath = "a";
|
||||||
{
|
options.AuthenticationPaths.LogInFailedPath = "a";
|
||||||
LogInPath = "login",
|
options.AuthenticationPaths.RegisterPath = "a";
|
||||||
LogInCallbackPath = "a",
|
options.AuthenticationPaths.ProfilePath = "a";
|
||||||
LogInFailedPath = "a",
|
options.AuthenticationPaths.RemoteRegisterPath = "a";
|
||||||
RegisterPath = "a",
|
options.AuthenticationPaths.RemoteProfilePath = "a";
|
||||||
ProfilePath = "a",
|
options.AuthenticationPaths.LogOutPath = "a";
|
||||||
RemoteRegisterPath = "a",
|
options.AuthenticationPaths.LogOutCallbackPath = "a";
|
||||||
RemoteProfilePath = "a",
|
options.AuthenticationPaths.LogOutFailedPath = "a";
|
||||||
LogOutPath = "a",
|
options.AuthenticationPaths.LogOutSucceededPath = "a";
|
||||||
LogOutCallbackPath = "a",
|
options.UserOptions.AuthenticationType = "a";
|
||||||
LogOutFailedPath = "a",
|
options.UserOptions.ScopeClaim = scopeClaim;
|
||||||
LogOutSucceededPath = "a",
|
options.UserOptions.RoleClaim = "coolRole";
|
||||||
},
|
options.UserOptions.NameClaim = "coolName";
|
||||||
UserOptions = new RemoteAuthenticationUserOptions
|
options.ProviderOptions.Authority = "a";
|
||||||
{
|
options.ProviderOptions.ClientId = "a";
|
||||||
AuthenticationType = "a",
|
options.ProviderOptions.DefaultScopes.Add("openid");
|
||||||
ScopeClaim = scopeClaim,
|
options.ProviderOptions.RedirectUri = "https://www.example.com/base/custom-login";
|
||||||
RoleClaim = "coolRole",
|
options.ProviderOptions.PostLogoutRedirectUri = "https://www.example.com/base/custom-logout";
|
||||||
NameClaim = "coolName",
|
|
||||||
},
|
return Options.Create(options);
|
||||||
ProviderOptions = new OidcProviderOptions
|
|
||||||
{
|
|
||||||
Authority = "a",
|
|
||||||
ClientId = "a",
|
|
||||||
DefaultScopes = new[] { "openid" },
|
|
||||||
RedirectUri = "https://www.example.com/base/custom-login",
|
|
||||||
PostLogoutRedirectUri = "https://www.example.com/base/custom-logout",
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private class TestJsRuntime : IJSRuntime
|
private class TestJsRuntime : IJSRuntime
|
||||||
|
|
@ -571,9 +562,9 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class TestUserFactory : AccountClaimsPrincipalFactory<CoolRoleAccount>
|
internal class TestAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<CoolRoleAccount>
|
||||||
{
|
{
|
||||||
public TestUserFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
|
public TestAccountClaimsPrincipalFactory(IAccessTokenProviderAccessor accessor) : base(accessor)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -200,31 +200,22 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
builder.Services.AddApiAuthorization(options =>
|
builder.Services.AddApiAuthorization(options =>
|
||||||
{
|
{
|
||||||
options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
options.AuthenticationPaths.LogInPath = "a";
|
||||||
{
|
options.AuthenticationPaths.LogInCallbackPath = "b";
|
||||||
LogInPath = "a",
|
options.AuthenticationPaths.LogInFailedPath = "c";
|
||||||
LogInCallbackPath = "b",
|
options.AuthenticationPaths.RegisterPath = "d";
|
||||||
LogInFailedPath = "c",
|
options.AuthenticationPaths.ProfilePath = "e";
|
||||||
RegisterPath = "d",
|
options.AuthenticationPaths.RemoteRegisterPath = "f";
|
||||||
ProfilePath = "e",
|
options.AuthenticationPaths.RemoteProfilePath = "g";
|
||||||
RemoteRegisterPath = "f",
|
options.AuthenticationPaths.LogOutPath = "h";
|
||||||
RemoteProfilePath = "g",
|
options.AuthenticationPaths.LogOutCallbackPath = "i";
|
||||||
LogOutPath = "h",
|
options.AuthenticationPaths.LogOutFailedPath = "j";
|
||||||
LogOutCallbackPath = "i",
|
options.AuthenticationPaths.LogOutSucceededPath = "k";
|
||||||
LogOutFailedPath = "j",
|
options.UserOptions.AuthenticationType = "l";
|
||||||
LogOutSucceededPath = "k",
|
options.UserOptions.ScopeClaim = "m";
|
||||||
};
|
options.UserOptions.RoleClaim = "n";
|
||||||
options.UserOptions = new RemoteAuthenticationUserOptions
|
options.UserOptions.NameClaim = "o";
|
||||||
{
|
options.ProviderOptions.ConfigurationEndpoint = "p";
|
||||||
AuthenticationType = "l",
|
|
||||||
ScopeClaim = "m",
|
|
||||||
RoleClaim = "n",
|
|
||||||
NameClaim = "o",
|
|
||||||
};
|
|
||||||
options.ProviderOptions = new ApiAuthorizationProviderOptions
|
|
||||||
{
|
|
||||||
ConfigurationEndpoint = "p"
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
@ -298,35 +289,26 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
var builder = new WebAssemblyHostBuilder(new TestWebAssemblyJSRuntimeInvoker());
|
||||||
builder.Services.AddOidcAuthentication(options =>
|
builder.Services.AddOidcAuthentication(options =>
|
||||||
{
|
{
|
||||||
options.AuthenticationPaths = new RemoteAuthenticationApplicationPathsOptions
|
options.AuthenticationPaths.LogInPath = "a";
|
||||||
{
|
options.AuthenticationPaths.LogInCallbackPath = "b";
|
||||||
LogInPath = "a",
|
options.AuthenticationPaths.LogInFailedPath = "c";
|
||||||
LogInCallbackPath = "b",
|
options.AuthenticationPaths.RegisterPath = "d";
|
||||||
LogInFailedPath = "c",
|
options.AuthenticationPaths.ProfilePath = "e";
|
||||||
RegisterPath = "d",
|
options.AuthenticationPaths.RemoteRegisterPath = "f";
|
||||||
ProfilePath = "e",
|
options.AuthenticationPaths.RemoteProfilePath = "g";
|
||||||
RemoteRegisterPath = "f",
|
options.AuthenticationPaths.LogOutPath = "h";
|
||||||
RemoteProfilePath = "g",
|
options.AuthenticationPaths.LogOutCallbackPath = "i";
|
||||||
LogOutPath = "h",
|
options.AuthenticationPaths.LogOutFailedPath = "j";
|
||||||
LogOutCallbackPath = "i",
|
options.AuthenticationPaths.LogOutSucceededPath = "k";
|
||||||
LogOutFailedPath = "j",
|
options.UserOptions.AuthenticationType = "l";
|
||||||
LogOutSucceededPath = "k",
|
options.UserOptions.ScopeClaim = "m";
|
||||||
};
|
options.UserOptions.RoleClaim = "n";
|
||||||
options.UserOptions = new RemoteAuthenticationUserOptions
|
options.UserOptions.NameClaim = "o";
|
||||||
{
|
options.ProviderOptions.Authority = "p";
|
||||||
AuthenticationType = "l",
|
options.ProviderOptions.ClientId = "q";
|
||||||
ScopeClaim = "m",
|
options.ProviderOptions.DefaultScopes.Clear();
|
||||||
RoleClaim = "n",
|
options.ProviderOptions.RedirectUri = "https://www.example.com/base/custom-login";
|
||||||
NameClaim = "o",
|
options.ProviderOptions.PostLogoutRedirectUri = "https://www.example.com/base/custom-logout";
|
||||||
};
|
|
||||||
options.ProviderOptions = new OidcProviderOptions
|
|
||||||
{
|
|
||||||
Authority = "p",
|
|
||||||
ClientId = "q",
|
|
||||||
DefaultScopes = Array.Empty<string>(),
|
|
||||||
RedirectUri = "https://www.example.com/base/custom-login",
|
|
||||||
PostLogoutRedirectUri = "https://www.example.com/base/custom-logout",
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
|
|
|
||||||
|
|
@ -52,13 +52,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
// Private right now because we don't have much reason to expose it. This can be exposed
|
// Private right now because we don't have much reason to expose it. This can be exposed
|
||||||
// in the future if we want to give people a choice between CreateDefault and something
|
// in the future if we want to give people a choice between CreateDefault and something
|
||||||
// less opinionated.
|
// less opinionated.
|
||||||
Configuration = new ConfigurationBuilder();
|
Configuration = new WebAssemblyHostConfiguration();
|
||||||
RootComponents = new RootComponentMappingCollection();
|
RootComponents = new RootComponentMappingCollection();
|
||||||
Services = new ServiceCollection();
|
Services = new ServiceCollection();
|
||||||
Logging = new LoggingBuilder(Services);
|
Logging = new LoggingBuilder(Services);
|
||||||
|
|
||||||
Logging.SetMinimumLevel(LogLevel.Warning);
|
|
||||||
|
|
||||||
// Retrieve required attributes from JSRuntimeInvoker
|
// Retrieve required attributes from JSRuntimeInvoker
|
||||||
InitializeNavigationManager(jsRuntimeInvoker);
|
InitializeNavigationManager(jsRuntimeInvoker);
|
||||||
InitializeDefaultServices();
|
InitializeDefaultServices();
|
||||||
|
|
@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets an <see cref="IConfigurationBuilder"/> that can be used to customize the application's
|
/// Gets an <see cref="WebAssemblyHostConfiguration"/> that can be used to customize the application's
|
||||||
/// configuration sources.
|
/// configuration sources and read configuration attributes.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public IConfigurationBuilder Configuration { get; }
|
public WebAssemblyHostConfiguration Configuration { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the collection of root component mappings configured for the application.
|
/// Gets the collection of root component mappings configured for the application.
|
||||||
|
|
@ -177,8 +175,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
public WebAssemblyHost Build()
|
public WebAssemblyHost Build()
|
||||||
{
|
{
|
||||||
// Intentionally overwrite configuration with the one we're creating.
|
// Intentionally overwrite configuration with the one we're creating.
|
||||||
var configuration = Configuration.Build();
|
Services.AddSingleton<IConfiguration>(Configuration);
|
||||||
Services.AddSingleton<IConfiguration>(configuration);
|
|
||||||
|
|
||||||
// A Blazor application always runs in a scope. Since we want to make it possible for the user
|
// A Blazor application always runs in a scope. Since we want to make it possible for the user
|
||||||
// to configure services inside *that scope* inside their startup code, we create *both* the
|
// to configure services inside *that scope* inside their startup code, we create *both* the
|
||||||
|
|
@ -186,7 +183,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
var services = _createServiceProvider();
|
var services = _createServiceProvider();
|
||||||
var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope();
|
||||||
|
|
||||||
return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray());
|
return new WebAssemblyHost(services, scope, Configuration, RootComponents.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
internal void InitializeDefaultServices()
|
internal void InitializeDefaultServices()
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,187 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// WebAssemblyHostConfiguration is a class that implements the interface of an IConfiguration,
|
||||||
|
/// IConfigurationRoot, and IConfigurationBuilder. It can be used to simulatneously build
|
||||||
|
/// and read from a configuration object.
|
||||||
|
/// </summary>
|
||||||
|
public class WebAssemblyHostConfiguration : IConfiguration, IConfigurationRoot, IConfigurationBuilder
|
||||||
|
{
|
||||||
|
private readonly List<IConfigurationProvider> _providers = new List<IConfigurationProvider>();
|
||||||
|
private readonly List<IConfigurationSource> _sources = new List<IConfigurationSource>();
|
||||||
|
|
||||||
|
private readonly List<IDisposable> _changeTokenRegistrations = new List<IDisposable>();
|
||||||
|
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the sources used to obtain configuration values.
|
||||||
|
/// </summary>
|
||||||
|
IList<IConfigurationSource> IConfigurationBuilder.Sources => new ReadOnlyCollection<IConfigurationSource>(_sources.ToList());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the providers used to obtain configuration values.
|
||||||
|
/// </summary>
|
||||||
|
IEnumerable<IConfigurationProvider> IConfigurationRoot.Providers => new ReadOnlyCollection<IConfigurationProvider>(_providers.ToList());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/>
|
||||||
|
/// and the registered <see cref="IConfigurationProvider"/> instances.
|
||||||
|
/// </summary>
|
||||||
|
// In this implementation, this largely exists as a way to satisfy the
|
||||||
|
// requirements of the IConfigurationBuilder and is not populated by
|
||||||
|
// the WebAssemblyHostConfiguration with any meaningful info.
|
||||||
|
IDictionary<string, object> IConfigurationBuilder.Properties { get; } = new Dictionary<string, object>();
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public string this[string key]
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
// Iterate through the providers in reverse to extract
|
||||||
|
// the value from the most recently inserted provider.
|
||||||
|
for (var i = _providers.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var provider = _providers[i];
|
||||||
|
|
||||||
|
if (provider.TryGet(key, out var value))
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (_providers.Count == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Can only set property if at least one provider has been inserted.");
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var provider in _providers)
|
||||||
|
{
|
||||||
|
provider.Set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a configuration sub-section with the specified key.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="key">The key of the configuration section.</param>
|
||||||
|
/// <returns>The <see cref="IConfigurationSection"/>.</returns>
|
||||||
|
/// <remarks>
|
||||||
|
/// This method will never return <c>null</c>. If no matching sub-section is found with the specified key,
|
||||||
|
/// an empty <see cref="IConfigurationSection"/> will be returned.
|
||||||
|
/// </remarks>
|
||||||
|
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the immediate descendant configuration sub-sections.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The configuration sub-sections.</returns>
|
||||||
|
IEnumerable<IConfigurationSection> IConfiguration.GetChildren()
|
||||||
|
{
|
||||||
|
return _providers
|
||||||
|
.SelectMany(s => s.GetChildKeys(Enumerable.Empty<string>(), null))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.Select(key => this.GetSection(key))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>The <see cref="IChangeToken"/>.</returns>
|
||||||
|
public IChangeToken GetReloadToken() => _changeToken;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Force the configuration values to be reloaded from the underlying sources.
|
||||||
|
/// </summary>
|
||||||
|
public void Reload()
|
||||||
|
{
|
||||||
|
foreach (var provider in _providers)
|
||||||
|
{
|
||||||
|
provider.Load();
|
||||||
|
}
|
||||||
|
RaiseChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaiseChanged()
|
||||||
|
{
|
||||||
|
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
|
||||||
|
previousToken.OnReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new configuration source, retrieves the provider for the source, and
|
||||||
|
/// adds a change listener that triggers a reload of the provider whenever a change
|
||||||
|
/// is detected.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The configuration source to add.</param>
|
||||||
|
/// <returns>The same <see cref="IConfigurationBuilder"/>.</returns>
|
||||||
|
public IConfigurationBuilder Add(IConfigurationSource source)
|
||||||
|
{
|
||||||
|
if (source == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ads this source and its associated provider to the source
|
||||||
|
// and provider references in this class. We make sure to load
|
||||||
|
// the data from the provider so that values are properly initialized.
|
||||||
|
_sources.Add(source);
|
||||||
|
var provider = source.Build(this);
|
||||||
|
provider.Load();
|
||||||
|
|
||||||
|
// Add a handler that will detect when the the configuration
|
||||||
|
// provider has reloaded data. This will invoke the RaiseChanged
|
||||||
|
// method which maps changes in individual providers to the change
|
||||||
|
// token on the WebAssemblyHostConfiguration object.
|
||||||
|
_changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
|
||||||
|
|
||||||
|
// We keep a list of providers in this class so that we can map
|
||||||
|
// set and get methods on this class to the set and get methods
|
||||||
|
// on the individual configuration providers.
|
||||||
|
_providers.Add(provider);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds an <see cref="IConfiguration"/> with keys and values from the set of registered providers.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns>
|
||||||
|
public IConfigurationRoot Build()
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
// dispose change token registrations
|
||||||
|
foreach (var registration in _changeTokenRegistrations)
|
||||||
|
{
|
||||||
|
registration.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
// dispose providers
|
||||||
|
foreach (var provider in _providers)
|
||||||
|
{
|
||||||
|
(provider as IDisposable)?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using Xunit;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Configuration.Memory;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
|
||||||
|
{
|
||||||
|
public class WebAssemblyHostConfigurationTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void CanSetAndGetConfigurationValue()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var initialData = new Dictionary<string, string>() {
|
||||||
|
{ "color", "blue" },
|
||||||
|
{ "type", "car" },
|
||||||
|
{ "wheels:year", "2008" },
|
||||||
|
{ "wheels:count", "4" },
|
||||||
|
{ "wheels:brand", "michelin" },
|
||||||
|
{ "wheels:brand:type", "rally" },
|
||||||
|
};
|
||||||
|
var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memoryConfig);
|
||||||
|
configuration["type"] = "car";
|
||||||
|
configuration["wheels:count"] = "6";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("car", configuration["type"]);
|
||||||
|
Assert.Equal("blue", configuration["color"]);
|
||||||
|
Assert.Equal("6", configuration["wheels:count"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void SettingValueUpdatesAllProviders()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var initialData = new Dictionary<string, string>() { { "color", "blue" } };
|
||||||
|
var source1 = new MemoryConfigurationSource { InitialData = initialData };
|
||||||
|
var source2 = new CustomizedTestConfigurationSource();
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(source1);
|
||||||
|
configuration.Add(source2);
|
||||||
|
configuration["type"] = "car";
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("car", configuration["type"]);
|
||||||
|
IConfigurationRoot root = configuration;
|
||||||
|
Assert.All(root.Providers, provider =>
|
||||||
|
{
|
||||||
|
provider.TryGet("type", out var value);
|
||||||
|
Assert.Equal("car", value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanGetChildren()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var initialData = new Dictionary<string, string>() { { "color", "blue" } };
|
||||||
|
var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memoryConfig);
|
||||||
|
IConfiguration readableConfig = configuration;
|
||||||
|
var children = readableConfig.GetChildren();
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.NotNull(children);
|
||||||
|
Assert.NotEmpty(children);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanGetSection()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var initialData = new Dictionary<string, string>() {
|
||||||
|
{ "color", "blue" },
|
||||||
|
{ "type", "car" },
|
||||||
|
{ "wheels:year", "2008" },
|
||||||
|
{ "wheels:count", "4" },
|
||||||
|
{ "wheels:brand", "michelin" },
|
||||||
|
{ "wheels:brand:type", "rally" },
|
||||||
|
};
|
||||||
|
var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memoryConfig);
|
||||||
|
var section = configuration.GetSection("wheels").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal(4, section.Count);
|
||||||
|
Assert.Equal("2008", section["year"]);
|
||||||
|
Assert.Equal("4", section["count"]);
|
||||||
|
Assert.Equal("michelin", section["brand"]);
|
||||||
|
Assert.Equal("rally", section["brand:type"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanDisposeProviders()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var initialData = new Dictionary<string, string>() { { "color", "blue" } };
|
||||||
|
var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memoryConfig);
|
||||||
|
Assert.Equal("blue", configuration["color"]);
|
||||||
|
var exception = Record.Exception(() => configuration.Dispose());
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Null(exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void CanSupportDeeplyNestedConfigs()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dic1 = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Mem1", "Value1"},
|
||||||
|
{"Mem1:", "NoKeyValue1"},
|
||||||
|
{"Mem1:KeyInMem1", "ValueInMem1"},
|
||||||
|
{"Mem1:KeyInMem1:Deep1", "ValueDeep1"}
|
||||||
|
};
|
||||||
|
var dic2 = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Mem2", "Value2"},
|
||||||
|
{"Mem2:", "NoKeyValue2"},
|
||||||
|
{"Mem2:KeyInMem2", "ValueInMem2"},
|
||||||
|
{"Mem2:KeyInMem2:Deep2", "ValueDeep2"}
|
||||||
|
};
|
||||||
|
var dic3 = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Mem3", "Value3"},
|
||||||
|
{"Mem3:", "NoKeyValue3"},
|
||||||
|
{"Mem3:KeyInMem3", "ValueInMem3"},
|
||||||
|
{"Mem3:KeyInMem4", "ValueInMem4"},
|
||||||
|
{"Mem3:KeyInMem3:Deep3", "ValueDeep3"},
|
||||||
|
{"Mem3:KeyInMem3:Deep4", "ValueDeep4"}
|
||||||
|
};
|
||||||
|
var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 };
|
||||||
|
var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };
|
||||||
|
var memConfigSrc3 = new MemoryConfigurationSource { InitialData = dic3 };
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memConfigSrc1);
|
||||||
|
configuration.Add(memConfigSrc2);
|
||||||
|
configuration.Add(memConfigSrc3);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
var dict = configuration.GetSection("Mem1").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
Assert.Equal(3, dict.Count);
|
||||||
|
Assert.Equal("NoKeyValue1", dict[""]);
|
||||||
|
Assert.Equal("ValueInMem1", dict["KeyInMem1"]);
|
||||||
|
Assert.Equal("ValueDeep1", dict["KeyInMem1:Deep1"]);
|
||||||
|
|
||||||
|
var dict2 = configuration.GetSection("Mem2").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
Assert.Equal(3, dict2.Count);
|
||||||
|
Assert.Equal("NoKeyValue2", dict2[""]);
|
||||||
|
Assert.Equal("ValueInMem2", dict2["KeyInMem2"]);
|
||||||
|
Assert.Equal("ValueDeep2", dict2["KeyInMem2:Deep2"]);
|
||||||
|
|
||||||
|
var dict3 = configuration.GetSection("Mem3").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
|
||||||
|
Assert.Equal(5, dict3.Count);
|
||||||
|
Assert.Equal("NoKeyValue3", dict3[""]);
|
||||||
|
Assert.Equal("ValueInMem3", dict3["KeyInMem3"]);
|
||||||
|
Assert.Equal("ValueInMem4", dict3["KeyInMem4"]);
|
||||||
|
Assert.Equal("ValueDeep3", dict3["KeyInMem3:Deep3"]);
|
||||||
|
Assert.Equal("ValueDeep4", dict3["KeyInMem3:Deep4"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void NewConfigurationProviderOverridesOldOneWhenKeyIsDuplicated()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var dic1 = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Key1:Key2", "ValueInMem1"}
|
||||||
|
};
|
||||||
|
var dic2 = new Dictionary<string, string>()
|
||||||
|
{
|
||||||
|
{"Key1:Key2", "ValueInMem2"}
|
||||||
|
};
|
||||||
|
var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 };
|
||||||
|
var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };
|
||||||
|
|
||||||
|
var configuration = new WebAssemblyHostConfiguration();
|
||||||
|
|
||||||
|
// Act
|
||||||
|
configuration.Add(memConfigSrc1);
|
||||||
|
configuration.Add(memConfigSrc2);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.Equal("ValueInMem2", configuration["Key1:Key2"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomizedTestConfigurationProvider : ConfigurationProvider
|
||||||
|
{
|
||||||
|
public CustomizedTestConfigurationProvider(string key, string value)
|
||||||
|
=> Data.Add(key, value.ToUpper());
|
||||||
|
|
||||||
|
public override void Set(string key, string value)
|
||||||
|
{
|
||||||
|
Data[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CustomizedTestConfigurationSource : IConfigurationSource
|
||||||
|
{
|
||||||
|
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||||
|
{
|
||||||
|
return new CustomizedTestConfigurationProvider("initialKey", "initialValue");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
@page "/fetchdata"
|
@page "/fetchdata"
|
||||||
@using Wasm.Authentication.Shared
|
@using Wasm.Authentication.Shared
|
||||||
|
@implements IDisposable
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
@inject IAccessTokenProvider AuthenticationService
|
@inject WeatherForecastClient WeatherForecast
|
||||||
@inject NavigationManager Navigation
|
|
||||||
|
|
||||||
<h1>Weather forecast</h1>
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
<p>This component demonstrates fetching data from the server.</p>
|
<p>This component demonstrates fetching data from the server.</p>
|
||||||
|
|
@ -42,15 +41,18 @@ else
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var httpClient = new HttpClient();
|
try
|
||||||
httpClient.BaseAddress = new Uri(Navigation.BaseUri);
|
|
||||||
|
|
||||||
var tokenResult = await AuthenticationService.RequestAccessToken();
|
|
||||||
|
|
||||||
if (tokenResult.TryGetToken(out var token, redirect: true))
|
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
|
forecasts = await WeatherForecast.GetForecastAsync();
|
||||||
forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
}
|
||||||
|
catch (AccessTokenNotAvailableException exception)
|
||||||
|
{
|
||||||
|
exception.Redirect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
WeatherForecast.Dispose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Net.Http;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
|
|
@ -15,7 +17,10 @@ namespace Wasm.Authentication.Client
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
|
|
||||||
builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
|
builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
|
||||||
.AddUserFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
|
.AddAccountClaimsPrincipalFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient<WeatherForecastClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
|
||||||
|
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<StateService>();
|
builder.Services.AddSingleton<StateService>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
<Reference Include="System.Net.Http.Json" />
|
<Reference Include="System.Net.Http.Json" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Http" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,30 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Wasm.Authentication.Shared;
|
||||||
|
|
||||||
|
namespace Wasm.Authentication.Client
|
||||||
|
{
|
||||||
|
public class WeatherForecastClient : IDisposable
|
||||||
|
{
|
||||||
|
private readonly HttpClient _client;
|
||||||
|
private readonly CancellationTokenSource _cts = new CancellationTokenSource();
|
||||||
|
|
||||||
|
public WeatherForecastClient(HttpClient client)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<WeatherForecast[]> GetForecastAsync() =>
|
||||||
|
_client.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast", _cts.Token);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_client?.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -47,6 +47,22 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
|
||||||
Assert.Equal("Development key3-value", _appElement.FindElement(By.Id("key3")).Text);
|
Assert.Equal("Development key3-value", _appElement.FindElement(By.Id("key3")).Text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WebAssemblyConfiguration_ReloadingWorks()
|
||||||
|
{
|
||||||
|
// Verify values from the default 'appsettings.json' are read.
|
||||||
|
Browser.Equal("Default key1-value", () => _appElement.FindElement(By.Id("key1")).Text);
|
||||||
|
|
||||||
|
// Change the value of key1 using the form in the UI
|
||||||
|
var input = _appElement.FindElement(By.Id("key1-input"));
|
||||||
|
input.SendKeys("newValue");
|
||||||
|
var submit = _appElement.FindElement(By.Id("trigger-change"));
|
||||||
|
submit.Click();
|
||||||
|
|
||||||
|
// Asser that the value of the key has been updated
|
||||||
|
Browser.Equal("newValue", () => _appElement.FindElement(By.Id("key1")).Text);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void WebAssemblyHostingEnvironment_Works()
|
public void WebAssemblyHostingEnvironment_Works()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
|
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
|
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
|
||||||
<Reference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" />
|
<Reference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" />
|
||||||
|
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -8,3 +8,18 @@
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div id="environment">@HostEnvironment.Environment</div>
|
<div id="environment">@HostEnvironment.Environment</div>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<input id="key1-input" @bind-value=newKey1 @bind-value:event="oninput" />
|
||||||
|
<button id="trigger-change" @onclick="@(() => TriggerChange())">Change key1</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
string newKey1 { get; set; }
|
||||||
|
|
||||||
|
void TriggerChange()
|
||||||
|
{
|
||||||
|
Config["key1"] = newKey1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Http;
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
using Microsoft.AspNetCore.Components.WebAssembly.Services;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.Logging.Configuration;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
|
|
||||||
namespace BasicTestApp
|
namespace BasicTestApp
|
||||||
|
|
@ -45,8 +46,9 @@ namespace BasicTestApp
|
||||||
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
|
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.Logging.Services.AddSingleton<ILoggerProvider, PrependMessageLoggerProvider>(s => new PrependMessageLoggerProvider("Custom logger", s.GetService<IJSRuntime>()));
|
builder.Logging.Services.AddSingleton<ILoggerProvider, PrependMessageLoggerProvider>(s =>
|
||||||
builder.Logging.SetMinimumLevel(LogLevel.Information);
|
new PrependMessageLoggerProvider(builder.Configuration["Logging:PrependMessage:Message"], s.GetService<IJSRuntime>()));
|
||||||
|
builder.Logging.AddConfiguration(builder.Configuration);
|
||||||
|
|
||||||
var host = builder.Build();
|
var host = builder.Build();
|
||||||
ConfigureCulture(host);
|
ConfigureCulture(host);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,12 @@
|
||||||
{
|
{
|
||||||
"key1": "Default key1-value",
|
"key1": "Default key1-value",
|
||||||
"key2": "Default key2-value"
|
"key2": "Default key2-value",
|
||||||
|
"Logging": {
|
||||||
|
"PrependMessage": {
|
||||||
|
"Message": "Custom logger",
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="${MicrosoftAspNetCoreComponentsWebAssemblyDevServerPackageVersion}" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="${MicrosoftAspNetCoreComponentsWebAssemblyDevServerPackageVersion}" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="${MicrosoftAspNetCoreComponentsWebAssemblyAuthenticationPackageVersion}" Condition="'$(IndividualLocalAuth)' == 'true'" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="${MicrosoftAspNetCoreComponentsWebAssemblyAuthenticationPackageVersion}" Condition="'$(IndividualLocalAuth)' == 'true'" />
|
||||||
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="${MicrosoftAuthenticationWebAssemblyMsalPackageVersion}" Condition="'$(OrganizationalAuth)' == 'true' OR '$(IndividualB2CAuth)' == 'true'" />
|
<PackageReference Include="Microsoft.Authentication.WebAssembly.Msal" Version="${MicrosoftAuthenticationWebAssemblyMsalPackageVersion}" Condition="'$(OrganizationalAuth)' == 'true' OR '$(IndividualB2CAuth)' == 'true'" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http" Version="${MicrosoftExtensionsHttpPackageVersion}" Condition="'$(NoAuth)' != 'true' AND '$(Hosted)' == 'true'" />
|
||||||
<PackageReference Include="System.Net.Http.Json" Version="${SystemNetHttpJsonPackageVersion}" />
|
<PackageReference Include="System.Net.Http.Json" Version="${SystemNetHttpJsonPackageVersion}" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@
|
||||||
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
|
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
|
||||||
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
||||||
MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion);
|
MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion);
|
||||||
|
MicrosoftExtensionsHttpPackageVersion=$(MicrosoftExtensionsHttpPackageVersion);
|
||||||
SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion)
|
SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion)
|
||||||
</GeneratedContentProperties>
|
</GeneratedContentProperties>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,6 @@
|
||||||
@*#if (!NoAuth && Hosted)
|
@*#if (!NoAuth && Hosted)
|
||||||
@using Microsoft.AspNetCore.Authorization
|
@using Microsoft.AspNetCore.Authorization
|
||||||
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
|
||||||
@inject IAccessTokenProvider AuthenticationService
|
|
||||||
@inject NavigationManager Navigation
|
|
||||||
#endif*@
|
#endif*@
|
||||||
@*#if (Hosted)
|
@*#if (Hosted)
|
||||||
@using ComponentsWebAssembly_CSharp.Shared
|
@using ComponentsWebAssembly_CSharp.Shared
|
||||||
|
|
@ -11,9 +9,7 @@
|
||||||
@*#if (!NoAuth && Hosted)
|
@*#if (!NoAuth && Hosted)
|
||||||
@attribute [Authorize]
|
@attribute [Authorize]
|
||||||
#endif*@
|
#endif*@
|
||||||
@*#if (NoAuth || !Hosted)
|
|
||||||
@inject HttpClient Http
|
@inject HttpClient Http
|
||||||
#endif*@
|
|
||||||
|
|
||||||
<h1>Weather forecast</h1>
|
<h1>Weather forecast</h1>
|
||||||
|
|
||||||
|
|
@ -55,17 +51,14 @@ else
|
||||||
{
|
{
|
||||||
@*#if (Hosted)
|
@*#if (Hosted)
|
||||||
@*#if (!NoAuth)
|
@*#if (!NoAuth)
|
||||||
var httpClient = new HttpClient();
|
try
|
||||||
httpClient.BaseAddress = new Uri(Navigation.BaseUri);
|
|
||||||
|
|
||||||
var tokenResult = await AuthenticationService.RequestAccessToken();
|
|
||||||
|
|
||||||
if (tokenResult.TryGetToken(out var token, redirect: true))
|
|
||||||
{
|
{
|
||||||
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
|
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
||||||
forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
}
|
||||||
|
catch (AccessTokenNotAvailableException exception)
|
||||||
|
{
|
||||||
|
exception.Redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
#else
|
#else
|
||||||
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
|
||||||
#endif*@
|
#endif*@
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,9 @@ using System.Net.Http;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
#if (!NoAuth && Hosted)
|
||||||
|
using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
|
||||||
|
#endif
|
||||||
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
|
@ -19,7 +22,18 @@ namespace ComponentsWebAssembly_CSharp
|
||||||
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
var builder = WebAssemblyHostBuilder.CreateDefault(args);
|
||||||
builder.RootComponents.Add<App>("app");
|
builder.RootComponents.Add<App>("app");
|
||||||
|
|
||||||
builder.Services.AddSingleton(new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
#if (!Hosted || NoAuth)
|
||||||
|
builder.Services.AddTransient(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });
|
||||||
|
#else
|
||||||
|
builder.Services.AddHttpClient("ComponentsWebAssembly_CSharp.ServerAPI", client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
|
||||||
|
.AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
|
||||||
|
|
||||||
|
// Supply HttpClient instances that include access tokens when making requests to the server project
|
||||||
|
builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("ComponentsWebAssembly_CSharp.ServerAPI"));
|
||||||
|
#endif
|
||||||
|
#if(!NoAuth)
|
||||||
|
|
||||||
|
#endif
|
||||||
#if (IndividualLocalAuth)
|
#if (IndividualLocalAuth)
|
||||||
#if (Hosted)
|
#if (Hosted)
|
||||||
builder.Services.AddApiAuthorization();
|
builder.Services.AddApiAuthorization();
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue