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

  1. Ensure data-protection keys are persisted and shared across all server instances (e.g., Redis, file share, or Azure Key Vault).
  2. Verify the token hasn't expired — re-run PreValidate to mint a fresh token if needed.
  3. 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

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


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/3653a33735db31f0. Report an issue: GitHub.