Components auth: basic services and components (#10227)
This commit is contained in:
parent
9f4aa98ee2
commit
1dbc203e19
|
|
@ -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
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -40,6 +40,7 @@ namespace Microsoft.AspNetCore.Components
|
|||
=> type.IsPrimitive
|
||||
|| type == typeof(string)
|
||||
|| type == typeof(DateTime)
|
||||
|| type == typeof(Type)
|
||||
|| type == typeof(decimal);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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)}.");
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue