{"record":{"id":"4d3aa2b80b18d5a4","repo":"signalapp/Signal-Server","slug":"receipt-is-already-expired-4d3aa2","errorCode":null,"errorMessage":"Receipt is already expired","messagePattern":"Receipt is already expired","errorType":"http","errorClass":"NotAuthorizedException","httpStatus":401,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java","lineNumber":356,"sourceCode":"        .filter(ArrayUtils::isNotEmpty)\n        .orElseThrow(() -> new WebApplicationException(\"Account recovery password is required\", 422));\n\n    final ReceiptCredentialPresentation receiptCredentialPresentation;\n    try {\n      receiptCredentialPresentation = receiptCredentialPresentationFactory\n          .build(registrationRequest.receiptCredentialPresentation());\n    } catch (InvalidInputException _) {\n      throw new BadRequestException(\"Invalid receipt credential presentation\");\n    }\n    try {\n      serverZkReceiptOperations.verifyReceiptCredentialPresentation(receiptCredentialPresentation);\n    } catch (VerificationFailedException _) {\n      throw new NotAuthorizedException(\"Receipt credential presentation verification failed\");\n    }\n\n    final Instant receiptExpiration = Instant.ofEpochSecond(receiptCredentialPresentation.getReceiptExpirationTime());\n    if (clock.instant().isAfter(receiptExpiration)) {\n      throw new NotAuthorizedException(\"Receipt is already expired\");\n    }\n\n    final long receiptLevel = receiptCredentialPresentation.getReceiptLevel();\n    if (receiptLevel != ReceiptLevel.LOGIN.getValue()) {\n      throw new BadRequestException(\"Invalid receipt level\");\n    }\n\n    try {\n      final Account account = accounts.create(\n          registrationRequest.accountAttributes(),\n          registrationRequest.aciIdentityKey(),\n          receiptCredentialPresentation,\n          new DeviceSpec(\n              registrationRequest.accountAttributes().getName(),\n              password,\n              signalAgent,\n              registrationRequest.accountAttributes().getCapabilities(),\n              new DeviceIdentityInfo(registrationRequest.accountAttributes().getRegistrationId(), registrationRequest.deviceActivationRequest()","sourceCodeStart":338,"sourceCodeEnd":374,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/RegistrationController.java#L338-L374","documentation":"The receipt credential presentation verified cryptographically, but its embedded receiptExpirationTime (epoch seconds) is in the past relative to the server clock. The controller throws a NotAuthorizedException because a login purchase whose receipt has lapsed cannot be used to create an account. This is a time-validity check applied after signature verification.","triggerScenarios":"Registering with a receipt credential whose expiration time has passed: the client waited too long between purchase and registration, the device clock skew delayed use, or a long-stored receipt is replayed after expiry.","commonSituations":"Cached receipts in app storage used after the subscription window ended; offline devices presenting old receipts once they reconnect; testing with fixtures containing hardcoded past timestamps.","solutions":["Purchase/issue a new receipt and retry registration with the fresh receipt credential presentation.","Sync the client device clock (wrong local clocks can cause the client to believe the receipt is still valid).","Check the receipt's receiptExpirationTime client-side before submitting and request renewal if it is expired or near expiry.","If testing, update fixture receipts to have future expiration timestamps."],"exampleFix":"// before: blindly submitting a stored presentation\nsubmit(presentation);\n// after: check expiry first\nif (Instant.ofEpochSecond(presentation.getReceiptExpirationTime()).isBefore(Instant.now())) {\n  presentation = renewReceipt();\n}\nsubmit(presentation);","handlingStrategy":"validation","validationCode":"Instant expiry = Instant.ofEpochSecond(presentation.getReceiptExpirationTime());\nif (Instant.now().isAfter(expiry)) {\n  throw new IllegalStateException(\"receipt expired at \" + expiry + \" — renew before registering\");\n}","typeGuard":"boolean isReceiptUsable(ReceiptCredentialPresentation p) {\n  return Instant.ofEpochSecond(p.getReceiptExpirationTime()).isAfter(Instant.now());\n}","tryCatchPattern":"try {\n  register(request);\n} catch (NotAuthorizedException e) {\n  if (e.getMessage().contains(\"already expired\")) {\n    purchaseNewReceiptAndRegister();\n  } else throw e;\n}","preventionTips":["Check receiptExpirationTime client-side before every redemption attempt.","Keep device clocks synced (NTP); avoid submitting receipts after long offline periods.","Proactively renew receipts nearing expiry."],"tags":["zk-receipt","expired-credential","registration","not-authorized"],"backgroundTag":"jwt-token-expired","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"}