Skip to content

Components auth: basic services and components #10227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 25 commits into from
May 16, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eeda567
Define IAuthenticationState and IAuthenticationStateProvider
SteveSandersonMS May 8, 2019
031ba22
For server-side Blazor, add FixedAuthenticationStateProvider
SteveSandersonMS May 8, 2019
aa07dbc
Define AuthenticationStateChanged event (not yet used)
SteveSandersonMS May 8, 2019
296e1a4
Implement AuthenticationStateProvider component
SteveSandersonMS May 10, 2019
0143c3b
Replace EmptyAuthenticationState with PendingAuthenticationState so i…
SteveSandersonMS May 10, 2019
1fd4c08
Convert AuthenticationStateProvider to a .razor file
SteveSandersonMS May 10, 2019
8a21d3b
AuthorizeView: render child content only if authorized
SteveSandersonMS May 14, 2019
cfdab41
AuthorizeView: render "not authorized" content
SteveSandersonMS May 14, 2019
cfa81b9
AuthorizeView: allow 'Authorized' param as friendlier alias for 'Chil…
SteveSandersonMS May 14, 2019
0bd49e6
AuthorizeView: Render "Authorizing" content while async auth is in pr…
SteveSandersonMS May 14, 2019
553f9f4
AuthorizeView: add 'todo' about roles/policies
SteveSandersonMS May 14, 2019
d9e5c4e
Update ref sources
SteveSandersonMS May 14, 2019
b05a7d3
Make the "pending" state part of public API so custom IAuthentication…
SteveSandersonMS May 14, 2019
3679217
CR: Rename AuthStateProviderService
SteveSandersonMS May 14, 2019
99ee6f7
Fix build
SteveSandersonMS May 14, 2019
3d20454
How did this even compile
SteveSandersonMS May 14, 2019
5f95d3d
Change AuthenticationStateProvider to represent the pending state as …
SteveSandersonMS May 14, 2019
f78d427
Remove redundant thing
SteveSandersonMS May 14, 2019
12bd326
CR: Use @inject
SteveSandersonMS May 15, 2019
d93774b
CR: Rename and make private the AuthStateTask
SteveSandersonMS May 15, 2019
dcc7e11
CR: Rename
SteveSandersonMS May 15, 2019
4eef170
CR: Convert IAuthenticationStateProvider to an ABC
SteveSandersonMS May 15, 2019
4c78ded
CR: Convert IAuthenticationState to be a class
SteveSandersonMS May 15, 2019
3f1143f
Refresh ref assembly sources
SteveSandersonMS May 15, 2019
12a2f36
Make "forceRefresh" not part of the base class API
SteveSandersonMS May 15, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@SteveSandersonMS - something else I thought of late (sorry).

These need to go into the manually maintained section of the ref assembly. The reason why is that ref assemblies don't preserve the setter if it's non-public. You can use cc1b294 as an example.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great reminder - thanks. This should be covered in today's PR in this commit: ad9ac55

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")]
Expand Down Expand Up @@ -57,6 +85,15 @@ public static partial class BindMethods
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
{
Expand Down
28 changes: 28 additions & 0 deletions src/Components/Components/src/Auth/AuthenticationState.cs
Original file line number Diff line number Diff line change
@@ -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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: fields -> constructors -> properties

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Rather than go through another multi-hour CI process to make this tweak, I'm going to include it in my next auth PR.


/// <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));
}
}
}
49 changes: 49 additions & 0 deletions src/Components/Components/src/Auth/AuthenticationStateProvider.cs
Original file line number Diff line number Diff line change
@@ -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>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

technically not correct, it returns "a task that blah blah blah"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK. Rather than go through another multi-hour CI process to make this tweak, I'm going to include it in my next auth PR.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this actually needed now that NotifyAuthenticationStateChanged is there?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well spotted. Rather than go through another multi-hour CI process to make this tweak, I'm going to include it in my next auth PR.


/// <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);
}
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Food for thought. It appears that we want to (and support) raising this event on any thread. Do we want to document that as supported? Or do we want to try and make it enforced that you raise this on the sync context.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don’t think we want that. The callback should be responsible for calling Invoke or InvokeAsync when handling the event.

I’m wondering how good our support story is when a component calls Invoke

Copy link
Member Author

@SteveSandersonMS SteveSandersonMS May 16, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's an interesting question. We could make the AuthenticationStateProvider aware of the sync context (it's already a scoped service), and we could use an async-event pattern (Task-returning delegates) so that all the handlers would be async functions running on the sync context, whether or not they need to be.

However, the AuthenticationStateProvider is intended as a low-level service that developers will commonly only be the producer of (if implementing a custom authentication system), not the consumer. To consume the authentication state, developers will typically use either the cascaded value or the <AuthorizeView>, both of which already marshal the flow onto the sync context for you.

So, since in common usage patterns this won't be an issue anyway, I'd prefer to keep the AuthenticationStateProvider simple and independent of this, letting any developers who choose to use this low-level service deal with their own Invoke/InvokeAsync as Javier mentions, like they would when subscribing to any other service that's separate from the rendering system.


/// <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);
}
68 changes: 68 additions & 0 deletions src/Components/Components/src/Auth/AuthorizeView.razor
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
1 change: 1 addition & 0 deletions src/Components/Components/src/ChangeDetection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ private static bool IsKnownImmutableType(Type type)
=> type.IsPrimitive
|| type == typeof(string)
|| type == typeof(DateTime)
|| type == typeof(Type)
|| type == typeof(decimal);
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk.Razor">

<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Description>Components feature for ASP.NET Core.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<IsShippingPackage>true</IsShippingPackage>
<IsAspNetCoreApp>true</IsAspNetCoreApp>
<RazorLangVersion>3.0</RazorLangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
Loading