{"record":{"id":"f1ce29b4b3172ca6","repo":"signalapp/Signal-Server","slug":"group-send-endorsement-tokens-should-not-be-combin","errorCode":null,"errorMessage":"Group send endorsement tokens should not be combined with other authentication","messagePattern":"Group send endorsement tokens should not be combined with other authentication","errorType":"http","errorClass":"BadRequestException","httpStatus":400,"severity":"error","filePath":"service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java","lineNumber":221,"sourceCode":"      @HeaderParam(HeaderUtils.GROUP_SEND_TOKEN)\n      @Nullable final GroupSendTokenHeader groupSendToken,\n\n      @HeaderParam(HttpHeaders.USER_AGENT) final String userAgent,\n\n      @Parameter(description=\"The recipient’s account or phone-number identifier\")\n      @PathParam(\"destination\") final ServiceIdentifier destinationIdentifier,\n\n      @Parameter(description=\"If true, the message is a story; access tokens are not checked and sending to nonexistent recipients is permitted\")\n      @QueryParam(\"story\") final boolean isStory,\n\n      @Parameter(description=\"The encrypted message payloads for each recipient device\")\n      @NotNull @Valid final IncomingMessageList messages,\n\n      @Context final ContainerRequestContext context) throws RateLimitExceededException {\n\n    if (groupSendToken != null) {\n      if (source.isPresent() || accessKey.isPresent()) {\n        throw new BadRequestException(\"Group send endorsement tokens should not be combined with other authentication\");\n      } else if (isStory) {\n        throw new BadRequestException(\"Group send endorsement tokens should not be sent for story messages\");\n      }\n    }\n\n    final Sample sample = Timer.start();\n    final boolean needsSync;\n\n    try {\n      if (isStory) {\n        needsSync = false;\n        sendStoryMessage(destinationIdentifier, messages, context);\n      } else if (source.isPresent()) {\n        final AuthenticatedDevice authenticatedDevice = source.get();\n        final Account account = accountsManager.getByAccountIdentifier(authenticatedDevice.accountIdentifier())\n            .orElseThrow(() -> new WebApplicationException(Status.UNAUTHORIZED));\n\n        if (account.isIdentifiedBy(destinationIdentifier)) {","sourceCodeStart":203,"sourceCodeEnd":239,"githubUrl":"https://github.com/signalapp/Signal-Server/blob/100ab61c82627582c867d19e1c0561ba2781e927/service/src/main/java/org/whispersystems/textsecuregcm/controllers/MessageController.java#L203-L239","documentation":"The sendMessage endpoint rejects requests that supply a group send endorsement token header together with another form of sender authentication (an explicit source account or unidentified access key). Group send endorsement tokens are a standalone authentication mechanism, so combining them is invalid and yields a 400.","triggerScenarios":"POST /v1/messages with the group send endorsement token header set while also providing an authorization (account) identity or unidentified-sender access key header.","commonSituations":"Clients migrating from access-key auth to group send endorsements leaving both headers in place; middleware/proxies injecting default auth headers; copy-pasted header configuration.","solutions":["Remove either the group send endorsement token header or the source/accessKey authentication from the request.","If using endorsements, send the token alone with no account credential headers.","Update client code to choose one authentication mode per request explicitly."],"exampleFix":"// before\nrequest.header(\"X-Group-Send-Token\", token).header(\"Authorization\", accountAuth); // conflicting\n// after\nif (useGroupSendToken) { request.header(\"X-Group-Send-Token\", token); } else { request.header(\"Authorization\", accountAuth); }","handlingStrategy":"validation","validationCode":"const hasToken = headers[\"X-Group-Send-Token\"] != null;\nconst hasOtherAuth = headers[\"Authorization\"] != null || headers[\"X-Unidentified-Access-Key\"] != null;\nif (hasToken && hasOtherAuth) throw new Error(\"choose one auth mode\");","typeGuard":null,"tryCatchPattern":"try { await send(messages); } catch (e) { if (e.status === 400 && /other authentication/.test(e.body)) { stripRedundantAuthHeadersAndRetry(); } }","preventionTips":["Pick one authentication mode per send request explicitly","Audit middleware/proxies that auto-inject auth headers","During auth migration, remove legacy headers when enabling endorsements"],"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"}