Components auth: basic services and components (#10227)

This commit is contained in:
Steve Sanderson 2019-05-16 09:59:12 +01:00 committed by GitHub
parent 9f4aa98ee2
commit 1dbc203e19
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 855 additions and 2 deletions

View File

@ -3,6 +3,34 @@
namespace Microsoft.AspNetCore.Components
{
public partial class AuthenticationState
{
public AuthenticationState(System.Security.Claims.ClaimsPrincipal user) { }
public System.Security.Claims.ClaimsPrincipal User { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
}
public delegate void AuthenticationStateChangedHandler(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> task);
public abstract partial class AuthenticationStateProvider
{
protected AuthenticationStateProvider() { }
public event Microsoft.AspNetCore.Components.AuthenticationStateChangedHandler AuthenticationStateChanged { add { } remove { } }
public abstract System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> GetAuthenticationStateAsync();
protected void NotifyAuthenticationStateChanged(System.Threading.Tasks.Task<Microsoft.AspNetCore.Components.AuthenticationState> task) { }
}
public partial class AuthorizeView : Microsoft.AspNetCore.Components.ComponentBase
{
public AuthorizeView() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> Authorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }
}
[Microsoft.AspNetCore.Components.BindElementAttribute("select", null, "value", "onchange")]
[Microsoft.AspNetCore.Components.BindElementAttribute("textarea", null, "value", "onchange")]
[Microsoft.AspNetCore.Components.BindInputElementAttribute("checkbox", null, "checked", "onchange")]
@ -57,6 +85,15 @@ namespace Microsoft.AspNetCore.Components
public static System.Action<Microsoft.AspNetCore.Components.UIEventArgs> SetValueHandler(System.Action<string> setter, string existingValue) { throw null; }
public static System.Action<Microsoft.AspNetCore.Components.UIEventArgs> SetValueHandler<T>(System.Action<T> setter, T existingValue) { throw null; }
}
public partial class CascadingAuthenticationState : Microsoft.AspNetCore.Components.ComponentBase, System.IDisposable
{
public CascadingAuthenticationState() { }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
protected override void OnInit() { }
void System.IDisposable.Dispose() { }
}
[System.AttributeUsageAttribute(System.AttributeTargets.Property, AllowMultiple=false, Inherited=false)]
public sealed partial class CascadingParameterAttribute : System.Attribute
{

View File

@ -0,0 +1,28 @@
// 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.Security.Claims;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Provides information about the currently authenticated user, if any.
/// </summary>
public class AuthenticationState
{
/// <summary>
/// Gets a <see cref="ClaimsPrincipal"/> that describes the current user.
/// </summary>
public ClaimsPrincipal User { get; }
/// <summary>
/// Constructs an instance of <see cref="AuthenticationState"/>.
/// </summary>
/// <param name="user">A <see cref="ClaimsPrincipal"/> representing the user.</param>
public AuthenticationState(ClaimsPrincipal user)
{
User = user ?? throw new ArgumentNullException(nameof(user));
}
}
}

View File

@ -0,0 +1,49 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
/// <summary>
/// Provides information about the authentication state of the current user.
/// </summary>
public abstract class AuthenticationStateProvider
{
/// <summary>
/// Gets an <see cref="AuthenticationState"/> instance that describes
/// the current user.
/// </summary>
/// <returns>An <see cref="AuthenticationState"/> instance that describes the current user.</returns>
public abstract Task<AuthenticationState> GetAuthenticationStateAsync();
/// <summary>
/// An event that provides notification when the <see cref="AuthenticationState"/>
/// has changed. For example, this event may be raised if a user logs in or out.
/// </summary>
#pragma warning disable 0067 // "Never used" (it's only raised by subclasses)
public event AuthenticationStateChangedHandler AuthenticationStateChanged;
#pragma warning restore 0067
/// <summary>
/// Raises the <see cref="AuthenticationStateChanged"/> event.
/// </summary>
/// <param name="task">A <see cref="Task"/> that supplies the updated <see cref="AuthenticationState"/>.</param>
protected void NotifyAuthenticationStateChanged(Task<AuthenticationState> task)
{
if (task == null)
{
throw new ArgumentNullException(nameof(task));
}
AuthenticationStateChanged?.Invoke(task);
}
}
/// <summary>
/// A handler for the <see cref="AuthenticationStateProvider.AuthenticationStateChanged"/> event.
/// </summary>
/// <param name="task">A <see cref="Task"/> that supplies the updated <see cref="AuthenticationState"/>.</param>
public delegate void AuthenticationStateChangedHandler(Task<AuthenticationState> task);
}

