Ver Fonte

Preserve RemoteAuthenticationContext during JS interop (#54225) (#54655)

Stephen Halter há 1 ano atrás
pai
commit
53e662dc86
18 ficheiros alterados com 354 adições e 5 exclusões
  1. 19 0
      AspNetCore.sln
  2. 1 0
      src/Components/Components.slnf
  3. 7 2
      src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs
  4. 1 1
      src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs
  5. 6 1
      src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj
  6. 79 0
      src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs
  7. 1 1
      src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs
  8. 2 0
      src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj
  9. 1 0
      src/Components/test/testassets/Components.TestServer/Program.cs
  10. 24 0
      src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor
  11. 97 0
      src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs
  12. 20 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Components.WasmRemoteAuthentication.csproj
  13. 9 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Pages/Authentication.razor
  14. 13 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Pages/TestRemoteAuthentication.razor
  15. 12 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Program.cs
  16. 21 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Properties/launchSettings.json
  17. 14 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/RedirectToLogin.razor
  18. 27 0
      src/Components/test/testassets/Components.WasmRemoteAuthentication/Routes.razor

+ 19 - 0
AspNetCore.sln

@@ -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
@@ -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
@@ -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}

+ 1 - 0
src/Components/Components.slnf

@@ -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",

+ 7 - 2
src/Components/WebAssembly/WebAssembly.Authentication/src/Services/RemoteAuthenticationService.cs

@@ -108,7 +108,7 @@ public class RemoteAuthenticationService<
         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;
@@ -130,7 +130,7 @@ public class RemoteAuthenticationService<
         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;
@@ -187,6 +187,11 @@ public class RemoteAuthenticationService<
             } : 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;
 

+ 1 - 1
src/Components/test/E2ETest/Infrastructure/ServerFixtures/AspNetSiteServerFixture.cs

@@ -32,7 +32,7 @@ public class AspNetSiteServerFixture : WebHostServerFixture
         }
 
         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)

+ 6 - 1
src/Components/test/E2ETest/Microsoft.AspNetCore.Components.E2ETests.csproj

@@ -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
@@ -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 -->

+ 79 - 0
src/Components/test/E2ETest/Tests/RemoteAuthenticationTest.cs

@@ -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;
+    }
+}

+ 1 - 1
src/Components/test/E2ETest/Tests/WebAssemblyPrerenderedTest.cs

@@ -56,7 +56,7 @@ public class WebAssemblyPrerenderedTest : ServerTestBase<AspNetSiteServerFixture
 
     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))
         {

+ 2 - 0
src/Components/test/testassets/Components.TestServer/Components.TestServer.csproj

@@ -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>

+ 1 - 0
src/Components/test/testassets/Components.TestServer/Program.cs

@@ -19,6 +19,7 @@ public class Program
         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"),

+ 24 - 0
src/Components/test/testassets/Components.TestServer/RazorComponents/RemoteAuthenticationApp.razor

@@ -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>

+ 97 - 0
src/Components/test/testassets/Components.TestServer/RemoteAuthenticationStartup.cs

@@ -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();
+            });
+        });
+    }
+}

+ 20 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Components.WasmRemoteAuthentication.csproj

@@ -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>

+ 9 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Pages/Authentication.razor

@@ -0,0 +1,9 @@
+@page "/authentication/{action}"
+
+@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
+
+<RemoteAuthenticatorView Action="@Action" />
+
+@code {
+    [Parameter] public string? Action { get; set; }
+}

+ 13 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Pages/TestRemoteAuthentication.razor

@@ -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>

+ 12 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Program.cs

@@ -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();

+ 21 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Properties/launchSettings.json

@@ -0,0 +1,21 @@
+{
+  "profiles": {
+    "http": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
+      "applicationUrl": "http://localhost:5102",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    },
+    "https": {
+      "commandName": "Project",
+      "launchBrowser": true,
+      "applicationUrl": "https://localhost:7293;http://localhost:5102",
+      "environmentVariables": {
+        "ASPNETCORE_ENVIRONMENT": "Development"
+      }
+    }
+  }
+}

+ 14 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/RedirectToLogin.razor

@@ -0,0 +1,14 @@
+@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
+
+@inject NavigationManager Navigation
+
+@code {
+    protected override void OnInitialized()
+    {
+        var request = new InteractiveRequestOptions { Interaction = InteractionType.SignIn, ReturnUrl = Navigation.Uri };
+        var extraQueryParams = new Dictionary<string, object> { ["preservedExtraQueryParams"] = "true" };
+        request.TryAddAdditionalParameter("extraQueryParams", extraQueryParams);
+
+        Navigation.NavigateToLogin("authentication/login", request);
+    }
+}

+ 27 - 0
src/Components/test/testassets/Components.WasmRemoteAuthentication/Routes.razor

@@ -0,0 +1,27 @@
+@using Microsoft.AspNetCore.Components.Authorization
+@using Microsoft.AspNetCore.Components.Routing
+@using Microsoft.AspNetCore.Components.Web
+
+<CascadingAuthenticationState>
+    <Router AppAssembly="@typeof(Program).Assembly">
+        <Found Context="routeData">
+            <AuthorizeRouteView RouteData="@routeData">
+                <NotAuthorized>
+                    @if (context.User.Identity?.IsAuthenticated != true)
+                    {
+                        <RedirectToLogin />
+                    }
+                    else
+                    {
+                        <p role="alert">You are not authorized to access this resource.</p>
+                    }
+                </NotAuthorized>
+            </AuthorizeRouteView>
+            <FocusOnNavigate RouteData="@routeData" Selector="h1" />
+        </Found>
+        <NotFound>
+            <PageTitle>Not found</PageTitle>
+            <p role="alert">Sorry, there's nothing at this address.</p>
+        </NotFound>
+    </Router>
+</CascadingAuthenticationState>