Ver Fonte

[Blazor][Wasm] Adds HttpMessageHandler to automatically attach tokens to outgoing requests (#20682)

* Adds an authorization handler for integration with HttpClient in different scnearios.
* Adds a message handler to streamline calling protected resources on the same base address.
Javier Calvarro Nelson há 6 anos atrás
pai
commit
546b52004c
18 ficheiros alterados com 469 adições e 57 exclusões
  1. 0 0
      src/Components/Web.JS/dist/Release/blazor.webassembly.js
  2. 0 2
      src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs
  3. 1 26
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs
  4. 124 0
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs
  5. 25 0
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs
  6. 33 0
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs
  7. 2 2
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs
  8. 3 0
      src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs
  9. 208 0
      src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs
  10. 1 2
      src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs
  11. 13 11
      src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor
  12. 5 0
      src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs
  13. 1 0
      src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj
  14. 30 0
      src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs
  15. 1 0
      src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in
  16. 1 0
      src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj
  17. 6 13
      src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor
  18. 15 1
      src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webassembly.js


+ 0 - 2
src/Components/WebAssembly/WebAssembly.Authentication/src/RemoteAuthenticationBuilderExtensions.cs

@@ -26,7 +26,6 @@ namespace Microsoft.Extensions.DependencyInjection
             where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<TAccount>
         {
             builder.Services.Replace(ServiceDescriptor.Scoped<AccountClaimsPrincipalFactory<TAccount>, TAccountClaimsPrincipalFactory>());
-            builder.Services.Replace(ServiceDescriptor.Scoped<AccountClaimsPrincipalFactory<TAccount>, TUserFactory>());
 
             return builder;
         }
@@ -52,6 +51,5 @@ namespace Microsoft.Extensions.DependencyInjection
         public static IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> AddAccountClaimsPrincipalFactory<TAccountClaimsPrincipalFactory>(
             this IRemoteAuthenticationBuilder<RemoteAuthenticationState, RemoteUserAccount> builder)
             where TAccountClaimsPrincipalFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddAccountClaimsPrincipalFactory<RemoteAuthenticationState, RemoteUserAccount, TAccountClaimsPrincipalFactory>();
-            where TUserFactory : AccountClaimsPrincipalFactory<RemoteUserAccount> => builder.AddUserFactory<RemoteAuthenticationState, RemoteUserAccount, TUserFactory>();
     }
 }

+ 1 - 26
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AccessTokenResult.cs

@@ -11,20 +11,17 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
     public class AccessTokenResult
     {
         private readonly AccessToken _token;
-        private readonly NavigationManager _navigation;
 
         /// <summary>
         /// Initializes a new instance of <see cref="AccessTokenResult"/>.
         /// </summary>
         /// <param name="status">The status of the result.</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>
-        public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, NavigationManager navigation, string redirectUrl)
+        public AccessTokenResult(AccessTokenResultStatus status, AccessToken token, string redirectUrl)
         {
             Status = status;
             _token = token;
-            _navigation = navigation;
             RedirectUrl = redirectUrl;
         }
 
@@ -56,27 +53,5 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
                 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;
-            }
-        }
     }
 }

+ 124 - 0
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/AuthorizationMessageHandler.cs

@@ -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;
+        }
+    }
+}

+ 25 - 0
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/BaseAddressAuthorizationMessageHandler.cs

@@ -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 });
+        }
+    }
+}

+ 33 - 0
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/ExpiredTokenException.cs

@@ -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);
+    }
+}

+ 2 - 2
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs

@@ -159,7 +159,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
                 result.RedirectUrl = redirectUrl.ToString();
             }
 
-            return new AccessTokenResult(parsedStatus, result.Token, Navigation, result.RedirectUrl);
+            return new AccessTokenResult(parsedStatus, result.Token, result.RedirectUrl);
         }
 
         /// <inheritdoc />
@@ -184,7 +184,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Authentication
                 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)

+ 3 - 0
src/Components/WebAssembly/WebAssembly.Authentication/src/WebAssemblyAuthenticationServiceCollectionExtensions.cs

@@ -38,6 +38,9 @@ namespace Microsoft.Extensions.DependencyInjection
                     return (IRemoteAuthenticationService<TRemoteAuthenticationState>)sp.GetRequiredService<AuthenticationStateProvider>();
                 });
 
+            services.TryAddTransient<BaseAddressAuthorizationMessageHandler>();
+            services.TryAddTransient<AuthorizationMessageHandler>();
+
             services.TryAddScoped(sp =>
             {
                 return (IAccessTokenProvider)sp.GetRequiredService<AuthenticationStateProvider>();

+ 208 - 0
src/Components/WebAssembly/WebAssembly.Authentication/test/AuthorizationMessageHandlerTests.cs

@@ -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);
+        }
+    }
+}

+ 1 - 2
src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs

@@ -160,8 +160,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
         }
 
         /// <summary>
-        /// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in
-        /// <see cref="Providers"/>.
+        /// 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()

+ 13 - 11
src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Pages/FetchData.razor