View File

@ -0,0 +1,68 @@
@namespace Microsoft.AspNetCore.Components
@if (currentAuthenticationState == null)
{
@Authorizing
}
else if (IsAuthorized())
{
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
}
else
{
@NotAuthorized
}
@functions {
private AuthenticationState currentAuthenticationState;
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> ChildContent { get; private set; }
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment NotAuthorized { get; private set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
/// If you specify a value for this parameter, do not also specify a value for <see cref="ChildContent"/>.
/// </summary>
[Parameter] public RenderFragment<AuthenticationState> Authorized { get; private set; }
/// <summary>
/// The content that will be displayed while asynchronous authorization is in progress.
/// </summary>
[Parameter] public RenderFragment Authorizing { get; private set; }
protected override async Task OnParametersSetAsync()
{
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
// with 'NotAuthorized' in other cases. Besides naming, they are equivalent. To avoid
// confusion, explicitly prevent the case where both are supplied.
if (ChildContent != null && Authorized != null)
{
throw new InvalidOperationException($"When using {nameof(AuthorizeView)}, do not specify both '{nameof(Authorized)}' and '{nameof(ChildContent)}'.");
}
// First render in pending state
// If the task has already completed, this render will be skipped
currentAuthenticationState = null;
// Then render in completed state
currentAuthenticationState = await AuthenticationState;
}
private bool IsAuthorized()
{
// TODO: Support various authorization condition parameters, equivalent to those offered
// by the [Authorize] attribute, e.g., "Roles" and "Policy". This is on hold until we're
// able to reference the policy evaluator APIs from this package.
return currentAuthenticationState.User?.Identity?.IsAuthenticated == true;
}
}

View File

@ -0,0 +1,36 @@
@namespace Microsoft.AspNetCore.Components
@implements IDisposable
@inject AuthenticationStateProvider AuthenticationStateProvider
<CascadingValue T="Task<AuthenticationState>" Value="@_currentAuthenticationStateTask" ChildContent="@ChildContent" />
@functions {
private Task<AuthenticationState> _currentAuthenticationStateTask;
/// <summary>
/// The content to which the authentication state should be provided.
/// </summary>
[Parameter] public RenderFragment ChildContent { get; private set; }
protected override void OnInit()
{
AuthenticationStateProvider.AuthenticationStateChanged += OnAuthenticationStateChanged;
_currentAuthenticationStateTask = AuthenticationStateProvider
.GetAuthenticationStateAsync();
}
private void OnAuthenticationStateChanged(Task<AuthenticationState> newAuthStateTask)
{
Invoke(() =>
{
_currentAuthenticationStateTask = newAuthStateTask;
StateHasChanged();
});
}
void IDisposable.Dispose()
{
AuthenticationStateProvider.AuthenticationStateChanged -= OnAuthenticationStateChanged;
}
}

View File

@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Components
=> type.IsPrimitive
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Type)
|| type == typeof(decimal);
}
}

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Razor">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
@ -6,6 +6,7 @@
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsShippingPackage>true</IsShippingPackage>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>
<ItemGroup>

View File

