---
title: "PasswordProtection source"
url: "https://nsta1.github.io/Orleans.Lattice/samples/PasswordProtection/source.html"
source: "https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/PasswordProtection"
documents: "Orleans.Lattice 9.9.0 (release line 9.9)"
built: "2026-10-04"
all-pages: "https://nsta1.github.io/Orleans.Lattice/llms.txt"
---
# PasswordProtection source

Part of [Password Protection](README.md).

The source of the [PasswordProtection](https://github.com/NSTA1/Orleans.Lattice/tree/release/9.9/samples/PasswordProtection) sample.

## Program.cs

````csharp
using System.Text;
using Grpc.Core;
using Grpc.Core.Interceptors;
using Grpc.Net.Client;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.Extensions.DependencyInjection;
using Orleans.Lattice;
using Orleans.Lattice.Api.State;
using Orleans.Lattice.Api.State.Grpc;
using Orleans.Lattice.Auth;
using Orleans.Lattice.Membership;
using Orleans.Lattice.Samples.PasswordProtection;

// ---------------------------------------------------------------------------
// PasswordProtection - the username/password credential mechanism (the reference
// EnvVarCredentialAuthorizer from Orleans.Lattice.Api.State.Grpc) combined with
// per-tree authorization on a single in-process silo.
//
// The demo:
//   1. Creates two operator accounts by hashing their passwords with
//      LatticePasswordHash (salted PBKDF2-SHA256) and publishing each as the
//      LATTICE_STATE_USER_<name> environment variable the authorizer reads.
//   2. Stands up ONE silo that co-hosts the read-only State API gRPC surface,
//      gated by the username/password authorizer at the transport and by the
//      Orleans.Lattice.Auth policy gate on the data path. 'admin' is a bootstrap
//      administrator; 'reader' is granted read-only access to tree 'orders'.
//   3. Connects over real gRPC and proves AUTHENTICATION: the right passwords
//      are accepted, a wrong password and an anonymous call are rejected.
//   4. Proves AUTHORIZATION: the authenticated username drives per-tree reads -
//      admin sees every tree, reader sees only the 'orders' tree it was granted.
//
// The whole thing runs in one process for convenience, but every client call
// crosses a real gRPC channel presenting a Basic credential exactly as an
// external dashboard or CLI would.
// ---------------------------------------------------------------------------

const int Port = 5223;
const string OrdersTree = "orders";   // reader is granted read-only access to this tree
const string LedgerTree = "ledger";   // admin-only; reader is never granted access

const string AdminUser = "admin";
const string AdminPassword = "Adm1n-Passw0rd";
const string ReaderUser = "reader";
const string ReaderPassword = "Read0nly-Passw0rd";

// -- Act 1: create two operator accounts ------------------------------------
// Each credential is a salted PBKDF2-SHA256 hash (never the plaintext), stored
// in the environment variable the authorizer looks up by username. In a real
// deployment an operator produces these with tools/New-LatticeStateCredential.ps1 and sets
// the variables out-of-band; here we mint them in-process for a one-command run.
Console.WriteLine("== Act 1: create two operator accounts ==");
PublishCredential(AdminUser, AdminPassword);
PublishCredential(ReaderUser, ReaderPassword);
Console.WriteLine(
    $"  {AdminUser,-6} -> env LATTICE_STATE_USER_{AdminUser}   (salted pbkdf2-sha256)  role: bootstrap administrator");
Console.WriteLine(
    $"  {ReaderUser,-6} -> env LATTICE_STATE_USER_{ReaderUser}  (salted pbkdf2-sha256)  role: read-only on tree '{OrdersTree}'\n");

// -- Act 2: stand up the single silo + State API gRPC (auth required) -------
// h2c (HTTP/2 without TLS) keeps the sample dependency-free - no dev cert.
// Kestrel binds HttpProtocols.Http2 with no certificate below, and grpc-dotnet
// speaks h2c by prior knowledge over an http:// address, so no process-global
// AppContext switch is needed.

var builder = WebApplication.CreateBuilder(args);
builder.Logging.ClearProviders();
builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(Port, listen => listen.Protocols = HttpProtocols.Http2);
});

