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
- 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
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
- Call login-purchase endpoints with a credential-less client
- Exclude these paths from automatic auth-header injection
- Perform login purchase flow before account authentication
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
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- must not use authenticated connection for call quality…
- Operation requires unauthenticated access
- recovery password could not be verified
- must not use authenticated connection for anonymous…
- Only primary devices may register attestations
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)