{"record":{"id":"52248672f2852f99","repo":"signalapp/Signal-Server","slug":"only-one-of-group-send-endorsement-token-and-unide","errorCode":null,"errorMessage":"Only one of group send endorsement token and unidentified access key may be provided","messagePattern":"Only one of group send endorsement token and unidentified access key may be provided","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":543,"sourceCode":"  private SendMultiRecipientMessageResponse sendMultiRecipientMessage(final SealedSenderMultiRecipientMessage multiRecipientMessage,\n      final long timestamp,\n      final boolean ephemeral,\n      final boolean urgent,\n      @Nullable final GroupSendTokenHeader groupSendTokenHeader,\n      @Nullable final CombinedUnidentifiedSenderAccessKeys combinedUnidentifiedSenderAccessKeys,\n      final ContainerRequestContext context) {\n\n    // Perform fast, inexpensive checks before attempting to resolve recipients\n    if (MessageUtil.hasDuplicateDevices(multiRecipientMessage)) {\n      throw new BadRequestException(\"Multi-recipient message contains duplicate recipient\");\n    }\n\n    if (groupSendTokenHeader == null && combinedUnidentifiedSenderAccessKeys == null) {\n      throw new NotAuthorizedException(\"A group send endorsement token or unidentified access key is required for non-story messages\");\n    }\n\n    if (groupSendTokenHeader != null && combinedUnidentifiedSenderAccessKeys != null) {\n      throw new BadRequestException(\"Only one of group send endorsement token and unidentified access key may be provided\");\n    }\n\n    if (groupSendTokenHeader != null) {\n      // Group send endorsements are checked before we even attempt to resolve any accounts, since\n      // the lists of service IDs in the envelope are all that we need to check against\n      checkGroupSendToken(multiRecipientMessage.getRecipients().keySet(), groupSendTokenHeader);\n    } else {\n      Metrics.counter(LEGACY_COMBINED_UAK_COUNTER_NAME, Tags.of(UserAgentTagUtil.getPlatformTag(context))).increment();\n    }\n\n    // At this point, the caller has at least superficially provided the information needed to send a multi-recipient\n    // message. Attempt to resolve the destination service identifiers to Signal accounts.\n    final Map<SealedSenderMultiRecipientMessage.Recipient, Account> resolvedRecipients =\n        MessageUtil.resolveRecipients(accountsManager, multiRecipientMessage);\n\n    final List<ServiceIdentifier> unresolvedRecipientServiceIdentifiers =\n        MessageUtil.getUnresolvedRecipients(multiRecipientMessage, resolvedRecipients);\n","sourceCodeStart":525,"sourceCodeEnd":561,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L525-L561","documentation":"The group send endorsement token and the combined unidentified-sender access keys are alternative authentication mechanisms for multi-recipient sends; supplying both is rejected with a 400. Exactly one must be provided for non-story messages.","triggerScenarios":"POST to the multi-recipient endpoint with both the group send endorsement token header and the combined unidentified access keys header set on a non-story message.","commonSituations":"Client migration from access keys to endorsements leaving both headers during the transition; default header injection plus explicit token; conditional logic that falls through and sets both.","solutions":["Send exactly one: prefer the group send endorsement token on newer clients and drop the access-key header.","Make the client's auth-mode selection exclusive (if/else, not two independent header setters).","During migration, disable legacy access-key attachment once endorsements are confirmed working."],"exampleFix":"// before\nbuilder.header(\"X-Group-Send-Token\", token);\nbuilder.header(\"X-Unidentified-Access-Keys\", combinedKeys); // both set\n// after\nif (token != null) { builder.header(\"X-Group-Send-Token\", token); }\nelse { builder.header(\"X-Unidentified-Access-Keys\", combinedKeys); }","handlingStrategy":"validation","validationCode":"if (headers[\"X-Group-Send-Token\"] != null && headers[\"X-Unidentified-Access-Keys\"] != null) { delete headers[\"X-Unidentified-Access-Keys\"]; }","typeGuard":"function hasExactlyOneAuth(headers) { const n = [headers[\"X-Group-Send-Token\"], headers[\"X-Unidentified-Access-Keys\"]].filter(h => h != null).length; return n === 1; }","tryCatchPattern":"try { await sendMultiRecipient(msg); } catch (e) { if (e.status === 400 && /Only one of/.test(e.body)) { resendWithSingleAuthMode(); } }","preventionTips":["Make endorsement vs access-key selection an if/else, not two independent header setters","During migration, remove access-key headers once endorsements work","Assert single-auth-mode in client unit tests"],"tags":["http-400","authentication","group-send-endorsements"],"backgroundTag":"mutually-exclusive-options","analyzedSha":"100ab61c82627582c867d19e1c0561ba2781e927","analyzedAt":"2026-09-09T13:29:47.883Z","contentChangedAt":"2026-09-09T13:29:47.883Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}