Jeffrey Dallatezza bf37b2947b
Add bindings for csharp modules to use JWT claims (#3414)
# Description of Changes

This exposes JWT claims for csharp modules, similar to how they are
exposed to rust modules in
https://github.com/clockworklabs/SpacetimeDB/pull/3288.

This adds the new types `AuthCtx` and `JwtClaims`, and adds an `AuthCtx`
to the `ReducerContext`.

`AuthCtx` represents the credentials associated with the request, and
`JwtClaims` represents a jwt token.

One difference from the rust version is that I didn't create helpers to
build an `AuthCtx` from a jwt payload. The reason is that we would need
to be able to compute the identity from the payload claims, which
requires a blake3 hash implementation. The first two c# libraries I
found had issues at runtime
([Blake3](https://www.nuget.org/packages/Blake3) is wrapping a rust
implementation, and
[HashifyNet](https://github.com/Deskasoft/HashifyNET/tree/main/HashifyNet/Algorithms/Blake3)
seems to be broken by our trimming because it uses reflection heavily).
I can look into taking the implementation from `HashifyNet`, since it is
MIT licensed, but I don't think we need to block merging on that.

# API and ABI breaking changes

This adds the new types `AuthCtx` and `JwtClaims`, and adds an `AuthCtx`
to the `ReducerContext`.

This also adds a csharp wrapper for the get_jwt ABI function added in
https://github.com/clockworklabs/SpacetimeDB/pull/3288.

# Expected complexity level and risk

2.

# Testing

This has a very minimal unit test of JwtClaims.

I manually tested using this locally with the csharp quickstart, and I
was able to print jwt tokens inside the module.
2025-10-21 19:17:33 +00:00

89 lines
2.5 KiB
C#

namespace SpacetimeDB;
using System;
public sealed class AuthCtx
{
private readonly bool _isInternal;
private readonly Lazy<JwtClaims?> _jwtLazy;
private AuthCtx(bool isInternal, Func<JwtClaims?> jwtFactory)
{
_isInternal = isInternal;
_jwtLazy = new Lazy<JwtClaims?>(() => jwtFactory?.Invoke());
}
/// <summary>
/// Create an AuthCtx for an internal call, with no JWT.
/// </summary>
private static AuthCtx Internal()
{
return new AuthCtx(isInternal: true, jwtFactory: () => null);
}
/// <summary>
/// Create an AuthCtx by looking up the credentials for a connection id in system tables.
///
/// Ideally this would not be part of the public API.
/// This should only be called inside of a reducer.
/// </summary>
public static AuthCtx BuildFromSystemTables(ConnectionId? connectionId, Identity identity)
{
if (connectionId == null)
{
return Internal();
}
return FromConnectionId(connectionId.Value, identity);
}
/// <summary>
/// Create an AuthCtx that reads JWT for a given connection ID.
/// </summary>
private static AuthCtx FromConnectionId(ConnectionId connectionId, Identity identity)
{
return new AuthCtx(
isInternal: false,
jwtFactory: () =>
{
var result = SpacetimeDB.Internal.FFI.get_jwt(ref connectionId, out var source);
SpacetimeDB.Internal.FFI.CheckedStatus.Marshaller.ConvertToManaged(result);
var bytes = SpacetimeDB.Internal.Module.Consume(source);
if (bytes == null || bytes.Length == 0)
{
return null;
}
var jwt = System.Text.Encoding.UTF8.GetString(bytes);
return jwt != null ? new JwtClaims(jwt, identity) : null;
}
);
}
/// <summary>
/// True if this reducer was spawned from inside the database.
/// </summary>
public bool IsInternal => _isInternal;
/// <summary>
/// Check if there is a JWT present.
/// If IsInternal is true, this will be false.
/// </summary>
public bool HasJwt
{
get
{
if (_isInternal)
{
return false;
}
// At this point we do load the bytes.
return _jwtLazy.Value != null;
}
}
/// <summary>
/// Load and get the JwtClaims.
/// </summary>
public JwtClaims? Jwt => _jwtLazy.Value;
}