@@ -1,9 +1,8 @@
 @page "/fetchdata"
 @using Wasm.Authentication.Shared
+@implements IDisposable
 @attribute [Authorize]
-@inject IAccessTokenProvider AuthenticationService
-@inject NavigationManager Navigation
-
+@inject WeatherForecastClient WeatherForecast
 <h1>Weather forecast</h1>
 
 <p>This component demonstrates fetching data from the server.</p>
@@ -42,15 +41,18 @@ else
 
     protected override async Task OnInitializedAsync()
     {
-        var httpClient = new HttpClient();
-        httpClient.BaseAddress = new Uri(Navigation.BaseUri);
-
-        var tokenResult = await AuthenticationService.RequestAccessToken();
-
-        if (tokenResult.TryGetToken(out var token, redirect: true))
+        try
+        {
+            forecasts = await WeatherForecast.GetForecastAsync();
+        }
+        catch (AccessTokenNotAvailableException exception)
         {
-            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
-            forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
+            exception.Redirect();
         }
     }
+
+    public void Dispose()
+    {
+        WeatherForecast.Dispose();
+    }
 }

+ 5 - 0
src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Program.cs

@@ -1,6 +1,8 @@
 // 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.Http;
 using System.Threading.Tasks;
 using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
@@ -17,6 +19,9 @@ namespace Wasm.Authentication.Client
             builder.Services.AddApiAuthorization<RemoteAppState, OidcAccount>()
                 .AddAccountClaimsPrincipalFactory<RemoteAppState, OidcAccount, PreferencesUserFactory>();
 
+            builder.Services.AddHttpClient<WeatherForecastClient>(client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
+                .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
+
             builder.Services.AddSingleton<StateService>();
 
             builder.RootComponents.Add<App>("app");

+ 1 - 0
src/Components/WebAssembly/testassets/Wasm.Authentication.Client/Wasm.Authentication.Client.csproj

@@ -10,6 +10,7 @@
     <Reference Include="System.Net.Http.Json" />
     <Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
     <Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
+    <Reference Include="Microsoft.Extensions.Http" />
   </ItemGroup>
 
   <ItemGroup>

+ 30 - 0
src/Components/WebAssembly/testassets/Wasm.Authentication.Client/WeatherForecastClient.cs

@@ -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();
+        }
+    }
+}

+ 1 - 0
src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/ComponentsWebAssembly-CSharp.Client.csproj.in

@@ -17,6 +17,7 @@
     <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.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}" />
   </ItemGroup>
 

+ 1 - 0
src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/Microsoft.AspNetCore.Components.WebAssembly.Templates.csproj

@@ -29,6 +29,7 @@
       MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
       MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
       MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion);
+      MicrosoftExtensionsHttpPackageVersion=$(MicrosoftExtensionsHttpPackageVersion);
       SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion)
     </GeneratedContentProperties>
   </PropertyGroup>

+ 6 - 13
src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Pages/FetchData.razor

@@ -2,8 +2,6 @@
 @*#if  (!NoAuth && Hosted)
 @using Microsoft.AspNetCore.Authorization
 @using Microsoft.AspNetCore.Components.WebAssembly.Authentication
-@inject IAccessTokenProvider AuthenticationService
-@inject NavigationManager Navigation
 #endif*@
 @*#if  (Hosted)
 @using ComponentsWebAssembly_CSharp.Shared
@@ -11,9 +9,7 @@
 @*#if  (!NoAuth && Hosted)
 @attribute [Authorize]
 #endif*@
-@*#if  (NoAuth || !Hosted)
 @inject HttpClient Http
-#endif*@
 
 <h1>Weather forecast</h1>
 
@@ -55,17 +51,14 @@ else
     {
         @*#if (Hosted)
             @*#if (!NoAuth)
-        var httpClient = new HttpClient();
-        httpClient.BaseAddress = new Uri(Navigation.BaseUri);
-
-        var tokenResult = await AuthenticationService.RequestAccessToken();
-
-        if (tokenResult.TryGetToken(out var token, redirect: true))
+        try
         {
-            httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
-            forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
+            forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
+        }
+        catch (AccessTokenNotAvailableException exception)
+        {
+            exception.Redirect();
         }
-
             #else
         forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
             #endif*@

+ 15 - 1
src/ProjectTemplates/ComponentsWebAssembly.ProjectTemplates/content/ComponentsWebAssembly-CSharp/Client/Program.cs

@@ -3,6 +3,9 @@ using System.Net.Http;
 using System.Collections.Generic;
 using System.Threading.Tasks;
 using System.Text;
+#if (!NoAuth && Hosted)
+using Microsoft.AspNetCore.Components.WebAssembly.Authentication;
+#endif
 using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
 using Microsoft.Extensions.DependencyInjection;
 
@@ -19,7 +22,18 @@ namespace ComponentsWebAssembly_CSharp
             var builder = WebAssemblyHostBuilder.CreateDefault(args);
             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 (Hosted)
             builder.Services.AddApiAuthorization();

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff