From 32a6cc0a29e9600004c0c390f2dab1f998774ad7 Mon Sep 17 00:00:00 2001 From: feruzm Date: Fri, 4 Sep 2026 11:35:59 +0000 Subject: [PATCH] Token validation refuses login-typed messages A HiveSigner-style message typed login proves who signed it and nothing more: HiveSigner answers /api/me for one and refuses it everywhere else, and the Ecency clients never send one (wallet logins send code, HiveSigner issues posting for the scopes they request). It is not a session here either, decided before any key lookup. Everything else is left to the signature checks as before. --- dotnet/EcencyApi.Tests/TokenTypeTests.cs | 42 ++++++++++++++++++++ dotnet/EcencyApi/Handlers/PrivateApi.Core.cs | 21 ++++++++++ 2 files changed, 63 insertions(+) create mode 100644 dotnet/EcencyApi.Tests/TokenTypeTests.cs diff --git a/dotnet/EcencyApi.Tests/TokenTypeTests.cs b/dotnet/EcencyApi.Tests/TokenTypeTests.cs new file mode 100644 index 00000000..a38c9bd2 --- /dev/null +++ b/dotnet/EcencyApi.Tests/TokenTypeTests.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Nodes; +using EcencyApi.Handlers; +using Xunit; + +namespace EcencyApi.Tests; + +/// +/// A HiveSigner-style message typed "login" proves who signed it and nothing +/// more: HiveSigner answers /api/me for one and refuses it everywhere else, and +/// the Ecency clients never send one (wallet logins send "code", HiveSigner +/// issues "posting" for the scopes they request). Token validation refuses it +/// before any key lookup, and leaves every other shape to the signature checks. +/// +public class TokenTypeTests +{ + private static JsonNode? Parse(string json) => JsonNode.Parse(json); + + [Fact] + public void LoginTypedMessageIsRefused() + { + Assert.True(PrivateApi.IsLoginOnlyMessage(Parse("""{"type":"login","app":"ecency.app"}"""))); + Assert.True(PrivateApi.IsLoginOnlyMessage( + Parse("""{"type":"login","app":"ecency.app","audience":"honeyback://hive"}"""))); + } + + [Theory] + [InlineData("""{"type":"code","app":"ecency.app"}""")] + [InlineData("""{"type":"posting","app":"ecency.app"}""")] + [InlineData("""{"type":"offline","app":"ecency.app"}""")] + [InlineData("""{"type":"refresh","app":"ecency.app"}""")] + [InlineData("""{"type":"Login","app":"ecency.app"}""")] + [InlineData("""{"app":"ecency.app"}""")] + [InlineData("""{"type":null}""")] + [InlineData("""{"type":1}""")] + [InlineData("""{"type":["login"]}""")] + [InlineData("""["login"]""")] + [InlineData("null")] + public void EverythingElseIsLeftToTheSignatureChecks(string signedMessage) + { + Assert.False(PrivateApi.IsLoginOnlyMessage(Parse(signedMessage))); + } +} diff --git a/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs b/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs index 5f65bec2..7da7f821 100644 --- a/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs +++ b/dotnet/EcencyApi/Handlers/PrivateApi.Core.cs @@ -120,6 +120,15 @@ timestampNode is JsonValue timestampValue return null; } + // A message typed "login" only says who signed it. HiveSigner answers + // /api/me for one and refuses everything else, and no Ecency client + // ever sends one here, so it is not a session here either. Decided + // before any node lookup: the shape alone settles it. + if (IsLoginOnlyMessage(signedMessage)) + { + return null; + } + var currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); // rawMessage = JSON.stringify({ signed_message, authors, timestamp }) @@ -192,6 +201,18 @@ timestampNode is JsonValue timestampValue } } + /// + /// True for a signed_message whose type is the string "login": a proof of + /// identity for another app, never a session. Anything else, including a + /// missing or non-string type, is left to the signature checks as before. + /// + internal static bool IsLoginOnlyMessage(JsonNode? signedMessage) => + signedMessage is JsonObject obj + && obj.TryGetPropertyValue("type", out var typeNode) + && typeNode is JsonValue typeValue + && JsVal.TryGetStringLenient(typeValue, out var type) + && type == "login"; + /// key_auths.map(([key]) => key) — throws on non-array input like .map on a non-array. private static List MapKeyAuths(JsonNode? keyAuths) {