bitwarden/server · error · BadRequestException

Invalid license

Error message

Invalid license

What it means

Thrown (HTTP 400) on the self-hosted-only license upload endpoint when ApiHelpers.ReadJsonFileFromBody<UserLicense> returns null. That helper returns null when no file is present, the payload exceeds the 50KB maxSize, or JSON deserialization throws (silently caught) — so 'Invalid license' means the upload could not be parsed as a UserLicense, not that the license signature failed.

Source

Thrown at src/Api/Billing/Controllers/AccountsController.cs:51

    /*
     * TODO: A new version of this exists in the AccountBillingVNextController.
     * The individual-self-hosting-license-uploader.component needs to be updated to use it.
     * Then, this can be removed.
     */
    [HttpPost("license")]
    [SelfHosted(SelfHostedOnly = true)]
    public async Task PostLicenseAsync(LicenseRequestModel model)
    {
        var user = await userService.GetUserByPrincipalAsync(User);
        if (user == null)
        {
            throw new UnauthorizedAccessException();
        }

        var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);
        if (license == null)
        {
            throw new BadRequestException("Invalid license");
        }

        await userService.UpdateLicenseAsync(user, license);
    }

    // TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription
    [HttpPost("cancel")]
    public async Task PostCancelAsync(
        [FromBody] SubscriptionCancellationRequestModel request,
        [FromServices] ISubscriberService subscriberService)
    {
        var user = await userService.GetUserByPrincipalAsync(User);

        if (user == null)
        {
            throw new UnauthorizedAccessException();
        }

View on GitHub (pinned to e93b962371)

Solutions

  1. Re-download the user license JSON from the cloud portal and re-upload the unmodified file.
  2. Validate locally that the file parses as JSON and contains the expected UserLicense fields before uploading.
  3. Confirm the file is under 50KB and is a user (not organization) license.

Example fix

// before: upload whatever file the user picked
await postLicense({ license: file });

// after: validate JSON shape client-side first
var json = JSON.parse(await file.text());
if (!json.licenseKey || !json.email) throw new Error('Not a valid user license');
await postLicense({ license: file });
Defensive patterns

Strategy: validation

Validate before calling

// Validate the file is parseable JSON with expected UserLicense fields before uploading.
const text = await file.text();
const parsed = JSON.parse(text);
if (!parsed.licenseKey || !parsed.email || !parsed.version) {
  throw new Error('File is not a valid Bitwarden user license');
}

Type guard

// function isUserLicenseLike(o: unknown): o is { licenseKey: string; email: string } {
//   return typeof o === 'object' && o !== null
//     && typeof (o as any).licenseKey === 'string'
//     && typeof (o as any).email === 'string';
// }

Try / catch

try {
  await postLicense({ license: file });
} catch (e) {
  if (e.isBadRequest && /invalid license/i.test(e.message)) {
    showUploadError('License file could not be read. Re-download it from the cloud portal.');
  } else { throw e; }
}

Prevention

When it happens

Trigger: Uploading a non-JSON or binary file; a truncated/malformed license JSON; an organization license where a user license is expected; a license whose schema is from an incompatible Bitwarden version; a file larger than 50KB.

Common situations: Wrong license type downloaded from the cloud portal; copy-paste corruption of the .json; cloud/server version skew where UserLicense fields changed; accidental upload of a .txt wrapper.

Related errors


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