{"record":{"id":"5f1bbee75ad49008","repo":"signalapp/Signal-Server","slug":"recovery-password-could-not-be-verified-5f1bbe","errorCode":null,"errorMessage":"recovery password could not be verified","messagePattern":"recovery password could not be verified","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":245,"sourceCode":"    }\n\n    final PhoneVerificationRequest.VerificationType verificationType;\n    try {\n      verificationType = phoneVerificationTokenManager.verify(\n          number,\n          requestContext.getHeaderString(HttpHeaders.USER_AGENT),\n          requestContext.getHeaderString(HttpHeaders.ACCEPT_LANGUAGE),\n          (String) requestContext.getProperty(RemoteAddressFilter.REMOTE_ADDRESS_ATTRIBUTE_NAME),\n          StringUtils.isNotBlank(registrationRequest.sessionId()) ? registrationRequest.decodeSessionId() : null,\n          registrationRequest.recoveryPassword());\n    } catch (final UnverifiedRegistrationSessionException e) {\n      throw new NotAuthorizedException(\"registration session is unverified\");\n    } catch (final InvalidRegistrationSessionException e) {\n      throw new BadRequestException(e.getMessage());\n    } catch (final IOException e) {\n      throw new ServiceUnavailableException(e.getMessage());\n    } catch (final RecoveryPasswordVerificationFailedException e) {\n      throw new ForbiddenException(\"recovery password could not be verified\");\n    }\n\n    rateLimiters.getRegistrationLimiter().validate(number);\n\n    // There can be at most one existing account for a set of numbers in the same equivalence class, so it's sufficient\n    // to find the first one.\n    final Optional<Account> existingAccount = Util.getAlternateForms(number)\n        .stream()\n        .map(accounts::getByE164)\n        .filter(Optional::isPresent)\n        .map(Optional::get)\n        .findFirst();\n\n    existingAccount.ifPresent(account -> {\n      final Instant accountLastSeen = Instant.ofEpochMilli(account.getLastSeen());\n      final Duration timeSinceLastSeen = Duration.between(accountLastSeen, Instant.now());\n      REREGISTRATION_IDLE_DAYS_DISTRIBUTION.record(timeSinceLastSeen.toDays());\n    });","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L227-L263","documentation":"When registering with a recovery password instead of a verified session, the server validates the recovery password against the registration service. If validation fails (RecoveryPasswordVerificationFailedException), the controller returns 403 Forbidden 'recovery password could not be verified'. This means the presented recovery password doesn't match or wasn't issued for this registration attempt.","triggerScenarios":"POST /v1/registration passing a recoveryPassword whose verification via phoneVerificationTokenManager.verify throws RecoveryPasswordVerificationFailedException — wrong, expired, already-used, or truncated recovery password bytes.","commonSituations":"Client supplying the recovery password from a previous registration attempt; base64 encoding/copy errors corrupting the password; password expired by the time registration is attempted.","solutions":["Obtain a fresh recovery password from the registration service and use it immediately in the same flow","Verify the recovery password is transmitted intact (correct base64, no truncation/whitespace)","If the password was already consumed, restart the registration flow to get a new one"],"exampleFix":"// before\nrecoveryPassword = oldStoredPassword; // from a prior, already-used attempt\n// after\nrecoveryPassword = fetchFreshRecoveryPassword(number); // newly issued for this attempt","handlingStrategy":"try-catch","validationCode":"if (recoveryPassword == null || recoveryPassword.isBlank()) { throw new IllegalArgumentException(\"recoveryPassword required for this registration path\"); }","typeGuard":null,"tryCatchPattern":"try { /* registration */ } catch (ForbiddenException e) { if (\"recovery password could not be verified\".equals(e.getMessage())) { requestNewRecoveryPasswordAndRetry(); } }","preventionTips":["Use recovery passwords immediately after issuance — they are single-use/expiring","Guard against encoding corruption (base64, whitespace) before sending","On 403, always fetch a fresh password rather than retrying the old one"],"tags":["registration","recovery-password","forbidden","verification"],"backgroundTag":"verification-failed","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"}