OIDC login appends a duplicate entry to external group member_entity_ids on every login
- Dominant language
- Go
- Stars
- 36.3k
- Forks
- 4.8k
- PR merge metrics
- PR metrics pending
Description
## Summary
Every OIDC login appends the authenticating entity's ID to the `member_entity_ids` list of
each external identity group named in the user's groups claim — without checking whether the
entity is already a member. The list grows by one entry per login per group, unbounded, while
the set of actual members never changes.
The LDAP auth path does not exhibit this. Neither does the `identity/group` API path, which
deduplicates. Only the OIDC login path appears to skip the normalisation.
## Environment
| | |
|---|---|
| Vault server | 2.0.3, built 2026-06-17T12:49:01Z |
| Vault CLI | v2.0.0 (cf1ce4d) |
| Storage | Consul |
| HA | active |
| OIDC plugin | v2.0.3+builtin.vault |
| Auth methods in use | oidc, ldap, approle |
OIDC role config (relevant fields):
```
role_type oidc
groups_claim groups
user_claim preferred_username
oidc_scopes [openid profile email groups]
```
Group layout: eight external groups — four logical roles × two auth methods. Each external
group has exactly one group alias with the correct mount accessor; no duplicate aliases. The
four `*-ldap` and four `*-oidc` groups alias the same four IdP group names. Group and user
names below are anonymised; `group-d` is the one whose IdP group is **not** in this user's
claim, and serves as a control.
## Expected behaviour
After N logins by the same user, an external group whose alias matches a group in that user's
claim contains **one** entry for that user's entity in `member_entity_ids`.
## Actual behaviour
It contains **N** entries.
## Reproduction
Baseline for one entity (`user-a`):
```
GROUP raw entity-count modify_index
group-a-oidc 757 230 828
group-b-oidc 627 230 695
group-c-oidc 689 230 759
group-d-oidc 53 0 87 <- control: alias not in claim
group-a-ldap 7 1 28
group-b-ldap 4 1 18
group-c-ldap 4 1 25
group-d-ldap 3 0 8
```
Then **one** OIDC login (`vault login -method=oidc role=default`):
```
group-a-oidc 757 -> 758 (+1) entity 230 -> 231 (+1) modify_index 828 -> 829
group-b-oidc 627 -> 628 (+1) entity 230 -> 231 (+1) modify_index 695 -> 696
group-c-oidc 689 -> 690 (+1) entity 230 -> 231 (+1) modify_index 759 -> 760
group-d-oidc 53 -> 53 entity 0 -> 0 modify_index 87 -> 87
all four *-ldap unchanged
```
`len(set(member_entity_ids))` is unchanged throughout — no member is added, only a duplicate
appended.
Then **two** further OIDC logins, one with `role=default` and one with a second role
(`role-b`) that stamps an extra policy via `token_policies`:
```
group-a-oidc 757 -> 760 (+3 total) entity 230 -> 233 (+3)
group-b-oidc 627 -> 630 (+3 total) entity 230 -> 233 (+3)
group-c-oidc 689 -> 692 (+3 total) entity 230 -> 233 (+3)
group-d-oidc 53 -> 53 (unchanged)
all four *-ldap unchanged
```
Strictly linear: one appended entry per login, per claim-matched group.
## What this rules out
- **Not a read-time artefact.** Three consecutive reads return identical counts and an
unchanged `modify_index`. The duplicates are persisted.
- **Not driven by the role used.** The third login used `role-b`, yet `group-d-oidc` did not
grow — that group's alias is not in the user's claim. The update follows claim content only.
- **Not low LDAP usage masking the same bug.** The `*-ldap` groups have `modify_index` values
far above their member counts (e.g. `group-a-ldap`: 7 members, `modify_index` 28), so LDAP
logins do write to the group object repeatedly, and the list stays deduplicated. One LDAP
alias in this install last authenticated 2025-09-25 against a group created in 2022 and
still holds exactly one entry per entity.
- **Not a misconfiguration of group aliases.** Eight group aliases, one per external group,
each with the correct mount accessor; no duplicates, no cross-mount collisions.
- **Not a missing dedup in the identity API.** Writing five identical entity IDs to a
throwaway internal group via `identity/group` stores exactly one.
## Suspected cause
The identity group write path applies `strutil.RemoveDuplicates()` to `member_entity_ids`
(cf. #16088). The membership update performed during OIDC login evidently does not reach that
normalisation, whereas the LDAP equivalent does.
Not verified against source — inferred from the behavioural split between the two auth mounts
plus the API-path result above.
## Impact
Functionally benign: policy resolution is correct and the effective member set is right. Two
practical consequences:
1. **Unbounded object growth.** One group here holds 760 entries for 21 distinct members; the
most active entity contributes 233 of them, accumulated over roughly 29 months (~8/month).
The object grows for the lifetime of the install.
2. **`member_entity_ids` cannot be used to count members.** Any tooling, report or audit that
reads the list length gets a number inflated by up to ~157× in this install. This is
actively misleading: it caused a group of 4 people to be read as 627.
## Workaround
None found that is safe from outside. External groups reject manually supplied
`member_entity_ids`, so the deduplicating API path cannot be triggered for them, and writing
to the group object risks emptying membership until each affected user next logs in. Consumers
must deduplicate client-side.
Contributor guide
Research direction
Start at the OIDC login membership update and compare it with the LDAP auth path and the identity/group API path, focusing on where member_entity_ids is normalized. Reproduce repeated logins for a claim-matched external group and verify that the list contains one entry per entity while an unmatched group remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- go
- Domain
- authentication, backend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 50/100