@ -0,0 +1,308 @@
// 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.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class AuthorizeViewTest
{
[Fact]
public void RendersNothingIfNotAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
childContent:
context => builder => builder.AddContent(0, "This should not be rendered"));
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Empty(diff.Edits);
}
[Fact]
public void RendersNotAuthorizedContentIfNotAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
childContent:
context => builder => builder.AddContent(0, "This should not be rendered"),
notAuthorizedContent:
builder => builder.AddContent(0, "You are not authorized"));
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are not authorized");
});
}
[Fact]
public void RendersNothingIfAuthorizedButNoChildContentOrAuthorizedContentProvided()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView();
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Empty(diff.Edits);
}
[Fact]
public void RendersChildContentIfAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
childContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Nellie");
});
}
[Fact]
public void RendersAuthorizedContentIfAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
authorizedContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Nellie");
});
}
[Fact]
public void RespondsToChangeInAuthorizationState()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
childContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Render in initial state. From other tests, we know this renders
// a single batch with the correct output.
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var authorizeViewComponentId = renderer.Batches.Single()
.GetComponentFrames<AuthorizeView>().Single().ComponentId;
// Act
rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
rootComponent.TriggerRender();
// Assert: It's only one new diff. We skip the intermediate "await" render state
// because the task was completed synchronously.
Assert.Equal(2, renderer.Batches.Count);
var batch = renderer.Batches.Last();
var diff = batch.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Ronaldo");
});
}
[Fact]
public void ThrowsIfBothChildContentAndAuthorizedContentProvided()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
authorizedContent: context => builder => { },
childContent: context => builder => { });
// Act/Assert
renderer.AssignRootComponentId(rootComponent);
var ex = Assert.Throws<InvalidOperationException>(() =>
rootComponent.TriggerRender());
Assert.Equal("When using AuthorizeView, do not specify both 'Authorized' and 'ChildContent'.", ex.Message);
}
[Fact]
public void RendersNothingUntilAuthorizationCompleted()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
notAuthorizedContent: builder => builder.AddContent(0, "You are not authorized"));
var authTcs = new TaskCompletionSource<AuthenticationState>();
rootComponent.AuthenticationState = authTcs.Task;
// Act/Assert 1: Auth pending
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var batch1 = renderer.Batches.Single();
var authorizeViewComponentId = batch1.GetComponentFrames<AuthorizeView>().Single().ComponentId;
var diff1 = batch1.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Empty(diff1.Edits);
// Act/Assert 2: Auth process completes asynchronously
authTcs.SetResult(new AuthenticationState(new ClaimsPrincipal()));
Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff2.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
"You are not authorized");
});
}
[Fact]
public void RendersAuthorizingContentUntilAuthorizationCompleted()
{
// Arrange
var renderer = new TestRenderer();
var rootComponent = WrapInAuthorizeView(
authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
var authTcs = new TaskCompletionSource<AuthenticationState>();
rootComponent.AuthenticationState = authTcs.Task;
// Act/Assert 1: Auth pending
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
var batch1 = renderer.Batches.Single();
var authorizeViewComponentId = batch1.GetComponentFrames<AuthorizeView>().Single().ComponentId;
var diff1 = batch1.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff1.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch1.ReferenceFrames[edit.ReferenceFrameIndex],
"Auth pending...");
});
// Act/Assert 2: Auth process completes asynchronously
authTcs.SetResult(CreateAuthenticationState("Monsieur").Result);
Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches[1];
var diff2 = batch2.DiffsByComponentId[authorizeViewComponentId].Single();
Assert.Collection(diff2.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.RemoveFrame, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
},
edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Text(
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
"Hello, Monsieur!");
});
}
private static TestAuthStateProviderComponent WrapInAuthorizeView(
RenderFragment<AuthenticationState> childContent = null,
RenderFragment<AuthenticationState> authorizedContent = null,
RenderFragment notAuthorizedContent = null,
RenderFragment authorizingContent = null)
{
return new TestAuthStateProviderComponent(builder =>
{
builder.OpenComponent<AuthorizeView>(0);
builder.AddAttribute(1, nameof(AuthorizeView.ChildContent), childContent);
builder.AddAttribute(2, nameof(AuthorizeView.Authorized), authorizedContent);
builder.AddAttribute(3, nameof(AuthorizeView.NotAuthorized), notAuthorizedContent);
builder.AddAttribute(4, nameof(AuthorizeView.Authorizing), authorizingContent);
builder.CloseComponent();
});
}
class TestAuthStateProviderComponent : AutoRenderComponent
{
private readonly RenderFragment _childContent;
public Task<AuthenticationState> AuthenticationState { get; set; }
= Task.FromResult(new AuthenticationState(new ClaimsPrincipal()));
public TestAuthStateProviderComponent(RenderFragment childContent)
{
_childContent = childContent;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
builder.AddAttribute(1, nameof(CascadingValue<Task<AuthenticationState>>.Value), AuthenticationState);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, _childContent);
builder.CloseComponent();
}
}
public static Task<AuthenticationState> CreateAuthenticationState(string username)
=> Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = username })));
class TestIdentity : IIdentity
{
public string AuthenticationType => "Test";
public bool IsAuthenticated => true;
public string Name { get; set; }
}
}
}

