diff --git a/.changeset/external-account-id.md b/.changeset/external-account-id.md new file mode 100644 index 00000000000..3a772f68318 --- /dev/null +++ b/.changeset/external-account-id.md @@ -0,0 +1,5 @@ +--- +'@clerk/backend': patch +--- + +Add an optional `externalAccountId` to the backend `ExternalAccount` resource. For Google and Facebook accounts the resource `id` is the `idn_`-prefixed identification id, which `users.deleteUserExternalAccount()` rejects; `externalAccountId` now exposes the `eac_`-prefixed id those calls expect. For all other providers `id` is already the `eac_` id and `externalAccountId` is `undefined`, so use `externalAccountId ?? id` to get an id you can delete with. diff --git a/packages/backend/src/api/resources/ExternalAccount.ts b/packages/backend/src/api/resources/ExternalAccount.ts index a623a7f8a51..561efee0212 100644 --- a/packages/backend/src/api/resources/ExternalAccount.ts +++ b/packages/backend/src/api/resources/ExternalAccount.ts @@ -69,6 +69,14 @@ export class ExternalAccount { * An object holding information on the verification of this external account. */ readonly verification: Verification | null, + /** + * The `eac_`-prefixed id of the external account resource, which is the id + * `users.deleteUserExternalAccount()` expects. Only returned for Google and + * Facebook accounts, where `id` holds the `idn_` identification id instead; + * for other providers it is `undefined` and `id` is already the `eac_` value. + * Use `externalAccountId ?? id` to get an id you can delete with. + */ + readonly externalAccountId?: string, ) {} static fromJSON(data: ExternalAccountJSON): ExternalAccount { @@ -88,6 +96,7 @@ export class ExternalAccount { data.public_metadata, data.label, data.verification && Verification.fromJSON(data.verification), + data.external_account_id, ); } } diff --git a/packages/backend/src/api/resources/JSON.ts b/packages/backend/src/api/resources/JSON.ts index 3193401523d..65fc8b3a9ac 100644 --- a/packages/backend/src/api/resources/JSON.ts +++ b/packages/backend/src/api/resources/JSON.ts @@ -231,6 +231,10 @@ export interface EnterpriseAccountJSON extends ClerkResourceJSON { export interface ExternalAccountJSON extends ClerkResourceJSON { object: typeof ObjectType.ExternalAccount; + /** + * The `eac_`-prefixed external account id. Only present for Google and Facebook accounts. + */ + external_account_id?: string; provider: string; identification_id: string; provider_user_id: string; diff --git a/packages/backend/src/api/resources/__tests__/ExternalAccount.test.ts b/packages/backend/src/api/resources/__tests__/ExternalAccount.test.ts new file mode 100644 index 00000000000..2faa1047033 --- /dev/null +++ b/packages/backend/src/api/resources/__tests__/ExternalAccount.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; + +import { ExternalAccount } from '../ExternalAccount'; +import type { ExternalAccountJSON } from '../JSON'; + +describe('ExternalAccount', () => { + describe('fromJSON', () => { + const base = { + object: 'external_account', + provider: 'oauth_google', + provider_user_id: '1029384756', + approved_scopes: 'email profile', + email_address: 'jane@example.com', + first_name: 'Jane', + last_name: 'Doe', + image_url: 'https://img.clerk.com/jane.png', + username: 'jane', + phone_number: null, + public_metadata: {}, + label: null, + verification: null, + }; + + it('maps external_account_id to externalAccountId for Google/Facebook accounts', () => { + // Google/Facebook responses set `id` to the `idn_` identification id and add `external_account_id`. + const data = { + ...base, + id: 'idn_2ABXLLckIF5kLikvzAVRxuuN31M', + external_account_id: 'eac_2ABXLObDmeHsnLsLgOd5panvOPJ', + identification_id: 'idn_2ABXLLckIF5kLikvzAVRxuuN31M', + } as ExternalAccountJSON; + + const externalAccount = ExternalAccount.fromJSON(data); + + expect(externalAccount.externalAccountId).toBe('eac_2ABXLObDmeHsnLsLgOd5panvOPJ'); + // `id` and `identificationId` keep the `idn_` value for these providers. + expect(externalAccount.id).toBe('idn_2ABXLLckIF5kLikvzAVRxuuN31M'); + expect(externalAccount.identificationId).toBe('idn_2ABXLLckIF5kLikvzAVRxuuN31M'); + }); + + it('leaves externalAccountId undefined for other providers, where id is already the eac_ id', () => { + // Other providers omit `external_account_id`; `id` already holds the `eac_` value. + const data = { + ...base, + provider: 'oauth_github', + id: 'eac_2ABXLObDmeHsnLsLgOd5panvOPJ', + identification_id: 'idn_2ABXLLckIF5kLikvzAVRxuuN31M', + } as ExternalAccountJSON; + + const externalAccount = ExternalAccount.fromJSON(data); + + expect(externalAccount.externalAccountId).toBeUndefined(); + expect(externalAccount.id).toBe('eac_2ABXLObDmeHsnLsLgOd5panvOPJ'); + }); + }); +});