Repository: firebase/firebase-tools (ts)

Issue:
Database emulator + App Check on Android (firebase-android-sdk + react-native-firebase) hangs
<!-- 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:** [9.23.1](https://github.com/invertase/react-native-firebase/blob/9908b5ca389a16d9f34ef5c2bb8f8cbfae45decd/tests/package.json#L39)

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform:** macOS 11.6.1

### [REQUIRED] Test case

With apologies, it is not that easy to provide a minimal complete example of App Check on Android causing the database emulator to hang. At least the minimal part. But please bear with me, I'm the maintainer of react-native-firebase so we have a fully reproducible end-to-end test harness that shows this.

I've prepared a branch in our repository that is fairly focused so reproduction is quick:

https://github.com/invertase/react-native-firebase/tree/%40mikehardy/appcheck-android-database-emu-hang

To set things up you want to clone that, then follow the steps in our test harness README - https://github.com/invertase/react-native-firebase/blob/master/tests/README.md

(they boil down to: `yarn`, `yarn tests:android:build`, create an android emulator called "TestingAVD" and start it, `yarn tests:emulator:start` to start the firebase emulator, `yarn tests:packager:jet` to start our test bundler then you are ready)


### [REQUIRED] Steps to reproduce

After doing the above things, run `yarn tests:android:test`, it will start our e2e harness but I've boiled it down to just app-check running then the database tests. It will fail almost immediately - just hanging the first time it tries to hit the database emulator (to do a server increment, for what it's worth, but it fails on all access that I saw?)

### [REQUIRED] Expected behavior

It rolls right through the database tests like it used to
it pass all tests if you comment line 47 of tests/app.js (so it goes against live database).

Note that if you do `yarn tests:ios:pod:install` (assuming you have cocoapods installed?) and then `yarn tests:ios:build` and `yarn tests:ios:test`, iOS + app check + database emulator has no problem.

Seems android specific. Javascript is untested.

### [REQUIRED] Actual behavior

Hangs on first attempt to access database emulator after including app check library.

