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:
- Provides a typed payload container with a
Validproperty the consumer can check. - Encodes / decodes via a registered
IDataProtectorTokenFactory<T>. - 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
- Create a class deriving from
TokenableorExpiringTokenableundersrc/Core/<Domain>/Models/Business/Tokenables/. - Declare the payload fields, define a unique
Identifierconstant, overrideValidto assert the payload is well-formed. - Register the token factory in the relevant
services.Add...extension:services.AddSingleton<IDataProtectorTokenFactory<MyTokenable>>(svc => new DataProtectorTokenFactory<MyTokenable>( "BitwardenMyToken", "MyTokenable", svc.GetRequiredService<IDataProtectionProvider>())); - Issue tokens in the relevant command, hand them out (e.g. via email link).
- Validate in the consuming controller / command (
if (!factory.TryUnprotect(...) || !token.Valid) throw new BadRequestException(...);).
Entry points for modification
- Tighten validity → add fields to your
Tokenableand check them inValid. - 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.