bitwarden/server · error · BadRequestException

Invalid license.

Error message

Invalid license.

What it means

Thrown (HTTP 400) on the VNext self-hosted premium-subscription license endpoint when ApiHelpers.ReadJsonFileFromBody<UserLicense> returns null. Identical semantics to [264]: returns null for a missing file, a payload over 50KB, or JSON that does not deserialize into UserLicense (deserialization errors are silently caught).

Source

Thrown at src/Api/Billing/Controllers/VNext/SelfHostedAccountBillingVNextController.cs:30

namespace Bit.Api.Billing.Controllers.VNext;

[Authorize("Application")]
[Route("account/billing/vnext/self-host")]
[SelfHosted(SelfHostedOnly = true)]
public class SelfHostedAccountBillingVNextController(
    ICreatePremiumSelfHostedSubscriptionCommand createPremiumSelfHostedSubscriptionCommand) : BaseBillingController
{
    [HttpPost("license")]
    [InjectUser]
    public async Task<IResult> UploadLicenseAsync(
        [BindNever] User user,
        PremiumSelfHostedSubscriptionRequest request)
    {
        var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, request.License);
        if (license == null)
        {
            throw new BadRequestException("Invalid license.");
        }
        var result = await createPremiumSelfHostedSubscriptionCommand.Run(user, license);
        return Handle(result);
    }
}

View on GitHub (pinned to e93b962371)

Solutions

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

Example fix

// before: upload blindly
await uploadLicenseVNext({ license: pickedFile });

// after: validate first
var text = await pickedFile.text();
var parsed = JSON.parse(text); // throws on malformed JSON
if (!parsed.licenseKey || !parsed.email) throw new Error('Not a valid user license');
await uploadLicenseVNext({ license: pickedFile });
Defensive patterns

Strategy: validation

Validate before calling

// Validate the uploaded file is JSON with expected UserLicense fields before calling the VNext endpoint.
const text = await request.license.text();
const parsed = JSON.parse(text);
if (!parsed.licenseKey || !parsed.email || !parsed.version) {
  throw new Error('Not a valid Bitwarden user license');
}

Type guard

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

Try / catch

try {
  await uploadLicenseVNext({ license: file });
} catch (e) {
  if (e.isBadRequest && /invalid license/i.test(e.message)) {
    showUploadError('Re-download the user license from the cloud portal and retry.');
  } else { throw e; }
}

Prevention

When it happens

Trigger: Non-JSON or binary file uploaded; truncated license JSON; an organization license where a user license is required; a license whose schema is from an incompatible version; file larger than 50KB.

Common situations: Cloud/self-hosted version skew where UserLicense fields changed; wrong license file downloaded; upload wrapper corrupts the JSON.

Related errors


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