[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:
parent
cc0407877f
commit
546b52004c
File diff suppressed because one or more lines are too long
|
|
@ -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>();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@
|
|||
MicrosoftEntityFrameworkCoreSqlServerPackageVersion=$(MicrosoftEntityFrameworkCoreSqlServerPackageVersion);
|
||||
MicrosoftEntityFrameworkCoreSqlitePackageVersion=$(MicrosoftEntityFrameworkCoreSqlitePackageVersion);
|
||||
MicrosoftEntityFrameworkCoreToolsPackageVersion=$(MicrosoftEntityFrameworkCoreToolsPackageVersion);
|
||||
MicrosoftExtensionsHttpPackageVersion=$(MicrosoftExtensionsHttpPackageVersion);
|
||||
SystemNetHttpJsonPackageVersion=$(SystemNetHttpJsonPackageVersion)
|
||||
</GeneratedContentProperties>
|
||||
</PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -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*@
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
Loading…
Reference in New Issue