Integrate AuthorizeView with actual authorization (#10487)

This commit is contained in:
Steve Sanderson 2019-05-24 15:28:37 +01:00 committed by GitHub
parent 405d8bbdc9
commit d18a033b1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 529 additions and 51 deletions

View File

@ -8,6 +8,8 @@ using Microsoft.AspNetCore.Blazor.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
namespace Microsoft.AspNetCore.Blazor.Hosting
@ -92,6 +94,7 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
services.AddSingleton<IComponentContext, WebAssemblyComponentContext>();
services.AddSingleton<IUriHelper>(WebAssemblyUriHelper.Instance);
services.AddSingleton<INavigationInterception>(WebAssemblyNavigationInterception.Instance);
services.AddSingleton<ILoggerFactory, WebAssemblyLoggerFactory>();
services.AddSingleton<HttpClient>(s =>
{
// Creating the URI helper needs to wait until the JS Runtime is initialized, so defer it.
@ -102,6 +105,10 @@ namespace Microsoft.AspNetCore.Blazor.Hosting
};
});
// Needed for authorization
services.AddOptions();
services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(WebAssemblyConsoleLogger<>)));
foreach (var configureServicesAction in _configureServicesActions)
{
configureServicesAction(_BrowserHostBuilderContext, services);

View File

@ -0,0 +1,34 @@
// 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 Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Blazor.Services
{
internal class WebAssemblyConsoleLogger<T> : ILogger<T>, ILogger
{
public IDisposable BeginScope<TState>(TState state)
{
return NoOpDisposable.Instance;
}
public bool IsEnabled(LogLevel logLevel)
{
return logLevel >= LogLevel.Warning;
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
var formattedMessage = formatter(state, exception);
Console.WriteLine($"[{logLevel}] {formattedMessage}");
}
private class NoOpDisposable : IDisposable
{
public static NoOpDisposable Instance = new NoOpDisposable();
public void Dispose() { }
}
}
}

View File

@ -0,0 +1,23 @@
// 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.Extensions.Logging;
namespace Microsoft.AspNetCore.Blazor.Services
{
internal class WebAssemblyLoggerFactory : ILoggerFactory
{
public void AddProvider(ILoggerProvider provider)
{
// No-op
}
public ILogger CreateLogger(string categoryName)
=> new WebAssemblyConsoleLogger<object>();
public void Dispose()
{
// No-op
}
}
}

View File

@ -5,6 +5,7 @@
</PropertyGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0'">
<Compile Include="Microsoft.AspNetCore.Components.netstandard2.0.cs" />
<Reference Include="Microsoft.AspNetCore.Authorization" />
<Reference Include="Microsoft.JSInterop" />
<Reference Include="System.ComponentModel.Annotations" />
</ItemGroup>

View File

@ -59,7 +59,13 @@ namespace Microsoft.AspNetCore.Components
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> ChildContent { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
public Microsoft.AspNetCore.Components.RenderFragment<Microsoft.AspNetCore.Components.AuthenticationState> NotAuthorized { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string Policy { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public string Roles { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public object Resource { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task OnParametersSetAsync() { throw null; }

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 Microsoft.AspNetCore.Authorization;
namespace Microsoft.AspNetCore.Components
{
// This is so the AuthorizeView can avoid implementing IAuthorizeData (even privately)
internal class AuthorizeDataAdapter : IAuthorizeData
{
private readonly AuthorizeView _component;
public AuthorizeDataAdapter(AuthorizeView component)
{
_component = component ?? throw new ArgumentNullException(nameof(component));
}
public string Policy
{
get => _component.Policy;
set => throw new NotSupportedException();
}
public string Roles
{
get => _component.Roles;
set => throw new NotSupportedException();
}
// AuthorizeView doesn't expose any such parameter, as it wouldn't be used anyway,
// since we already have the ClaimsPrincipal by the time AuthorizeView gets involved.
public string AuthenticationSchemes
{
get => null;
set => throw new NotSupportedException();
}
}
}

View File

@ -1,20 +1,26 @@
@namespace Microsoft.AspNetCore.Components
@using System.Security.Claims
@using Microsoft.AspNetCore.Authorization
@inject IAuthorizationService AuthorizationService
@inject IAuthorizationPolicyProvider AuthorizationPolicyProvider
@if (currentAuthenticationState == null)
{
@Authorizing
}
else if (IsAuthorized())
else if (isAuthorized)
{
@((Authorized ?? ChildContent)?.Invoke(currentAuthenticationState))
}
else
{
@NotAuthorized
@(NotAuthorized?.Invoke(currentAuthenticationState))
}
@functions {
private IAuthorizeData[] selfAsAuthorizeData;
private AuthenticationState currentAuthenticationState;
private bool isAuthorized;
[CascadingParameter] private Task<AuthenticationState> AuthenticationState { get; set; }
@ -26,7 +32,7 @@ else
/// <summary>
/// The content that will be displayed if the user is not authorized.
/// </summary>
[Parameter] public RenderFragment NotAuthorized { get; private set; }
[Parameter] public RenderFragment<AuthenticationState> NotAuthorized { get; private set; }
/// <summary>
/// The content that will be displayed if the user is authorized.
@ -39,6 +45,29 @@ else
/// </summary>
[Parameter] public RenderFragment Authorizing { get; private set; }
/// <summary>
/// The policy name that determines whether the content can be displayed.
/// </summary>
[Parameter] public string Policy { get; private set; }
/// <summary>
/// A comma delimited list of roles that are allowed to display the content.
/// </summary>
[Parameter] public string Roles { get; private set; }
/// <summary>
/// The resource to which access is being controlled.
/// </summary>
[Parameter] public object Resource { get; private set; }
protected override void OnInit()
{
selfAsAuthorizeData = new[]
{
new AuthorizeDataAdapter((AuthorizeView)(object)this)
};
}
protected override async Task OnParametersSetAsync()
{
// We allow 'ChildContent' for convenience in basic cases, and 'Authorized' for symmetry
@ -54,15 +83,17 @@ else
currentAuthenticationState = null;
// Then render in completed state
// Importantly, we *don't* call StateHasChanged between the following async steps,
// otherwise we'd display an incorrect UI state while waiting for IsAuthorizedAsync
currentAuthenticationState = await AuthenticationState;
isAuthorized = await IsAuthorizedAsync(currentAuthenticationState.User);
}
private bool IsAuthorized()
private async Task<bool> IsAuthorizedAsync(ClaimsPrincipal user)
{
// 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;
var policy = await AuthorizationPolicy.CombineAsync(
AuthorizationPolicyProvider, selfAsAuthorizeData);
var result = await AuthorizationService.AuthorizeAsync(user, Resource, policy);
return result.Succeeded;
}
}

View File

@ -10,6 +10,7 @@
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Authorization" />
<Reference Include="Microsoft.JSInterop" />
<Reference Include="System.ComponentModel.Annotations" />
</ItemGroup>

View File

@ -2,14 +2,18 @@
// 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.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Infrastructure;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Components
@ -24,7 +28,8 @@ namespace Microsoft.AspNetCore.Components
public void RendersNothingIfNotAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
childContent:
context => builder => builder.AddContent(0, "This should not be rendered"));
@ -36,18 +41,27 @@ namespace Microsoft.AspNetCore.Components
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Empty(diff.Edits);
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Null(call.user.Identity);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void RendersNotAuthorizedContentIfNotAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
childContent:
context => builder => builder.AddContent(0, "This should not be rendered"),
notAuthorizedContent:
builder => builder.AddContent(0, "You are not authorized"));
context => builder => builder.AddContent(0, $"You are not authorized, even though we know you are {context.User.Identity.Name}"));
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
@ -60,7 +74,16 @@ namespace Microsoft.AspNetCore.Components
Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
AssertFrame.Text(
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are not authorized");
"You are not authorized, even though we know you are Nellie");
});
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
@ -68,7 +91,9 @@ namespace Microsoft.AspNetCore.Components
public void RendersNothingIfAuthorizedButNoChildContentOrAuthorizedContentProvided()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView();
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
@ -79,13 +104,24 @@ namespace Microsoft.AspNetCore.Components
// Assert
var diff = renderer.Batches.Single().GetComponentDiffs<AuthorizeView>().Single();
Assert.Empty(diff.Edits);
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void RendersChildContentIfAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
childContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
@ -104,13 +140,24 @@ namespace Microsoft.AspNetCore.Components
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Nellie");
});
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void RendersAuthorizedContentIfAuthorized()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
authorizedContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
@ -129,13 +176,24 @@ namespace Microsoft.AspNetCore.Components
renderer.Batches.Single().ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Nellie");
});
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void RespondsToChangeInAuthorizationState()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
childContent: context => builder =>
builder.AddContent(0, $"You are authenticated as {context.User.Identity.Name}"));
@ -147,6 +205,7 @@ namespace Microsoft.AspNetCore.Components
rootComponent.TriggerRender();
var authorizeViewComponentId = renderer.Batches.Single()
.GetComponentFrames<AuthorizeView>().Single().ComponentId;
authorizationService.AuthorizeCalls.Clear();
// Act
rootComponent.AuthenticationState = CreateAuthenticationState("Ronaldo");
@ -164,13 +223,23 @@ namespace Microsoft.AspNetCore.Components
batch.ReferenceFrames[edit.ReferenceFrameIndex],
"You are authenticated as Ronaldo");
});
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Ronaldo", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void ThrowsIfBothChildContentAndAuthorizedContentProvided()
{
// Arrange
var renderer = new TestRenderer();
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(
authorizedContent: context => builder => { },
childContent: context => builder => { });
@ -187,12 +256,12 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
OnUpdateDisplayComplete = () => { @event.Set(); },
};
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
var rootComponent = WrapInAuthorizeView(
notAuthorizedContent: builder => builder.AddContent(0, "You are not authorized"));
notAuthorizedContent:
context => builder => builder.AddContent(0, "You are not authorized"));
var authTcs = new TaskCompletionSource<AuthenticationState>();
rootComponent.AuthenticationState = authTcs.Task;
@ -228,10 +297,10 @@ namespace Microsoft.AspNetCore.Components
{
// Arrange
var @event = new ManualResetEventSlim();
var renderer = new TestRenderer()
{
OnUpdateDisplayComplete = () => { @event.Set(); },
};
var authorizationService = new TestAuthorizationService();
authorizationService.NextResult = AuthorizationResult.Success();
var renderer = CreateTestRenderer(authorizationService);
renderer.OnUpdateDisplayComplete = () => { @event.Set(); };
var rootComponent = WrapInAuthorizeView(
authorizingContent: builder => builder.AddContent(0, "Auth pending..."),
authorizedContent: context => builder => builder.AddContent(0, $"Hello, {context.User.Identity.Name}!"));
@ -276,13 +345,96 @@ namespace Microsoft.AspNetCore.Components
batch2.ReferenceFrames[edit.ReferenceFrameIndex],
"Hello, Monsieur!");
});
// Assert: The IAuthorizationService was given expected criteria
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Monsieur", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
[Fact]
public void IncludesPolicyInAuthorizeCall()
{
// Arrange
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(policy: "MyTestPolicy");
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements,
req => Assert.Equal("MyTestPolicy", ((TestPolicyRequirement)req).PolicyName));
});
}
[Fact]
public void IncludesRolesInAuthorizeCall()
{
// Arrange
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var rootComponent = WrapInAuthorizeView(roles: "SuperTestRole1, SuperTestRole2");
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Null(call.resource);
Assert.Collection(call.requirements, req => Assert.Equal(
new[] { "SuperTestRole1", "SuperTestRole2" },
((RolesAuthorizationRequirement)req).AllowedRoles));
});
}
[Fact]
public void IncludesResourceInAuthorizeCall()
{
// Arrange
var authorizationService = new TestAuthorizationService();
var renderer = CreateTestRenderer(authorizationService);
var resource = new object();
var rootComponent = WrapInAuthorizeView(resource: resource);
rootComponent.AuthenticationState = CreateAuthenticationState("Nellie");
// Act
renderer.AssignRootComponentId(rootComponent);
rootComponent.TriggerRender();
// Assert
Assert.Collection(authorizationService.AuthorizeCalls, call =>
{
Assert.Equal("Nellie", call.user.Identity.Name);
Assert.Same(resource, call.resource);
Assert.Collection(call.requirements, req =>
Assert.IsType<DenyAnonymousAuthorizationRequirement>(req));
});
}
private static TestAuthStateProviderComponent WrapInAuthorizeView(
RenderFragment<AuthenticationState> childContent = null,
RenderFragment<AuthenticationState> authorizedContent = null,
RenderFragment notAuthorizedContent = null,
RenderFragment authorizingContent = null)
RenderFragment<AuthenticationState> notAuthorizedContent = null,
RenderFragment authorizingContent = null,
string policy = null,
string roles = null,
object resource = null)
{
return new TestAuthStateProviderComponent(builder =>
{
@ -291,6 +443,9 @@ namespace Microsoft.AspNetCore.Components
builder.AddAttribute(2, nameof(AuthorizeView.Authorized), authorizedContent);
builder.AddAttribute(3, nameof(AuthorizeView.NotAuthorized), notAuthorizedContent);
builder.AddAttribute(4, nameof(AuthorizeView.Authorizing), authorizingContent);
builder.AddAttribute(5, nameof(AuthorizeView.Policy), policy);
builder.AddAttribute(6, nameof(AuthorizeView.Roles), roles);
builder.AddAttribute(7, nameof(AuthorizeView.Resource), resource);
builder.CloseComponent();
});
}
@ -311,11 +466,31 @@ namespace Microsoft.AspNetCore.Components
{
builder.OpenComponent<CascadingValue<Task<AuthenticationState>>>(0);
builder.AddAttribute(1, nameof(CascadingValue<Task<AuthenticationState>>.Value), AuthenticationState);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, _childContent);
builder.AddAttribute(2, RenderTreeBuilder.ChildContent, (RenderFragment)(builder =>
{
builder.OpenComponent<NeverReRenderComponent>(0);
builder.AddAttribute(1, RenderTreeBuilder.ChildContent, _childContent);
builder.CloseComponent();
}));
builder.CloseComponent();
}
}
// This is useful to show that the reason why a CascadingValue refreshes is because the
// value itself changed, not just that we're re-rendering the entire tree and have to
// recurse into all descendants because we're passing ChildContent
class NeverReRenderComponent : ComponentBase
{
[Parameter] RenderFragment ChildContent { get; set; }
protected override bool ShouldRender() => false;
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, ChildContent);
}
}
public static Task<AuthenticationState> CreateAuthenticationState(string username)
=> Task.FromResult(new AuthenticationState(
new ClaimsPrincipal(new TestIdentity { Name = username })));
@ -328,5 +503,59 @@ namespace Microsoft.AspNetCore.Components
public string Name { get; set; }
}
public TestRenderer CreateTestRenderer(IAuthorizationService authorizationService)
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton(authorizationService);
serviceCollection.AddSingleton<IAuthorizationPolicyProvider>(new TestAuthorizationPolicyProvider());
return new TestRenderer(serviceCollection.BuildServiceProvider());
}
private class TestAuthorizationService : IAuthorizationService
{
public AuthorizationResult NextResult { get; set; }
= AuthorizationResult.Failed();
public List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)> AuthorizeCalls { get; }
= new List<(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)>();
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, IEnumerable<IAuthorizationRequirement> requirements)
{
AuthorizeCalls.Add((user, resource, requirements));
// The TestAuthorizationService doesn't actually apply any authorization requirements
// It just returns the specified NextResult, since we're not trying to test the logic
// in DefaultAuthorizationService or similar here. So it's up to tests to set a desired
// NextResult and assert that the expected criteria were passed by inspecting AuthorizeCalls.
return Task.FromResult(NextResult);
}
public Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object resource, string policyName)
=> throw new NotImplementedException();
}
private class TestAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
private readonly AuthorizationOptions options = new AuthorizationOptions();
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
=> Task.FromResult(options.DefaultPolicy);
public Task<AuthorizationPolicy> GetFallbackPolicyAsync()
=> Task.FromResult(options.FallbackPolicy);
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName) => Task.FromResult(
new AuthorizationPolicy(new[]
{
new TestPolicyRequirement { PolicyName = policyName }
},
new[] { $"TestScheme:{policyName}" }));
}
public class TestPolicyRequirement : IAuthorizationRequirement
{
public string PolicyName { get; set; }
}
}
}

