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

  1. Find the conflicting group: list groups via GET /v2/{organizationId}/Users and search for the duplicate externalId.
  2. Assign a unique externalId to the group being patched, or remove the externalId from the conflicting group first.
  3. Fix the IdP attribute mapping so each group gets a distinct, stable externalId.
  4. 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

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


AI-assisted analysis of bitwarden/server@e93b962371 (2026-08-13). Data as JSON: /api/errors/a94ec29d53dcf351. Report an issue: GitHub.