Files
ClaudeDo/src/ClaudeDo.Worker/Online/JwtClaims.cs
T
mika kunsandClaude Opus 4.8 cee051bb6d feat(online-inbox): carry ownerId on sync to prepare for multi-user
Plumb a per-resource owner (Zitadel sub) through the sync contract without
enforcing isolation client-side — the server stays the authority.

- Dtos: add optional ownerId to RemoteList/RemoteTask/MirrorTask
- JwtClaims: decode the sub claim from the access token (never throws)
- OnlineSyncService: stamp ownerId on pushed lists + mirror; defensively skip
  pulled tasks owned by a different user (unowned tasks still sync, so
  single-user behavior is unchanged)
- docs: contract documents ownerId + multi-user readiness

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-10 13:57:39 +02:00

50 lines
1.4 KiB
C#

using System.Text.Json;
namespace ClaudeDo.Worker.Online;
/// <summary>
/// Minimal, dependency-free reader for a JWT access token's payload claims. Used to resolve the
/// current user's Zitadel subject (<c>sub</c>) so sync payloads can be stamped with an owner.
/// Never throws — returns null when the token is absent or cannot be parsed.
/// </summary>
public static class JwtClaims
{
/// <summary>
/// Returns the <c>sub</c> claim of the JWT, or null if the token is absent/unparseable or
/// carries no subject.
/// </summary>
public static string? GetSubject(string? jwt)
{
if (string.IsNullOrWhiteSpace(jwt))
return null;
var parts = jwt.Split('.');
if (parts.Length < 2)
return null;
try
{
using var doc = JsonDocument.Parse(Base64UrlDecode(parts[1]));
if (doc.RootElement.TryGetProperty("sub", out var sub) &&
sub.ValueKind == JsonValueKind.String)
return sub.GetString();
return null;
}
catch
{
return null;
}
}
private static byte[] Base64UrlDecode(string input)
{
var s = input.Replace('-', '+').Replace('_', '/');
switch (s.Length % 4)
{
case 2: s += "=="; break;
case 3: s += "="; break;
}
return Convert.FromBase64String(s);
}
}