SharePointVault ~/gotchas/graph ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ group-member-ref-needs-object-id.md
sharepoint-vault›gotchas›graph›group-member-ref-needs-object-id.mdhistoryedit on GitHub
● GOTCHAgraph· 2 min read

Group membership via Graph needs the object ID, not the UPN

applies to: Microsoft Graph v1.0 (Microsoft Entra ID groups)reviewed: 2026-10-24#graph#groups#membership#entra-id#spfx#troubleshooting

POST /groups/{id}/members/$ref with "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/{id}" and DELETE /groups/{id}/members/{id}/$ref accept only the object ID (GUID). Pass a UPN (what most people pickers return) and Graph answers 400 Invalid object identifier 'user@contoso.com' — even though GroupMember.ReadWrite.All is granted. Resolve the UPN first with GET /users/{upn}?$select=id.

Přidání a odebrání člena skupiny přes Graph (members/$ref) bere jen object ID (GUID). UPN z výběru osoby vrátí 400 Invalid object identifier, i když je oprávnění GroupMember.ReadWrite.All udělené. UPN nejdřív přelož přes GET /users/{upn}?$select=id.

Symptom

An SPFx solution lets an admin pick a person and add them to a security group. The permission request (GroupMember.ReadWrite.All) is approved in the SharePoint admin center, yet the write fails:

POST https://graph.microsoft.com/v1.0/groups/{groupId}/members/$ref
{ "@odata.id": "https://graph.microsoft.com/v1.0/directoryObjects/user@contoso.com" }

400 Bad Request
Invalid object identifier 'user@contoso.com'.

Reads with the same identifier work (GET /users/user@contoso.com/transitiveMemberOf), so the bug only appears when you finally write.

Cause

/users/{id | userPrincipalName} accepts both forms, but the directoryObjects/{id} reference used by $ref resolves directory objects by object ID only — a UPN is not an object identifier there (measured on POST …/members/$ref). The removal path DELETE /groups/{id}/members/{id}/$ref is documented with the directory object ID as well; we resolve the UPN there too rather than rely on undocumented behaviour.

A missing permission looks different: the documentation lists 403 Forbidden for adding a member you don't have permissions to add, and 400 for input problems (already a member, unsupported member type, a just-created group that hasn't replicated). A 400 right after granting consent points at the input, not at the grant.

Fix

Translate anything that is not a GUID before writing:

const GUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

async function objectId(client: MSGraphClientV3, user: string): Promise<string> {
  if (GUID.test(user)) return user;
  const u = await client.api(`/users/${encodeURIComponent(user)}`).select('id').get();
  if (!u?.id) throw new Error(`User "${user}" not found in Entra ID.`);
  return u.id;
}

// add
await client.api(`/groups/${groupId}/members/$ref`)
  .post({ '@odata.id': `https://graph.microsoft.com/v1.0/directoryObjects/${await objectId(client, user)}` });

// remove
await client.api(`/groups/${groupId}/members/${await objectId(client, user)}/$ref`).delete();

Treat 400 "One or more added object references already exist" on add as success (the user is already a member).

See also

Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-10-24last updated: 2026-10-24historyedit on GitHubview source