| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| | 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 |
| | { |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public class ComputeCredential : ServiceCredential, IOidcTokenProvider, IGoogleCredential, IBlobSigner |
| | { |
| | |
| | |
| | public const string MetadataServerUrl = GoogleAuthConsts.DefaultMetadataServerUrl; |
| |
|
| | |
| | private readonly static Lazy<Task<bool>> isRunningOnComputeEngineCache = new Lazy<Task<bool>>(IsRunningOnComputeEngineUncachedAsync); |
| |
|
| | private readonly static Lazy<Task<string>> computeEngineUniverseDomainCache = new Lazy<Task<string>>(GetComputeEngineUniverseDomainUncachedAsync); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | private const int MetadataServerPingTimeoutInMilliseconds = 500; |
| |
|
| | private const int MetadataServerPingAttempts = 3; |
| |
|
| | |
| | internal const string MetadataFlavor = "Metadata-Flavor"; |
| |
|
| | |
| | internal const string GoogleMetadataHeader = "Google"; |
| |
|
| | |
| | |
| | |
| | |
| | |
| | private readonly Lazy<Task<string>> _defaultServiceAccountEmailCache; |
| |
|
| | |
| | |
| | |
| | |
| | private readonly Lazy<ConfigurableHttpClient> _signBlobHttpClient; |
| |
|
| | |
| | |
| | |
| | public string OidcTokenUrl { get; } |
| |
|
| | |
| | |
| | |
| | |
| | internal string ExplicitUniverseDomain { get; } |
| |
|
| | |
| | bool IGoogleCredential.HasExplicitScopes => HasExplicitScopes; |
| |
|
| | |
| | bool IGoogleCredential.SupportsExplicitScopes => true; |
| |
|
| | internal string EffectiveTokenServerUrl { get; } |
| |
|
| | |
| | |
| | |
| | |
| | new public class Initializer : ServiceCredential.Initializer |
| | { |
| | |
| | |
| | |
| | public string OidcTokenUrl { get; } |
| |
|
| | |
| | |
| | |
| | |
| | internal string UniverseDomain { get; set; } |
| |
|
| | |
| | |
| | public Initializer() |
| | : this(GoogleAuthConsts.EffectiveComputeTokenUrl) {} |
| |
|
| | |
| | |
| | public Initializer(string tokenUrl) |
| | : this(tokenUrl, GoogleAuthConsts.EffectiveComputeOidcTokenUrl) {} |
| |
|
| | |
| | |
| | public Initializer(string tokenUrl, string oidcTokenUrl) |
| | : base(tokenUrl) => OidcTokenUrl = oidcTokenUrl; |
| |
|
| | internal Initializer(ComputeCredential other) |
| | : base(other) |
| | { |
| | OidcTokenUrl = other.OidcTokenUrl; |
| | UniverseDomain = other.ExplicitUniverseDomain; |
| | } |
| | } |
| |
|
| | |
| | public ComputeCredential() : this(new Initializer()) { } |
| |
|
| | |
| | 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)}"; |
| |
|
| | |
| | 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); |
| | } |
| |
|
| | |
| | public GoogleCredential ToGoogleCredential() => new GoogleCredential(this); |
| |
|
| | |
| | async Task<string> IGoogleCredential.GetUniverseDomainAsync(CancellationToken cancellationToken) => |
| | ExplicitUniverseDomain ?? await computeEngineUniverseDomainCache.Value.WithCancellationToken(cancellationToken).ConfigureAwait(false); |
| |
|
| | |
| | string IGoogleCredential.GetUniverseDomain() => |
| | ExplicitUniverseDomain ?? Task.Run(() => computeEngineUniverseDomainCache.Value).Result; |
| |
|
| | |
| | IGoogleCredential IGoogleCredential.WithQuotaProject(string quotaProject) => |
| | new ComputeCredential(new Initializer(this) { QuotaProject = quotaProject }); |
| |
|
| | |
| | IGoogleCredential IGoogleCredential.MaybeWithScopes(IEnumerable<string> scopes) => |
| | new ComputeCredential(new Initializer(this) { Scopes = scopes }); |
| |
|
| | |
| | IGoogleCredential IGoogleCredential.WithUserForDomainWideDelegation(string user) => |
| | throw new InvalidOperationException($"{nameof(ComputeCredential)} does not support Domain-Wide Delegation"); |
| |
|
| | |
| | IGoogleCredential IGoogleCredential.WithHttpClientFactory(IHttpClientFactory httpClientFactory) => |
| | new ComputeCredential(new Initializer(this) { HttpClientFactory = httpClientFactory }); |
| |
|
| | |
| | IGoogleCredential IGoogleCredential.WithUniverseDomain(string universeDomain) => |
| | new ComputeCredential(new Initializer(this) { UniverseDomain = universeDomain }); |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | public Task<string> GetDefaultServiceAccountEmailAsync(CancellationToken cancellationToken = default) => |
| | Task.Run(() => _defaultServiceAccountEmailCache.Value, cancellationToken); |
| |
|
| | #region ServiceCredential overrides |
| |
|
| | |
| | public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken) |
| | { |
| | |
| | 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 |
| |
|
| | |
| | public Task<OidcToken> GetOidcTokenAsync(OidcTokenOptions options, CancellationToken cancellationToken = default) |
| | { |
| | options.ThrowIfNull(nameof(options)); |
| | |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | 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); |
| | |
| | |
| | |
| | var scopedCredential = ((IGoogleCredential)this).MaybeWithScopes(new string[] { GoogleAuthConsts.IamScope }); |
| | httpClientArgs.Initializers.Add(scopedCredential); |
| | return HttpClientFactory.CreateHttpClient(httpClientArgs); |
| | } |
| |
|
| | |
| | |
| | |
| | |
| | |
| | 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 (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; |
| | } |
| |
|
| | |
| | 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) |
| | { |
| | |
| | |
| | |
| | |
| | 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); |
| | } |
| | } |
| | } |
| | |
| | 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; |
| | } |
| |
|
| | |
| | |
| |
|
| | bool IsWindows() |
| | { |
| | #if NET462 |
| | |
| | |
| | |
| | |
| | |
| | return false; |
| | #else |
| | return RuntimeInformation.IsOSPlatform(OSPlatform.Windows); |
| | #endif |
| | } |
| |
|
| | bool IsLinux() |
| | { |
| | #if NET462 |
| | |
| | |
| | |
| | |
| | |
| | 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) |
| | { |
| | |
| | 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 (var httpClient = new HttpClient()) |
| | { |
| | int attempts = 0; |
| | |
| | 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) |
| | { |
| | |
| | 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 (attempts == TokenRefreshManager.RefreshTimeouts.Length) |
| | { |
| | throw; |
| | } |
| | } |
| | } |
| | } |
| | |
| | throw new InvalidOperationException("There's a bug in code. We should never reach this point."); |
| | } |
| | } |
| | } |
| |
|