Skip to content

SwissLife-OSS/bewit

Repository files navigation

Bewit

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.

Features

  • Self-Contained tokens — expiry embedded in the token (stateless)
  • Server-Controlled tokens — expiry managed in the database; requires UseMongoDb() or UseNonceRepository()
  • 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

Quick Start

Install

dotnet add package Bewit
dotnet add package Bewit.Generation
dotnet add package Bewit.Validation

Registration

From 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.

Configuration

v7.0 supports appsettings.json binding, code-based configuration, or both. When combined, code wins (standard .NET options layering: Bind → Configure → PostConfigure).

From 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>();
});

Code overrides on top of config

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>();
});

Per-payload config section

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
});

Code-only (no appsettings)

services.AddBewit(bewit =>
{
    bewit.ConfigureOptions(o =>
    {
        o.Secret = "your-secret";
        o.TokenDuration = TimeSpan.FromMinutes(5);
    });
    bewit.AddPayload<string>();
});

Generate a Token

var generator = serviceProvider.GetRequiredService<IBewitTokenGenerator<string>>();

BewitToken<string> token = await generator.GenerateBewitTokenAsync(
    "my-payload", null, cancellationToken);

Validate a Token

var validator = serviceProvider.GetRequiredService<IBewitTokenValidator<string>>();

string payload = await validator.ValidateBewitTokenAsync(token, cancellationToken);

Server-Controlled Tokens

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>();
});

Token Revocation

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;
    }
}

MongoDB with OIDC (Azure Cosmos DB)

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.

Multiple Payloads with Shared MongoDB

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);
        });
    });
});

HotChocolate Integration

dotnet add package Bewit.Extensions.HotChocolate

[Bewit<T>] Attribute

[Mutation]
[Bewit<NominationBewitContext>(ExceptionType = typeof(BewitValidationException))]
public static async Task<NominationDto> AssignNomineeAsync(
    [Service] IHttpContextAccessor httpContextAccessor, ...)
{
    var context = httpContextAccessor.GetBewitPayload<NominationBewitContext>();
}

Setup

app.UseBewitTokenExtraction();

HTTP Endpoint Integration

dotnet add package Bewit.Http

Minimal API — Endpoint Filter (recommended)

Apply 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>().

Middleware (global)

Protect all endpoints via middleware:

app.UseBewitEndpointAuthorization<MyPayload>();

Token Extraction

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

Code configuration

services.AddBewit(bewit =>
{
    bewit.ConfigureTokenExtraction(o =>
    {
        o.HeaderName = "X-Custom-Token";
        o.QueryParamName = "token";
    });

    bewit.AddPayload<string>();
});

appsettings.json

{
  "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).

MVC Integration

dotnet add package Bewit.Extensions.Mvc
[BewitUrlAuthorization]
[HttpGet("download/{id}")]
public IActionResult Download(string id) { ... }

Packages

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

Migration

See Migration Guide v7.

Community

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.

About

Security without cookies or tokens

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages

Generated from SwissLife-OSS/template