Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
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
1 change: 1 addition & 0 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="$(MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.OpenApi" Version="$(MicrosoftAspNetCoreOpenApiPackageVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.OutputCaching.StackExchangeRedis" Version="$(MicrosoftAspNetCoreOutputCachingStackExchangeRedisPackageVersion)" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="$(MicrosoftAspNetCoreTestHostPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="$(MicrosoftExtensionsCachingStackExchangeRedisPackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks.EntityFrameworkCore" Version="$(MicrosoftExtensionsDiagnosticsHealthChecksEntityFrameworkCorePackageVersion)" />
<PackageVersion Include="Microsoft.Extensions.Diagnostics.HealthChecks" Version="$(MicrosoftExtensionsDiagnosticsHealthChecksPackageVersion)" />
Expand Down
1 change: 1 addition & 0 deletions eng/Versions.props
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
<MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>8.0.7</MicrosoftAspNetCoreAuthenticationOpenIdConnectPackageVersion>
<MicrosoftAspNetCoreOpenApiPackageVersion>8.0.7</MicrosoftAspNetCoreOpenApiPackageVersion>
<MicrosoftAspNetCoreOutputCachingStackExchangeRedisPackageVersion>8.0.7</MicrosoftAspNetCoreOutputCachingStackExchangeRedisPackageVersion>
<MicrosoftAspNetCoreTestHostPackageVersion>8.0.7</MicrosoftAspNetCoreTestHostPackageVersion>
<MicrosoftExtensionsCachingStackExchangeRedisPackageVersion>8.0.7</MicrosoftExtensionsCachingStackExchangeRedisPackageVersion>
<MicrosoftExtensionsDiagnosticsHealthChecksEntityFrameworkCorePackageVersion>8.0.7</MicrosoftExtensionsDiagnosticsHealthChecksEntityFrameworkCorePackageVersion>
<MicrosoftExtensionsDiagnosticsHealthChecksPackageVersion>8.0.7</MicrosoftExtensionsDiagnosticsHealthChecksPackageVersion>
Expand Down
34 changes: 20 additions & 14 deletions src/Aspire.Dashboard/Model/ValidateTokenMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,22 +26,21 @@ public ValidateTokenMiddleware(RequestDelegate next, IOptionsMonitor<DashboardOp

public async Task InvokeAsync(HttpContext context)
{
if (context.Request.Path.Equals("/login", StringComparisons.UrlPath) && context.Request.Query.TryGetValue("t", out var value))
if (context.Request.Path.Equals("/login", StringComparisons.UrlPath))
{
if (_options.CurrentValue.Frontend.AuthMode == FrontendAuthMode.BrowserToken)
if (_options.CurrentValue.Frontend.AuthMode != FrontendAuthMode.BrowserToken)
{
_logger.LogDebug($"Request to validate token URL but auth mode isn't set to {FrontendAuthMode.BrowserToken}.");

RedirectAfterValidation(context);
}
else if (context.Request.Query.TryGetValue("t", out var value) && _options.CurrentValue.Frontend.AuthMode == FrontendAuthMode.BrowserToken)
{
var dashboardOptions = context.RequestServices.GetRequiredService<IOptionsMonitor<DashboardOptions>>();
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
{
Expand All @@ -62,14 +61,21 @@ 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<bool> TryAuthenticateAsync(string incomingBrowserToken, HttpContext httpContext, IOptionsMonitor<DashboardOptions> dashboardOptions)
{
Expand Down
1 change: 1 addition & 0 deletions tests/Aspire.Dashboard.Tests/Aspire.Dashboard.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<ItemGroup>
<PackageReference Include="Grpc.Tools" />
<PackageReference Include="Microsoft.DotNet.XUnitExtensions" />
<PackageReference Include="Microsoft.AspNetCore.TestHost" />

<ProjectReference Include="..\..\src\Aspire.Dashboard\Aspire.Dashboard.csproj" />

Expand Down
Original file line number Diff line number Diff line change
@@ -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<IHost> SetUpHostAsync(FrontendAuthMode authMode, string expectedToken)
{
return await new HostBuilder()
.ConfigureWebHost(webBuilder =>
{
webBuilder
.UseTestServer()
.ConfigureServices(services =>
{
services.AddRouting();
services.AddAuthentication().AddCookie();

services.Configure<DashboardOptions>(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<ValidateTokenMiddleware>();
});
})
.StartAsync();
}
}