```

[2021-08-11T20:13:10.206Z] 15:13:10.205 [FirebaseWorkerPool-1-3:react-native-firebase-testing$553253896:] WARN com.firebase.server.realtime.RealtimeClient - ClientId[1]:ErrorId[1]: Error on incoming message. (JsonMappingException) Failed to parseMessage: '{"t":"d","d":{"a":"appcheck","r":1,"b":{"token":"eyJraWQiOiJmQjdFNnciLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjQ0ODYxODU3ODEwMTphbmRyb2lkOmNjNmMxZGM3YTY1Y2M4M2MiLCJhdWQiOlsicHJvamVjdHNcLzQ0ODYxODU3ODEwMSIsInByb2plY3RzXC9yZWFjdC1uYXRpdmUtZmlyZWJhc2UtdGVzdGluZyJdLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvNDQ4NjE4NTc4MTAxIiwiZXhwIjoxNjI4NzE1NTEwLCJpYXQiOjE2Mjg3MTE5MTB9.iiBA6aO1_6auvr1sipvjfsWUIDwqUHsLkKUHugxMNb1YF4Uyf1b5UMxTA3nIGCllqMssKJ8taeh_bWqkmtc91B38_DZBilVnQb-NHl2-qQcwhqjkgN2p7ztX79OP3JVY5XWZtOhSazoeWE2yM67XA2mQGH62O-KZRExJUHych5kPZKCIFPucA0h7soeGIMW6bsF8KFi2cQNk9D7E874wf-0WSwuPgnXv1cyyuFq6S25-s58XWvpN6jCXe7NySEExXhkTtXJo0eLFYgIHun0mjFZvTeaxOrDz1hZWc02N3-2bDvq3sAVT81rmF8QZgBsSOiMBDGmnb8Ay2qFYD4BUIp9_I4l4HjhrYZirG1ZLSjQBWUgme02Byj8ZwUxQM0McYMF-uKvrs54JJ_7MpM6p22SG4TMsPNNCk9EyFazSQ_OJkgPIv7-B9RQBaJ25W_vS2VQeAf-F_2qc5cesc-HVh3GaV8qCchCWxZEZLkLYTNcN4uQCEI22GsGbLJjmhkJf"}}}'
 at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 918] Namespace:react-native-firebase-testing
 {"metadata":{"emulator":{"name":"database"},"message":"15:13:10.205 [FirebaseWorkerPool-1-3:react-native-firebase-testing$553253896:] WARN com.firebase.server.realtime.RealtimeClient - ClientId[1]:ErrorId[1]: Error on incoming message. (JsonMappingException) Failed to parseMessage: '{\"t\":\"d\",\"d\":{\"a\":\"appcheck\",\"r\":1,\"b\":{\"token\":\"eyJraWQiOiJmQjdFNnciLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxOjQ0ODYxODU3ODEwMTphbmRyb2lkOmNjNmMxZGM3YTY1Y2M4M2MiLCJhdWQiOlsicHJvamVjdHNcLzQ0ODYxODU3ODEwMSIsInByb2plY3RzXC9yZWFjdC1uYXRpdmUtZmlyZWJhc2UtdGVzdGluZyJdLCJpc3MiOiJodHRwczpcL1wvZmlyZWJhc2VhcHBjaGVjay5nb29nbGVhcGlzLmNvbVwvNDQ4NjE4NTc4MTAxIiwiZXhwIjoxNjI4NzE1NTEwLCJpYXQiOjE2Mjg3MTE5MTB9.iiBA6aO1_6auvr1sipvjfsWUIDwqUHsLkKUHugxMNb1YF4Uyf1b5UMxTA3nIGCllqMssKJ8taeh_bWqkmtc91B38_DZBilVnQb-NHl2-qQcwhqjkgN2p7ztX79OP3JVY5XWZtOhSazoeWE2yM67XA2mQGH62O-KZRExJUHych5kPZKCIFPucA0h7soeGIMW6bsF8KFi2cQNk9D7E874wf-0WSwuPgnXv1cyyuFq6S25-s58XWvpN6jCXe7NySEExXhkTtXJo0eLFYgIHun0mjFZvTeaxOrDz1hZWc02N3-2bDvq3sAVT81rmF8QZgBsSOiMBDGmnb8Ay2qFYD4BUIp9_I4l4HjhrYZirG1ZLSjQBWUgme02Byj8ZwUxQM0McYMF-uKvrs54JJ_7MpM6p22SG4TMsPNNCk9EyFazSQ_OJkgPIv7-B9RQBaJ25W_vS2VQeAf-F_2qc5cesc-HVh3GaV8qCchCWxZEZLkLYTNcN4uQCEI22GsGbLJjmhkJf\"}}}'\n at [Source: (io.netty.buffer.ByteBufInputStream); line: 1, column: 918] Namespace:react-native-firebase-testing\n"}}
```


I'd be happy to collaborate if there's anything I can do to help put a spotlight on this but (despite the size of our e2e test suite when considered as a "reproducible example") I think this repro is about as clean as I can make it
auth token fetch hang on latest auth emulator + latest @firebase/rules-unit-testing
<!-- 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:** 9.16.1 or 9.16.2 - 9.16.0 works - this is combined with `@firebase/rules-unit-testing` from firebase-js-sdk repo, version 1.3.14 has issues, 1.3.12 works

<!-- e.g. macOS, Windows, Ubuntu -->

**Platform:** macOS

### [REQUIRED] Test case

I'm not sure how best to present the issue but I do have a reproducible test case, similar to #3663 - so if someone does the work to get setup there, they may run this really quickly as well

### [REQUIRED] Steps to reproduce

Get the react-native-firebase e2e test suite setup - https://github.com/invertase/react-native-firebase/blob/master/tests/README.md - and note that getting the auth token hangs at commit https://github.com/invertase/react-native-firebase/pull/5590/commits/2001b6340e27cd776271295b727c7cb9f7bbccfd

After reverting the last two patch releases (which appear to have work around JWTs?) things work: https://github.com/invertase/react-native-firebase/pull/5590/commits/e192f120cb48e3a121bfea1d5e9bc92c7fc4bc2f

