bitwarden/server · error · ConflictException
ExternalId already exists for another group.
Error message
ExternalId already exists for another group.
What it means
Thrown as a ConflictException (HTTP 409) by PatchGroupCommand.EnsureExternalIdIsValidAsync when another group in the same organization already claims the same externalId (case-insensitive comparison). ExternalId must be unique within an organization so the IdP can reliably address exactly one group.
Source
Thrown at bitwarden_license/src/Scim/Groups/PatchGroupCommand.cs:205
}
await EnsureExternalIdIsValidAsync(group, newExternalId);
return newExternalId;
}
private async Task EnsureExternalIdIsValidAsync(Group group, string newExternalId)
{
if (newExternalId.Length > 300)
{
throw new BadRequestException("ExternalId cannot exceed 300 characters.");
}
var existingGroups = await _groupRepository.GetManyByOrganizationIdAsync(group.OrganizationId);
if (existingGroups.Any(g => g.Id != group.Id &&
!string.IsNullOrWhiteSpace(g.ExternalId) &&
g.ExternalId.Equals(newExternalId, StringComparison.OrdinalIgnoreCase)))
{
throw new ConflictException("ExternalId already exists for another group.");
}
}
private async Task AddMembersAsync(Group group, HashSet<Guid> usersToAdd)
{
// Azure Entra ID is known to send redundant "add" requests for each existing member every time any member
// is removed. To avoid excessive load on the database, we check against the high availability replica and
// return early if they already exist.
var groupMembers = await _groupRepository.GetManyUserIdsByIdAsync(group.Id, useReadOnlyReplica: true);
if (usersToAdd.IsSubsetOf(groupMembers))
{
_logger.LogDebug("Ignoring duplicate SCIM request to add members {Members} to group {Group}", usersToAdd, group.Id);
return;
}
await _groupRepository.AddGroupUsersByIdAsync(group.Id, usersToAdd, _timeProvider.GetUtcNow().UtcDateTime);
}
View on GitHub (pinned to e93b962371)
Solutions
- Find the conflicting group: list groups via GET /v2/{organizationId}/Users and search for the duplicate externalId.
- Assign a unique externalId to the group being patched, or remove the externalId from the conflicting group first.
- Fix the IdP attribute mapping so each group gets a distinct, stable externalId.
- If the externalId belongs to a deleted group, verify it was fully removed.
Defensive patterns
Strategy: validation
Validate before calling
// Check for duplicate externalId before patching
var groups = await scimClient.ListGroupsAsync(orgId);
var dup = groups.FirstOrDefault(g => g.ExternalId?.Equals(newExternalId, StringComparison.OrdinalIgnoreCase) == true && g.Id != groupId);
if (dup != null) throw new InvalidOperationException($"externalId already used by group {dup.Id}"); Try / catch
try { await scimClient.PatchGroupExternalIdAsync(orgId, groupId, newExternalId); }
catch (ScimException ex) when (ex.StatusCode == 409)
{ /* resolve the duplicate: change the externalId or clear it from the other group */ } Prevention
- Ensure externalIds are unique across all groups in the source directory.
- Run a deduplication check before syncing.
- Treat 409 on externalId as a data conflict requiring manual resolution, not a retry.
When it happens
Trigger: SCIM PATCH /v2/{organizationId}/Groups/{id} with a 'replace' on 'externalId' where the new value matches an existing group's externalId in the same org (excluding the group being patched itself). Happens when two directory groups share an identifier or after a rename/re-import.
Common situations: IdP re-exported groups with overlapping externalIds after a directory restructure. An admin manually duplicated a group's externalId. Case-variant collisions (e.g., 'GroupA' vs 'groupa') from IdPs that treat externalId as case-insensitive.
Related errors
- Conflict.
- ExternalId already exists for another user.
- User already exists.
- ExternalId cannot exceed 300 characters.
- ExternalId cannot exceed 300 characters.
AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13).
Data as JSON: /api/errors/a94ec29d53dcf351.
Report an issue: GitHub.