Skip to content

Commit

Permalink
Preserve RemoteAuthenticationContext during JS interop (#54225) (#54655)
Browse files Browse the repository at this point in the history
  • Loading branch information
halter73 committed Apr 2, 2024
1 parent 7869bfc commit 53e662d
Show file tree
Hide file tree
Showing 18 changed files with 354 additions and 5 deletions.
19 changes: 19 additions & 0 deletions AspNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -1782,6 +1782,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Output
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NotReferencedInWasmCodePackage", "src\Components\test\testassets\NotReferencedInWasmCodePackage\NotReferencedInWasmCodePackage.csproj", "{433F91E4-E39D-4EB0-B798-2998B3969A2C}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Components.WasmRemoteAuthentication", "src\Components\test\testassets\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj", "{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -10735,6 +10737,22 @@ Global
{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x64.Build.0 = Release|Any CPU
{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x86.ActiveCfg = Release|Any CPU
{433F91E4-E39D-4EB0-B798-2998B3969A2C}.Release|x86.Build.0 = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|Any CPU.Build.0 = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|arm64.ActiveCfg = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|arm64.Build.0 = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|x64.ActiveCfg = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|x64.Build.0 = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|x86.ActiveCfg = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Debug|x86.Build.0 = Debug|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|Any CPU.ActiveCfg = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|Any CPU.Build.0 = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|arm64.ActiveCfg = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|arm64.Build.0 = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.ActiveCfg = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x64.Build.0 = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.ActiveCfg = Release|Any CPU
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -11615,6 +11633,7 @@ Global
{A939893A-B3CD-48F6-80D3-340C8A6E275B} = {AA5ABFBC-177C-421E-B743-005E0FD1248B}
{F232B503-D412-45EE-8B31-EFD46B9FA302} = {AA5ABFBC-177C-421E-B743-005E0FD1248B}
{433F91E4-E39D-4EB0-B798-2998B3969A2C} = {6126DCE4-9692-4EE2-B240-C65743572995}
{8A021D6D-7935-4AB3-BB47-38D4FF9B0D13} = {6126DCE4-9692-4EE2-B240-C65743572995}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {3E8720B3-DBDD-498C-B383-2CC32A054E8F}
Expand Down
1 change: 1 addition & 0 deletions src/Components/Components.slnf
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"src\\Components\\test\\testassets\\BasicTestApp\\BasicTestApp.csproj",
"src\\Components\\test\\testassets\\Components.TestServer\\Components.TestServer.csproj",
"src\\Components\\test\\testassets\\Components.WasmMinimal\\Components.WasmMinimal.csproj",
"src\\Components\\test\\testassets\\Components.WasmRemoteAuthentication\\Components.WasmRemoteAuthentication.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.App\\ComponentsApp.App.csproj",
"src\\Components\\test\\testassets\\ComponentsApp.Server\\ComponentsApp.Server.csproj",
"src\\Components\\test\\testassets\\GlobalizationWasmApp\\GlobalizationWasmApp.csproj",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{
await EnsureAuthService();
var result = await JsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context);
var result = await JSInvokeWithContextAsync<RemoteAuthenticationContext<TRemoteAuthenticationState>, RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signIn", context);
await UpdateUserOnSuccess(result);

return result;
Expand All @@ -130,7 +130,7 @@ public virtual async Task<RemoteAuthenticationResult<TRemoteAuthenticationState>
RemoteAuthenticationContext<TRemoteAuthenticationState> context)
{
await EnsureAuthService();
var result = await JsRuntime.InvokeAsync<RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context);
var result = await JSInvokeWithContextAsync<RemoteAuthenticationContext<TRemoteAuthenticationState>, RemoteAuthenticationResult<TRemoteAuthenticationState>>("AuthenticationService.signOut", context);
await UpdateUserOnSuccess(result);

return result;
Expand Down Expand Up @@ -187,6 +187,11 @@ public virtual async ValueTask<AccessTokenResult> RequestAccessToken(AccessToken
} : null);
}

// JSRuntime.InvokeAsync does not properly annotate all arguments with DynamicallyAccessedMembersAttribute. https://github.com/dotnet/aspnetcore/issues/39839
// Calling JsRuntime.InvokeAsync directly results allows the RemoteAuthenticationContext.State getter to be trimmed. https://github.com/dotnet/aspnetcore/issues/49956
private ValueTask<TResult> JSInvokeWithContextAsync<[DynamicallyAccessedMembers(JsonSerialized)] TContext, [DynamicallyAccessedMembers(JsonSerialized)] TResult>(
string identifier, TContext context) => JsRuntime.InvokeAsync<TResult>(identifier, context);

private string GetReturnUrl(string? customReturnUrl) =>
customReturnUrl != null ? Navigation.ToAbsoluteUri(customReturnUrl).AbsoluteUri : Navigation.Uri;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ protected override IHost CreateWebHost()
}

var assembly = ApplicationAssembly ?? BuildWebHostMethod.Method.DeclaringType.Assembly;
var sampleSitePath = DefaultGetContentRoot(assembly);
var sampleSitePath = GetContentRootMethod(assembly);