This was also noted by colleagues in work on the FlutterFire repo, which makes all of the firebase APIs available to the Flutter community.

### [REQUIRED] Expected behavior

The behavior from my rollback - where auth token fetching works - is expected

### [REQUIRED] Actual behavior

No debug logs were emitted when this happened unfortunately, unlike with #3663 so I'm unsure what's going on. It hangs when trying to fetch the auth token though

This is reproducible on ios or android (#3663 is reprodicible only on android, so perhaps stick to that platform for both issues, for efficiency?)

The hang is in this method: https://github.com/invertase/react-native-firebase/blob/1448bb2022ba640a0f835f48bc831e57d0217d56/packages/auth/e2e/user.e2e.js#L53-L86

As with #3663 I'm happy to collaborate to demonstrate the issue to help you folks fix it.



Auth Emulator does not work in v9.16.1, v9.16.2 or v9.16.3 because auth_time is null when token is generated
<!-- DO NOT DELETE
validate_template=true
template_path=.github/ISSUE_TEMPLATE/bug_report.md
-->

The auth emulator seems to always generate a token where auth_time is null. This causes the auth emulator to throw "auth/internal-error" when calling getIdTokenResult.

This is most likely the bug causing https://github.com/firebase/firebase-tools/issues/3671 and https://github.com/firebase/firebase-tools/issues/3663

### [REQUIRED] Environment info

firebase-tools: 9.16.1, 9.16.2, 9.16.3

Platform: Windows

### [REQUIRED] Test case

Try calling getIdTokenResult in a client app that is connected to the latest version of firebase-tools's auth emulator. Notice the error that is thrown.

### [REQUIRED] Steps to reproduce

1. Run `npm run serve` using firebase-tools 9.16.3
2. Run a client application that connects to the firebase auth emulator
3. Signup a new user
4. Call getIdTokenResult on that new user

### [REQUIRED] Expected behavior

You are able to decode the user's token and retrieve their claims.

### [REQUIRED] Actual behavior

"auth/internal-error" is thrown

---

If you breakpoint the line of code where "auth/internal-error" is thrown, you'll see that the error is thrown because auth_time is null in the decoded token.

@yuchenshi This bug may possibly have something to do with the PR I submitted a few weeks ago. Perhaps lastLoginAt is undefined?

https://github.com/firebase/firebase-tools/pull/3611

If so, perhaps try changing these lines of code

https://github.com/firebase/firebase-tools/blob/8e8043b434d3489125805e866a73d6f4bfeaab65/src/emulator/auth/operations.ts#L1729-L1734

to

```
    auth_time:
      user.lastLoginAt != null && user.lastLoginAt != undefined
        ? toUnixTimestamp(new Date(user.lastLoginAt))
        : user.lastRefreshAt != null && user.lastRefreshAt != undefined
        ? toUnixTimestamp(new Date(user.lastRefreshAt))
        : toUnixTimestamp(new Date()),
```

Pull request:
Fix auth_time in 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 #3671.
Fixes #3663.
Fixes #3673.

Date parsing is hard. This was broken in #3611. I've added comments and assertions for the details.

### Scenarios Tested

Tests added in #3611 was also wrong and I've fixed that as well. Again, date parsing is hard.

### Sample Commands

N/A

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e69de29bb..6769de89c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -0,0 +1,1 @@
+- Fixes Auth Emulator tokens having invalid auth_time field. (#3674)
diff --git a/src/emulator/auth/operations.ts b/src/emulator/auth/operations.ts
index f5f98964b..0082d9e5f 100644
--- a/src/emulator/auth/operations.ts
+++ b/src/emulator/auth/operations.ts
@@ -1709,9 +1709,9 @@ function generateJwt(
     }
   }
 
-  const customAttributes = JSON.parse(user.customAttributes || "{}");
+  const customAttributes = JSON.parse(user.customAttributes || "{}") as Record<string, unknown>;
   /* eslint-disable camelcase */
-  const customPayloadFields: FirebaseJwtPayload = {
+  const customPayloadFields: Partial<FirebaseJwtPayload> = {
     // Non-reserved fields (set before custom attributes):
     name: user.displayName,
     picture: user.photoUrl,
@@ -1726,12 +1726,7 @@ function generateJwt(
     // This field is only set for anonymous sign-in but not for any other
     // provider (such as email or Google) in production. Let's match that.
     provider_id: signInProvider === "anonymous" ? signInProvider : undefined,
-    auth_time:
-      user.lastLoginAt != null
-        ? toUnixTimestamp(new Date(user.lastLoginAt))
-        : user.lastRefreshAt != null
-        ? toUnixTimestamp(new Date(user.lastRefreshAt))
-        : toUnixTimestamp(new Date()),
+    auth_time: toUnixTimestamp(getAuthTime(user)),
     user_id: user.localId,
     firebase: {
       identities,
@@ -1755,6 +1750,27 @@ function generateJwt(
   return jwtStr;
 }
 
+function getAuthTime(user: UserInfo): Date {
+  if (user.lastLoginAt != null) {
+    const millisSinceEpoch = parseInt(user.lastLoginAt, 10);
+    const authTime = new Date(millisSinceEpoch);
+    if (isNaN(authTime.getTime())) {
+      throw new Error(`Internal assertion error: invalid user.lastLoginAt = ${user.lastLoginAt}`);
+    }
+    return authTime;
+  } else if (user.lastRefreshAt != null) {
+    const authTime = new Date(user.lastRefreshAt); // Parse from ISO date string.
+    if (isNaN(authTime.getTime())) {
+      throw new Error(
+        `Internal assertion error: invalid user.lastRefreshAt = ${user.lastRefreshAt}`
+      );
+    }
+    return authTime;
+  } else {
+    throw new Error(`Internal assertion error: Missing user.lastLoginAt and user.lastRefreshAt`);
+  }
+}
+
 function verifyPhoneNumber(state: ProjectState, sessionInfo: string, code: string): string {
   const verification = state.getVerificationCodeBySessionInfo(sessionInfo);
   assert(verification, "INVALID_SESSION_INFO");
@@ -2127,12 +2143,17 @@ export interface FirebaseJwtPayload {
   exp: number; // expiresAt (in seconds since epoch)
   iss: string; // issuer
   aud: string; // audience (=projectId)
-  auth_time: number; // lastLoginAt (in seconds since epoch)
   // ...and other fields that we don't care for now.
 
   // Firebase-specific fields:
+
+  // the last login time (in seconds since epoch), may be different from iat
+  auth_time: number;
   email?: string;
+  email_verified?: boolean;
   phone_number?: string;
+  name?: string;
+  picture?: string;
   user_id: string;
   provider_id?: string;
   firebase: {


Test patch:
diff --git a/src/test/emulators/auth/misc.spec.ts b/src/test/emulators/auth/misc.spec.ts
index c35078f65..56a0033f7 100644
--- a/src/test/emulators/auth/misc.spec.ts
+++ b/src/test/emulators/auth/misc.spec.ts
@@ -45,6 +45,7 @@ describeAuthEmulator("token refresh", ({ authApi, getClock }) => {
   });
 
   it("should populate auth_time to match lastLoginAt (in seconds since epoch)", async () => {
+    getClock().tick(444); // Make timestamps a bit more interesting (non-zero).
     const emailUser = { email: "alice@example.com", password: "notasecret" };
     const { refreshToken } = await registerUser(authApi(), emailUser);
 
@@ -60,7 +61,7 @@ describeAuthEmulator("token refresh", ({ authApi, getClock }) => {
     const idToken = res.body.id_token;
     const user = await getAccountInfoByIdToken(authApi(), idToken);
     expect(user.lastLoginAt).not.to.be.undefined;
-    const lastLoginAtSeconds = toUnixTimestamp(new Date(user.lastLoginAt!));
+    const lastLoginAtSeconds = Math.floor(parseInt(user.lastLoginAt!, 10) / 1000);
     const decoded = decodeJwt(idToken, { complete: true }) as {
       header: JwtHeader;
       payload: FirebaseJwtPayload;