Repository: firebase/firebase-tools (ts)

Issue:
Error emulating the method generateEmailVerificationLink (auth emulator) - The provided ID token is not a valid Firebase ID token.
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

<!--
Thank you for contributing to the Firebase community!

Think you found a bug?
=======================
Yeah, we're definitely not perfect! Please use this template and include a minimal repro when opening the issue. If you know how to solve the issue, please create a Pull Request, and we'd be happy to review it!

Have a feature request?
========================
Great, we love hearing how we can improve our products! However, do not use this template to submit a feature request. Please submit your feature requests to: https://firebase.google.com/support/contact/bugs-features/

Have a usage question?
=======================
We get lots of those and we love helping you, but GitHub is not the best place for them and they will be closed. Please take a look at the guide first: https://firebase.google.com/docs/cli/

If the official documentation doesn't help, try asking through our official support channel: https://firebase.google.com/support/

Additional locations to check for solutions or assistance from the community:
- Stack Overflow: https://stackoverflow.com/
- Firebase Slack Community: https://firebase.community/

*Please avoid duplicate posting across multiple channels!*
-->

### [REQUIRED] Environment info

<!-- What version of the Firebase CLI (`firebase-tools`) are you using? Note that your issue may already be fixed in the latest versions. The latest version can be found at https://github.com/firebase/firebase-tools/releases -->

<!-- Output of `firebase --version` -->

**firebase-tools: 8.16.2**

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform: macOS Big Sur 11.0.1, Node.js 12.19.0**

### [REQUIRED] Test case

<!-- Provide a minimal, complete, and verifiable example (http://stackoverflow.com/help/mcve) -->
Error emulating the method `generateEmailVerificationLink`:
```FirebaseAuthError: There is no user record corresponding to the provided identifier.```

### [REQUIRED] Steps to reproduce

<!-- Provide the steps needed to reproduce the issue with the above test case. -->
- Create User: ```admin.auth().createUser({email: 'test@test.com'});```
- Generate email verificaation link: ```admin.auth().generateEmailVerificationLink("test@test.com");```

### [REQUIRED] Expected behavior

<!-- What is the expected behavior? -->
Resolve string (url)

### [REQUIRED] Actual behavior

<!-- Run the command with --debug flag, and include the logs below. -->
Reject with error: ```FirebaseAuthError: The provided ID token is not a valid Firebase ID token.```

Pull request description:
Fix #2880 - generating email link from Admin SDK fails with Auth Emulator
<!--

Thank you for contributing to the Firebase community! Please fill out the form below.

Run the linter and test suite
==============================
Run `npm test` to make sure your changes compile properly and the tests all pass on your local machine. We've hooked up this repo with continuous integration to double check those things for you.

-->

### Description

Fixes #2880.

### Scenarios Tested

See tests added.

### Sample Commands

N/A

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5979c5249..fb576111e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,2 +1,3 @@
 - Shows missing documents in Emulator UI Firestore viewer.
 - Better supports paths with special characters in Emulator UI Firestore viewer.
+- Fixes generating email link from Admin SDK failing with Auth Emulator (#2933).
diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts
index f6df90f01..37e3b8a05 100644
--- a/src/emulator/auth/operations.ts
+++ b/src/emulator/auth/operations.ts
@@ -495,7 +495,6 @@ function sendOobCode(
     );
   }
 
-  let user: UserInfo | undefined;
   let email: string;
   let mode: string;
 
@@ -509,15 +508,24 @@ function sendOobCode(
       mode = "resetPassword";
       assert(reqBody.email, "MISSING_EMAIL");
       email = canonicalizeEmailAddress(reqBody.email);
-      user = state.getUserByEmail(email);
-      assert(user, "EMAIL_NOT_FOUND");
+      assert(state.getUserByEmail(email), "EMAIL_NOT_FOUND");
       break;
     case "VERIFY_EMAIL":
       mode = "verifyEmail";
-      // Get the user from idToken, reqBody.email is ignored.
-      user = parseIdToken(state, reqBody.idToken || "").user;
-      assert(user.email, "MISSING_EMAIL");
-      email = user.email;
+
+      // Matching production behavior, reqBody.returnOobLink is used as a signal
+      // for Admin usage (instead of whether request is OAuth 2 authenticated.)
+      if (reqBody.returnOobLink && !reqBody.idToken) {
+        assert(reqBody.email, "MISSING_EMAIL");
+        email = canonicalizeEmailAddress(reqBody.email);
+        const maybeUser = state.getUserByEmail(email);
+        assert(maybeUser, "USER_NOT_FOUND");
+      } else {
+        // Get the user from idToken, reqBody.email is ignored.
+        const user = parseIdToken(state, reqBody.idToken || "").user;
+        assert(user.email, "MISSING_EMAIL");
+        email = user.email;
+      }
       break;
 
     default:


Test patch:
diff --git a/src/test/emulators/auth/oob.spec.ts b/src/test/emulators/auth/oob.spec.ts
index eb6f7bbc0..07f5230e2 100644
--- a/src/test/emulators/auth/oob.spec.ts
+++ b/src/test/emulators/auth/oob.spec.ts
@@ -63,16 +63,75 @@ describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => {
         expect(res.body.oobCode).to.be.a("string");
         expect(res.body.oobLink).to.be.a("string");
       });
+
+    await authApi()
+      .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
+      .set("Authorization", "Bearer owner")
+      .send({ email: user.email, requestType: "VERIFY_EMAIL", returnOobLink: true })
+      .then((res) => {
+        expectStatusCode(200, res);
+        expect(res.body.email).to.equal(user.email);
+        expect(res.body.oobCode).to.be.a("string");
+        expect(res.body.oobLink).to.be.a("string");
+      });
+  });
+
+  it("should return OOB code by idToken for OAuth 2 requests as well", async () => {
+    const user = { email: "alice@example.com", password: "notasecret" };
+    const { idToken } = await registerUser(authApi(), user);
+    await authApi()
+      .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
+      .set("Authorization", "Bearer owner")
+      .send({ idToken, requestType: "VERIFY_EMAIL", returnOobLink: true })
+      .then((res) => {
+        expectStatusCode(200, res);
+        expect(res.body.email).to.equal(user.email);
+        expect(res.body.oobCode).to.be.a("string");
+        expect(res.body.oobLink).to.be.a("string");
+      });
   });
 
