| File: Integration\TerminalWebSocketAuthenticationTests.cs | Web Access |
| Project: src\tests\Aspire.Dashboard.Tests\Aspire.Dashboard.Tests.csproj (Aspire.Dashboard.Tests) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Net; using System.Net.WebSockets; using Aspire.Dashboard.Configuration; using Aspire.Hosting; using Microsoft.AspNetCore.InternalTesting; using Xunit; namespace Aspire.Dashboard.Tests.Integration; public class TerminalWebSocketAuthenticationTests(ITestOutputHelper testOutputHelper) { [Theory] [InlineData(FrontendAuthMode.BrowserToken, "/api/terminal?resource=test")] [InlineData(FrontendAuthMode.BrowserToken, "/api/apphost-terminal?terminalId=test")] [InlineData(FrontendAuthMode.OpenIdConnect, "/api/terminal?resource=test")] [InlineData(FrontendAuthMode.OpenIdConnect, "/api/apphost-terminal?terminalId=test")] public async Task TerminalWebSocket_Unauthenticated_RejectsUpgradeWithAuthenticationChallenge(FrontendAuthMode authMode, string endpoint) { await using var authority = authMode is FrontendAuthMode.OpenIdConnect ? await MockOpenIdAuthority.CreateAsync().DefaultTimeout() : null; await using var app = IntegrationTestHelpers.CreateDashboardWebApplication(testOutputHelper, config => { config[DashboardConfigNames.DashboardFrontendAuthModeName.ConfigKey] = authMode.ToString(); config[DashboardConfigNames.DashboardFrontendBrowserTokenName.ConfigKey] = "test-token"; if (authority is not null) { config["Authentication:Schemes:OpenIdConnect:Authority"] = authority.Url; config["Authentication:Schemes:OpenIdConnect:ClientId"] = "MyClientId"; config["Authentication:Schemes:OpenIdConnect:ClientSecret"] = "MyClientSecret"; } }); await app.StartAsync().DefaultTimeout(); var frontendUri = new Uri(app.FrontendSingleEndPointAccessor().GetResolvedAddress()); var terminalUri = new Uri(frontendUri, endpoint); var websocketUri = new UriBuilder(terminalUri) { Scheme = "ws" }.Uri; // Do not follow the authentication redirect: a failure on the login page // would not prove that the terminal endpoint itself rejected the upgrade. using var invoker = new HttpMessageInvoker(new SocketsHttpHandler { AllowAutoRedirect = false }); using var socket = new ClientWebSocket(); socket.Options.CollectHttpResponseDetails = true; // A valid Origin ensures authentication, rather than the CSWSH defense, // is what prevents this cookie-less client from reaching the terminal. socket.Options.SetRequestHeader("Origin", frontendUri.GetLeftPart(UriPartial.Authority)); var exception = await Assert.ThrowsAsync<WebSocketException>(() => socket.ConnectAsync(websocketUri, invoker, CancellationToken.None).DefaultTimeout()); Assert.Equal(WebSocketError.NotAWebSocket, exception.WebSocketErrorCode); Assert.Equal(WebSocketState.Closed, socket.State); Assert.Equal(HttpStatusCode.Redirect, socket.HttpStatusCode); Assert.NotNull(socket.HttpResponseHeaders); var location = new Uri(Assert.Single(socket.HttpResponseHeaders["Location"])); if (authority is not null) { Assert.Equal(new Uri(authority.Url).GetLeftPart(UriPartial.Authority), location.GetLeftPart(UriPartial.Authority)); Assert.Equal("/authorize", location.AbsolutePath); } else { Assert.Equal(frontendUri.GetLeftPart(UriPartial.Authority), location.GetLeftPart(UriPartial.Authority)); Assert.Equal("/login", location.AbsolutePath); } } }