builder.Host.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();
    silo.AddMemoryGrainStorageAsDefault();
    silo.UseInMemoryReminderService();
    silo.AddLattice((services, name) => services.AddMemoryGrainStorage(name));

    // Membership resolves the ambient caller credential into a subject; Auth
    // installs the default-deny data-plane gate. 'admin' is a bootstrap
    // administrator, so the sample can seed the two trees before any rule
    // grants a write and Act 4 shows it reading every tree (the reader's rule
    // needs no grant: the policy store writes its reserved tree under system
    // origin).
    silo.AddLatticeMembership();
    silo.AddLatticeAuth(options =>
    {
        options.DefaultEffect = LatticeEffect.Deny;
        options.BootstrapAdministrators.Add(AdminUser);
    });

    // Maps the already-authenticated Basic username onto the caller subject id.
    silo.Services.AddSingleton<ILatticeCredentialAuthenticator, PasswordAuthenticator>();

    // The read-only state API facade the gRPC surface binds.
    silo.AddLatticeStateApi();
});

// Register the Basic-aware credential bridge BEFORE AddLatticeStateApiGrpc so
// its TryAdd preserves ours instead of the default bearer bridge.
builder.Services.AddSingleton<ILatticeStateApiCredentialBridge, PasswordCredentialBridge>();

// The gRPC binding (auth required) plus the username/password authorizer that
// validates the inbound Basic header against the LATTICE_STATE_USER_* variables.
builder.Services.AddLatticeStateApiGrpc(o => o.RequireAuthorization = true);
builder.Services.AddEnvVarCredentialAuthorizer();

var app = builder.Build();
app.MapLatticeStateApiGrpc();
await app.StartAsync();

Console.WriteLine("== Act 2: start single silo + State API gRPC (auth required) ==");
Console.WriteLine($"  Silo + state-API gRPC listening on http://localhost:{Port}");

// Seed the two trees as the bootstrap administrator (which bypasses the gate),
// and author the reader's per-tree read rule.
var grainFactory = app.Services.GetRequiredService<IGrainFactory>();
var store = app.Services.GetRequiredService<ILatticeAuthorizationPolicyStore>();

using (LatticeCredentialContext.Use(AdminUser, scheme: PasswordAuthenticator.Scheme))
{
    // reader may read (point + range) the 'orders' tree, and nothing else.
    await store.PutRuleAsync(new LatticeAuthorizationRule(
        "reader-read-orders",
        LatticeSubjectSelector.User(ReaderUser),
        LatticeScope.Tree(OrdersTree),
        LatticeOperation.Read | LatticeOperation.RangeRead,
        LatticeEffect.Allow));

    var orders = grainFactory.GetGrain<ILattice>(OrdersTree);
    for (var i = 1; i <= 3; i++)
    {
        await orders.SetAsync($"order-{i:D3}", Encoding.UTF8.GetBytes($"pending-{i}"));
    }

    var ledger = grainFactory.GetGrain<ILattice>(LedgerTree);
    for (var i = 1; i <= 2; i++)
    {
        await ledger.SetAsync($"entry-{i:D3}", Encoding.UTF8.GetBytes($"amount-{i}"));
    }
}

Console.WriteLine($"  Seeded tree '{OrdersTree}' with 3 entries; tree '{LedgerTree}' with 2 entries.\n");

using var channel = GrpcChannel.ForAddress($"http://localhost:{Port}");

// Wait for the compiled policy snapshot to reflect the authored rule (it
// rebuilds off the policy-tree change feed) before asserting enforcement.
await WaitUntilAsync(
    async () => await ScanCountAsync(ClientFor(ReaderUser, ReaderPassword), OrdersTree) == 3,
    TimeSpan.FromSeconds(15));

// -- Act 3: authenticate over gRPC (username/password) ----------------------
Console.WriteLine("== Act 3: authenticate over gRPC (username/password) ==");
Console.WriteLine($"  {AdminUser,-6} correct password -> {await AuthenticationOutcome(ClientFor(AdminUser, AdminPassword))}");
Console.WriteLine($"  {ReaderUser,-6} correct password -> {await AuthenticationOutcome(ClientFor(ReaderUser, ReaderPassword))}");
Console.WriteLine($"  {ReaderUser,-6} WRONG   password -> {await AuthenticationOutcome(ClientFor(ReaderUser, "not-the-password"))}");
Console.WriteLine($"  no credentials          -> {await AuthenticationOutcome(ClientFor(null, null))}\n");

// -- Act 4: authorize per-tree reads (tied to the authenticated user) -------
Console.WriteLine("== Act 4: authorize per-tree reads (tied to the authenticated user) ==");
var adminClient = ClientFor(AdminUser, AdminPassword);
var readerClient = ClientFor(ReaderUser, ReaderPassword);