var host = "127.0.0.1";
if (E2ETestOptions.Instance.SauceTest)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!--
Skip building and running the Components E2E tests in CI unless explicitly configured otherwise via
Expand Down Expand Up @@ -85,6 +85,11 @@
Include="..\..\WebAssembly\testassets\Wasm.Prerendered.Server\Wasm.Prerendered.Server.csproj"
Targets="Build;Publish"
Properties="BuildProjectReferences=false;TestTrimmedApps=true;PublishDir=$(MSBuildThisFileDirectory)$(OutputPath)trimmed\Wasm.Prerendered.Server\;" />

<ProjectReference
Include="..\testassets\Components.TestServer\Components.TestServer.csproj"
Targets="Build;Publish"
Properties="BuildProjectReferences=false;TestTrimmedOrMultithreadingApps=true;PublishDir=$(MSBuildThisFileDirectory)$(OutputPath)trimmed-or-threading\Components.TestServer\;" />
</ItemGroup>

<!-- Shared testing infrastructure for running E2E tests using selenium -->
Expand Down
79 changes: 79 additions & 0 deletions src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure;
using Microsoft.AspNetCore.Components.E2ETest.Infrastructure.ServerFixtures;
using Microsoft.AspNetCore.E2ETesting;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Testing;
using OpenQA.Selenium;
using TestServer;
using Xunit.Abstractions;

namespace Microsoft.AspNetCore.Components.E2ETest.Tests;

