A .NET Standard 2.1 library for generating and validating Branca tokens.
Branca tokens are authenticated and encrypted API tokens using modern crypto.
From Branca.io:
Branca is a secure, easy to use token format which makes it hard to shoot yourself in the foot. It uses IETF XChaCha20-Poly1305 AEAD symmetric encryption to create encrypted and tamperproof tokens. The encrypted token is base62 encoded which makes it URL safe. The payload itself is an arbitrary sequence of bytes. You could use a JSON object, plain text string or even binary data serialized by MessagePack or Protocol Buffers.
Although not a goal, it is possible to use Branca as an Alternative to JWT.
You can read the Branca Token Specification for design-specific details.
The core library targets .NET Standard 2.1 and .NET 10, and both packages are trimming and Native AOT compatible.
using Branca;The secret key is 32 bytes in size. The BrancaKey type can generate a random one and convert between raw bytes and several textual encodings:
BrancaKey key = BrancaKey.Generate();
string base64Url = key.ToBase64Url(); // 43 URL-safe charactersYou can also load a key from an existing encoding:
BrancaKey key = BrancaKey.FromBase64Url("c3VwZXJzZWNyZXRrZXl5b3VzaG91bGRub3Rjb21taXQ");
BrancaKey key = BrancaKey.FromHex("73757065727365637265746b6579796f7573686f756c646e6f74636f6d6d6974");
BrancaKey key = BrancaKey.FromBase62("RNUCYvOYtxwoz7WVNmoEJl4RBn2CU2GAewrnABzkNPA");
BrancaKey key = BrancaKey.FromBytes(bytes);Base64Url and Base62 both encode the same 32 bytes as hexadecimal does, in 43 characters instead of 64. Base62 shares its alphabet with the tokens Branca produces.
The older
HexKeytype still works but is deprecated in favour ofBrancaKey.FromHex.
Once you have a secret key, be it a byte[] or a BrancaKey, you can configure Branca as follows:
BrancaService branca = new(key);Branca can optionally take in BrancaSettings, which has a few parameters for configuration. They come with sensible defaults so you may not need to change it at all.
BrancaService branca = new(key, new BrancaSettings
{
MaxStackLimit = 1024,
TokenLifetimeInSeconds = 3600
});For the sake of performance, Branca allocates various resources during encoding and decoding on the stack. To prevent abuse of the stack, a stack limit is enforced. If the limit is reached due to a large payload, it defaults to allocating it on the heap for the particular encoding/decoding. This limit is by default set to 1024 bytes.
Branca also considers all tokens it generates to be valid for an hour from the time of their generation. You can configure this value based on your use-case. Alternatively, set it up as null to make Branca tokens valid forever.
To rotate secrets without invalidating outstanding tokens, list the older keys in PreviousKeys. New tokens are always encrypted with the current key; decoding tries the current key first and then each previous key in order.
BrancaService branca = new(currentKey, new BrancaSettings
{
PreviousKeys = [previousKey]
});If token issuers and validators run on machines with slightly different clocks, ClockSkewInSeconds tolerates the difference: tokens dated up to that many seconds in the future are accepted, and the lifetime is extended by the same amount. The default is zero, which also rejects any token dated in the future.
There exists a Timer configuration for BrancaSettings too. It comes with an internal implementation that uses current time accordingly. You can pass on your own implementation of this interface if you want to control the way time flows for Branca. This will not be needed in all general cases.
A single BrancaService instance is safe for concurrent use. Encoding and decoding do not mutate any shared state, so you can configure one instance, register it as a singleton and use it from multiple threads.
Now that Branca is configured, you can encode your payload. Branca can accept string or ReadOnlySpan<byte> as payload. So, you can send a byte[] as payload if you are using some binary serialiser to save space.
If your payload is "Hello, World", you can generate a Branca token as follows:
string token = branca.Encode("Hello, World!");The above will generate a token like:
XZAXYkd0ZbbCjxgr5xOelGcPXWhNFUy1oNfbQ3vfbi5LL4TIeyGq5rBKESaXXpyfjNBSOjbaOTlhWG
Likewise, you can make use of MessagePack or Protocol Buffer or even System.Text.Json to serialize your payload and pass in binary data to branca for generating a specific token.
Decoding a Branca token for validation is just as simple:
if (branca.TryDecode(token, out byte[] payload))
{
// Hello, World!
string message = Encoding.UTF8.GetString(payload);
}You get to decide how to interpret the payload which comes out as an array of bytes. If you used MessagePack, deserialize the payload using it to the object it is supposed to be.
Decoding validates the token and even confirms the expiry (if you have setup some lifetime for the token, default being one hour). In case of failure, the TryDecode returns false and you can reject the token accordingly.
For whatever reason, if you also want to get the time the Branca token was created, you can do the following:
if (branca.TryDecode(token, out byte[] payload, out uint createTime))
{
// successful decoding
}
else
{
// unauthorized attempt
}The Branca.AspNetCore package lets you protect endpoints with Branca tokens instead of JWTs. Register the scheme with a key:
builder.Services
.AddAuthentication(BrancaDefaults.AuthenticationScheme)
.AddBranca(options =>
{
options.Key = BrancaKey.FromBase64Url(builder.Configuration["Branca:Key"]!);
});
builder.Services.AddAuthorization();app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/me", (ClaimsPrincipal user) => user.Identity!.Name)
.RequireAuthorization();Clients send the token as Authorization: Bearer <token>. By default, the payload is read as a flat JSON object where each property becomes a claim. [Authorize(Roles = "admin")] works when the payload carries a role array. MapClaims can configure the mapping for non-JSON payloads such as MessagePack as well.
For example, with the MessagePack Payload record from the examples below, a typed mapping deserializes the payload once and returns exactly the claims it should carry:
options.MapClaims = payload =>
{
Payload data = MessagePackSerializer.Deserialize<Payload>(payload);
return
[
new Claim("sub", data.Subject.ToString()),
new Claim("name", data.Name),
new Claim("email", data.Email),
];
};Returning null rejects the token, and an exception thrown by the mapping fails authentication rather than the request.
Alternatively, register the key once on the service collection and let the scheme pick it up. The same IBrancaService singleton can then be injected wherever tokens are issued:
builder.Services.AddBranca(
BrancaKey.FromBase64Url(builder.Configuration["Branca:Key"]!));
builder.Services
.AddAuthentication(BrancaDefaults.AuthenticationScheme)
.AddBranca();app.MapPost("/login", (IBrancaService branca) =>
branca.Encode("""{ "name": "alice", "role": ["admin"] }"""));An explicit BrancaOptions.Key takes precedence over the registered service, with the scheme then using its own PreviousKeys, ClockSkewInSeconds and other options.
Since Branca uses symmetric encryption, whosoever validates a token can also issue one. This suits first-party session and API tokens where the same application (or a trusted set sharing the secret) does both. Token expiry is enforced by the token format itself via the TokenLifetimeInSeconds, so there is no separate exp claim to validate.
using System;
using System.Text.Json;
using System.Text.Json.Serialization;Suppose you have a payload structure as follows:
public sealed record Payload
{
[JsonPropertyName("s")]
public ulong Subject { get; init; }
[JsonPropertyName("n")]
public string Name { get; init; }
[JsonPropertyName("e")]
public string Email { get; init; }
}I explicitly setup the JsonPropertyName values for the properties to conserve token space.
Let's initialize the payload with some data:
Payload payload = new()
{
Subject = 123456789, Name = "Some Name", Email = "some@example.com"
};You can additionally setup the JsonSerializerOptions:
JsonSerializerOptions options = new()
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
};And we finally can print a serialized version of this payload:
Console.WriteLine(JsonSerializer.Serialize(payload, options));
// {"s":123456789,"n":"Some Name","e":"some@example.com"}We can tell JsonSerializer to serialize this payload directly to a byte array:
byte[] bytes = JsonSerializer.SerializeToUtf8Bytes(payload, options);And finally, with a BrancaService instance, we can encode and decode it as follows:
string token = branca.Encode(bytes);
// m6ehc87WdoSdE4WBJ1Phgt1a0OPXc3zOKn9NE3Rz7RWx2MJwaBZuI4s50wzRWahrGqJvGO2kXwvwbINpTdyP2qtJmuFuq9ADxFz05JEFAB2icTTF7GNr7TOS6reUU2nir8eU7if (branca.TryDecode(token, out byte[] data, out uint createTime))
{
var decodedPayload = JsonSerializer.Deserialize<Payload>(data, options);
if (decodedPayload is not null)
{
Console.WriteLine(decodedPayload.Subject); // 123456789
Console.WriteLine(decodedPayload.Name); // Some Name
Console.WriteLine(decodedPayload.Email); // some@example.com
}
Console.WriteLine(DateTimeOffset.FromUnixTimeSeconds(createTime));
}using MessagePack;
using System;Suppose you have a payload structure as follows:
[MessagePackObject]
public sealed record Payload
{
[Key(0)]
public ulong Subject { get; init; }
[Key(1)]
public string Name { get; init; }
[Key(2)]
public string Email { get; init; }
}Let's initialize the payload with some data:
Payload payload = new()
{
Subject = 123456789, Name = "Some Name", Email = "some@example.com"
};We can tell MessagePackSerializer to serialize this payload to a byte array:
byte[] bytes = MessagePackSerializer.Serialize(payload);And finally, with a BrancaService instance, we can encode and decode it as follows:
string token = branca.Encode(bytes);
// Jm79HC2CHnD1glDNBPucf02yCnyF70Kv2M7ELsNNqI1kJlX8ftrlV0IIYvuyHURpgrzXmvu4uuhmbmOkX3d0gBpxguUDXomxMgKeuCtpjif (branca.TryDecode(token, out byte[] data, out uint createTime))
{
var decodedPayload = MessagePackSerializer.Deserialize<Payload>(data);
Console.WriteLine(decodedPayload.Subject); // 123456789
Console.WriteLine(decodedPayload.Name); // Some Name
Console.WriteLine(decodedPayload.Email); // some@example.com
Console.WriteLine(DateTimeOffset.FromUnixTimeSeconds(createTime));
}Branca is a .NET Standard 2.1 library for generating and validating Branca tokens.
Copyright © 2022-2026 Aman Agnihotri (amanagnihotri@pm.me)
Branca is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Branca is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with Branca. If not, see GNU Licenses.
