signalapp/Signal-Server · error · SubscriptionForbiddenException
must not use authenticated connection for subscriber…
Error message
must not use authenticated connection for subscriber operations
What it means
SubscriberCredentials.process throws SubscriptionForbiddenException when a subscriber operation arrives over a connection already authenticated as a Signal account/device. Subscriber (donation/payment) operations use an anonymous, subscriberId-based credential model; mixing them with an authenticated account identity would let a logged-in user probe or hijack subscriber records, so the library deliberately forbids it.
Solutions
- Remove the AuthenticatedDevice parameter (and @Auth annotation) from the subscriber resource method so the connection is unauthenticated.
- If account identity is genuinely needed, split the endpoint: account-authenticated routes must not call SubscriberCredentials.process; use the account-based storage APIs instead.
- In tests/integration clients, stop sending Authorization/credential headers on subscriber-operation requests.
Example fix
// before
public void donate(@Auth Optional<AuthenticatedDevice> auth, @PathParam("subscriberId") String subscriberId) {
SubscriberCredentials creds = SubscriberCredentials.process(auth, subscriberId, clock);
}
// after
public void donate(@PathParam("subscriberId") String subscriberId) {
SubscriberCredentials creds = SubscriberCredentials.process(Optional.empty(), subscriberId, clock);
} Defensive patterns
Strategy: validation
Validate before calling
if (authenticatedAccount.isPresent()) {
throw new IllegalStateException("subscriber operations must not be performed on an authenticated connection");
} Type guard
boolean isUnauthenticatedConnection(Optional<AuthenticatedDevice> auth) { return auth.isEmpty(); } Try / catch
try {
SubscriberCredentials.process(auth, subscriberId, clock);
} catch (SubscriptionForbiddenException e) {
log.warn("authenticated connection used for subscriber op");
return Response.status(403).build();
} Prevention
- Keep subscriber/donation resource methods free of @Auth AuthenticatedDevice parameters
- Document the anonymous-credential contract on subscriber endpoints
- Add an integration test asserting subscriber endpoints reject Authorization headers
When it happens
Trigger: Calling SubscriberCredentials.process(Optional.of(authenticatedDevice), subscriberId, clock) — i.e. any resource method decorated with @Auth AuthenticatedDevice whose handler also resolves SubscriberCredentials from a subscriberId path/query parameter.
Common situations: A developer adds an authenticated account parameter to a subscription/donation endpoint for convenience and forgets the subscriber endpoints must be unauthenticated; copying an existing account-scoped resource class to build a subscriber-scoped one and keeping the @Auth annotation.
Understand the failure class
Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- subscriberId mismatch
- access key length must be 16
- Invalid combined unidentified sender access keys
- recovery password could not be verified
- must not use authenticated connection for one-time donation…
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/1e3536b537dd9613.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/storage/SubscriberCredentials.java:33
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.whispersystems.textsecuregcm.auth.AuthenticatedDevice;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionException;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionForbiddenException;
import org.whispersystems.textsecuregcm.subscriptions.SubscriptionNotFoundException;
public record SubscriberCredentials(@Nonnull byte[] subscriberBytes,
@Nonnull byte[] subscriberUser,
@Nonnull byte[] subscriberKey,
@Nonnull byte[] hmac,
@Nonnull Instant now) {
public static SubscriberCredentials process(
final Optional<AuthenticatedDevice> authenticatedAccount,
final String subscriberId,
final Clock clock) throws SubscriptionException {
if (authenticatedAccount.isPresent()) {
throw new SubscriptionForbiddenException("must not use authenticated connection for subscriber operations");
}
final byte[] subscriberBytes = convertSubscriberIdStringToBytes(subscriberId);
return process(subscriberBytes, clock);
}
public static SubscriberCredentials process(
final byte[] subscriberBytes,
final Clock clock) {
final Instant now = clock.instant();
final byte[] subscriberUser = getUser(subscriberBytes);
final byte[] subscriberKey = getKey(subscriberBytes);
final byte[] hmac = computeHmac(subscriberUser, subscriberKey);
return new SubscriberCredentials(subscriberBytes, subscriberUser, subscriberKey, hmac, now);
}
private static byte[] convertSubscriberIdStringToBytes(String subscriberId) throws SubscriptionNotFoundException {
try {
byte[] bytes = Base64.getUrlDecoder().decode(subscriberId);View on GitHub (pinned to 100ab61c82)