Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions dotnet/EcencyApi.Tests/TokenTypeTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
using System.Text.Json.Nodes;
using EcencyApi.Handlers;
using Xunit;

namespace EcencyApi.Tests;

/// <summary>
/// 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.
/// </summary>
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)));
}
}
21 changes: 21 additions & 0 deletions dotnet/EcencyApi/Handlers/PrivateApi.Core.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -192,6 +201,18 @@ timestampNode is JsonValue timestampValue
}
}

/// <summary>
/// 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.
/// </summary>
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";

/// <summary>key_auths.map(([key]) => key) — throws on non-array input like .map on a non-array.</summary>
private static List<string?> MapKeyAuths(JsonNode? keyAuths)
{
Expand Down
Loading