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

  1. Remove the AuthenticatedDevice parameter (and @Auth annotation) from the subscriber resource method so the connection is unauthenticated.
  2. If account identity is genuinely needed, split the endpoint: account-authenticated routes must not call SubscriberCredentials.process; use the account-based storage APIs instead.
  3. 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

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.

Related errors


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)