Bewit is an authentication scheme for secure, temporary access tokens.
Bewit enables authentication in use cases where cookies and auth headers can't be used — file downloads, temporary links, single-use tokens, and share links with admin-controlled expiry.
- Self-Contained tokens — expiry embedded in the token (stateless)
- Server-Controlled tokens — expiry managed in the database; requires
UseMongoDb()orUseNonceRepository() - Token revocation — type-safe
IBewitTokenRevoker<T>to revoke tokens by identifier - Multi-tenancy — different secrets and modes per payload type
- MongoDB persistence — with OIDC auth support (Azure.Identity)
- HotChocolate integration —
[Bewit<T>]attribute for GraphQL resolvers - MVC integration —
[BewitMvc],[FromBewit],[BewitUrlAuthorization]filters - Aspire-ready — connection string pattern for distributed apps
dotnet add package Bewit
dotnet add package Bewit.Generation
dotnet add package Bewit.ValidationFrom appsettings.json:
{
"Bewit": {
"Secret": "your-secret-at-least-32-chars!",
"TokenDuration": "00:05:00",
"ExpiryMode": "SelfContained"
}
}services.AddBewit(bewit =>
{
bewit.BindConfiguration("Bewit");
bewit.AddPayload<string>();
});
services.AddBewitGeneration<string>();
services.AddBewitValidation<string>();Or code-only:
services.AddBewit(bewit =>
{
bewit.ConfigureOptions(o =>
{
o.Secret = "your-secret-at-least-32-chars!";
o.TokenDuration = TimeSpan.FromMinutes(5);
o.ExpiryMode = ExpiryMode.SelfContained;
});
bewit.AddPayload<string>();
});
services.AddBewitGeneration<string>();
services.AddBewitValidation<string>();Both can be combined — BindConfiguration loads from appsettings first, then ConfigureOptions overrides specific values.
v7.0 supports appsettings.json binding, code-based configuration, or both. When combined, code wins (standard .NET options layering: Bind → Configure → PostConfigure).
{
"Bewit": {
"Secret": "your-secret-at-least-32-chars!",
"TokenDuration": "00:05:00",
"ExpiryMode": "SelfContained"
}
}services.AddBewit(bewit =>
{
bewit.BindConfiguration("Bewit");
bewit.AddPayload<string>();
});Aspire-friendly — IConfiguration is read at resolve time, not registration time:
services.AddBewit(bewit =>
{
bewit.BindConfiguration("Bewit");
bewit.ConfigureOptions(o =>
{
o.TokenDuration = TimeSpan.FromMinutes(30); // overrides appsettings value
});
bewit.AddPayload<string>();
});Each payload type can bind to its own config section:
{
"Bewit": {
"Secret": "global-secret",
"TokenDuration": "00:05:00"
},
"Bewit:Downloads": {
"Secret": "download-secret",
"TokenDuration": "00:01:00",
"ExpiryMode": "ServerControlled"
}
}services.AddBewit(bewit =>
{
bewit.BindConfiguration("Bewit");
bewit.AddPayload<string>();
bewit.AddPayload<DownloadPayload>(p =>
p.BindConfiguration("Bewit:Downloads")); // overrides global section
});services.AddBewit(bewit =>
{
bewit.ConfigureOptions(o =>
{
o.Secret = "your-secret";
o.TokenDuration = TimeSpan.FromMinutes(5);
});
bewit.AddPayload<string>();
});var generator = serviceProvider.GetRequiredService<IBewitTokenGenerator<string>>();
BewitToken<string> token = await generator.GenerateBewitTokenAsync(
"my-payload", null, cancellationToken);var validator = serviceProvider.GetRequiredService<IBewitTokenValidator<string>>();
string payload = await validator.ValidateBewitTokenAsync(token, cancellationToken);ExpiryMode.ServerControlled requires a persistent nonce repository.
The app fails at startup if ServerControlled is set without UseMongoDb() / UseNonceRepository().
services.AddBewit(bewit =>
{
bewit.ConfigureOptions(o =>
{
o.Secret = "your-secret";
o.TokenDuration = TimeSpan.FromDays(7);
o.ExpiryMode = ExpiryMode.ServerControlled;
});
bewit.UseMongoDb(mongo =>
{
mongo.ConnectionString = "mongodb://localhost:27017";
mongo.DatabaseName = "myapp";
});
bewit.AddPayload<ShareLinkPayload>();
});Inject IBewitTokenRevoker<T> to revoke all tokens for an identifier — no keyed DI magic strings needed:
public class Mutation(IBewitTokenRevoker<BarPayload> revoker)
{
public async Task<string> InvalidateTokens(string identifier, CancellationToken ct)
{
await revoker.RevokeByIdentifierAsync(identifier, ct);
return identifier;
}
}bewit.UseMongoDb(mongo =>
{
mongo.ConnectionString = "mongodb+srv://...";
mongo.DatabaseName = "mydb";
mongo.AuthType = MongoAuthType.Oidc;
mongo.OidcScopes = ["https://cosmos-db-scope/.default"];
});Or reuse an existing IMongoDatabase from DI — the recommended approach when the service already uses MongoDB.Extensions.Context:
// Register the context once
services.AddMongoDbContext<MyDbContext, IMyDbContext>(
configurationSection: "MongoDb");
// Bewit reuses its IMongoDatabase — no second connection opened
services.AddBewit(bewit =>
{
bewit.UseMongoDb(
sp => sp.GetRequiredService<IMyDbContext>().Database,
mongo => { mongo.NonceUsage = NonceUsage.OneTime; });
bewit.AddPayload<MyPayload>(p =>
p.ConfigureOptions(o => o.ExpiryMode = ExpiryMode.ServerControlled));
});This shares the connection pool and inherits the convention packs, serializer registrations, and read/write concerns configured on the context.
All payloads inherit the builder-level MongoDB and options. Per-payload overrides are possible:
services.AddBewit(bewit =>
{
bewit.ConfigureOptions(o =>
{
o.Secret = "your-secret";
o.ExpiryMode = ExpiryMode.ServerControlled;
});
bewit.UseMongoDb(mongo =>
{
mongo.ConnectionString = "mongodb://localhost:27017";
mongo.DatabaseName = "myapp";
mongo.NonceUsage = NonceUsage.ReUse;
});
bewit.AddPayload<NominationBewitContext>();
bewit.AddPayload<UserRegistrationBewitContext>();
bewit.AddPayload<DocumentDownloadBewitContext>();
// Override: self-contained, no MongoDB needed
bewit.AddPayload<DownloadBewitContext>(p =>
{
p.ConfigureOptions(o =>
{
o.ExpiryMode = ExpiryMode.SelfContained;
o.TokenDuration = TimeSpan.FromMinutes(5);
});
});
});dotnet add package Bewit.Extensions.HotChocolate[Mutation]
[Bewit<NominationBewitContext>(ExceptionType = typeof(BewitValidationException))]
public static async Task<NominationDto> AssignNomineeAsync(
[Service] IHttpContextAccessor httpContextAccessor, ...)
{
var context = httpContextAccessor.GetBewitPayload<NominationBewitContext>();
}app.UseBewitTokenExtraction();dotnet add package Bewit.HttpApply authorization to individual routes or route groups:
app.MapGet("/files/{id}", (string id) => ...)
.AddBewitAuthorization<MyPayload>();
// or protect a group of endpoints:
app.MapGroup("/api/files")
.AddBewitAuthorization<MyPayload>();The filter validates the token from the configured header, query parameter, or pre-extracted HttpContext.Items entry, and makes the payload available via GetBewitPayload<T>().
Protect all endpoints via middleware:
app.UseBewitEndpointAuthorization<MyPayload>();All extensions (HotChocolate, Http, Mvc) read the bewit token from the same configurable sources: an HTTP header and/or a query parameter. Header takes precedence when both are present.
Defaults match the v6.x behavior:
- Header:
bewitToken - Query parameter:
bewit
services.AddBewit(bewit =>
{
bewit.ConfigureTokenExtraction(o =>
{
o.HeaderName = "X-Custom-Token";
o.QueryParamName = "token";
});
bewit.AddPayload<string>();
});{
"Bewit:TokenExtraction": {
"HeaderName": "X-Custom-Token",
"QueryParamName": "token"
}
}services.AddBewit(bewit =>
{
bewit.BindTokenExtractionConfiguration("Bewit:TokenExtraction");
bewit.AddPayload<string>();
});Both can be combined — BindTokenExtractionConfiguration loads from appsettings first, then ConfigureTokenExtraction overrides specific values (standard .NET options layering).
dotnet add package Bewit.Extensions.Mvc[BewitUrlAuthorization]
[HttpGet("download/{id}")]
public IActionResult Download(string id) { ... }| Package | Description |
|---|---|
Bewit |
Core abstractions, models, crypto, DI |
Bewit.Generation |
Token generation |
Bewit.Validation |
Token validation |
Bewit.Storage.MongoDB |
MongoDB nonce repository with OIDC support |
Bewit.Extensions.HotChocolate |
HotChocolate [Bewit<T>], middleware |
Bewit.Extensions.Mvc |
MVC filters and parameter binding |
Bewit.Http |
Minimal API endpoint authorization |
See Migration Guide v7.
This project has adopted the code of conduct defined by the Contributor Covenant to clarify expected behavior in our community. For more information, see the Swiss Life OSS Code of Conduct.