{"record":{"id":"31d5ef36227313ef","repo":"bitwarden/server","slug":"invalid-license","errorCode":null,"errorMessage":"Invalid license","messagePattern":"Invalid license","errorType":"exception","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"src/Api/Billing/Controllers/AccountsController.cs","lineNumber":51,"sourceCode":"    /*\n     * TODO: A new version of this exists in the AccountBillingVNextController.\n     * The individual-self-hosting-license-uploader.component needs to be updated to use it.\n     * Then, this can be removed.\n     */\n    [HttpPost(\"license\")]\n    [SelfHosted(SelfHostedOnly = true)]\n    public async Task PostLicenseAsync(LicenseRequestModel model)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n\n        var license = await ApiHelpers.ReadJsonFileFromBody<UserLicense>(HttpContext, model.License);\n        if (license == null)\n        {\n            throw new BadRequestException(\"Invalid license\");\n        }\n\n        await userService.UpdateLicenseAsync(user, license);\n    }\n\n    // TODO: Migrate to Command / AccountBillingVNextController as DELETE /account/billing/vnext/subscription\n    [HttpPost(\"cancel\")]\n    public async Task PostCancelAsync(\n        [FromBody] SubscriptionCancellationRequestModel request,\n        [FromServices] ISubscriberService subscriberService)\n    {\n        var user = await userService.GetUserByPrincipalAsync(User);\n\n        if (user == null)\n        {\n            throw new UnauthorizedAccessException();\n        }\n","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/bitwarden/server/blob/e93b962371d80964556f5590c6615f5160a437a1/src/Api/Billing/Controllers/AccountsController.cs#L33-L69","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Re-download the user license JSON from the cloud portal and re-upload the unmodified file.","Validate locally that the file parses as JSON and contains the expected UserLicense fields before uploading.","Confirm the file is under 50KB and is a user (not organization) license."],"exampleFix":"// before: upload whatever file the user picked\nawait postLicense({ license: file });\n\n// after: validate JSON shape client-side first\nvar json = JSON.parse(await file.text());\nif (!json.licenseKey || !json.email) throw new Error('Not a valid user license');\nawait postLicense({ license: file });","handlingStrategy":"validation","validationCode":"// Validate the file is parseable JSON with expected UserLicense fields before uploading.\nconst text = await file.text();\nconst parsed = JSON.parse(text);\nif (!parsed.licenseKey || !parsed.email || !parsed.version) {\n  throw new Error('File is not a valid Bitwarden user license');\n}","typeGuard":"// function isUserLicenseLike(o: unknown): o is { licenseKey: string; email: string } {\n//   return typeof o === 'object' && o !== null\n//     && typeof (o as any).licenseKey === 'string'\n//     && typeof (o as any).email === 'string';\n// }","tryCatchPattern":"try {\n  await postLicense({ license: file });\n} catch (e) {\n  if (e.isBadRequest && /invalid license/i.test(e.message)) {\n    showUploadError('License file could not be read. Re-download it from the cloud portal.');\n  } else { throw e; }\n}","preventionTips":["Download the license JSON directly from the cloud portal without manual edits.","Confirm the file is a user license, not an organization license.","Keep the file under 50KB and avoid re-wrapping it in another container."],"tags":["billing","license","self-hosted","validation","json"],"backgroundTag":null,"analyzedSha":"e93b962371d80964556f5590c6615f5160a437a1","analyzedAt":"2026-08-13T14:22:19.382Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}