Skip to content

Commit

Permalink
Updated to fix issues-4170 (#7980)
Browse files Browse the repository at this point in the history
* Updated to fix issues-4170

* Fixed as per the review

* Fixed linting issues

* Update src/emulator/auth/operations.ts

Co-authored-by: Yuchen Shi <yuchenshi@google.com>

* Update src/emulator/auth/operations.ts

Co-authored-by: Yuchen Shi <yuchenshi@google.com>

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Yuchen Shi <yuchenshi@google.com>
  • Loading branch information
mohankumarelec and yuchenshi authored Dec 4, 2024
1 parent e895fd2 commit 6b107c9
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
- Deprecated `emulators.apphosting.startCommandOverride`. Please use `emulators.apphosting.startCommand` instead.
- Updated `superstatic` to `9.1.0` in package.json.
- Updated the Firebase Data Connect local toolkit to v1.7.4, which includes a fix for an issue that caused duplicate installations of the Firebase JS SDK. (#8028)
- Add support for `linkProviderUserInfo` in the Firebase Emulator to allow linking providers to user accounts. (#4170)
14 changes: 9 additions & 5 deletions src/emulator/auth/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1030,11 +1030,7 @@ export function setAccountInfoImpl(
{ privileged = false, emulatorUrl = undefined }: { privileged?: boolean; emulatorUrl?: URL } = {},
): Schemas["GoogleCloudIdentitytoolkitV1SetAccountInfoResponse"] {
// TODO: Implement these.
const unimplementedFields: (keyof typeof reqBody)[] = [
"provider",
"upgradeToFederatedLogin",
"linkProviderUserInfo",
];
const unimplementedFields: (keyof typeof reqBody)[] = ["provider", "upgradeToFederatedLogin"];
for (const field of unimplementedFields) {
if (field in reqBody) {
throw new NotImplementedError(`${field} is not implemented yet.`);
Expand Down Expand Up @@ -1232,8 +1228,16 @@ export function setAccountInfoImpl(
}
}

if (reqBody.linkProviderUserInfo) {
assert(reqBody.linkProviderUserInfo.providerId, "MISSING_PROVIDER_ID");
assert(reqBody.linkProviderUserInfo.rawId, "MISSING_RAW_ID");
}

user = state.updateUserByLocalId(user.localId, updates, {
deleteProviders: reqBody.deleteProvider,
upsertProviders: reqBody.linkProviderUserInfo
? [reqBody.linkProviderUserInfo as ProviderUserInfo]
: undefined,
});

// Only initiate the recover email OOB flow for non-anonymous users
Expand Down
96 changes: 96 additions & 0 deletions src/emulator/auth/setAccountInfo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1254,4 +1254,100 @@ describeAuthEmulator("accounts:update", ({ authApi, getClock }) => {
expect(oobs[0].requestType).to.equal("RECOVER_EMAIL");
expect(oobs[0].oobLink).to.include(tenant.tenantId);
});

it("should link provider account with existing user account", async () => {
const { idToken } = await registerUser(authApi(), {
email: "test@example.com",
password: "password",
});

const providerId = "google.com";
const rawId = "google_user_id";
const providerUserInfo = {
providerId,
rawId,
email: "linked@example.com",
displayName: "Linked User",
photoUrl: "https://example.com/photo.jpg",
};

await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:update")
.query({ key: "fake-api-key" })
.send({ idToken, linkProviderUserInfo: providerUserInfo })
.then((res) => {
expectStatusCode(200, res);
const providers = res.body.providerUserInfo;
expect(providers).to.have.length(2); // Original email/password + linked provider

const linkedProvider = providers.find((p: ProviderUserInfo) => p.providerId === providerId);
expect(linkedProvider).to.deep.equal(providerUserInfo);
});

const accountInfo = await getAccountInfoByIdToken(authApi(), idToken);
expect(accountInfo.providerUserInfo).to.have.length(2);
const linkedProviderInfo = accountInfo.providerUserInfo?.find(
(p: ProviderUserInfo) => p.providerId === providerId,
);
expect(linkedProviderInfo).to.deep.equal(providerUserInfo);
});

it("should error if linkProviderUserInfo is missing required fields", async () => {
const { idToken } = await registerUser(authApi(), {
email: "test@example.com",
password: "password",
});

const incompleteProviderUserInfo1 = {
providerId: "google.com",
email: "linked@example.com",
};

await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:update")
.query({ key: "fake-api-key" })
.send({ idToken, linkProviderUserInfo: incompleteProviderUserInfo1 })
.then((res) => {
expectStatusCode(400, res);
expect(res.body.error.message).to.contain("MISSING_RAW_ID");
});

const incompleteProviderUserInfo2 = {
rawId: "google_user_id",
email: "linked@example.com",
};

await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:update")
.query({ key: "fake-api-key" })
.send({ idToken, linkProviderUserInfo: incompleteProviderUserInfo2 })
.then((res) => {
expectStatusCode(400, res);
expect(res.body.error.message).to.contain("MISSING_PROVIDER_ID");
});
});

it("should error if user is disabled when linking a provider", async () => {
const { localId, idToken } = await registerUser(authApi(), {
email: "test@example.com",
password: "password",
});

await updateAccountByLocalId(authApi(), localId, { disableUser: true });

const providerUserInfo = {
providerId: "google.com",
rawId: "google_user_id",
email: "linked@example.com",
};

await authApi()
.post("/identitytoolkit.googleapis.com/v1/accounts:update")
.query({ key: "fake-api-key" })
.send({ idToken, linkProviderUserInfo: providerUserInfo })
.then((res) => {
expectStatusCode(400, res);
expect(res.body.error.message).to.equal("USER_DISABLED");
});
});
});

0 comments on commit 6b107c9

Please sign in to comment.