signalapp/Signal-Server · error · ForbiddenException

must not use authenticated connection for login purchase…

Error message

must not use authenticated connection for login purchase operations

What it means

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').

Solutions

  1. Remove the Authorization header and call the login-purchase endpoint from an unauthenticated connection
  2. Use a separate credential-less HTTP client (or exclude this path from global auth interceptors)
  3. Complete the login-purchase flow before registering/authenticating, per the intended protocol order

Example fix

// before
authenticatedClient.post("/v1/login/receipt-credential", request); // 403 ForbiddenException
// after
Request req = new Request.Builder().url(receiptUrl).post(body).build(); // no Authorization header
unauthenticatedClient.newCall(req).execute();
Defensive patterns

Strategy: validation

Validate before calling

if (request.getHeader("Authorization") != null) {
  throw new IllegalStateException("login purchase operations must use an unauthenticated connection");
}

Type guard

boolean isUnauthenticated(Request r) { return r.header("Authorization") == null; }

Try / catch

try {
  client.createLoginReceiptCredential(request);
} catch (WebApplicationException e) {
  if (e.getResponse().getStatus() == 403 && e.getMessage().contains("authenticated connection")) {
    // retry without credentials on a fresh connection
  } else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: A shared HTTP client that automatically attaches account credentials to all requests; developers testing while logged in; SDKs or interceptors injecting auth headers globally.

Understand the failure class

Related errors


AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09). Data as JSON: /api/errors/86ec5a8b058a7d47. Report an issue: GitHub.

Appendix: source

Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/LoginPurchaseController.java:112

          implementation = SubscriptionExceptionMapper.ChargeFailureResponse.class)))
  @ApiResponse(responseCode = "403", description = "The request was made on an authenticated channel")
  @ApiResponse(responseCode = "404", description = "The payment provider has no purchase with the provided purchaseIdentifier")
  @ApiResponse(responseCode = "409", description = "The purchase was already redeemed for a receipt credential, but with a different receipt credential request")
  @ApiResponse(responseCode = "429", description = "Too many attempts", headers = @Header(
      name = "Retry-After",
      description = "If present, a positive integer indicating the number of seconds before a subsequent attempt could succeed"))
  @ManagedAsync
  public Response createLoginReceiptCredential(
      @Auth final Optional<AuthenticatedDevice> authenticatedAccount,
      @NotNull @Valid final CreateLoginReceiptCredentialRequest request)
      throws IOException, SubscriptionPaymentRequiredException, SubscriptionInvalidArgumentsException, SubscriptionNotFoundException, RateLimitExceededException, SubscriptionReceiptAlreadyRedeemedException {

    if (!dynamicConfigurationManager.getConfiguration().getLoginPurchaseConfiguration().enabled()) {
      throw new BadRequestException("login purchases are not enabled");
    }

    if (authenticatedAccount.isPresent()) {
      throw new ForbiddenException("must not use authenticated connection for login purchase operations");
    }

    final ReceiptCredentialRequest receiptCredentialRequest;
    try {
      receiptCredentialRequest = new ReceiptCredentialRequest(request.receiptCredentialRequest);
    } catch (final InvalidInputException e) {
      throw new BadRequestException("invalid receipt credential request", e);
    }

    try {
      final ReceiptCredentialResponse receiptCredentialResponse = loginPurchaseManager.generateReceipt(
          request.paymentProvider, request.purchaseIdentifier, receiptCredentialRequest);
      return Response.ok(
              new CreateLoginReceiptCredentialResponse(receiptCredentialResponse.serialize()))
          .build();
    } catch (SubscriptionReceiptRequestedForOpenPaymentException e) {
      return Response.noContent().build();
    } catch (VerificationFailedException e) {

View on GitHub (pinned to 100ab61c82)