Components auth step 2 (#10293)

* CR feedback left over from #10227

* Begin adding E2E test case

* Add cookie auth and test login page

* Make E2E auth component work client-side too

* Restructure auth E2E tests around a router so there can easily be multiple such test components

* Add E2E test case for AuthorizeView

* Prepare for E2E test implementations

* Fix ToBaseRelativePath handling of hashes

... otherwise E2E test will fail, because we're using the hash to control server-or-client execution

* Decouple E2E execution mode from hosting mode

* Actual E2E tests for cascading authentication state

* Actual E2E tests for AuthorizeView (in "no authentication rule" mode)

* Fix inconsistent namespace

* CR: Manual ref assembly definitions for AuthorizeView/CascadingAuthenticationState
This commit is contained in:
Steve Sanderson 2019-05-20 15:41:02 +01:00 committed by GitHub
parent 5502c2080e
commit 147880f796
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 434 additions and 49 deletions

View File

@ -4,6 +4,8 @@ T:Microsoft.AspNetCore.Components.RenderTree.RenderTreeFrame
T:Microsoft.AspNetCore.Mvc.ApplicationModels.PageParameterModel
T:Microsoft.AspNetCore.Mvc.ApplicationModels.PagePropertyModel
# Manually implemented - https://github.com/aspnet/AspNetCore/issues/8825
T:Microsoft.AspNetCore.Components.AuthorizeView
T:Microsoft.AspNetCore.Components.CascadingAuthenticationState
T:Microsoft.AspNetCore.Components.CascadingValue`1
T:Microsoft.AspNetCore.Components.Forms.DataAnnotationsValidator
T:Microsoft.AspNetCore.Components.Forms.EditForm

View File

@ -29,6 +29,9 @@ namespace Microsoft.AspNetCore.Blazor.Services.Test
[InlineData("scheme://host/path/", "scheme://host/path/", "")]
[InlineData("scheme://host/path/", "scheme://host/path/more", "more")]
[InlineData("scheme://host/path/", "scheme://host/path", "")]
[InlineData("scheme://host/path/", "scheme://host/path#hash", "#hash")]
[InlineData("scheme://host/path/", "scheme://host/path/#hash", "#hash")]
[InlineData("scheme://host/path/", "scheme://host/path/more#hash", "more#hash")]
public void ComputesCorrectValidBaseRelativePaths(string baseUri, string absoluteUri, string expectedResult)
{
var actualResult = _uriHelper.ToBaseRelativePath(baseUri, absoluteUri);

View File

@ -49,6 +49,32 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// Built-in components: https://github.com/aspnet/AspNetCore/issues/8825
namespace Microsoft.AspNetCore.Components
{
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; } private set { throw null; } }
[Microsoft.AspNetCore.Components.ParameterAttribute]
public Microsoft.AspNetCore.Components.RenderFragment Authorizing { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } private set { throw null; } }
[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; } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
[System.Diagnostics.DebuggerStepThroughAttribute]
protected override System.Threading.Tasks.Task OnParametersSetAsync() { 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; } private set { throw null; } }
protected override void BuildRenderTree(Microsoft.AspNetCore.Components.RenderTree.RenderTreeBuilder builder) { }
protected override void OnInit() { }
void System.IDisposable.Dispose() { }
}
public partial class CascadingValue<T> : Microsoft.AspNetCore.Components.IComponent
{
public CascadingValue() { }

View File

@ -16,21 +16,6 @@ namespace Microsoft.AspNetCore.Components
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")]
@ -85,15 +70,6 @@ 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

@ -11,11 +11,6 @@ namespace Microsoft.AspNetCore.Components
/// </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>
@ -24,5 +19,10 @@ namespace Microsoft.AspNetCore.Components
{
User = user ?? throw new ArgumentNullException(nameof(user));
}
/// <summary>
/// Gets a <see cref="ClaimsPrincipal"/> that describes the current user.
/// </summary>
public ClaimsPrincipal User { get; }
}
}

View File

@ -12,19 +12,16 @@ namespace Microsoft.AspNetCore.Components
public abstract class AuthenticationStateProvider
{
/// <summary>
/// Gets an <see cref="AuthenticationState"/> instance that describes
/// the current user.
/// Asynchronously gets an <see cref="AuthenticationState"/> that describes the current user.
/// </summary>
/// <returns>An <see cref="AuthenticationState"/> instance that describes the current user.</returns>
/// <returns>A task that, when resolved, gives 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.

View File

@ -154,7 +154,10 @@ namespace Microsoft.AspNetCore.Components
// baseUri ends with a slash), and from that we return "something"
return locationAbsolute.Substring(baseUri.Length);
}
else if ($"{locationAbsolute}/".Equals(baseUri, StringComparison.Ordinal))
var hashIndex = locationAbsolute.IndexOf('#');
var locationAbsoluteNoHash = hashIndex < 0 ? locationAbsolute : locationAbsolute.Substring(0, hashIndex);
if ($"{locationAbsoluteNoHash}/".Equals(baseUri, StringComparison.Ordinal))
{
// Special case: for the base URI "/something/", if you're at
// "/something" then treat it as if you were at "/something/" (i.e.,
@ -162,7 +165,7 @@ namespace Microsoft.AspNetCore.Components
// whether the server would return the same page whether or not the
// slash is present, but ASP.NET Core at least does by default when
// using PathBase.
return string.Empty;
return locationAbsolute.Substring(baseUri.Length - 1);
}
var message = $"The URI '{locationAbsolute}' is not contained by the base URI '{baseUri}'.";

View File

@ -15,7 +15,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure
public class BasicTestAppTestBase : ServerTestBase<ToggleExecutionModeServerFixture<Program>>
{
public string ServerPathBase
=> "/subdir" + (_serverFixture.UsingAspNetHost ? "#server" : "");
=> "/subdir" + (_serverFixture.ExecutionMode == ExecutionMode.Server ? "#server" : "");
public BasicTestAppTestBase(
BrowserFixture browserFixture,

View File

@ -9,7 +9,8 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
: ServerFixture
{
public string PathBase { get; set; }
public bool UsingAspNetHost { get; private set; }
public ExecutionMode ExecutionMode { get; set; } = ExecutionMode.Client;
private AspNetSiteServerFixture.BuildWebHost _buildWebHostMethod;
private IDisposable _serverToDispose;
@ -18,7 +19,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
{
_buildWebHostMethod = buildWebHostMethod
?? throw new ArgumentNullException(nameof(buildWebHostMethod));
UsingAspNetHost = true;
}
protected override string StartAndGetRootUri()
@ -46,4 +46,6 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures
_serverToDispose?.Dispose();
}
}
public enum ExecutionMode { Client, Server }
}

View File

@ -8,7 +8,7 @@ using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETests.ServerExecutionTests
namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
public class PrerenderingTest : ServerTestBase<AspNetSiteServerFixture>
{

View File

@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
public static ToggleExecutionModeServerFixture<T> WithServerExecution<T>(this ToggleExecutionModeServerFixture<T> serverFixture)
{
serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost);
serverFixture.ExecutionMode = ExecutionMode.Server;
return serverFixture;
}
}

View File

@ -81,4 +81,12 @@ namespace Microsoft.AspNetCore.Components.E2ETest.ServerExecutionTests
{
}
}
public class ServerAuthTest : AuthTest
{
public ServerAuthTest(BrowserFixture browserFixture, ToggleExecutionModeServerFixture<Program> serverFixture, ITestOutputHelper output)
: base(browserFixture, serverFixture.WithServerExecution(), output)
{
}
}
}

View File

@ -0,0 +1,92 @@
// 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 BasicTestApp;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using OpenQA.Selenium;
using Xunit;
using Xunit.Abstractions;
namespace Microsoft.AspNetCore.Components.E2ETest.Tests
{
public class AuthTest : BasicTestAppTestBase
{
// These strings correspond to the links in BasicTestApp\AuthTest\Links.razor
const string CascadingAuthenticationStateLink = "Cascading authentication state";
const string AuthorizeViewCases = "AuthorizeView cases";
public AuthTest(
BrowserFixture browserFixture,
ToggleExecutionModeServerFixture<Program> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
// Normally, the E2E tests use the Blazor dev server if they are testing
// client-side execution. But for the auth tests, we always have to run
// in "hosted on ASP.NET Core" mode, because we get the auth state from it.
serverFixture.UseAspNetHost(TestServer.Program.BuildWebHost);
}
[Fact]
public void CascadingAuthenticationState_Unauthenticated()
{
SignInAs(null);
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
Browser.Equal("False", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
Browser.Equal(string.Empty, () => appElement.FindElement(By.Id("identity-name")).Text);
Browser.Equal("(none)", () => appElement.FindElement(By.Id("test-claim")).Text);
}
[Fact]
public void CascadingAuthenticationState_Authenticated()
{
SignInAs("someone cool");
var appElement = MountAndNavigateToAuthTest(CascadingAuthenticationStateLink);
Browser.Equal("True", () => appElement.FindElement(By.Id("identity-authenticated")).Text);
Browser.Equal("someone cool", () => appElement.FindElement(By.Id("identity-name")).Text);
Browser.Equal("Test claim value", () => appElement.FindElement(By.Id("test-claim")).Text);
}
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Unauthenticated()
{
SignInAs(null);
MountAndNavigateToAuthTest(AuthorizeViewCases);
WaitUntilExists(By.CssSelector("#no-authorization-rule .not-authorized"));
}
[Fact]
public void AuthorizeViewCases_NoAuthorizationRule_Authenticated()
{
SignInAs("Some User");
var appElement = MountAndNavigateToAuthTest(AuthorizeViewCases);
Browser.Equal("Welcome, Some User!", () =>
appElement.FindElement(By.CssSelector("#no-authorization-rule .authorized")).Text);
}
IWebElement MountAndNavigateToAuthTest(string authLinkText)
{
Navigate(ServerPathBase);
var appElement = MountTestComponent<BasicTestApp.AuthTest.AuthRouter>();
WaitUntilExists(By.Id("auth-links"));
appElement.FindElement(By.LinkText(authLinkText)).Click();
return appElement;
}
void SignInAs(string usernameOrNull)
{
const string authenticationPageUrl = "/Authentication";
var baseRelativeUri = usernameOrNull == null
? $"{authenticationPageUrl}?signout=true"
: $"{authenticationPageUrl}?username={usernameOrNull}";
Navigate(baseRelativeUri);
WaitUntilExists(By.CssSelector("h1#authentication"));
}
}
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
MountTestComponent<BindCasesComponent>();
WaitUntilExists(By.Id("bind-cases"));
}

View File

@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
MountTestComponent<BasicTestApp.CascadingValueTest.CascadingValueSupplier>();
}

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
}
[Fact]

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
MountTestComponent<EventBubblingComponent>();
WaitUntilExists(By.Id("event-bubbling"));
}

View File

@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
MountTestComponent<BasicTestApp.EventCallbackTest.EventCallbackCases>();
}

View File

@ -31,7 +31,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
}
[Fact]

View File

@ -104,7 +104,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
// Include the sync assertions only when running under WebAssembly
var expectedValues = expectedAsyncValues;
if (!_serverFixture.UsingAspNetHost)
if (_serverFixture.ExecutionMode == ExecutionMode.Client)
{
foreach (var kvp in expectedSyncValues)
{

View File

@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
protected override void InitializeAsyncCore()
{
// On WebAssembly, page reloads are expensive so skip if possible
Navigate(ServerPathBase, noReload: !_serverFixture.UsingAspNetHost);
Navigate(ServerPathBase, noReload: _serverFixture.ExecutionMode == ExecutionMode.Client);
}
[Fact]

View File

@ -0,0 +1,3 @@
@page "/AuthHome"
Select an auth test below.

View File

@ -0,0 +1,30 @@
@using Microsoft.AspNetCore.Components.Routing
@inject IUriHelper UriHelper
@*
This router is independent of any other router that may exist within the same project.
It exists so that (1) we can easily have multiple test cases that depend on the
CascadingAuthenticationState, and (2) we can test the integration between the router
and @page authorization rules.
*@
<CascadingAuthenticationState>
<Router AppAssembly=typeof(BasicTestApp.Program).Assembly />
</CascadingAuthenticationState>
<hr />
<Links />
@functions {
protected override void OnInit()
{
// Start at AuthHome, not at any other component in the same app that happens to
// register itself for the route ""
var absoluteUriPath = new Uri(UriHelper.GetAbsoluteUri()).GetLeftPart(UriPartial.Path);
var relativeUri = UriHelper.ToBaseRelativePath(UriHelper.GetBaseUri(), absoluteUriPath);
if (relativeUri == string.Empty)
{
UriHelper.NavigateTo("AuthHome");
}
}
}

View File

@ -0,0 +1,17 @@
@page "/AuthorizeViewCases"
<div id="no-authorization-rule">
<h3>Scenario: No authorization rule</h3>
<AuthorizeView>
<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 logged in.</p>
</NotAuthorized>
</AuthorizeView>
</div>

View File

@ -0,0 +1,47 @@
@page "/CascadingAuthenticationStateConsumer"
@using System.Security.Claims
<h1>Cascading authentication state</h1>
@if (user == null)
{
<span id="auth-state-pending">Requesting authentication state...</span>
}
else
{
<p>
Authenticated:
<strong id="identity-authenticated">@user.Identity.IsAuthenticated</strong>
</p>
<p>
Name:
<strong id="identity-name">@user.Identity.Name</strong>
</p>
<p>
Test claim:
@if (user.HasClaim(TestClaimPredicate) == true)
{
<strong id="test-claim">@user.Claims.Single(c => TestClaimPredicate(c)).Value</strong>
}
else
{
<strong id="test-claim">(none)</strong>
}
</p>
}
@functions
{
static Predicate<Claim> TestClaimPredicate = c => c.Type == "test-claim";
ClaimsPrincipal user;
[CascadingParameter] Task<AuthenticationState> AuthenticationStateTask { get; set; }
protected override async Task OnParametersSetAsync()
{
user = (await AuthenticationStateTask).User;
}
}

View File

@ -0,0 +1,17 @@
// 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.Collections.Generic;
namespace BasicTestApp.AuthTest
{
// DTO shared between server and client
public class ClientSideAuthenticationStateData
{
public bool IsAuthenticated { get; set; }
public string UserName { get; set; }
public Dictionary<string, string> ExposedClaims { get; set; }
}
}

View File

@ -0,0 +1,8 @@
@using Microsoft.AspNetCore.Components.Routing
<ul id="auth-links">
<li><NavLink href="AuthHome" Match=NavLinkMatch.All>Home</NavLink></li>
<li><NavLink href="CascadingAuthenticationStateConsumer">Cascading authentication state</NavLink></li>
<li><NavLink href="AuthorizeViewCases">AuthorizeView cases</NavLink></li>
</ul>
<p>To change the underlying authentication state, <a target="_blank" href="/Authentication">go here</a>.</p>

View File

@ -0,0 +1,43 @@
// 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.Net.Http;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
namespace BasicTestApp.AuthTest
{
// This is intended to be similar to the authentication stateprovider included by default
// with the client-side Blazor "Hosted in ASP.NET Core" template
public class ServerAuthenticationStateProvider : AuthenticationStateProvider
{
private readonly HttpClient _httpClient;
public ServerAuthenticationStateProvider(HttpClient httpClient)
{
_httpClient = httpClient;
}
public override async Task<AuthenticationState> GetAuthenticationStateAsync()
{
var uri = new Uri(_httpClient.BaseAddress, "/api/User");
var data = await _httpClient.GetJsonAsync<ClientSideAuthenticationStateData>(uri.AbsoluteUri);
ClaimsIdentity identity;
if (data.IsAuthenticated)
{
var claims = new[] { new Claim(ClaimTypes.Name, data.UserName) }
.Concat(data.ExposedClaims.Select(c => new Claim(c.Key, c.Value)));
identity = new ClaimsIdentity(claims, "Server authentication");
}
else
{
identity = new ClaimsIdentity();
}
return new AuthenticationState(new ClaimsPrincipal(identity));
}
}
}

View File

@ -54,6 +54,8 @@
<option value="BasicTestApp.KeyCasesComponent">Key cases</option>
<option value="BasicTestApp.ReorderingFocusComponent">Reordering focus retention</option>
<option value="BasicTestApp.RouterTest.UriHelperComponent">UriHelper Test</option>
<option value="BasicTestApp.AuthTest.CascadingAuthenticationStateParent">Cascading authentication state</option>
<option value="BasicTestApp.AuthTest.AuthRouter">Auth cases</option>
</select>
@if (SelectedComponentType != null)

View File

@ -2,7 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Runtime.InteropServices;
using BasicTestApp.AuthTest;
using Microsoft.AspNetCore.Blazor.Http;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Builder;
using Microsoft.Extensions.DependencyInjection;
@ -12,6 +14,7 @@ namespace BasicTestApp
{
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<AuthenticationStateProvider, ServerAuthenticationStateProvider>();
}
public void Configure(IComponentsApplicationBuilder app)

View File

@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>netcoreapp3.0</TargetFramework>
@ -6,6 +6,7 @@
<ItemGroup>
<Reference Include="Microsoft.AspNetCore" />
<Reference Include="Microsoft.AspNetCore.Authentication.Cookies" />
<Reference Include="Microsoft.AspNetCore.Blazor.Server" />
<Reference Include="Microsoft.AspNetCore.Cors" />
<Reference Include="Microsoft.AspNetCore.Mvc" />

View File

@ -0,0 +1,28 @@
using System.Linq;
using BasicTestApp.AuthTest;
using Microsoft.AspNetCore.Mvc;
namespace Components.TestServer.Controllers
{
[Route("api/[controller]")]
public class UserController : Controller
{
// 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)
};
}
}
}

View File

@ -0,0 +1,72 @@
@page
@using Microsoft.AspNetCore.Authentication
@using System.Security.Claims
<!DOCTYPE html>
<html>
<head>
<title>Authentication</title>
</head>
<body>
<h1 id="authentication">Authentication</h1>
<p>
This is a completely fake login mechanism for automated test purposes.
It accepts any username, with no password.
</p>
<p>
<strong>Obviously you should not do this in real applications.</strong>
See also: <a href="https://docs.microsoft.com/aspnet/core/security">documentation on configuring a real login system.</a>
</p>
<fieldset>
<h3>Sign in</h3>
@* Do not use method="get" for real login forms. This is just to simplify E2E tests. *@
<form method="get">
<p>
User name:
<input name="username" />
</p>
<p>
<button type="submit">Submit</button>
</p>
</form>
</fieldset>
<fieldset>
<h3>Status</h3>
<p>
Authenticated: <strong>@User.Identity.IsAuthenticated</strong>
Username: <strong>@User.Identity.Name</strong>
</p>
<a href="Authentication?signout=true">Sign out</a>
</fieldset>
</body>
</html>
@functions {
public async Task<IActionResult> OnGet()
{
if (Request.Query["signout"] == "true")
{
await HttpContext.SignOutAsync();
return Redirect("Authentication");
}
var username = Request.Query["username"];
if (!string.IsNullOrEmpty(username))
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, username),
new Claim("test-claim", "Test claim value"),
};
await HttpContext.SignInAsync(
new ClaimsPrincipal(new ClaimsIdentity(claims, "FakeAuthenticationType")));
return Redirect("Authentication");
}
return Page();
}
}

View File

@ -1,4 +1,5 @@
using BasicTestApp;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components.Server;
using Microsoft.AspNetCore.Hosting;
@ -28,6 +29,7 @@ namespace TestServer
options.AddPolicy("AllowAll", _ => { /* Controlled below */ });
});
services.AddServerSideBlazor();
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
@ -50,6 +52,7 @@ namespace TestServer
.AllowCredentials();
});
app.UseAuthentication();
// Mount the server-side Blazor app on /subdir
app.Map("/subdir", subdirApp =>
@ -70,6 +73,7 @@ namespace TestServer
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapRazorPages();
});
// Separately, mount a prerendered server-side Blazor app on /prerendered