View File

@ -81,7 +81,7 @@ namespace Microsoft.AspNetCore.Components.Test.Helpers
{
if (!ShouldHandleExceptions)
{
throw exception;
ExceptionDispatchInfo.Capture(exception).Throw();
}
HandledExceptions.Add(exception);

View File

@ -32,7 +32,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact]
public void CascadingAuthenticationState_Unauthenticated()
{
SignInAs(null);
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
@ -44,7 +44,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact]
public void CascadingAuthenticationState_Authenticated()
{
SignInAs("someone cool");
SignInAs("someone cool", null);
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
@ -56,20 +56,58 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
{
SignInAs(null);
MountAndNavigateToAuthTest(AuthorizeViewCases);
SignInAs(null, null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
Browser.Equal("You're not authorized, anonymous", () =>
appElement.FindElement(By.CssSelector("#no-authorization-rule .not-authorized")).Text);
}
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
{
SignInAs("Some User");
SignInAs("Some User", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Some User!", () =>
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
}
[Fact]
public void AuthorizeViewCases_RequireRole_Authenticated()
{
SignInAs("Some User", "IrrelevantRole,TestRole");
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Some User!", () =>
appElement.FindElement(By.CssSelector("#authorize-role .authorized")).Text);
}
[Fact]
public void AuthorizeViewCases_RequireRole_Unauthenticated()
{
SignInAs("Some User", "IrrelevantRole");
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("You're not authorized, Some User", () =>
appElement.FindElement(By.CssSelector("#authorize-role .not-authorized")).Text);
}
[Fact]
public void AuthorizeViewCases_RequirePolicy_Authenticated()
{
SignInAs("Bert", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Bert!", () =>
appElement.FindElement(By.CssSelector("#authorize-policy .authorized")).Text);
}
[Fact]
public void AuthorizeViewCases_RequirePolicy_Unauthenticated()
{
SignInAs("Mallory", null);
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("You're not authorized, Mallory", () =>
appElement.FindElement(By.CssSelector("#authorize-policy .not-authorized")).Text);
}
IWebElement MountAndNavigateToAuthTest(string authLinkText)
{
Navigate(ServerPathBase);
@ -79,12 +117,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
return appElement;
}
void SignInAs(string usernameOrNull)
void SignInAs(string usernameOrNull, string rolesOrNull)
{
const string authenticationPageUrl = "/Authentication";
var baseRelativeUri = usernameOrNull == null
? $"{authenticationPageUrl}?signout=true"
: $"{authenticationPageUrl}?username={usernameOrNull}";
: $"{authenticationPageUrl}?username={usernameOrNull}&roles={rolesOrNull}";
Navigate(baseRelativeUri);
WaitUntilExists(By.CssSelector("h1#authentication"));
}

View File

@ -11,7 +11,39 @@
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
</Authorized>
<NotAuthorized>
<p class="not-authorized">You're not logged in.</p>
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
</NotAuthorized>
</AuthorizeView>
</div>
<div id="authorize-role">
<h3>Scenario: Require role</h3>
<AuthorizeView Roles="TestRole">
<Authorizing>
<p class="authorizing">Authorizing...</p>
</Authorizing>
<Authorized>
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
</Authorized>
<NotAuthorized>
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
</NotAuthorized>
</AuthorizeView>
</div>
<div id="authorize-policy">
<h3>Scenario: Require policy</h3>
<AuthorizeView Policy="NameMustStartWithB">
<Authorizing>
<p class="authorizing">Authorizing...</p>
</Authorizing>
<Authorized>
<p class="authorized">Welcome, @context.User.Identity.Name!</p>
</Authorized>
<NotAuthorized>
<p class="not-authorized">You're not authorized, @(context.User.Identity.Name ?? "anonymous")</p>
</NotAuthorized>
</AuthorizeView>
</div>

View File

@ -12,6 +12,6 @@ namespace BasicTestApp.AuthTest
public string UserName { get; set; }
public Dictionary<string, string> ExposedClaims { get; set; }
public List<(string Type, string Value)> ExposedClaims { get; set; }
}
}