-  it("should error when trying to verify email without idToken", async () => {
+  it("should error when trying to verify email without idToken or email", async () => {
     const user = { email: "alice@example.com", password: "notasecret" };
     await registerUser(authApi(), user);
 
     await authApi()
       .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
       .query({ key: "fake-api-key" })
-      // Just email, no idToken. (It works for password reset but not verify.)
+      .send({ requestType: "VERIFY_EMAIL" })
+      .then((res) => {
+        expectStatusCode(400, res);
+        expect(res.body.error)
+          .to.have.property("message")
+          .equal("INVALID_ID_TOKEN");
+      });
+
+    await authApi()
+      .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
+      .set("Authorization", "Bearer owner")
+      // This causes a different error message to be returned, see below.
+      .send({ returnOobLink: true, requestType: "VERIFY_EMAIL" })
+      .then((res) => {
+        expectStatusCode(400, res);
+        expect(res.body.error)
+          .to.have.property("message")
+          .equal("MISSING_EMAIL");
+      });
+
+    const oobs = await inspectOobs(authApi());
+    expect(oobs).to.have.length(0);
+  });
+
+  it("should error when trying to verify email without idToken if not returnOobLink", async () => {
+    const user = await registerUser(authApi(), {
+      email: "alice@example.com",
+      password: "notasecret",
+    });
+
+    await authApi()
+      .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
+      .query({ key: "fake-api-key" })
+      // email here is ignored because returnOobLink is not set.
       .send({ email: user.email, requestType: "VERIFY_EMAIL" })
       .then((res) => {
         expectStatusCode(400, res);
@@ -85,6 +144,22 @@ describeAuthEmulator("accounts:sendOobCode", ({ authApi, getClock }) => {
     expect(oobs).to.have.length(0);
   });
 
+  it("should error when trying to verify email not associated with any user", async () => {
+    await authApi()
+      .post("/identitytoolkit.googleapis.com/v1/accounts:sendOobCode")
+      .set("Authorization", "Bearer owner")
+      .send({ email: "nosuchuser@example.com", returnOobLink: true, requestType: "VERIFY_EMAIL" })
+      .then((res) => {
+        expectStatusCode(400, res);
+        expect(res.body.error)
+          .to.have.property("message")
+          .equal("USER_NOT_FOUND");
+      });
+
+    const oobs = await inspectOobs(authApi());
+    expect(oobs).to.have.length(0);
+  });
+
   it("should error when verifying email for accounts without email", async () => {
     const { idToken } = await registerAnonUser(authApi());