signalapp/Signal-Server · error · BadRequestException
may not provide both group send token and unidentified…
Error message
may not provide both group send token and unidentified access key
What it means
When fetching an unversioned profile, a caller may authenticate either with a group send token or with an unidentified access key, but not both. Supplying both is ambiguous, so the controller rejects it with a 400 BadRequest before validating either credential.
Solutions
- Send exactly one credential: drop the unidentified access key when a group send token is present
- If using a group send token, clear/remove any default unidentified access key header the client library attaches
- Update client code so token selection is exclusive based on the lookup context (group-based vs access-key-based)
Example fix
// before
request.header("X-Signal-Group-Send-Token", token).header("Authorization-Unidentified", accessKey);
// after
if (groupSendToken != null) { request.header("X-Signal-Group-Send-Token", token); } else { request.header("Authorization-Unidentified", accessKey); } Defensive patterns
Strategy: validation
Validate before calling
if (groupSendToken != null && unidentifiedAccessKey != null) {
throw new IllegalArgumentException("provide either groupSendToken or unidentifiedAccessKey, not both");
} Type guard
boolean exactlyOneCredential(String groupSendToken, String accessKey) { return (groupSendToken != null) ^ (accessKey != null); } Try / catch
try { /* profile fetch */ } catch (BadRequestException e) { if (e.getMessage().contains("both group send token")) { retryWithSingleCredential(); } } Prevention
- Centralize profile-fetch auth header construction so credentials are mutually exclusive
- Strip default unidentified access keys when attaching a group send token
- Choose the credential type at call sites based on lookup context
When it happens
Trigger: GET /v1/profile/{identifier} with both a GroupSendToken (header/query) and an unidentified access key (Authorization-Unidentified header) present in the same request.
Common situations: Clients that always attach their unidentified access key and additionally add a group send token; middleware or libraries that inject default auth headers conflicting with explicit tokens.
Related errors
- Operation requires unauthenticated access
- must not use authenticated connection for anonymous…
- account does not have a phone number
- recovery password could not be verified
- must not use authenticated connection for call quality…
AI-assisted analysis of signalapp/Signal-Server@100ab61c82 (2026-09-09).
Data as JSON: /api/errors/d04c87b24f740250.
Report an issue: GitHub.
Appendix: source
Thrown at service/src/main/java/org/whispersystems/textsecuregcm/controllers/ProfileController.java:342
@ManagedAsync
public BaseProfileResponse getUnversionedProfile(
@Auth Optional<AuthenticatedDevice> maybeAuthenticatedDevice,
@HeaderParam(HeaderUtils.UNIDENTIFIED_ACCESS_KEY) Optional<Anonymous> accessKey,
@HeaderParam(HeaderUtils.GROUP_SEND_TOKEN) Optional<GroupSendTokenHeader> groupSendToken,
@Context ContainerRequestContext containerRequestContext,
@HeaderParam(HttpHeaders.USER_AGENT) String userAgent,
@PathParam("identifier") ServiceIdentifier identifier)
throws RateLimitExceededException {
final Optional<Account> maybeRequester =
maybeAuthenticatedDevice.map(
authenticatedDevice -> accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())
.orElseThrow(() -> new WebApplicationException(Response.Status.UNAUTHORIZED)));
final Account targetAccount;
if (groupSendToken.isPresent()) {
if (accessKey.isPresent()) {
throw new BadRequestException("may not provide both group send token and unidentified access key");
}
try {
final GroupSendFullToken token = groupSendToken.get().token();
token.verify(List.of(identifier.toLibsignal()), clock.instant(), GroupSendDerivedKeyPair.forExpiration(token.getExpiration(), serverSecretParams));
targetAccount = accountsManager.getByServiceIdentifier(identifier).orElseThrow(NotFoundException::new);
} catch (VerificationFailedException e) {
throw new NotAuthorizedException(e);
}
} else {
targetAccount = verifyPermissionToReceiveProfile(
maybeRequester, accessKey.filter(ignored -> identifier.identityType() == IdentityType.ACI), identifier, "getUnversionedProfile", userAgent);
}
return switch (identifier.identityType()) {
case ACI -> buildBaseProfileResponseForAccountIdentity(targetAccount,
maybeRequester.map(requester -> ProfileHelper.isSelfProfileRequest(requester.getAccountIdentifier(), identifier)).orElse(false),
containerRequestContext);
case PNI -> buildBaseProfileResponseForPhoneNumberIdentity(targetAccount);
};View on GitHub (pinned to 100ab61c82)