View File

@ -29,7 +29,7 @@ namespace BasicTestApp.AuthTest
if (data.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) }
.Concat(data.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
.Concat(data.ExposedClaims.Select(c => new Claim(c.Type, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
else

View File

@ -15,6 +15,12 @@ namespace BasicTestApp
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
services.AddAuthorizationCore(options =>
{
options.AddPolicy("NameMustStartWithB", policy =>
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
});
}
public void Configure(IComponentsApplicationBuilder app)

View File

@ -1,4 +1,6 @@
using System;
using System.Linq;
using System.Security.Claims;
using BasicTestApp.AuthTest;
using Microsoft.AspNetCore.Mvc;
@ -7,22 +9,28 @@ namespace Components.TestServer.Controllers
[Route("api/[controller]")]
public class UserController : Controller
{
// Servers are not expected to expose everything from the server-side ClaimsPrincipal
// to the client. It's up to the developer to choose what kind of authentication state
// data is needed on the client so it can display suitable options in the UI.
// In this class, we inform the client only about certain roles and certain other claims.
static string[] ExposedRoles = new[] { "IrrelevantRole", "TestRole" };
// GET api/user
[HttpGet]
public ClientSideAuthenticationStateData Get()
{
// Servers are not expected to expose everything from the server-side ClaimsPrincipal
// to the client. It's up to the developer to choose what kind of authentication state
// data is needed on the client so it can display suitable options in the UI.
return new ClientSideAuthenticationStateData
{
IsAuthenticated = User.Identity.IsAuthenticated,
UserName = User.Identity.Name,
ExposedClaims = User.Claims
.Where(c => c.Type == "test-claim")
.ToDictionary(c => c.Type, c => c.Value)
.Where(c => c.Type == "test-claim" || IsExposedRole(c))
.Select(c => (c.Type, c.Value)).ToList()
};
}
private bool IsExposedRole(Claim claim)
=> claim.Type == ClaimTypes.Role
&& ExposedRoles.Contains(claim.Value);
}
}

View File

@ -26,6 +26,10 @@
User name:
<input name="username" />
</p>
<p>
Roles:
<input name="roles" />
</p>
<p>
<button type="submit">Submit</button>
</p>
@ -37,7 +41,11 @@
<p>
Authenticated: <strong>@User.Identity.IsAuthenticated</strong>
Username: <strong>@User.Identity.Name</strong>
</p>
Roles:
<strong>
@string.Join(", ", User.Claims.Where(c => c.Type == ClaimTypes.Role).Select(c => c.Value).ToArray())
</strong>
</p>foreach
<a href="Authentication?signout=true">Sign out</a>
</fieldset>
</body>
@ -61,6 +69,15 @@
new Claim("test-claim", "Test claim value"),
};
var roles = Request.Query["roles"];
if (!string.IsNullOrEmpty(roles))
{
foreach (var role in roles.ToString().Split(','))
{
claims.Add(new Claim(ClaimTypes.Role, role));
}
}
await HttpContext.SignInAsync(
new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")));

View File

@ -30,6 +30,12 @@ namespace TestServer
});
services.AddServerSideBlazor();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddAuthorization(options =>
{
options.AddPolicy("NameMustStartWithB", policy =>
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.