{"record":{"id":"4b59eddd6d421275","repo":"signalapp/Signal-Server","slug":"registration-session-is-unverified-4b59ed","errorCode":null,"errorMessage":"registration session is unverified","messagePattern":"registration session is unverified","errorType":"http","errorClass":"NotAuthorizedException","httpStatus":401,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":239,"sourceCode":"      final String signalAgent)\n      throws RateLimitExceededException, InterruptedException, RegistrationLockFailureException {\n\n    if (registrationRequest.pniIdentityKey() == null) {\n      // RegistrationRequest checks that either all phone number-associated information is present or all is absent\n      throw new WebApplicationException(\"PNI keys and registration ID must be provided\", 422);\n    }\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();","sourceCodeStart":221,"sourceCodeEnd":257,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L221-L257","documentation":"Number-based registration can be tied to a registration session (e.g. verified via SMS/session tokens). If the supplied session exists but has not been verified (UnverifiedRegistrationSessionException), the controller converts it to a 401 NotAuthorizedException 'registration session is unverified'. The caller must complete the verification flow before attempting to create the account.","triggerScenarios":"POST /v1/registration with a sessionId whose registration session has not completed verification — verifyAccess via phoneVerificationTokenManager raises UnverifiedRegistrationSessionException when the session's verification state is still pending.","commonSituations":"Client skips the verification step and jumps straight to account creation; user abandons SMS/challenge flow midway; sessionId reused before verification finished.","solutions":["Complete the registration session verification (submit the verification code / challenge) before calling registration","Poll or await the session's verified state, then retry the registration call with the same sessionId","If verification isn't applicable, omit sessionId and use an alternative verification method (e.g. recovery password)"],"exampleFix":"// before\nPOST /v1/registration { \"sessionId\": \"abc\", ... } // session not yet verified\n// after\nPOST /v1/registration/verify { \"sessionId\": \"abc\", \"code\": \"123456\" } then POST /v1/registration { \"sessionId\": \"abc\", ... }","handlingStrategy":"try-catch","validationCode":"if (sessionId != null && !registrationSessionIsVerified(sessionId)) {\n  awaitVerificationCompletion(sessionId); // poll / wait before registering\n}","typeGuard":null,"tryCatchPattern":"try { /* registration */ } catch (NotAuthorizedException e) { if (\"registration session is unverified\".equals(e.getMessage())) { completeVerificationFlow(sessionId); } }","preventionTips":["Always finish the verification step before account creation","Never reuse a sessionId from an abandoned flow","Handle 401 by resuming (not restarting) the registration session"],"tags":["registration","session","unauthorized","verification"],"backgroundTag":"authentication-required","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"}