View File

@ -0,0 +1,221 @@
// 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.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components
{
public class CascadingAuthenticationStateTest
{
[Fact]
public void RequiresRegisteredService()
{
// Arrange
var renderer = new TestRenderer();
var component = new AutoRenderFragmentComponent(builder =>
{
builder.OpenComponent<CascadingAuthenticationState>(0);
builder.CloseComponent();
});
// Act/Assert
renderer.AssignRootComponentId(component);
var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
Assert.Contains($"There is no registered service of type '{typeof(AuthenticationStateProvider).FullName}'.", ex.Message);
}
[Fact]
public void SuppliesSynchronouslyAvailableAuthStateToChildContent()
{
// Arrange: Service
var services = new ServiceCollection();
var authStateProvider = new TestAuthStateProvider()
{
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState("Bert"))
};
services.AddSingleton<AuthenticationStateProvider>(authStateProvider);
// Arrange: Renderer and component
var renderer = new TestRenderer(services.BuildServiceProvider());
var component = new UseCascadingAuthenticationStateComponent();
// Act
renderer.AssignRootComponentId(component);
component.TriggerRender();
// Assert
var batch = renderer.Batches.Single();
var receiveAuthStateId = batch.GetComponentFrames<ReceiveAuthStateComponent>().Single().ComponentId;
var receiveAuthStateDiff = batch.DiffsByComponentId[receiveAuthStateId].Single();
Assert.Collection(receiveAuthStateDiff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Authenticated: True; Name: Bert; Pending: False; Renders: 1");
});
}
[Fact]
public void SuppliesAsynchronouslyAvailableAuthStateToChildContent()
{
// Arrange: Service
var services = new ServiceCollection();
var authStateTaskCompletionSource = new TaskCompletionSource<AuthenticationState>();
var authStateProvider = new TestAuthStateProvider()
{
CurrentAuthStateTask = authStateTaskCompletionSource.Task
};
services.AddSingleton<AuthenticationStateProvider>(authStateProvider);
// Arrange: Renderer and component
var renderer = new TestRenderer(services.BuildServiceProvider());
var component = new UseCascadingAuthenticationStateComponent();
// Act 1: Initial synchronous render
renderer.AssignRootComponentId(component);
component.TriggerRender();
// Assert 1: Empty state
var batch1 = renderer.Batches.Single();
var receiveAuthStateFrame = batch1.GetComponentFrames<ReceiveAuthStateComponent>().Single();
var receiveAuthStateId = receiveAuthStateFrame.ComponentId;
var receiveAuthStateComponent = (ReceiveAuthStateComponent)receiveAuthStateFrame.Component;
var receiveAuthStateDiff1 = batch1.DiffsByComponentId[receiveAuthStateId].Single();
Assert.Collection(receiveAuthStateDiff1.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
batch1.ReferenceFrames[edit.ReferenceFrameIndex],
"Authenticated: False; Name: ; Pending: True; Renders: 1");
});
// Act/Assert 2: Auth state fetch task completes in background
// No new renders yet, because the cascading parameter itself hasn't changed
authStateTaskCompletionSource.SetResult(CreateAuthenticationState("Bert"));
Assert.Single(renderer.Batches);
// Act/Assert 3: Refresh display
receiveAuthStateComponent.TriggerRender();
Assert.Equal(2, renderer.Batches.Count);
var batch2 = renderer.Batches.Last();
var receiveAuthStateDiff2 = batch2.DiffsByComponentId[receiveAuthStateId].Single();
Assert.Collection(receiveAuthStateDiff2.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
"Authenticated: True; Name: Bert; Pending: False; Renders: 2");
});
}
[Fact]
public void RespondsToNotificationsFromAuthenticationStateProvider()
{
// Arrange: Service
var services = new ServiceCollection();
var authStateProvider = new TestAuthStateProvider()
{
CurrentAuthStateTask = Task.FromResult(CreateAuthenticationState(null))
};
services.AddSingleton<AuthenticationStateProvider>(authStateProvider);
// Arrange: Renderer and component, initially rendered
var renderer = new TestRenderer(services.BuildServiceProvider());
var component = new UseCascadingAuthenticationStateComponent();
renderer.AssignRootComponentId(component);
component.TriggerRender();
var receiveAuthStateId = renderer.Batches.Single()
.GetComponentFrames<ReceiveAuthStateComponent>().Single().ComponentId;
// Act 2: AuthenticationStateProvider issues notification
authStateProvider.TriggerAuthenticationStateChanged(
Task.FromResult(CreateAuthenticationState("Bert")));
// Assert 2: Re-renders content
Assert.Equal(2, renderer.Batches.Count);
var batch = renderer.Batches.Last();
var receiveAuthStateDiff = batch.DiffsByComponentId[receiveAuthStateId].Single();
Assert.Collection(receiveAuthStateDiff.Edits, edit =>
{
Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
AssertFrame.Text(
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"Authenticated: True; Name: Bert; Pending: False; Renders: 2");
});
}
class ReceiveAuthStateComponent : AutoRenderComponent
{
int numRenders;
[CascadingParameter] Task<AuthenticationState> AuthStateTask { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
numRenders++;
if (AuthStateTask.IsCompleted)
{
var identity = AuthStateTask.Result.User.Identity;
builder.AddContent(0, $"Authenticated: {identity.IsAuthenticated}; Name: {identity.Name}; Pending: False; Renders: {numRenders}");
}
else
{
builder.AddContent(0, $"Authenticated: False; Name: ; Pending: True; Renders: {numRenders}");
}
}
}
class UseCascadingAuthenticationStateComponent : AutoRenderComponent
{
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.OpenComponent<CascadingAuthenticationState>(0);
builder.AddAttribute(1, RenderTreeBuilder.ChildContent, new RenderFragment(childBuilder =>
{
childBuilder.OpenComponent<ReceiveAuthStateComponent>(0);
childBuilder.CloseComponent();
}));
builder.CloseComponent();
}
}
class TestAuthStateProvider : AuthenticationStateProvider
{
public Task<AuthenticationState> CurrentAuthStateTask { get; set; }
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{
return CurrentAuthStateTask;
}
internal void TriggerAuthenticationStateChanged(Task<AuthenticationState> authState)
{
NotifyAuthenticationStateChanged(authState);
}
}
public static AuthenticationState CreateAuthenticationState(string username)
=> new AuthenticationState(new ClaimsPrincipal(username == null
? new ClaimsIdentity()
: (IIdentity)new TestIdentity { Name = username }));
class TestIdentity : IIdentity
{
public string AuthenticationType => "Test";
public bool IsAuthenticated => true;
public string Name { get; set; }
}
}
}

