Vier Punkte aus der Durchsicht von f923721. Alles Uebrige traegt: Anmeldung
greift (unangemeldet 401 auf jedem /api-Pfad ausser status und login), alle
zwanzig Endpunkte antworten, SSE laeuft, die Oberflaeche zeigt echte Zahlen,
die Daemon-Schalter funktionieren weiter, und die Modul-Routen sitzen richtig
im Hosting-Projekt statt im Core.
1. Die Endpunkte /positions/lock und /positions/reassign deuteten den
Instrumentenschluessel mit InstrumentRef.ForStock als Ticker. Bei einer Aktie
ging das zufaellig gut - die Waehrung fiel still auf USD, der Schluessel kam
trotzdem richtig heraus. Bei einem Optionskontrakt warf Normalize wegen des
Trennzeichens "|" eine Ausnahme: HTTP 500 ohne Text, auf der Testmaschine
reproduziert. Jetzt InstrumentRef.TryParse, und ein unlesbarer Schluessel
ebenso wie ein unsinniger Buchname ergibt eine Antwort statt eines Absturzes.
2. OrderManager las UseTwsApi als Erreichbarkeitsnachweis. Das ist eine
Einstellung, kein Zustand - sie steht auch dann auf true, wenn TWS nicht
laeuft. Die Ansicht haette waehrend jeder Stoerung "Broker erreichbar, keine
offenen Orders" gemeldet und jede Bindung als grundlos dargestellt. Genau
davor warnt die Beschreibung an IBrokerOrderBook.GetOpenOrdersAsync.
3. AllowRemoteAccess stand als Vorgabe auf true: erster Start heisst 0.0.0.0,
Klartext-HTTP, und der AuthToken ist anfangs leer. Ausnutzbar war das nicht
(ohne Token faellt die Anmeldung zu, alles antwortet 401), aber die Vorgabe
zeigte in die falsche Richtung. Jetzt lokal, mit Begruendung im Text.
4. Das Sitzungstoken stand im Query-String des SSE-Stroms. Unnoetig: EventSource
schickt das HttpOnly-Cookie bei gleichem Ursprung von selbst. Die Oberflaeche
nutzt den Weg nicht mehr, und AuthService nimmt ihn nur noch fuer
/api/logs/stream an - ein Query-String landet in Logs, Verlauf und Referer.
Geminis Test auf den Query-Token bestand auf dem alten, breiten Verhalten. Er
ist durch zwei ersetzt, die die enge Regel in beide Richtungen festhalten.
499 Tests gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
4.5 KiB
C#
138 lines
4.5 KiB
C#
using FluentAssertions;
|
||
using IBKRTrader.Core.Settings;
|
||
using IBKRTrader.Hosting.Web;
|
||
|
||
namespace IBKRTrader.Tests.Web;
|
||
|
||
[Trait("cat", "unit")]
|
||
public class AuthServiceTests
|
||
{
|
||
[Fact]
|
||
public void ValidateToken_ReturnsTrueForCorrectToken()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "secret-token-123";
|
||
var sut = new AuthService(settings);
|
||
|
||
sut.IsConfigured.Should().BeTrue();
|
||
sut.ValidateToken("secret-token-123").Should().BeTrue();
|
||
sut.ValidateToken("wrong-token").Should().BeFalse();
|
||
sut.ValidateToken("").Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public void ValidateToken_ReturnsFalseWhenNotConfigured()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "";
|
||
var sut = new AuthService(settings);
|
||
|
||
sut.IsConfigured.Should().BeFalse();
|
||
sut.ValidateToken("any-token").Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public void SessionToken_RoundTrip_ValidatesSuccessfully()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "my-secret-key";
|
||
var sut = new AuthService(settings);
|
||
|
||
var sessionToken = sut.CreateSessionToken(TimeSpan.FromHours(1));
|
||
|
||
sessionToken.Should().NotBeNullOrWhiteSpace();
|
||
sut.ValidateSessionToken(sessionToken).Should().BeTrue();
|
||
}
|
||
|
||
[Fact]
|
||
public void SessionToken_TamperedToken_FailsValidation()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "my-secret-key";
|
||
var sut = new AuthService(settings);
|
||
|
||
var sessionToken = sut.CreateSessionToken(TimeSpan.FromHours(1));
|
||
var tampered = sessionToken + "tamper";
|
||
|
||
sut.ValidateSessionToken(tampered).Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public void SessionToken_ExpiredToken_FailsValidation()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "my-secret-key";
|
||
var sut = new AuthService(settings);
|
||
|
||
// Abgelaufenes Token (minus 1 Sekunde)
|
||
var sessionToken = sut.CreateSessionToken(TimeSpan.FromSeconds(-10));
|
||
|
||
sut.ValidateSessionToken(sessionToken).Should().BeFalse();
|
||
}
|
||
|
||
[Fact]
|
||
public void IsAuthenticated_RecognizesBearerHeader()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "my-secret-key";
|
||
var sut = new AuthService(settings);
|
||
|
||
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
|
||
ctx.Request.Headers.Authorization = "Bearer my-secret-key";
|
||
|
||
sut.IsAuthenticated(ctx).Should().BeTrue();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Der Token im Query-String gilt <b>nur</b> für den SSE-Strom. EventSource kann keine
|
||
/// Kopfzeilen setzen – das ist der einzige Grund, aus dem es diesen Weg gibt.
|
||
/// </summary>
|
||
[Fact]
|
||
public void IsAuthenticated_AcceptsQueryToken_OnTheLogStream()
|
||
{
|
||
var sut = SutWithToken("my-secret-key");
|
||
|
||
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
|
||
ctx.Request.Path = "/api/logs/stream";
|
||
ctx.Request.QueryString = new Microsoft.AspNetCore.Http.QueryString("?token=my-secret-key");
|
||
|
||
sut.IsAuthenticated(ctx).Should().BeTrue();
|
||
}
|
||
|
||
/// <summary>
|
||
/// Überall sonst zählt er nicht. Ein Query-String landet in Server-Logs, im Browserverlauf und
|
||
/// in Referer-Kopfzeilen; ein Geheimnis hat dort nichts zu suchen, wo ein anderer Weg offensteht.
|
||
/// </summary>
|
||
[Fact]
|
||
public void IsAuthenticated_IgnoresQueryToken_EverywhereElse()
|
||
{
|
||
var sut = SutWithToken("my-secret-key");
|
||
|
||
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
|
||
ctx.Request.Path = "/api/positions";
|
||
ctx.Request.QueryString = new Microsoft.AspNetCore.Http.QueryString("?token=my-secret-key");
|
||
|
||
sut.IsAuthenticated(ctx).Should().BeFalse();
|
||
}
|
||
|
||
private static AuthService SutWithToken(string token)
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = token;
|
||
return new AuthService(settings);
|
||
}
|
||
|
||
[Fact]
|
||
public void IsAuthenticated_RejectsUnauthenticatedRequest()
|
||
{
|
||
var settings = new SettingsService();
|
||
settings.Settings.Webserver.AuthToken = "my-secret-key";
|
||
var sut = new AuthService(settings);
|
||
|
||
var ctx = new Microsoft.AspNetCore.Http.DefaultHttpContext();
|
||
|
||
sut.IsAuthenticated(ctx).Should().BeFalse();
|
||
}
|
||
}
|
||
|