{"record":{"id":"30e7dc33272c29f0","repo":"signalapp/Signal-Server","slug":"recovery-password-required-for-authentication-when","errorCode":null,"errorMessage":"Recovery password required for authentication when recovering an account by identifier","messagePattern":"Recovery password required for authentication when recovering an account by identifier","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":404,"sourceCode":"      final AccountIdentityResponse accountIdentityResponse = new AccountIdentityResponseBuilder(account).build();\n      return new AccountCreationResponse(accountIdentityResponse, false);\n    } catch (ReceiptAlreadyRedeemedException _) {\n      throw new NotAuthorizedException(\"Receipt already redeemed\");\n    }\n  }\n\n  private AccountCreationResponse recoverAccount(final UUID accountIdentifier,\n      final String password,\n      final RegistrationRequest registrationRequest,\n      final String userAgent,\n      final String signalAgent) throws RegistrationLockFailureException, RateLimitExceededException {\n\n    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {\n      throw new BadRequestException(\"login purchases are not enabled\");\n    }\n\n    if (ArrayUtils.isEmpty(registrationRequest.recoveryPassword())) {\n      throw new BadRequestException(\"Recovery password required for authentication when recovering an account by identifier\");\n    }\n\n    if (registrationRequest.accountAttributes().recoveryPassword().isEmpty()) {\n      throw new BadRequestException(\"Recovery password required for for storage when recovering an account by identifier\");\n    }\n\n    if (registrationRequest.pniIdentityKey() == null) {\n      throw new BadRequestException(\"Must specify a PNI-associated identity key when recovering an account by identifier\");\n    }\n\n    final Account existingAccount = accounts.getByAccountIdentifier(accountIdentifier)\n            .orElseThrow(ForbiddenException::new);\n\n    final boolean passwordVerified = existingAccount.getAccountRecoveryPassword()\n        .map(saltedRecoveryPasswordHash -> PhoneNumberRecoveryPasswordsManager.verify(saltedRecoveryPasswordHash, registrationRequest.recoveryPassword()))\n        .orElse(false);\n\n    if (!passwordVerified) {","sourceCodeStart":386,"sourceCodeEnd":422,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L386-L422","documentation":"recoverAccount requires recoveryPassword (the authentication recovery password) in the RegistrationRequest when recovering an account by identifier. If ArrayUtils.isEmpty finds it missing or empty, the controller throws a BadRequestException (HTTP 400). This password authenticates the caller to the existing account.","triggerScenarios":"Submitting a recovery-by-identifier registration request whose top-level recoveryPassword field is null or an empty/blank array.","commonSituations":"Clients upgraded to a server version requiring the recovery password field while still sending the old request shape; app storage lost the recovery password (fresh install, cleared data) so the field is serialized as null; API tests omitting the new field.","solutions":["Populate RegistrationRequest.recoveryPassword with the stored account recovery password before sending the recovery request.","Ensure the client persists the recovery password (from registration/backup time) so it survives restarts and can be supplied here.","Update client request serialization to include recoveryPassword — older client builds predating this requirement will always 400.","If the recovery password is genuinely lost, use an alternate account recovery path (e.g. registration lock / recovery phrase) instead."],"exampleFix":"// before\nRegistrationRequest req = new RegistrationRequest(attrs, aciKey, pniKey, null /* recoveryPassword */);\n// after\nRegistrationRequest req = new RegistrationRequest(attrs, aciKey, pniKey, storedRecoveryPassword);","handlingStrategy":"validation","validationCode":"if (ArrayUtils.isEmpty(request.recoveryPassword())) {\n  throw new IllegalArgumentException(\"authentication recoveryPassword is required for recovery by identifier\");\n}","typeGuard":"boolean hasAuthRecoveryPassword(RegistrationRequest r) {\n  return r.recoveryPassword() != null && r.recoveryPassword().length > 0;\n}","tryCatchPattern":"try {\n  recoverAccount(request);\n} catch (BadRequestException e) {\n  if (e.getMessage().startsWith(\"Recovery password required for authentication\")) {\n    promptUserToReenterRecoveryPassword();\n  } else throw e;\n}","preventionTips":["Persist the account recovery password at registration time so recovery requests can include it.","Update pre-feature client builds to serialize the recoveryPassword field.","Validate request payloads before sending."],"tags":["account-recovery","missing-field","bad-request","recovery-password"],"backgroundTag":"missing-required-argument","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}