Repository: firebase/firebase-tools (ts)

Issue:
Functions shell/emulator unable to load default credentials
<!-- 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:** 7.12.1

<!-- e.g. macOS, Windows, Ubuntu -->
**Platform:** Debian on WSL on Windows 10

### [REQUIRED] Test case

<!-- Provide a minimal, complete, and verifiable example (http://stackoverflow.com/help/mcve) -->

``` ts
import * as functions from 'firebase-functions';
const admin = require('firebase-admin').initializeApp(functions.config().firebase);

exports.action1 = functions.https.onCall(() => {
  return admin.database().ref('action').push({ 'a': 1 })
});
```

### [REQUIRED] Steps to reproduce

<!-- Provide the steps needed to reproduce the issue with the above test case. -->

`npm run shell` with the above code, execute function `action({})`

### [REQUIRED] Expected behavior

<!-- What is the expected behavior? -->

The shell environment should be able to locally simulate the function and access the database, according to the documentation: [Cloud Firestore and Realtime Database triggers already have sufficient credentials, and do not require additional setup.
](https://firebase.google.com/docs/functions/local-emulator#set_up_admin_credentials_optional)

### [REQUIRED] Actual behavior

<!-- Run the command with --debug flag, and include the logs below. -->

I receive the following message:

> @firebase/database: FIREBASE WARNING: {"code":"app/invalid-credential","message":"Credential implementation provided to initializeApp() via the \"credential\" property failed to fetch a valid Google OAuth2 access token with the following error: \"Error fetching access token: Error while making request: getaddrinfo ENOTFOUND metadata.google.internal. Error code: ENOTFOUND\"."}

Some digging through the documentation provided me with various different answers, none of which solved my issue. Please note that I would like to locally develop with the same code as the one that I deploy. Some things I found:
1. I tried with and without `functions.config().firebase` but it's not clear when and if this is required

2. [Setup Admin Credentials](https://firebase.google.com/docs/functions/local-shell#set_up_admin_credentials_optional) using the export command. I changed the default command to:
```
"shell": "export GOOGLE_APPLICATION_CREDENTIALS=\"my-owner-key.json\" && tsc --watch --preserveWatchOutput & firebase functions:shell",
```
According to the (closed) issue #595 for some reason a `protect_env` flag sets this environment variable to an empty string. **I don't see the reasoning for this 'protection'** as the documentation explicitly explains you need to set this variable if you wish to use the shell in an authenticated fashion. PR #1252 added the functionality to disable `protect_env` but I couldn't get the `--disable-features` flag working or even find any documentation on it anywhere.

3. [Manually updating the autopopulated config](https://firebase.google.com/docs/functions/config-env)
```
serviceAccount = require('./serviceAccount.json');

const adminConfig = JSON.parse(process.env.FIREBASE_CONFIG);
adminConfig.credential = admin.credential.cert(serviceAccount);
admin.initializeApp(adminConfig);
```

Somehow injecting the json is supposed to work according to the documentation however this code fails to compile with the following error:
```
error TS2345: Argument of type 'string | undefined' is not assignable to parameter of type 'string'.
  Type 'undefined' is not assignable to type 'string'.

const adminConfig = JSON.parse(process.env.FIREBASE_CONFIG);
```

It also feels hacky and would not be transferrable to the server as-is as it explicitly depends on a `.json` credential file being deployed.

Edit: I found a 4 month old issue on this topic that is still regularly receiving reports of users encountering the same problem: #1708 

Pull request description:
Authorize using Firebase CLI credentials
<!--

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 #1940

TODO:
- [x] Add pre-execution warnings about accessing prod
- [x] Add a description of how auth works in the README

This change will allow the Firebase emulators to pass the credentials from `firebase login` as Cloud application default credentials.

Currently the emulators rely on picking up gcloud auth application-default login credentials on the machine which is at best annoying and at worst means that the emulators are authorized as the wrong user.


Here's the new behavior matrix:

| # | Firebase Login | Application Default | `GAC` Env | Result |
|---|----------------|---------------------|-----------|-------|
| 1 | ❌  | ❌  | ❌  | Emulator has no auth at all |
| 2 | ✅  | ❌  | ❌  | CLI passes `GAC` env to emulator with user credential |
| 3 | ❌  | ✅ | ❌  | ADC may be discovered and used|
| 4 | ❌  | ❌  | ✅  | Emulator will use explicit `GAC` credential |
| 5 | ✅  |✅  | ❌  | CLI passes `GAC` env, ADC is ignored |
| 6 | ✅  | ❌  | ✅  | CLI does not pass cred, Emulator will use explicit `GAC` credential |
| 7 | ❌  | ✅  | ✅  | Emulator will use explicit `GAC` credential, ADC is ignored |
| 8 |✅ | ✅  | ✅  | Emulator will use explicit `GAC` credential, all others |


Note: we can/should change the behavior of Scenario 3 when this bug is fixed:
https://github.com/firebase/firebase-js-sdk/issues/3144

### Scenarios Tested

Functions used for all tests:
```js
const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

async function tryRtdbWrite() {
  const db = admin.database();
  try {
    await db.ref().child('test').set({
      date: new Date().toISOString()
    });
    return true;
  } catch (e) {
    console.warn(e);
    return false;
  }
}

exports.helloWorld = functions.https.onRequest(async (request, response) => {
  response.json({
    creds: process.env.GOOGLE_APPLICATION_CREDENTIALS || null,
    canWriteToDatabase: await tryRtdbWrite()
  });
});
```

**Scenario 1**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld

http: error: Request timed out (30s).
```

Emulator logs
```
>  [2020-07-07T18:30:07.387Z]  @firebase/database: FIREBASE WARNING: {"code":"app/invalid-credential","message":"Credential implementation provided to initializeApp() via the \"credential\" property failed to fetch a valid Google OAuth2 access token with the following error: \"Error fetching access token: Error while making request: getaddrinfo ENOTFOUND metadata.google.int
```

</details>

**Scenario 2**
<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 127
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:32:07 GMT
etag: W/"7f-QwapyiMrFDzSQjxgEK52zAhX9pU"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/.config/firebase/samstern_google_com_application_default_credentials.json"
}
```

</details>

**Scenario 3**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 40
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:40:37 GMT
etag: W/"28-0p3KpSLf1GtAozwuGH12/ekZOZY"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": null
}
```

</details>

**Scenario 4**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 104
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:36:43 GMT
etag: W/"68-YwHpi6KFuPjLagvh8WBTCXYvjUY"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/service-accounts/fir-dumpster/service-account.json"
}
```

</details>

**Scenario 5**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 127
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:38:08 GMT
etag: W/"7f-QwapyiMrFDzSQjxgEK52zAhX9pU"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/.config/firebase/samstern_google_com_application_default_credentials.json"
}
```

</details>

**Scenario 6**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 104
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:34:23 GMT
etag: W/"68-YwHpi6KFuPjLagvh8WBTCXYvjUY"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/service-accounts/fir-dumpster/service-account.json"
}
```

</details>

**Scenario 7**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 104
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:39:34 GMT
etag: W/"68-YwHpi6KFuPjLagvh8WBTCXYvjUY"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/service-accounts/fir-dumpster/service-account.json"
}
```

</details>

**Scenario 8**

<details>
<summary>See logs</summary>

```
$ http http://localhost:5001/fir-dumpster/us-central1/helloWorld
HTTP/1.1 200 OK
connection: keep-alive
content-length: 104
content-type: application/json; charset=utf-8
date: Tue, 07 Jul 2020 18:38:46 GMT
etag: W/"68-YwHpi6KFuPjLagvh8WBTCXYvjUY"
x-powered-by: Express

{
    "canWriteToDatabase": true,
    "creds": "/Users/samstern/service-accounts/fir-dumpster/service-account.json"
}
```

</details>


### Sample Commands

N/A

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5da02cf18..6c5a2a7c2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,3 +2,4 @@
 - Fixes a bug with RTDB Rules hot reloading in the RTDB emulator. (#2371)
 - Changes default functions runtime to Node.js 10 for `firebase init`.
 - Adds import/export support to the Realtime Database emulator.
+- Adds application default credentials inside the Functions emulator (#1940)
diff --git a/README.md b/README.md
index 1ff8c219a..2e49f7b38 100644
--- a/README.md
+++ b/README.md
@@ -149,6 +149,30 @@ Detailed doc is [here](https://firebase.google.com/docs/cli/auth).
 | ------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
 | **hosting:disable** | Stop serving Firebase Hosting traffic for the active project. A "Site Not Found" message will be displayed at your project's Hosting URL after running this command. |
 
+## Authentication
+
+### General
+
+The Firebase CLI can use one of three authentication methods listed in descending priority:
+
+- `GOOGLE_APPLICATION_CREDENTIALS` - if this environment variable points to a service account key file, the Firebase CLI will authenticate as a service account.
+- `firebase login` - you can log in to the CLI directly as yourself. The CLI will store an authorized user credential.
+  - `FIREBASE_TOKEN` - you can explicitly use this environment variable to pass in a long-lived user token from `firebase login:ci`.
+- `gcloud auth application-default login` - if your development machine has application default credentials from the Google Cloud CLI, we will use them if none of the above credentials are present.
+
+### Cloud Functions Emulator
+
+The Cloud Functions emulator is exposed through commands like `emulators:start`,
+`serve` and `functions:shell`. Emulated Cloud Functions run as independent `node` processes
+on your development machine which means they have their own credential discovery mechanism.
+
+By default these `node` processes are not able to discover credentials from `firebase login`.
+In order to provide a better development experience, when you are logged in to the CLI
+through `firebase login` we take the user credentials and construct a temporary credential
+that we pass into the emulator through `GOOGLE_APPLICATION_CREDENTIALS`. We **only** do this
+if you have not already set the `GOOGLE_APPLICATION_CREDENTIALS` environment variable
+yourself.
+
 ## Using with CI Systems
 
 The Firebase CLI requires a browser to complete authentication, but is fully
diff --git a/mockdata/mock.apk b/mockdata/mock.apk
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/commands/logout.js b/src/commands/logout.js
index edd4c364d..a4c67cf66 100644
--- a/src/commands/logout.js
+++ b/src/commands/logout.js
@@ -9,6 +9,7 @@ var utils = require("../utils");
 var api = require("../api");
 var auth = require("../auth");
 var _ = require("lodash");
+var { clearCredentials } = require("../defaultCredentials");
 
 module.exports = new Command("logout")
   .description("log the CLI out of Firebase")
@@ -20,6 +21,7 @@ module.exports = new Command("logout")
     api.setRefreshToken(token);
     var next;
     if (token) {
+      clearCredentials();
       next = auth.logout(token);
     } else {
       next = Promise.resolve();
diff --git a/src/defaultCredentials.ts b/src/defaultCredentials.ts
new file mode 100644
index 000000000..641df897f
--- /dev/null
+++ b/src/defaultCredentials.ts
@@ -0,0 +1,110 @@
+import * as fs from "fs";
+import * as path from "path";
+
+import * as api from "./api";
+import { configstore } from "./configstore";
+import * as logger from "./logger";
+
+// Interface for a valid JSON refresh token credential, so the
+// fields must be snake_case not camelCase.
+interface RefreshTokenCredential {
+  client_id: string; // eslint-disable-line
+  client_secret: string; // eslint-disable-line
+  refresh_token: string; // eslint-disable-line
+  type: string; // eslint-disable-line
+}
+
+/**
+ * Get a path to the application default credentials JSON file.
+ */
+export async function getCredentialPathAsync(): Promise<string | undefined> {
+  const filePath = credFilePath();
+  if (!filePath) {
+    logger.debug("defaultcredentials: could not create path to default credentials file.");
+    return undefined;
+  }
+
+  const cred = getCredential();
+  if (!cred) {
+    logger.debug("defaultcredentials: no credential available.");
+    return undefined;
+  }
+
+  // We could use fs.writeFileSync() here but it's important that the caller understands
+  // that this is a somewhat expensive operation so we make it a Promise.
+  logger.debug(`defaultcredentials: writing to file ${filePath}`);
+  return new Promise((res, rej) => {
+    fs.writeFile(filePath, JSON.stringify(cred, undefined, 2), "utf8", (err) => {
+      if (err) {
+        rej(err);
+      } else {
+        res(filePath);
+      }
+    });
+  });
+}
+
+/**
+ * Delete the credentials, to be used when logging out.
+ */
+export function clearCredentials(): void {
+  const filePath = credFilePath();
+  if (!filePath) {
+    return;
+  }
+
+  if (!fs.existsSync(filePath)) {
+    return;
+  }
+
+  fs.unlinkSync(filePath);
+}
+
+function getCredential(): RefreshTokenCredential | undefined {
+  const tokens = configstore.get("tokens");
+  if (tokens && tokens.refresh_token) {
+    return {
+      client_id: api.clientId,
+      client_secret: api.clientSecret,
+      refresh_token: tokens.refresh_token,
+      type: "authorized_user",
+    };
+  }
+}
+
+function credFilePath(): string | undefined {
+  // This logic is stolen from firebase-admin-node:
+  // https://github.com/firebase/firebase-admin-node/blob/0f6c02e3377c3337e4f206e176b2d96ec6dd6c3c/src/auth/credential.ts#L36
+  let configDir = undefined;
+  if (process.platform.startsWith("win")) {
+    configDir = process.env["APPDATA"];
+  } else {
+    const home = process.env["HOME"];
+    if (home) {
+      configDir = path.join(home, ".config");
+    }
+  }
+
+  if (!configDir) {
+    return undefined;
+  }
+
+  if (!fs.existsSync(configDir)) {
+    fs.mkdirSync(configDir);
+  }
+
+  const fbtConfigDir = path.join(configDir, "firebase");
+  if (!fs.existsSync(fbtConfigDir)) {
+    fs.mkdirSync(fbtConfigDir);
+  }
+
+  return path.join(fbtConfigDir, `${userEmailSlug()}_application_default_credentials.json`);
+}
+
+function userEmailSlug(): string {
+  const user = configstore.get("user");
+  const email = user && user.email ? user.email : "unknown_user";
+  const slug = email.replace("@", "_").replace(".", "_");
+
+  return slug;
+}
diff --git a/src/emulator/controller.ts b/src/emulator/controller.ts
index 28a35b574..11aac532e 100644
--- a/src/emulator/controller.ts
+++ b/src/emulator/controller.ts
@@ -7,6 +7,7 @@ import * as http from "http";
 import * as logger from "../logger";
 import * as track from "../track";
 import * as utils from "../utils";
+import { getCredentialPathAsync } from "../defaultCredentials";
 import { EmulatorRegistry } from "../emulator/registry";
 import {
   Address,
@@ -244,6 +245,7 @@ export async function startAll(options: any, noUi: boolean = false): Promise<voi
   }
 
   if (shouldStart(options, Emulators.FUNCTIONS)) {
+    const functionsLogger = EmulatorLogger.forEmulator(Emulators.FUNCTIONS);
     const functionsAddr = await getAndCheckAddress(Emulators.FUNCTIONS, options);
     const projectId = getProjectId(options, false);
     const functionsDir = path.join(
@@ -256,20 +258,61 @@ export async function startAll(options: any, noUi: boolean = false): Promise<voi
       inspectFunctions = commandUtils.parseInspectionPort(options);
 
       // TODO(samstern): Add a link to documentation
-      EmulatorLogger.forEmulator(Emulators.FUNCTIONS).logLabeled(
+      functionsLogger.logLabeled(
         "WARN",
         "functions",
         `You are running the functions emulator in debug mode (port=${inspectFunctions}). This means that functions will execute in sequence rather than in parallel.`
       );
     }
 
+    // Provide default application credentials when appropriate
+    const credentialEnv: Record<string, string> = {};
+    if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
+      functionsLogger.logLabeled(
+        "WARN",
+        "functions",
+        `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`
+      );
+    } else {
+      const defaultCredPath = await getCredentialPathAsync();
+      if (defaultCredPath) {
+        functionsLogger.log("DEBUG", `Setting GAC to ${defaultCredPath}`);
+        credentialEnv.GOOGLE_APPLICATION_CREDENTIALS = defaultCredPath;
+      } else {
+        // TODO: It would be safer to set GOOGLE_APPLICATION_CREDENTIALS to /dev/null here but we can't because some SDKs don't work
+        //       without credentials even when talking to the emulator: https://github.com/firebase/firebase-js-sdk/issues/3144
+        functionsLogger.logLabeled(
+          "WARN",
+          "functions",
+          "You are not signed in to the Firebase CLI. If you have authorized this machine using gcloud application-default credentials those may be discovered and used to access production services."
+        );
+      }
+    }
+
+    // Warn the developer that the Functions emulator can call out to production.
+    const emulatorsNotRunning = ALL_SERVICE_EMULATORS.filter((e) => {
+      return e !== Emulators.FUNCTIONS && !shouldStart(options, e);
+    });
+    if (emulatorsNotRunning.length > 0) {
+      functionsLogger.logLabeled(
+        "WARN",
+        "functions",
+        `The following emulators are not running, calls to these services from the Functions emulator will affect production: ${clc.bold(
+          emulatorsNotRunning.join(", ")
+        )}`
+      );
+    }
+
     const functionsEmulator = new FunctionsEmulator({
       projectId,
       functionsDir,
       host: functionsAddr.host,
       port: functionsAddr.port,
       debugPort: inspectFunctions,
-      env: options.extensionEnv,
+      env: {
+        ...credentialEnv,
+        ...options.extensionEnv,
+      },
       predefinedTriggers: options.extensionTriggers,
       nodeMajorVersion: options.extensionNodeVersion,
     });
diff --git a/src/emulator/functionsEmulatorRuntime.ts b/src/emulator/functionsEmulatorRuntime.ts
index b11e386f6..4fe6bd1be 100644
--- a/src/emulator/functionsEmulatorRuntime.ts
+++ b/src/emulator/functionsEmulatorRuntime.ts
@@ -966,14 +966,6 @@ async function initializeRuntime(
   }
 
   initializeEnvironmentalVariables(frb);
-  if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
-    new EmulatorLog(
-      "WARN",
-      "runtime-status",
-      `Your GOOGLE_APPLICATION_CREDENTIALS environment variable points to ${process.env.GOOGLE_APPLICATION_CREDENTIALS}. Non-emulated services will access production using these credentials. Be careful!`
-    ).log();
-  }
-
   initializeNetworkFiltering(frb);
   await initializeFunctionsConfigHelper(frb);
   await initializeFirebaseFunctionsStubs(frb);


Test patch:
diff --git a/src/test/defaultCredentials.spec.ts b/src/test/defaultCredentials.spec.ts
new file mode 100644
index 000000000..01d6105eb
--- /dev/null
+++ b/src/test/defaultCredentials.spec.ts
@@ -0,0 +1,86 @@
+import { expect } from "chai";
+import * as sinon from "sinon";
+import * as fs from "fs";
+import * as path from "path";
+import * as os from "os";
+
+import * as api from "../api";
+import { configstore } from "../configstore";
+import * as defaultCredentials from "../defaultCredentials";
+
+describe("defaultCredentials", () => {
+  const sandbox: sinon.SinonSandbox = sinon.createSandbox();
+  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "firebase-tools"));
+
+  const FAKE_TOKEN = {
+    refresh_token: "abc123",
+  };
+
+  const FAKE_USER = {
+    email: "user@domain.com",
+  };
+
+  let configStub: sinon.SinonStub;
+  let oldHome: any;
+
+  beforeEach(() => {
+    oldHome = process.env.HOME;
+    process.env.HOME = tmpDir;
+
+    // Default configStore mock
+    configStub = sandbox.stub(configstore, "get");
+    configStub.callsFake((key: string) => {
+      if (key === "tokens") {
+        return FAKE_TOKEN;
+      }
+
+      if (key === "user") {
+        return FAKE_USER;
+      }
+    });
+  });
+
+  afterEach(() => {
+    process.env.HOME = oldHome;
+    sandbox.restore();
+  });
+
+  it("does not create a credential file when there are no tokens in the config", async () => {
+    configStub.returns(undefined);
+
+    const credPath = await defaultCredentials.getCredentialPathAsync();
+    expect(credPath).to.be.undefined;
+  });
+
+  it("creates a credential file when there are tokens in the config", async () => {
+    const credPath = await defaultCredentials.getCredentialPathAsync();
+    expect(credPath)
+      .to.be.a("string")
+      .that.satisfies((x: string) => {
+        return x.startsWith(tmpDir);
+      });
+
+    const fileContents = JSON.parse(fs.readFileSync(credPath!).toString());
+    expect(fileContents).to.eql({
+      client_id: api.clientId,
+      client_secret: api.clientSecret,
+      refresh_token: FAKE_TOKEN.refresh_token,
+      type: "authorized_user",
+    });
+  });
+
+  it("can clear credentials", async () => {
+    const credPath = await defaultCredentials.getCredentialPathAsync();
+    expect(fs.existsSync(credPath!)).to.be.true;
+
+    defaultCredentials.clearCredentials();
+    expect(fs.existsSync(credPath!)).to.be.false;
+  });
+
+  it("includes the users email in the path", async () => {
+    const credPath = await defaultCredentials.getCredentialPathAsync();
+    const baseName = path.basename(credPath!);
+
+    expect(baseName).to.eq("user_domain_com_application_default_credentials.json");
+  });
+});