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)
{