View File

@ -44,6 +44,10 @@ namespace Microsoft.AspNetCore.Components.Server.Circuits
jsRuntime.Initialize(client);
componentContext.Initialize(client);
// You can replace the AuthenticationStateProvider with a custom one, but in that case initialization is up to you
var authenticationStateProvider = scope.ServiceProvider.GetService<AuthenticationStateProvider>();
(authenticationStateProvider as FixedAuthenticationStateProvider)?.Initialize(httpContext.User);
var uriHelper = (RemoteUriHelper)scope.ServiceProvider.GetRequiredService<IUriHelper>();
if (client.Connected)
{

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.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Components.Server.Circuits
{
/// <summary>
/// An <see cref="AuthenticationStateProvider"/> intended for use in server-side
/// Blazor. The circuit factory will supply a <see cref="ClaimsPrincipal"/> from
/// the current <see cref="HttpContext.User"/>, which will stay fixed for the
/// lifetime of the circuit since <see cref="HttpContext.User"/> cannot change.
///
/// This can therefore only be used with redirect-style authentication flows,
/// since it requires a new HTTP request in order to become a different user.
/// </summary>
internal class FixedAuthenticationStateProvider : AuthenticationStateProvider
{
private Task<AuthenticationState> _authenticationStateTask;
public void Initialize(ClaimsPrincipal user)
{
_authenticationStateTask = Task.FromResult(new AuthenticationState(user));
}
public override Task<AuthenticationState> GetAuthenticationStateAsync()
=> _authenticationStateTask
?? throw new InvalidOperationException($"{nameof(GetAuthenticationStateAsync)} was called before {nameof(Initialize)}.");
}
}

View File

@ -67,6 +67,7 @@ namespace Microsoft.Extensions.DependencyInjection
services.AddScoped<IUriHelper, RemoteUriHelper>();
services.AddScoped<IJSRuntime, RemoteJSRuntime>();
services.AddScoped<IComponentContext, RemoteComponentContext>();
services.AddScoped<AuthenticationStateProvider, FixedAuthenticationStateProvider>();
return builder;
}

