File size: 26,479 Bytes
7b715bc
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
/*
Copyright 2014 Google Inc

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

using Google.Apis.Auth.OAuth2.Requests;
using Google.Apis.Auth.OAuth2.Responses;
using Google.Apis.Http;
using Google.Apis.Util;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;

namespace Google.Apis.Auth.OAuth2
{
    /// <summary>
    /// Google OAuth 2.0 credential for accessing protected resources using an access token. The Google OAuth 2.0 
    /// Authorization Server supports server-to-server interactions such as those between a web application and Google
    /// Cloud Storage. The requesting application has to prove its own identity to gain access to an API, and an 
    /// end-user doesn't have to be involved. 
    /// <para>
    /// More details about Compute Engine authentication is available at:
    /// https://cloud.google.com/compute/docs/authentication.
    /// </para>
    /// </summary>
    public class ComputeCredential : ServiceCredential, IOidcTokenProvider, IGoogleCredential, IBlobSigner
    {
        /// <summary>The metadata server url. This can be overridden (for the purposes of Compute environment detection and
        /// auth token retrieval) using the GCE_METADATA_HOST environment variable.</summary>
        public const string MetadataServerUrl = GoogleAuthConsts.DefaultMetadataServerUrl;

        /// <summary>Caches result from first call to <c>IsRunningOnComputeEngine</c> </summary>
        private readonly static Lazy<Task<bool>> isRunningOnComputeEngineCache = new Lazy<Task<bool>>(IsRunningOnComputeEngineUncachedAsync);

        private readonly static Lazy<Task<string>> computeEngineUniverseDomainCache = new Lazy<Task<string>>(GetComputeEngineUniverseDomainUncachedAsync);

        /// <summary>
        /// Originally 1000ms was used without a retry. This proved inadequate; even 2000ms without
        /// a retry occasionally failed. We have observed that after a timeout, the next attempt
        /// succeeds very quickly (sub-50ms) which suggests that this should be fine.
        /// </summary>
        private const int MetadataServerPingTimeoutInMilliseconds = 500;

        private const int MetadataServerPingAttempts = 3;

        /// <summary>The Metadata flavor header name.</summary>
        internal const string MetadataFlavor = "Metadata-Flavor";

        /// <summary>The Metadata header response indicating Google.</summary>
        internal const string GoogleMetadataHeader = "Google";

        /// <summary>
        /// Caches the task that fetches the default service account email from the metadata server.
        /// The default service account email can be cached because changing the service
        /// account associated to a Compute instance requires a machine shutdown.
        /// </summary>
        private readonly Lazy<Task<string>> _defaultServiceAccountEmailCache;

        /// <summary>
        /// HttpClient used to call the IAM sign blob endpoint, authenticated as this credential.
        /// </summary>
        /// <remarks>Lazy to build one HtppClient only if it is needed.</remarks>
        private readonly Lazy<ConfigurableHttpClient> _signBlobHttpClient;

        /// <summary>
        /// Gets the OIDC Token URL.
        /// </summary>
        public string OidcTokenUrl { get; }

        /// <summary>
        /// The explicitly set universe domain.
        /// May be null, in which case the universe domain will be fetched from the metadata server.
        /// </summary>
        internal string ExplicitUniverseDomain { get; }

        /// <inheritdoc/>
        bool IGoogleCredential.HasExplicitScopes => HasExplicitScopes;

        /// <inheritdoc/>
        bool IGoogleCredential.SupportsExplicitScopes => true;

        internal string EffectiveTokenServerUrl { get; }

        /// <summary>
        /// An initializer class for the Compute credential. It uses <see cref="GoogleAuthConsts.EffectiveComputeTokenUrl"/>
        /// as the token server URL (optionally overriding the host using the GCE_METADATA_HOST environment variable).
        /// </summary>
        new public class Initializer : ServiceCredential.Initializer
        {
            /// <summary>
            /// Gets the OIDC Token URL.
            /// </summary>
            public string OidcTokenUrl { get; }

            /// <summary>
            /// The universe domain this credential belongs to.
            /// May be null, in which case the GCE universe domain will be used.
            /// </summary>
            internal string UniverseDomain { get; set; }

            /// <summary>Constructs a new initializer using the default compute token URL
            /// and the default OIDC token URL.</summary>
            public Initializer()
                : this(GoogleAuthConsts.EffectiveComputeTokenUrl) {}

            /// <summary>Constructs a new initializer using the given token URL
            /// and the default OIDC token URL.</summary>
            public Initializer(string tokenUrl)
                : this(tokenUrl, GoogleAuthConsts.EffectiveComputeOidcTokenUrl) {}

            /// <summary>Constructs a new initializer using the given token URL
            /// and OIDC token URL (optionally overriding the host using the GCE_METADATA_HOST environment variable).</summary>
            public Initializer(string tokenUrl, string oidcTokenUrl)
                : base(tokenUrl) => OidcTokenUrl = oidcTokenUrl;

            internal Initializer(ComputeCredential other)
                : base(other)
            {
                OidcTokenUrl = other.OidcTokenUrl;
                UniverseDomain = other.ExplicitUniverseDomain;
            }
        }

        /// <summary>Constructs a new Compute credential instance.</summary>
        public ComputeCredential() : this(new Initializer()) { }

        /// <summary>Constructs a new Compute credential instance.</summary>
        public ComputeCredential(Initializer initializer) : base(initializer)
        {
            OidcTokenUrl = initializer.OidcTokenUrl;
            ExplicitUniverseDomain = initializer.UniverseDomain;

            if (HasExplicitScopes)
            {
                var uriBuilder = new UriBuilder(TokenServerUrl);
                string scopesQuery = $"scopes={string.Join(",", Scopes)}";

                // As per https://docs.microsoft.com/en-us/dotnet/api/system.uribuilder.query?view=net-6.0#examples
                if (uriBuilder.Query is null || uriBuilder.Query.Length <= 1)
                {
                    uriBuilder.Query = scopesQuery;
                }
                else
                {
                    uriBuilder.Query = $"{uriBuilder.Query.Substring(1)}&{scopesQuery}";
                }
                EffectiveTokenServerUrl = uriBuilder.Uri.AbsoluteUri;
            }
            else
            {
                EffectiveTokenServerUrl = TokenServerUrl;
            }
            _defaultServiceAccountEmailCache = new Lazy<Task<string>>(FetchDefaultServiceAccountEmailAsync, LazyThreadSafetyMode.ExecutionAndPublication);
            _signBlobHttpClient = new Lazy<ConfigurableHttpClient>(BuildSignBlobHttpClient, LazyThreadSafetyMode.ExecutionAndPublication);
        }

        /// <inheritdoc/>
        public GoogleCredential ToGoogleCredential() => new GoogleCredential(this);

        /// <inheritdoc/>
        async Task<string> IGoogleCredential.GetUniverseDomainAsync(CancellationToken cancellationToken) =>
            ExplicitUniverseDomain ?? await computeEngineUniverseDomainCache.Value.WithCancellationToken(cancellationToken).ConfigureAwait(false);

        /// <inheritdoc/>
        string IGoogleCredential.GetUniverseDomain() =>
            ExplicitUniverseDomain ?? Task.Run(() => computeEngineUniverseDomainCache.Value).Result;

        /// <inheritdoc/>
        IGoogleCredential IGoogleCredential.WithQuotaProject(string quotaProject) =>
            new ComputeCredential(new Initializer(this) { QuotaProject = quotaProject });

        /// <inheritdoc/>
        IGoogleCredential IGoogleCredential.MaybeWithScopes(IEnumerable<string> scopes) =>
            new ComputeCredential(new Initializer(this) { Scopes = scopes });

        /// <inheritdoc/>
        IGoogleCredential IGoogleCredential.WithUserForDomainWideDelegation(string user) =>
            throw new InvalidOperationException($"{nameof(ComputeCredential)} does not support Domain-Wide Delegation");

        /// <inheritdoc/>
        IGoogleCredential IGoogleCredential.WithHttpClientFactory(IHttpClientFactory httpClientFactory) =>
            new ComputeCredential(new Initializer(this) { HttpClientFactory = httpClientFactory });

        /// <inheritdoc/>
        IGoogleCredential IGoogleCredential.WithUniverseDomain(string universeDomain) =>
            new ComputeCredential(new Initializer(this) { UniverseDomain = universeDomain });

        /// <summary>
        /// Returns a task whose result, when completed, is the default service account email associated to
        /// this Compute credential.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This value is cached, because for changing the default service account associated to a
        /// Compute VM, the machine needs to be turned off. This means that the operation is only
        /// asynchronous when calling for the first time.
        /// </para>
        /// <para>
        /// Note that if, when fetching this value, an exception is thrown, the exception is cached and
        /// will be rethrown by the task returned by any future call to this method.
        /// You can create a new <see cref="ComputeCredential"/> instance if that happens so fetching
        /// the service account default email is re-attempted.
        /// </para>
        /// </remarks>
        public Task<string> GetDefaultServiceAccountEmailAsync(CancellationToken cancellationToken = default) =>
            Task.Run(() => _defaultServiceAccountEmailCache.Value, cancellationToken);

        #region ServiceCredential overrides

        /// <inheritdoc/>
        public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken)
        {
            // Create and send the HTTP request to compute server token URL.
            var httpRequest = new HttpRequestMessage(HttpMethod.Get, EffectiveTokenServerUrl);
            httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);
            var response = await HttpClient.SendAsync(httpRequest, taskCancellationToken).ConfigureAwait(false);
            Token = await TokenResponse.FromHttpResponseAsync(response, Clock, Logger).ConfigureAwait(false);
            return true;
        }

        #endregion

        /// <inheritdoc/>
        public Task<OidcToken> GetOidcTokenAsync(OidcTokenOptions options, CancellationToken cancellationToken = default)
        {
            options.ThrowIfNull(nameof(options));
            // If at some point some properties are added to OidcToken that depend on the token having been fetched
            // then initialize the token here.
            TokenRefreshManager tokenRefreshManager = null;
            tokenRefreshManager = new TokenRefreshManager(
                ct => RefreshOidcTokenAsync(tokenRefreshManager, options, ct), Clock, Logger);
            return Task.FromResult(new OidcToken(tokenRefreshManager));
        }

        private async Task<bool> RefreshOidcTokenAsync(TokenRefreshManager caller, OidcTokenOptions options, CancellationToken cancellationToken)
        {
            string uri = $"{OidcTokenUrl}?audience={options.TargetAudience}";
            if (options.TokenFormat == OidcTokenFormat.Full || options.TokenFormat == OidcTokenFormat.FullWithLicences)
            {
                uri = $"{uri}&format=full";
                if (options.TokenFormat == OidcTokenFormat.FullWithLicences)
                {
                    uri = $"{uri}&licenses=true";
                }
            }

            var httpRequest = new HttpRequestMessage(HttpMethod.Get, uri);
            httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);

            var response = await HttpClient.SendAsync(httpRequest, cancellationToken).ConfigureAwait(false);
            var token = await TokenResponse.FromHttpResponseAsync(response, Clock, Logger).ConfigureAwait(false);
            caller.Token = token;
            return true;
        }

        /// <summary>
        /// Signs the provided blob using the private key associated with the service account
        /// this ComputeCredential represents.
        /// </summary>
        /// <param name="blob">The blob to sign.</param>
        /// <param name="cancellationToken">Cancellation token to cancel the operation.</param>
        /// <returns>The base64 encoded signature.</returns>
        /// <exception cref="HttpRequestException">When the signing request fails.</exception>
        /// <exception cref="JsonException">When the signing response is not valid JSON.</exception>
        /// <remarks>
        /// The private key associated with the Compute service account is not known locally
        /// by a ComputeCredential. Signing happens by executing a request to the IAM Credentials API
        /// which increases latency and counts towards IAM Credentials API quotas. Aditionally, the first
        /// time a ComputeCredential is used to sign data, a request to the metadata server is made to
        /// to obtain the email of the default Compute service account.
        /// </remarks>
        public async Task<string> SignBlobAsync(byte[] blob, CancellationToken cancellationToken = default)
        {
            var request = new IamSignBlobRequest { Payload = blob };
            var serviceAccountEmail = await GetDefaultServiceAccountEmailAsync(cancellationToken).ConfigureAwait(false);
            var universeDomain = await (this as IGoogleCredential).GetUniverseDomainAsync(cancellationToken).ConfigureAwait(false);
            var signBlobUrl = string.Format(GoogleAuthConsts.IamSignEndpointFormatString, universeDomain, serviceAccountEmail);

            var response = await request.PostJsonAsync<IamSignBlobResponse>(_signBlobHttpClient.Value, signBlobUrl, cancellationToken)
                .ConfigureAwait(false);

            return response.SignedBlob;
        }

        private async Task<string> FetchDefaultServiceAccountEmailAsync()
        {
            var httpRequest = new HttpRequestMessage(HttpMethod.Get, GoogleAuthConsts.EffectiveComputeDefaultServiceAccountEmailUrl);
            httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);

            var response = await HttpClient.SendAsync(httpRequest).ConfigureAwait(false);
            response.EnsureSuccessStatusCode();
            return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
        }

        private ConfigurableHttpClient BuildSignBlobHttpClient()
        {
            var httpClientArgs = BuildCreateHttpClientArgsWithNoRetries();
            AddIamSignBlobRetryConfiguration(httpClientArgs);
            // We scope the credential because, although normal ComputeCredentials are scoped on origin,
            // GKE Workload Identity credentials accept scopes.
            // We know that the HttpClient is only used for IAM requests, so we scope it only for IAM.
            var scopedCredential = ((IGoogleCredential)this).MaybeWithScopes(new string[] { GoogleAuthConsts.IamScope });
            httpClientArgs.Initializers.Add(scopedCredential);
            return HttpClientFactory.CreateHttpClient(httpClientArgs);
        }

        /// <summary>
        /// Detects if application is running on Google Compute Engine. This is achieved by attempting to contact
        /// GCE metadata server, that is only available on GCE. The check is only performed the first time you
        /// call this method, subsequent invocations used cached result of the first call.
        /// </summary>
        public static Task<bool> IsRunningOnComputeEngine()
        {
            return isRunningOnComputeEngineCache.Value;
        }

        private static async Task<bool> IsRunningOnComputeEngineUncachedAsync() =>
            await IsMetadataServerAvailableAsync().ConfigureAwait(false)
            || await IsGoogleBiosAsync().ConfigureAwait(false);

        private static async Task<bool> IsMetadataServerAvailableAsync()
        {
            Logger.Info("Checking connectivity to ComputeEngine metadata server.");

            // Using the built-in HttpClient, as we want bare bones functionality - we'll control retries.
            // Use the same one across all attempts, which may contribute to speedier retries.
            using (var httpClient = new HttpClient())
            {
                for (int i = 0; i < MetadataServerPingAttempts; i++)
                {
                    var cts = new CancellationTokenSource();
                    cts.CancelAfter(MetadataServerPingTimeoutInMilliseconds);
                    try
                    {
                        var httpRequest = new HttpRequestMessage(HttpMethod.Get, GoogleAuthConsts.EffectiveMetadataServerUrl);
                        httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);
                        var response = await httpClient.SendAsync(httpRequest, cts.Token).ConfigureAwait(false);
                        if (response.Headers.TryGetValues(MetadataFlavor, out var headerValues)
                            && headerValues.Contains(GoogleMetadataHeader))
                        {
                            return true;
                        }

                        // Response came from another source, possibly a proxy server in the caller's network.
                        Logger.Info("Response came from a source other than the Google Compute Engine metadata server.");
                        return false;
                    }
                    catch (Exception e) when (e is HttpRequestException || e is WebException || e is OperationCanceledException)
                    {
                        // We'll retry, but let's log the exception.
                        // We may eventually want to handle the different exception types in different ways,
                        // e.g. returning false rather than retrying for some exception types. However,
                        // for now it's safe just to retry.
                        Logger.Debug(
                            "An exception ocurred while attempting to reach Google's metadata service on attempt {0}. " +
                            "A total of {1} attempts will be made. " +
                            "The exception was: {2}.",
                            i + 1, MetadataServerPingAttempts, e);
                    }
                }
            }
            // Only log after all attempts have failed.
            Logger.Debug("Could not reach the Google Compute Engine metadata service. " +
                "That is expected if this application is not running on GCE " +
                "or on some cases where the metadata service is not available during application startup.");
            return false;
        }

        private static async Task<bool> IsGoogleBiosAsync()
        {
            Logger.Info("Checking BIOS values to determine GCE residency.");
            try
            {
                if (IsLinux())
                {
                    return await IsLinuxGoogleBiosAsync().ConfigureAwait(false);
                }
                else if (IsWindows())
                {
                    return IsWindowsGoogleBios();
                }
                else
                {
                    Logger.Info("GCE residency detection through BIOS checking is only supported on Windows and Linux platforms.");
                    return false;
                }
            }
            catch (Exception ex)
            {
                Logger.Debug($"Could not read BIOS: {ex}");
                return false;
            }

            // Some of these will be simpler once we have acted on
            // https://github.com/googleapis/google-api-dotnet-client/issues/2561.

            bool IsWindows()
            {
#if NET462
                // RuntimeInformation.IsOsPlatform is not available for .NET 4.6.2.
                // We are probably on Windows, unless we are using Mono which means we might be
                // elsewhere. But we don't have a reliable way to determine that, so let's
                // return false, always.
                // Note that this check can go away after we update to 4.7.1 or higher.
                return false;
#else
                return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
#endif
            }

            bool IsLinux()
            {
#if NET462
                // RuntimeInformation.IsOsPlatform is not available for .NET 4.6.2.
                // There's a chance we are on Linux if we are using Mono.
                // But we don't have a reliable way to determine that, so let's
                // return false, always.
                // Note that this check can go away after we update to 4.7.1 or higher.
                return false;
#else
                return RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
#endif
            }

            bool IsWindowsGoogleBios()
            {
                Logger.Info("Checking BIOS values on Windows.");
                System.Management.ManagementClass biosClass = new ("Win32_BIOS");
                using var instances = biosClass.GetInstances();

                bool isGoogle = false;
                foreach(var instance in instances)
                {
                    // We should only find one instance for Win32_BIOS class.
                    using (instance)
                    {
                        try
                        {
                            isGoogle = isGoogle || instance["Manufacturer"]?.ToString() == "Google";
                        }
                        catch (Exception ex)
                        {
                            Logger.Debug($"Error checking Win32_BIOS management object: {ex}.");
                        }
                    }
                }
                if (!isGoogle)
                {
                    Logger.Debug("No Win32_BIOS management object found.");
                }
                return isGoogle;
            }

            async Task<bool> IsLinuxGoogleBiosAsync()
            {
                Logger.Info("Checking BIOS values on Linux.");

                string fileName = "/sys/class/dmi/id/product_name";
                if (!File.Exists(fileName))
                {
                    Logger.Debug($"Couldn't read file {fileName} containing BIOS mapped values.");
                    return false;
                }

                using var reader = File.OpenText(fileName);
                string productName = await reader.ReadLineAsync().ConfigureAwait(false);
                productName = productName?.Trim();
                return productName == "Google" || productName == "Google Compute Engine";
            }
        }

        private async static Task<string> GetComputeEngineUniverseDomainUncachedAsync()
        {
            Logger.Info("Attempting to fetch the universe domain from the metadata server.");

            // Using the built-in HttpClient, as we want bare bones functionality - we'll control retries.
            // Use the same one across all attempts, which may contribute to speedier retries.
            using (var httpClient = new HttpClient())
            {
                int attempts = 0;
                // We use the same timeouts as we do for token fetching.
                foreach (var timeout in TokenRefreshManager.RefreshTimeouts)
                {
                    attempts++;
                    var cts = new CancellationTokenSource(timeout);
                    HttpResponseMessage response = null;
                    try
                    {
                        var httpRequest = new HttpRequestMessage(HttpMethod.Get, GoogleAuthConsts.EffectiveComputeUniverDomainUrl);
                        httpRequest.Headers.Add(MetadataFlavor, GoogleMetadataHeader);

                        response = await httpClient.SendAsync(httpRequest, cts.Token).ConfigureAwait(false);
                        response.EnsureSuccessStatusCode();

                        return await response.Content.ReadAsStringAsync().ConfigureAwait(false);
                    }
                    catch (Exception) when (response?.StatusCode == HttpStatusCode.NotFound)
                    {
                        Logger.Info($"The metadata server replied {HttpStatusCode.NotFound} when attempting to fetch the universe domain. " +
                            $"Assuming default univer domain.");
                        return GoogleAuthConsts.DefaultUniverseDomain;
                    }
                    catch (OperationCanceledException)
                    {
                        // We'll retry, but let's log the timeout.
                        Logger.Debug(
                            $"Fetching the universe domain from the metadata server timed out on attempt number {attempts} " +
                            $"out of a total of {TokenRefreshManager.RefreshTimeouts.Length} attempts.");

                        // If we've exhausted all our retries, throw.
                        if (attempts == TokenRefreshManager.RefreshTimeouts.Length)
                        {
                            throw;
                        }
                    }
                }
            }
            // We should have never reached here.
            throw new InvalidOperationException("There's a bug in code. We should never reach this point.");
        }
    }
}