bitwarden/server · error · Exception
InvalidSsoToken
Error message
InvalidSsoToken
What it means
Thrown in AccountController.ValidateSchemeAgainstSsoToken (line 255) when _dataProtector.Unprotect(ssoToken) throws an exception. The SSO token is a data-protected payload (ASP.NET Core Data Protection) that encodes the organization ID and an expiry. Unprotect fails if the token is corrupt, expired, tampered with, or was protected under a different key ring.
Source
Thrown at bitwarden_license/src/Sso/Controllers/AccountController.cs:255
}
/// <summary>
/// Validates the scheme (organization ID) against the organization ID found in the ssoToken.
/// </summary>
/// <param name="scheme">The authentication scheme (organization ID) to validate.</param>
/// <param name="ssoToken">The SSO token to validate against.</param>
/// <exception cref="Exception">Thrown if the scheme (organization ID) does not match the organization ID found in the ssoToken.</exception>
private void ValidateSchemeAgainstSsoToken(string scheme, string ssoToken)
{
SsoTokenable tokenable;
try
{
tokenable = _dataProtector.Unprotect(ssoToken);
}
catch
{
throw new Exception(_i18nService.T("InvalidSsoToken"));
}
if (!Guid.TryParse(scheme, out var schemeOrgId) || tokenable.OrganizationId != schemeOrgId)
{
throw new Exception(_i18nService.T("SsoOrganizationIdMismatch"));
}
}
[HttpGet]
public async Task<IActionResult> ExternalCallback()
{
// Read external identity from the temporary cookie
var result = await HttpContext.AuthenticateAsync(
AuthenticationSchemes.BitwardenExternalCookieAuthenticationScheme);
if (!result.Succeeded)
{
throw new Exception(_i18nService.T("ExternalAuthenticationError"));View on GitHub (pinned to e93b962371)
Solutions
- Ensure data-protection keys are persisted and shared across all server instances (e.g., Redis, file share, or Azure Key Vault).
- Verify the token hasn't expired — re-run PreValidate to mint a fresh token if needed.
- Confirm the token is passed intact (no double-encoding, no truncation) from client to ExternalChallenge.
Example fix
// before — stale token reused after restart with new key ring
var token = oldPreValidateToken;
// after — mint a fresh token via PreValidate and persist DP keys
services.AddDataProtection()
.PersistKeysToFileSystem(new DirectoryInfo("/var/keys"))
.SetApplicationName("Bitwarden"); Defensive patterns
Strategy: validation
Validate before calling
// Validate token before passing to ExternalChallenge
try { _dataProtector.Unprotect(ssoToken); }
catch { /* re-mint via PreValidate or return error */
return BadRequest("SSO token is expired or invalid. Please restart the login flow.");
} Try / catch
try { ValidateSchemeAgainstSsoToken(scheme, ssoToken); }
catch (Exception ex) when (ex.Message.Contains("InvalidSsoToken"))
{ /* redirect to PreValidate to mint a fresh token */ } Prevention
- Persist ASP.NET Core Data Protection keys to a shared store (Redis, file share, Key Vault) across all instances.
- Set an appropriate SsoTokenLifetimeInSeconds and educate users to complete login promptly.
- Pass tokens via secure, non-truncating channels; avoid URL parameters where possible.
When it happens
Trigger: ExternalChallenge is called with an ssoToken that cannot be decrypted/verified by IDataProtectorTokenFactory<SsoTokenable>.Unprotect.
Common situations: Token expired (default lifetime from SsoTokenLifetimeInSeconds); data-protection keys rotated or not persisted across server restarts (common in self-hosted multi-instance without shared key ring); token from a different environment (staging vs prod); token truncated or URL-decoded incorrectly in transit.
Related errors
- NoDomainHintProvided
- SsoOrganizationIdMismatch
- ExternalAuthenticationError
- OrganizationOrSsoConfigNotFound
- AcrMissingOrInvalid
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/3653a33735db31f0.
Report an issue: GitHub.