var adminOrders = await ScanCountAsync(adminClient, OrdersTree);
var adminLedger = await ScanCountAsync(adminClient, LedgerTree);
var readerOrders = await ScanCountAsync(readerClient, OrdersTree);
var readerLedger = await ScanCountAsync(readerClient, LedgerTree);

Console.WriteLine(
    $"  {AdminUser,-6} scan '{OrdersTree}' -> {adminOrders} entries ; scan '{LedgerTree}' -> {adminLedger} entries   (bootstrap admin: sees all)");
Console.WriteLine(
    $"  {ReaderUser,-6} scan '{OrdersTree}' -> {readerOrders} entries ; scan '{LedgerTree}' -> {readerLedger} entries   (granted '{OrdersTree}' read; '{LedgerTree}' hidden)\n");

var success = adminOrders == 3 && adminLedger == 2 && readerOrders == 3 && readerLedger == 0;
Console.WriteLine(success
    ? $"[OK] username/password authenticated both users; per-tree rules limited '{ReaderUser}' to '{OrdersTree}'."
    : "[FAIL] unexpected authentication or authorization outcome.");

await app.StopAsync();
return success ? 0 : 1;

// --- helpers ---------------------------------------------------------------

// Hashes the password with a fresh random salt and publishes it as the
// environment variable the authorizer looks up for this username.
static void PublishCredential(string username, string password) =>
    Environment.SetEnvironmentVariable(
        "LATTICE_STATE_USER_" + username,
        LatticePasswordHash.Hash(password));

// Builds a State API gRPC client that presents the given account's Basic
// credential on every call, or an anonymous client when username is null.
LatticeStateApiGrpcClient ClientFor(string? username, string? password)
{
    var invoker = channel.CreateCallInvoker();
    if (username is not null)
    {
        var header = "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes($"{username}:{password}"));
        invoker = invoker.Intercept(metadata =>
        {
            metadata.Add("authorization", header);
            return metadata;
        });
    }

    return LatticeStateApiGrpcClient.Create(invoker, app.Services);
}

// "authenticated" when the credential is accepted at the transport, or
// "rejected (PermissionDenied)" when the username/password gate refuses the call.
static async Task<string> AuthenticationOutcome(LatticeStateApiGrpcClient client)
{
    try
    {
        await client.ListTreesAsync(new CatalogRequest { PageSize = 50 });
        return "authenticated";
    }
    catch (RpcException ex) when (ex.StatusCode == StatusCode.PermissionDenied)
    {
        return "rejected (PermissionDenied)";
    }
}

// Number of entries a client can read from a tree via the snapshot-isolated
// scan cursor. A tree the caller is not permitted to see reads as empty: the
// gate hides it (NotFound / PermissionDenied) rather than disclosing its
// existence or contents.
static async Task<int> ScanCountAsync(LatticeStateApiGrpcClient client, string treeId)
{
    var total = 0;
    string? token = null;
    try
    {
        do
        {
            var page = await client.ScanEntriesAsync(new EntryScanRequest
            {
                TreeId = treeId,
                PageSize = 50,
                ContinuationToken = token,
            });
            total += page.Entries.Count;
            token = page.ContinuationToken;
        }
        while (!string.IsNullOrEmpty(token));
    }
    catch (RpcException ex) when (ex.StatusCode is StatusCode.NotFound or StatusCode.PermissionDenied)
    {
        return 0;
    }

    return total;
}

// Polls the predicate until it is true or the budget elapses.
static async Task<bool> WaitUntilAsync(Func<Task<bool>> predicate, TimeSpan budget)
{
    var deadline = DateTime.UtcNow + budget;
    while (DateTime.UtcNow < deadline)
    {
        if (await predicate())
        {
            return true;
        }

        await Task.Delay(TimeSpan.FromMilliseconds(500));
    }

    return await predicate();
}
````

## PasswordAuthenticator.cs

````csharp
using Orleans.Lattice.Membership;

namespace Orleans.Lattice.Samples.PasswordProtection;

