Open-Source Wikis

/

Bitwarden Server

/

Systems

/

Tokenable & data protection

bitwarden/server

Tokenable & data protection

Active contributors: auth team.

Purpose

Bitwarden uses signed payload tokens for many one-shot flows that don't fit OAuth: email-confirmation links, password-reset links, organization invitation tokens, emergency-access tokens, send-access tokens, SSO tokens. The Tokenable<T> base class (src/Core/Tokens/) is a small framework on top of ASP.NET Core data protection that:

  1. Provides a typed payload container with a Valid property the consumer can check.
  2. Encodes / decodes via a registered IDataProtectorTokenFactory<T>.
  3. Uses ASP.NET Core data protection (with Azure Blob persistence in cloud) for encryption + signing.

Directory layout

src/Core/Tokens/
├── Tokenable.cs                        # Abstract base class
├── ExpiringTokenable.cs                # Adds an ExpirationDate
├── IDataProtectorTokenFactory.cs       # Tokenizer abstraction
└── DataProtectorTokenFactory.cs        # Default impl using IDataProtector

src/Core/Auth/Models/Business/Tokenables/
├── EmergencyAccessInviteTokenable.cs
├── HCaptchaTokenable.cs
├── OrgUserInviteTokenable.cs
├── RegistrationEmailVerificationTokenable.cs
├── ResetPasswordTokenable.cs
├── SsoTokenable.cs
└── ...

src/Core/Tools/Models/Business/Tokenables/
├── SendAccessTokenable.cs
└── ...

Key abstractions

Type Purpose
Tokenable Base class with Valid virtual + ToString() (Base64 of the protected payload).
ExpiringTokenable Adds ExpirationDate; overrides Valid to assert the time is still in range.
IDataProtectorTokenFactory<T> string Protect(T tokenable) + bool TryUnprotect(string raw, out T tokenable).
DataProtectorTokenFactory<T> Default impl: serialises with JSON, calls IDataProtector.Protect. Each token type has its own purpose string so tokens can't be cross-replayed.

How it works

sequenceDiagram
    participant Sender
    participant Factory as DataProtectorTokenFactory<T>
    participant DP as ASP.NET Data Protection
    participant Recipient

    Sender->>Sender: var t = new ResetPasswordTokenable(user)
    Sender->>Factory: Protect(t)
    Factory->>DP: encrypt + sign
    Factory-->>Sender: protected base64
    Sender-->>Recipient: e-mail with link?token=...

    Recipient->>Factory: TryUnprotect(token, out resolved)
    Factory->>DP: verify + decrypt
    Factory-->>Recipient: ResetPasswordTokenable resolved
    Recipient->>Recipient: if (resolved.Valid) { ... }

Data protection setup

AddCustomDataProtectionServices (in src/SharedWeb/Utilities/ServiceCollectionExtensions.cs) registers ASP.NET Core data protection with persistence:

  • Cloud — keys are stored in an Azure Blob and protected by Azure Key Vault, ensuring keys survive deploys and scale-outs.
  • Self-host — keys go to a mounted directory under /etc/bitwarden/core/aspnet-dataprotection/ so all containers share them.
  • Dev — defaults to the user-profile keyring.

Dependency: Azure.Extensions.AspNetCore.DataProtection.Blobs (moved into Platform ownership in PR #5442, 2025-11-28).

Adding a token type

  1. Create a class deriving from Tokenable or ExpiringTokenable under src/Core/<Domain>/Models/Business/Tokenables/.
  2. Declare the payload fields, define a unique Identifier constant, override Valid to assert the payload is well-formed.
  3. Register the token factory in the relevant services.Add... extension:
    services.AddSingleton<IDataProtectorTokenFactory<MyTokenable>>(svc =>
        new DataProtectorTokenFactory<MyTokenable>(
            "BitwardenMyToken",
            "MyTokenable",
            svc.GetRequiredService<IDataProtectionProvider>()));
  4. Issue tokens in the relevant command, hand them out (e.g. via email link).
  5. Validate in the consuming controller / command (if (!factory.TryUnprotect(...) || !token.Valid) throw new BadRequestException(...);).

Entry points for modification

  • Tighten validity → add fields to your Tokenable and check them in Valid.
  • Change key persistence → edit AddCustomDataProtectionServices.
  • Rotate keys → ASP.NET data protection rotates automatically based on the persisted key ring; nothing to do per-token.

Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.

Tokenable & data protection – Bitwarden Server wiki | Factory