public class RemoteAuthenticationTest :
ServerTestBase<BasicTestAppServerSiteFixture<RemoteAuthenticationStartup>>
{
public readonly bool TestTrimmedApps = typeof(ToggleExecutionModeServerFixture<>).Assembly
.GetCustomAttributes<AssemblyMetadataAttribute>()
.First(m => m.Key == "Microsoft.AspNetCore.E2ETesting.TestTrimmedOrMultithreadingApps")
.Value == "true";

public RemoteAuthenticationTest(
BrowserFixture browserFixture,
BasicTestAppServerSiteFixture<RemoteAuthenticationStartup> serverFixture,
ITestOutputHelper output)
: base(browserFixture, serverFixture, output)
{
serverFixture.ApplicationAssembly = typeof(RemoteAuthenticationStartup).Assembly;

if (TestTrimmedApps)
{
serverFixture.BuildWebHostMethod = BuildPublishedWebHost;
serverFixture.GetContentRootMethod = GetPublishedContentRoot;
}
}

[Fact]
public void NavigateToLogin_PreservesExtraQueryParams()
{
// If the preservedExtraQueryParams passed to NavigateToLogin by RedirectToLogin gets trimmed,
// the OIDC endpoints will fail to authenticate the user.
Navigate("/subdir/test-remote-authentication");

var heading = Browser.Exists(By.TagName("h1"));
Browser.Equal("Hello, Jane Doe!", () => heading.Text);
}

private static IHost BuildPublishedWebHost(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureLogging((ctx, lb) =>
{
TestSink sink = new TestSink();
lb.AddProvider(new TestLoggerProvider(sink));
lb.Services.AddSingleton(sink);
})
.ConfigureWebHostDefaults(webHostBuilder =>
{
webHostBuilder.UseStartup<RemoteAuthenticationStartup>();
// Avoid UseStaticAssets or we won't use the trimmed published output.
})
.Build();

private static string GetPublishedContentRoot(Assembly assembly)
{
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name);

if (!Directory.Exists(contentRoot))
{
throw new DirectoryNotFoundException($"Test is configured to use trimmed outputs, but trimmed outputs were not found in {contentRoot}.");
}

return contentRoot;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ private void WaitUntilLoaded()

private static string GetPublishedContentRoot(Assembly assembly)
{
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed", assembly.GetName().Name);
var contentRoot = Path.Combine(AppContext.BaseDirectory, "trimmed-or-threading", assembly.GetName().Name);

if (!Directory.Exists(contentRoot))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,13 @@
<Reference Include="Microsoft.AspNetCore.SignalR" />
<Reference Include="Microsoft.AspNetCore.Testing" />
<Reference Include="Microsoft.Extensions.Hosting" />
<Reference Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\BasicTestApp\BasicTestApp.csproj" />
<ProjectReference Include="..\Components.WasmMinimal\Components.WasmMinimal.csproj" />
<ProjectReference Include="..\Components.WasmRemoteAuthentication\Components.WasmRemoteAuthentication.csproj" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public static async Task Main(string[] args)
var createIndividualHosts = new Dictionary<string, (IHost host, string basePath)>
{
["Client authentication"] = (BuildWebHost<AuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"),
["Remote client authentication"] = (BuildWebHost<RemoteAuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"),
["Server authentication"] = (BuildWebHost<ServerAuthenticationStartup>(CreateAdditionalArgs(args)), "/subdir"),
["CORS (WASM)"] = (BuildWebHost<CorsStartup>(CreateAdditionalArgs(args)), "/subdir"),
["Prerendering (Server-side)"] = (BuildWebHost<PrerenderedStartup>(CreateAdditionalArgs(args)), "/prerendered"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="utf-8" />
<base href="/subdir/" />

<HeadOutlet @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
</head>

<body>
<Components.WasmRemoteAuthentication.Routes @rendermode="new InteractiveWebAssemblyRenderMode(prerender: false)" />
<script src="_framework/blazor.web.js" autostart="false"></script>
<script src="_content/Microsoft.AspNetCore.Components.WebAssembly.Authentication/AuthenticationService.js"></script>
<script>
Blazor.start({
webAssembly: {
loadBootResource: (type, name, defaultUri, integrity) => `WasmRemoteAuthentication/_framework/${name}`
}
});
</script>
</body>

</html>
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Globalization;
using System.Reflection;
using Components.TestServer.RazorComponents;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

namespace TestServer;

public class RemoteAuthenticationStartup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorComponents()
.AddInteractiveWebAssemblyComponents();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.Map("/subdir", app =>
{
app.UseStaticFiles();
app.UseRouting();
app.UseAntiforgery();
app.UseEndpoints(endpoints =>
{
endpoints.MapRazorComponents<RemoteAuthenticationApp>()
.AddAdditionalAssemblies(Assembly.Load("Components.WasmRemoteAuthentication"))
.AddInteractiveWebAssemblyRenderMode(options => options.PathPrefix = "/WasmRemoteAuthentication");
var oidcEndpoints = endpoints.MapGroup("oidc");
// This is designed to test a single login at a time.
var issuer = "";
oidcEndpoints.MapGet(".well-known/openid-configuration", (HttpRequest request, [FromHeader] string host) =>
{
issuer = $"{(request.IsHttps ? "https" : "http")}://{host}";
return Results.Json(new
{
issuer,
authorization_endpoint = $"{issuer}/subdir/oidc/authorize",
token_endpoint = $"{issuer}/subdir/oidc/token",
});
});
var lastCode = "";
oidcEndpoints.MapGet("authorize", (string redirect_uri, string? state, string? prompt, bool? preservedExtraQueryParams) =>
{
// Require interaction so silent sign-in does not skip RedirectToLogin.razor.
if (prompt == "none")
{
return Results.Redirect($"{redirect_uri}?error=interaction_required&state={state}");
}
// Verify that the extra query parameters added by RedirectToLogin.razor are preserved.
if (preservedExtraQueryParams != true)
{
return Results.Redirect($"{redirect_uri}?error=invalid_request&error_description=extraQueryParams%20not%20preserved&state={state}");
}
lastCode = Random.Shared.Next().ToString(CultureInfo.InvariantCulture);
return Results.Redirect($"{redirect_uri}?code={lastCode}&state={state}");
});
var jwtHandler = new JsonWebTokenHandler();
oidcEndpoints.MapPost("token", ([FromForm] string code) =>
{
if (string.IsNullOrEmpty(lastCode) && code != lastCode)
{
return Results.BadRequest("Bad code");
}
return Results.Json(new
{
token_type = "Bearer",
scope = "openid profile",
expires_in = 3600,
id_token = jwtHandler.CreateToken(new SecurityTokenDescriptor
{
Issuer = issuer,
Audience = "s6BhdRkqt3",
Claims = new Dictionary<string, object>
{
["sub"] = "248289761001",
["name"] = "Jane Doe",
},
}),
});
}).DisableAntiforgery();
});
});
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">

<PropertyGroup>
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<StaticWebAssetBasePath>WasmRemoteAuthentication</StaticWebAssetBasePath>
</PropertyGroup>

<PropertyGroup Condition="'$(TestTrimmedOrMultithreadingApps)' == 'true'">
<!-- Avoid spending time brotli compression publish output.-->
<_BlazorBrotliCompressionLevel>NoCompression</_BlazorBrotliCompressionLevel>
</PropertyGroup>

<ItemGroup>
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@page "/authentication/{action}"

@using Microsoft.AspNetCore.Components.WebAssembly.Authentication

<RemoteAuthenticatorView Action="@Action" />

@code {
[Parameter] public string? Action { get; set; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
@page "/test-remote-authentication"

@using Microsoft.AspNetCore.Components.Authorization

<AuthorizeView>
<Authorized>
<h1>Hello, @context.User.Identity?.Name!</h1>
</Authorized>
<NotAuthorized>
@* Do this rather than rely on the [Authorize] attribute to avoid endpoint routing. *@
<RedirectToLogin />
</NotAuthorized>
</AuthorizeView>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;

var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.Services.AddOidcAuthentication(options =>
{
options.ProviderOptions.Authority = $"{builder.HostEnvironment.BaseAddress}oidc";
options.ProviderOptions.ClientId = "s6BhdRkqt3";
options.ProviderOptions.ResponseType = "code";
});

await builder.Build().RunAsync();
Loading

0 comments on commit 53e662d

Please sign in to comment.