fix(auth): encrypt stored cookie keys; tighten the link and sign-out flows

- Data-protection keys are encrypted (AES-256-GCM, key derived from the
  server secret) before they reach the DataProtectionKeys table.
- /link only follows local return paths and keeps the form when a
  signed-in visitor has an identity waiting to be linked.
- A failed sign-out says so instead of reloading as if it worked.
This commit is contained in:
Alexa
2026-10-25 15:52:29 +02:00
parent f0e4679887
commit 891ec89899
8 changed files with 150 additions and 13 deletions
@@ -147,6 +147,37 @@ public sealed class ExternalSignInTests : IClassFixture<DorkNetServerFactory>
Assert.Equal(HttpStatusCode.Forbidden, relink.StatusCode);
}
[Fact]
public async Task Cookie_keys_are_stored_encrypted()
{
var (player, password) = await PlayerAsync();
using var site = Client();
using var link = await PostJsonAsync(site, "/api/site/v1/account/link",
new { username = player.Username, password }, ExternalCookie(Guid.NewGuid().ToString("N"), ""));
Assert.Equal(HttpStatusCode.OK, link.StatusCode);
await using var scope = _factory.Services.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<DorkNetDbContext>();
var stored = await db.DataProtectionKeys.Select(k => k.Xml).ToListAsync();
Assert.NotEmpty(stored);
Assert.All(stored, xml =>
{
Assert.Contains("dorknetEncryptedKey", xml);
Assert.DoesNotContain("masterKey", xml);
});
}
[Fact]
public void Key_encryption_round_trips_and_rejects_another_secret()
{
var element = new System.Xml.Linq.XElement("key", new System.Xml.Linq.XElement("masterKey", "secret-bytes"));
var encrypted = new DataProtectionKeyEncryption("first-secret").Encrypt(element).EncryptedElement;
Assert.DoesNotContain("secret-bytes", encrypted.ToString());
Assert.Equal(element.ToString(), new DataProtectionKeyEncryption("first-secret").DecryptElement(encrypted).ToString());
Assert.ThrowsAny<System.Security.Cryptography.CryptographicException>(
() => new DataProtectionKeyEncryption("other-secret").DecryptElement(encrypted));
}
[Fact]
public async Task Downloads_can_require_sign_in()
{