fix(auth): fall back to GOOGLE_CLOUD_QUOTA_PROJECT when ADC has no quota_project_id (#751)
diff --git a/googleapis_auth/CHANGELOG.md b/googleapis_auth/CHANGELOG.md
index 0b7b716..78e21da 100644
--- a/googleapis_auth/CHANGELOG.md
+++ b/googleapis_auth/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 2.3.3
+
+- `GOOGLE_CLOUD_QUOTA_PROJECT` now takes precedence over a credential's
+  `quota_project_id` when set.
+
 ## 2.3.2
 
 - Fix web compatibility by removing transitive `dart:io` and
diff --git a/googleapis_auth/lib/auth_io.dart b/googleapis_auth/lib/auth_io.dart
index 371f645..73230e3 100644
--- a/googleapis_auth/lib/auth_io.dart
+++ b/googleapis_auth/lib/auth_io.dart
@@ -39,6 +39,12 @@
 ///  3. On Google Compute Engine and App Engine Flex we fetch credentials from
 ///     [GCE metadata service][meta-data].
 ///
+/// **Note on Quota Project:**
+/// When using credentials from a file, the quota project is determined by
+/// checking the `GOOGLE_CLOUD_QUOTA_PROJECT` environment variable first. If
+/// it is not set, it falls back to the `quota_project_id` defined within the
+/// credential file itself.
+///
 /// [meta-data]: https://cloud.google.com/compute/docs/storing-retrieving-metadata
 /// [svc-keys]: https://cloud.google.com/docs/authentication/getting-started
 /// [gcloud-login]: https://cloud.google.com/sdk/gcloud/reference/auth/application-default/login
diff --git a/googleapis_auth/lib/src/adc_utils.dart b/googleapis_auth/lib/src/adc_utils.dart
index c99ec28..b979df9 100644
--- a/googleapis_auth/lib/src/adc_utils.dart
+++ b/googleapis_auth/lib/src/adc_utils.dart
@@ -59,7 +59,9 @@
   Client baseClient, {
   String? fileSource,
 }) async {
-  final quotaProject = credentials['quota_project_id'] as String?;
+  final quotaProject =
+      Platform.environment['GOOGLE_CLOUD_QUOTA_PROJECT'] ??
+      credentials['quota_project_id'] as String?;
   if (credentials case {
     'type': 'authorized_user',
     'client_id': final String clientIdString,
diff --git a/googleapis_auth/pubspec.yaml b/googleapis_auth/pubspec.yaml
index 9d942a1..86380c3 100644
--- a/googleapis_auth/pubspec.yaml
+++ b/googleapis_auth/pubspec.yaml
@@ -1,5 +1,5 @@
 name: googleapis_auth
-version: 2.3.2
+version: 2.3.3
 description: Obtain Access credentials for Google services using OAuth 2.0
 repository: https://github.com/google/googleapis.dart/tree/master/googleapis_auth
 
@@ -23,6 +23,7 @@
   build_web_compilers: ^4.1.1
   test: ^1.25.9
   test_descriptor: ^2.0.2
+  test_process: ^2.0.3
 
 false_secrets:
   - test/test_utils.dart
diff --git a/googleapis_auth/test/adc_test.dart b/googleapis_auth/test/adc_test.dart
index fbaf635..1ff202c 100644
--- a/googleapis_auth/test/adc_test.dart
+++ b/googleapis_auth/test/adc_test.dart
@@ -9,6 +9,7 @@
 
 import 'dart:convert';
 import 'dart:io';
+import 'dart:isolate';
 
 import 'package:googleapis_auth/src/adc_utils.dart'
     show fromApplicationsCredentialsFile;
@@ -16,6 +17,7 @@
 import 'package:http/http.dart';
 import 'package:test/test.dart';
 import 'package:test_descriptor/test_descriptor.dart' as d;
+import 'package:test_process/test_process.dart';
 
 import 'test_utils.dart';
 
@@ -78,6 +80,60 @@
     c.close();
   });
 
+  group('GOOGLE_CLOUD_QUOTA_PROJECT environment variable', () {
+    late String quotaProjectPrint;
+
+    setUpAll(() async {
+      final packageUri = await Isolate.resolvePackageUri(
+        Uri.parse('package:googleapis_auth/'),
+      );
+      quotaProjectPrint = packageUri!
+          .resolve('../test/src/quota_project_print.dart')
+          .toFilePath();
+    });
+
+    Future<void> writeCreds({String? quotaProjectId}) => d
+        .file(
+          'quota_project_env_creds.json',
+          json.encode({
+            'client_id': 'id',
+            'client_secret': 'secret',
+            'refresh_token': 'refresh',
+            'type': 'authorized_user',
+            'quota_project_id': ?quotaProjectId,
+          }),
+        )
+        .create();
+
+    test('is used when credentials have no quota_project_id', () async {
+      await writeCreds();
+
+      final proc = await TestProcess.start(
+        Platform.resolvedExecutable,
+        [quotaProjectPrint, d.path('quota_project_env_creds.json')],
+        environment: {'GOOGLE_CLOUD_QUOTA_PROJECT': 'env-project'},
+        includeParentEnvironment: false,
+      );
+
+      await expectLater(proc.stdout, emits('env-project'));
+      await proc.shouldExit(0);
+    });
+
+    test('takes precedence over quota_project_id in credentials', () async {
+      await writeCreds(quotaProjectId: 'file-project');
+
+      final proc = await TestProcess.start(
+        Platform.resolvedExecutable,
+        [quotaProjectPrint, d.path('quota_project_env_creds.json')],
+        environment: {'GOOGLE_CLOUD_QUOTA_PROJECT': 'env-project'},
+        includeParentEnvironment: false,
+      );
+
+      await expectLater(proc.stdout, emits('env-project'));
+      await proc.shouldExit(0);
+    });
+  });
+
   test('authorized_user credentials with quota_project_id', () async {
     await d
         .file(
diff --git a/googleapis_auth/test/src/quota_project_print.dart b/googleapis_auth/test/src/quota_project_print.dart
new file mode 100644
index 0000000..b1b99a7
--- /dev/null
+++ b/googleapis_auth/test/src/quota_project_print.dart
@@ -0,0 +1,41 @@
+// Copyright 2021 Google LLC
+//
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file or at
+// https://developers.google.com/open-source/licenses/bsd
+
+import 'dart:convert';
+import 'dart:io';
+
+import 'package:googleapis_auth/src/adc_utils.dart'
+    show fromApplicationsCredentialsFile;
+import 'package:googleapis_auth/src/known_uris.dart';
+import 'package:http/http.dart';
+import 'package:http/testing.dart';
+
+Future<void> main(List<String> args) async {
+  final client = await fromApplicationsCredentialsFile(
+    File(args[0]),
+    'test-credentials-file',
+    [],
+    MockClient((request) async {
+      if (request.url == googleOauth2TokenEndpoint) {
+        return Response(
+          jsonEncode({
+            'token_type': 'Bearer',
+            'access_token': 'atoken',
+            'expires_in': 3600,
+          }),
+          200,
+          headers: {'content-type': 'application/json'},
+        );
+      }
+
+      print(request.headers['X-Goog-User-Project'] ?? 'NONE');
+      return Response('ok', 200);
+    }),
+  );
+
+  await client.get(Uri.https('storage.googleapis.com', '/b/bucket/o/obj'));
+  client.close();
+}