View File

@ -0,0 +1,39 @@
// 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.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.Server.Circuits;
using Microsoft.AspNetCore.Http;
using Xunit;
namespace Microsoft.AspNetCore.Components.Server.Tests.Circuits
{
public class FixedAuthenticationStateProviderTest
{
[Fact]
public async Task CannotProvideAuthenticationStateBeforeInitialization()
{
await Assert.ThrowsAsync<InvalidOperationException>(() =>
new FixedAuthenticationStateProvider()
.GetAuthenticationStateAsync());
}
[Fact]
public async Task SuppliesAuthenticationStateWithFixedUser()
{
// Arrange
var user = new ClaimsPrincipal();
var provider = new FixedAuthenticationStateProvider();
provider.Initialize(user);
// Act
var authenticationState = await provider.GetAuthenticationStateAsync();
// Assert
Assert.NotNull(authenticationState);
Assert.Same(user, authenticationState.User);
}
}
}

View File

@ -0,0 +1,20 @@
// 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 Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Test.Helpers
{
public class AutoRenderFragmentComponent : AutoRenderComponent
{
private readonly RenderFragment _renderFragment;
public AutoRenderFragmentComponent(RenderFragment renderFragment)
{
_renderFragment = renderFragment;
}
protected override void BuildRenderTree(RenderTreeBuilder builder)
=> _renderFragment(builder);
}
}

View File

@ -1,8 +1,9 @@
// 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.
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Test.Helpers
@ -18,6 +19,12 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
public IList<int> DisposedComponentIDs { get; set; }
public RenderTreeFrame[] ReferenceFrames { get; set; }
public IEnumerable<RenderTreeFrame> GetComponentFrames<T>() where T : IComponent
=> ReferenceFrames.Where(f => f.FrameType == RenderTreeFrameType.Component && f.Component is T);
public IEnumerable<RenderTreeDiff> GetComponentDiffs<T>() where T : IComponent
=> GetComponentFrames<T>().SelectMany(f => DiffsByComponentId[f.ComponentId]);
internal void AddDiff(RenderTreeDiff diff)
{
var componentId = diff.ComponentId;