[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.
This commit is contained in:
Javier Calvarro Nelson 2020-04-13 13:27:53 +02:00 committed by GitHub
parent cc0407877f
commit 546b52004c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 470 additions and 58 deletions

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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)

View File

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

View File

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

View File

@ -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()

View File

@ -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
{
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token.Value}");
forecasts = await httpClient.GetFromJsonAsync<WeatherForecast[]>("WeatherForecast");
forecasts = await WeatherForecast.GetForecastAsync();
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}
public void Dispose()
{
WeatherForecast.Dispose();
}
}

View File

@ -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");

View File

@ -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>

View File

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

View File

@ -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>

View File

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

View File

@ -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*@

View File

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