Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/external-account-id.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 9 additions & 0 deletions packages/backend/src/api/resources/ExternalAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -88,6 +96,7 @@ export class ExternalAccount {
data.public_metadata,
data.label,
data.verification && Verification.fromJSON(data.verification),
data.external_account_id,
);
}
}
4 changes: 4 additions & 0 deletions packages/backend/src/api/resources/JSON.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
});