diff --git a/Directory.Packages.props b/Directory.Packages.props index a553461eb27..c80124920f5 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -57,6 +57,7 @@ + diff --git a/eng/Versions.props b/eng/Versions.props index c9269f86ce7..ced6d07c914 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -49,6 +49,7 @@ 8.0.7 8.0.7 8.0.7 + 8.0.7 8.0.7 8.0.7 8.0.7 diff --git a/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs b/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs index 70a49ab972a..e741099f7b1 100644 --- a/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs +++ b/src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs @@ -26,22 +26,21 @@ public ValidateTokenMiddleware(RequestDelegate next, IOptionsMonitor>(); if (await TryAuthenticateAsync(value.ToString(), context, dashboardOptions).ConfigureAwait(false)) { // Success. Redirect to the app. - if (context.Request.Query.TryGetValue("returnUrl", out var returnUrl)) - { - context.Response.Redirect(returnUrl.ToString()); - } - else - { - context.Response.Redirect(DashboardUrls.ResourcesUrl()); - } + RedirectAfterValidation(context); } else { @@ -62,15 +61,23 @@ public async Task InvokeAsync(HttpContext context) return; } - else - { - _logger.LogDebug($"Request to validate token URL but auth mode isn't set to {FrontendAuthMode.BrowserToken}."); - } } await _next(context).ConfigureAwait(false); } + private static void RedirectAfterValidation(HttpContext context) + { + if (context.Request.Query.TryGetValue("returnUrl", out var returnUrl)) + { + context.Response.Redirect(returnUrl.ToString()); + } + else + { + context.Response.Redirect(DashboardUrls.ResourcesUrl()); + } + } + public static async Task TryAuthenticateAsync(string incomingBrowserToken, HttpContext httpContext, IOptionsMonitor dashboardOptions) { if (string.IsNullOrEmpty(incomingBrowserToken) || dashboardOptions.CurrentValue.Frontend.GetBrowserTokenBytes() is not { } expectedBrowserTokenBytes) diff --git a/tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj b/tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj index 001879a9549..9cbf298ca5f 100644 --- a/tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj +++ b/tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj @@ -15,6 +15,7 @@ + diff --git a/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs b/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs new file mode 100644 index 00000000000..77add73762e --- /dev/null +++ b/tests/Aspire.Dashboard.Tests/Middleware/ValidateTokenMiddlewareTests.cs @@ -0,0 +1,96 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Dashboard.Configuration; +using Aspire.Dashboard.Model; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Xunit; + +namespace Aspire.Dashboard.Tests.Middleware; + +public class ValidateTokenMiddlewareTests +{ + [Fact] + public async Task ValidateToken_NotBrowserTokenAuth_RedirectedToHomepage() + { + using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty); + var response = await host.GetTestClient().GetAsync("/login?t=test"); + Assert.Equal("/", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task ValidateToken_NotBrowserTokenAuth_RedirectedToReturnUrl() + { + using var host = await SetUpHostAsync(FrontendAuthMode.Unsecured, string.Empty); + var response = await host.GetTestClient().GetAsync("/login?t=test&returnUrl=/test"); + Assert.Equal("/test", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task ValidateToken_BrowserTokenAuth_WrongToken_RedirectsToLogin() + { + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); + var response = await host.GetTestClient().GetAsync("/login?t=wrong"); + Assert.Equal("/login", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task ValidateToken_BrowserTokenAuth_WrongToken_RedirectsToLogin_WithReturnUrl() + { + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); + var response = await host.GetTestClient().GetAsync("/login?t=wrong&returnUrl=/test"); + Assert.Equal("/login?returnUrl=%2ftest", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task ValidateToken_BrowserTokenAuth_RightToken_RedirectsToHome() + { + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); + var response = await host.GetTestClient().GetAsync("/login?t=token"); + Assert.Equal("/", response.Headers.Location?.OriginalString); + } + + [Fact] + public async Task ValidateToken_BrowserTokenAuth_RightToken_RedirectsToReturnUrl() + { + using var host = await SetUpHostAsync(FrontendAuthMode.BrowserToken, "token"); + var response = await host.GetTestClient().GetAsync("/login?t=token&returnUrl=/test"); + Assert.Equal("/test", response.Headers.Location?.OriginalString); + } + + private static async Task SetUpHostAsync(FrontendAuthMode authMode, string expectedToken) + { + return await new HostBuilder() + .ConfigureWebHost(webBuilder => + { + webBuilder + .UseTestServer() + .ConfigureServices(services => + { + services.AddRouting(); + services.AddAuthentication().AddCookie(); + + services.Configure(o => + { + o.Frontend = new FrontendOptions + { + AuthMode = authMode, + BrowserToken = expectedToken, + EndpointUrls = "http://localhost/" // required for TryParseOptions + }; + + Assert.True(o.Frontend.TryParseOptions(out _)); + }); + }) + .Configure(app => + { + app.UseMiddleware(); + }); + }) + .StartAsync(); + } +}