{"record":{"id":"86ec5a8b058a7d47","repo":"signalapp/Signal-Server","slug":"must-not-use-authenticated-connection-for-login-pu","errorCode":null,"errorMessage":"must not use authenticated connection for login purchase operations","messagePattern":"must not use authenticated connection for login purchase operations","errorType":"http","errorClass":"ForbiddenException","httpStatus":403,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java","lineNumber":112,"sourceCode":"          implementation = SubscriptionExceptionMapper.ChargeFailureResponse.class)))\n  @ApiResponse(responseCode = \"403\", description = \"The request was made on an authenticated channel\")\n  @ApiResponse(responseCode = \"404\", description = \"The payment provider has no purchase with the provided purchaseIdentifier\")\n  @ApiResponse(responseCode = \"409\", description = \"The purchase was already redeemed for a receipt credential, but with a different receipt credential request\")\n  @ApiResponse(responseCode = \"429\", description = \"Too many attempts\", headers = @Header(\n      name = \"Retry-After\",\n      description = \"If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed\"))\n  @ManagedAsync\n  public Response createLoginReceiptCredential(\n      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,\n      @NotNull @Valid final CreateLoginReceiptCredentialRequest request)\n      throws IOException, SubscriptionPaymentRequiredException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException, SubscriptionReceiptAlreadyRedeemedException {\n\n    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {\n      throw new BadRequestException(\"login purchases are not enabled\");\n    }\n\n    if (authenticatedAccount.isPresent()) {\n      throw new ForbiddenException(\"must not use authenticated connection for login purchase operations\");\n    }\n\n    final ReceiptCredentialRequest receiptCredentialRequest;\n    try {\n      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);\n    } catch (final InvalidInputException e) {\n      throw new BadRequestException(\"invalid receipt credential request\", e);\n    }\n\n    try {\n      final ReceiptCredentialResponse receiptCredentialResponse = loginPurchaseManager.generateReceipt(\n          request.paymentProvider, request.purchaseIdentifier, receiptCredentialRequest);\n      return Response.ok(\n              new CreateLoginReceiptCredentialResponse(receiptCredentialResponse.serialize()))\n          .build();\n    } catch (SubscriptionReceiptRequestedForOpenPaymentException e) {\n      return Response.noContent().build();\n    } catch (VerificationFailedException e) {","sourceCodeStart":94,"sourceCodeEnd":130,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java#L94-L130","documentation":"LoginPurchaseController.createLoginReceiptCredential requires the caller to use an UNAUTHENTICATED connection (like the key-transparency endpoints' inverse pattern): login purchases must be tied to the anonymous, pre-authorization session. If an AuthenticatedDevice is present it throws ForbiddenException('must not use authenticated connection for login purchase operations').","triggerScenarios":"POST to the login purchase receipt-credential endpoint with an Authorization header / signed-in session that resolves to an AuthenticatedDevice, even when the login-purchase feature flag is enabled.","commonSituations":"A shared HTTP client that automatically attaches account credentials to all requests; developers testing while logged in; SDKs or interceptors injecting auth headers globally.","solutions":["Remove the Authorization header and call the login-purchase endpoint from an unauthenticated connection","Use a separate credential-less HTTP client (or exclude this path from global auth interceptors)","Complete the login-purchase flow before registering/authenticating, per the intended protocol order"],"exampleFix":"// before\nauthenticatedClient.post(\"/v1/login/receipt-credential\", request); // 403 ForbiddenException\n// after\nRequest req = new Request.Builder().url(receiptUrl).post(body).build(); // no Authorization header\nunauthenticatedClient.newCall(req).execute();","handlingStrategy":"validation","validationCode":"if (request.getHeader(\"Authorization\") != null) {\n  throw new IllegalStateException(\"login purchase operations must use an unauthenticated connection\");\n}","typeGuard":"boolean isUnauthenticated(Request r) { return r.header(\"Authorization\") == null; }","tryCatchPattern":"try {\n  client.createLoginReceiptCredential(request);\n} catch (WebApplicationException e) {\n  if (e.getResponse().getStatus() == 403 && e.getMessage().contains(\"authenticated connection\")) {\n    // retry without credentials on a fresh connection\n  } else throw e;\n}","preventionTips":["Call login-purchase endpoints with a credential-less client","Exclude these paths from automatic auth-header injection","Perform login purchase flow before account authentication"],"tags":["authentication","http-403","login-purchase"],"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"}