/// <summary>
/// A minimal <see cref="ILatticeCredentialAuthenticator"/> that trusts the
/// username lifted off the inbound Basic credential (already validated at the
/// transport by <c>EnvVarCredentialAuthorizer</c>) as the caller subject id, so
/// the data-plane authorization gate can apply the per-tree rules authored for
/// that user. It handles only credentials stamped with <see cref="Scheme"/> by
/// <see cref="PasswordCredentialBridge"/>, so it never shadows the built-in
/// anonymous authenticator for an unstamped (system-origin) turn.
/// </summary>
/// <remarks>
/// The transport authorizer is the authentication boundary (it verifies the
/// password against the salted PBKDF2 hash in the environment variable); this
/// authenticator only maps the already-authenticated username onto a subject.
/// A real deployment would resolve the subject from a validated JWT / Entra
/// token instead.
/// </remarks>
internal sealed class PasswordAuthenticator : ILatticeCredentialAuthenticator
{
    /// <summary>The scheme hint stamped by the credential bridge.</summary>
    public const string Scheme = "password-scheme";

    /// <summary>The issuer stamped on the resolved principal.</summary>
    public const string Issuer = "https://issuer.passwordprotection.sample/";

    /// <inheritdoc />
    public bool CanHandle(in LatticeCredential credential) =>
        string.Equals(credential.Scheme, Scheme, StringComparison.Ordinal);

    /// <inheritdoc />
    public ValueTask<LatticePrincipal?> AuthenticateAsync(
        LatticeCredential credential,
        CancellationToken cancellationToken = default) =>
        new(new LatticePrincipal(credential.Token, Issuer));
}
````

## PasswordCredentialBridge.cs

````csharp
using System.Text;
using Grpc.Core;
using Orleans.Lattice.Api.State.Grpc;

namespace Orleans.Lattice.Samples.PasswordProtection;

/// <summary>
/// A custom <see cref="ILatticeStateApiCredentialBridge"/> that lifts the
/// <b>username</b> out of an inbound <c>authorization: Basic base64(user:pass)</c>
/// header onto the ambient <see cref="LatticeCredential"/> so the data-plane
/// authorization gate can resolve the caller's subject. The password itself is
/// never carried past the transport: it has already been verified against the
/// salted PBKDF2 credential dictionary by <c>EnvVarCredentialAuthorizer</c>
/// (the transport authorizer) before this bridge runs.
/// </summary>
/// <remarks>
/// The default <see cref="ILatticeStateApiCredentialBridge"/> is bearer-oriented
/// (it strips a <c>Bearer</c> scheme prefix and forwards the remaining token
/// verbatim); this Basic-aware bridge decodes the header and forwards only the
/// username, stamped with <see cref="PasswordAuthenticator.Scheme"/>. An absent,
/// malformed, or empty header yields <see langword="null"/> (an anonymous
/// caller), which auth-backed visibility fails closed.
/// </remarks>
internal sealed class PasswordCredentialBridge : ILatticeStateApiCredentialBridge
{
    private const string BasicScheme = "Basic ";

    /// <inheritdoc />
    public LatticeCredential? Resolve(ServerCallContext context)
    {
        ArgumentNullException.ThrowIfNull(context);

        var raw = context.RequestHeaders?.GetValue("authorization");
        if (string.IsNullOrWhiteSpace(raw) ||
            !raw.StartsWith(BasicScheme, StringComparison.OrdinalIgnoreCase))
        {
            return null;
        }

        string decoded;
        try
        {
            decoded = Encoding.UTF8.GetString(Convert.FromBase64String(raw[BasicScheme.Length..].Trim()));
        }
        catch (FormatException)
        {
            return null;
        }

        var separator = decoded.IndexOf(':', StringComparison.Ordinal);
        var username = separator < 0 ? decoded : decoded[..separator];
        return string.IsNullOrEmpty(username)
            ? null
            : new LatticeCredential(username, PasswordAuthenticator.Scheme);
    }
}
````

## PasswordProtection.csproj

````xml
<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net10.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
    <RootNamespace>Orleans.Lattice.Samples.PasswordProtection</RootNamespace>
    <AssemblyName>Orleans.Lattice.Samples.PasswordProtection</AssemblyName>
    <IsPackable>false</IsPackable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.Orleans.Server" Version="10.2.2" />
    <PackageReference Include="Grpc.Net.Client" Version="2.83.0" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\..\src\lattice\Orleans.Lattice.csproj" />
    <ProjectReference Include="..\..\src\lattice.membership\Orleans.Lattice.Membership.csproj" />
    <ProjectReference Include="..\..\src\lattice.auth\Orleans.Lattice.Auth.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.state\Orleans.Lattice.Api.State.csproj" />
    <ProjectReference Include="..\..\src\lattice.api.state.grpc\Orleans.Lattice.Api.State.Grpc.csproj" />
  </ItemGroup>

</Project>
````
