| /* | |
| Copyright 2022 Google LLC | |
| 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 | |
| https://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.Util; | |
| using System; | |
| using System.Threading; | |
| using System.Threading.Tasks; | |
| namespace Google.Apis.Auth.OAuth2 | |
| { | |
| /// <summary> | |
| /// Exception thrown when the subject token cannot be obtained for a given | |
| /// external account credential. | |
| /// </summary> | |
| public class SubjectTokenException : Exception | |
| { | |
| internal SubjectTokenException(ExternalAccountCredential credential, Exception innerException) : base( | |
| $"An error occurred while attempting to obtain the subject token for {credential.ThrowIfNull(nameof(credential)).GetType().Name}", | |
| innerException) | |
| { } | |
| } | |
| /// <summary> | |
| /// Base class for external account credentials. | |
| /// </summary> | |
| /// <remarks> | |
| /// This credential type does not validate the credential configuration. A security | |
| /// risk occurs when a credential configuration configured with malicious urls | |
| /// is used. | |
| /// You should validate credential configurations provided by untrusted sources. | |
| /// See <see href="https://cloud.google.com/docs/authentication/external/externally-sourced-credentials"> | |
| /// Security requirements when using credential configurations from an external source | |
| /// </see> for more details. | |
| /// </remarks> | |
| public abstract class ExternalAccountCredential : ServiceCredential | |
| { | |
| private const string GrantType = "urn:ietf:params:oauth:grant-type:token-exchange"; | |
| private const string RequestedTokenType = "urn:ietf:params:oauth:token-type:access_token"; | |
| /// <summary> | |
| /// Initializer for <see cref="ExternalAccountCredential"/>. | |
| /// </summary> | |
| new public class Initializer : ServiceCredential.Initializer | |
| { | |
| /// <summary> | |
| /// The STS audience which contains the resource name for the | |
| /// workload identity pool or the workforce pool | |
| /// and the provider identifier in that pool. | |
| /// </summary> | |
| public string Audience { get; } | |
| /// <summary> | |
| /// The STS subject token type based on the OAuth 2.0 token exchange spec. | |
| /// </summary> | |
| public string SubjectTokenType { get; } | |
| /// <summary> | |
| /// This is the URL for the service account impersonation request. | |
| /// If this is not set, the STS-returned access token | |
| /// should be directly used without impersonation. | |
| /// </summary> | |
| public string ServiceAccountImpersonationUrl { get; set; } | |
| /// <summary> | |
| /// The GCP project number to be used for Workforce Identity Pools | |
| /// external credentials. | |
| /// </summary> | |
| /// <remarks> | |
| /// If this external account credential represents a Workforce Identity Pool | |
| /// enabled identity and this values is not specified, then an API key needs to be | |
| /// used alongside this credential to call Google APIs. | |
| /// </remarks> | |
| public string WorkforcePoolUserProject { get; set; } | |
| /// <summary> | |
| /// The Client ID. | |
| /// </summary> | |
| /// <remarks> | |
| /// Client ID and client secret are currently only required if the token info endpoint | |
| /// needs to be called with the generated GCP access token. | |
| /// When provided, STS will be called with additional basic authentication using | |
| /// ClientId as username and ClientSecret as password. | |
| /// </remarks> | |
| public string ClientId { get; set; } | |
| /// <summary> | |
| /// The client secret. | |
| /// </summary> | |
| /// <remarks> | |
| /// Client ID and client secret are currently only required if the token info endpoint | |
| /// needs to be called with the generated GCP access token. | |
| /// When provided, STS will be called with additional basic authentication using | |
| /// ClientId as username and ClientSecret as password. | |
| /// </remarks> | |
| public string ClientSecret { get; set; } | |
| /// <summary> | |
| /// The universe domain this credential belongs to. | |
| /// May be null, in which case the default universe domain will be used. | |
| /// </summary> | |
| public string UniverseDomain { get; set; } | |
| internal Initializer(string tokenUrl, string audience, string subjectTokenType) : base(tokenUrl) | |
| { | |
| Audience = audience; | |
| SubjectTokenType = subjectTokenType; | |
| } | |
| internal Initializer(Initializer other) : base(other) | |
| { | |
| Audience = other.Audience; | |
| SubjectTokenType = other.SubjectTokenType; | |
| ServiceAccountImpersonationUrl = other.ServiceAccountImpersonationUrl; | |
| WorkforcePoolUserProject = other.WorkforcePoolUserProject; | |
| ClientId = other.ClientId; | |
| ClientSecret = other.ClientSecret; | |
| UniverseDomain = other.UniverseDomain; | |
| } | |
| internal Initializer(ExternalAccountCredential other) : base(other) | |
| { | |
| Audience = other.Audience; | |
| SubjectTokenType = other.SubjectTokenType; | |
| ServiceAccountImpersonationUrl= other.ServiceAccountImpersonationUrl; | |
| WorkforcePoolUserProject= other.WorkforcePoolUserProject; | |
| ClientId = other.ClientId; | |
| ClientSecret = other.ClientSecret; | |
| UniverseDomain = other.UniverseDomain; | |
| } | |
| } | |
| /// <summary> | |
| /// The STS audience which contains the resource name for the | |
| /// workload identity pool or the workforce pool | |
| /// and the provider identifier in that pool. | |
| /// </summary> | |
| public string Audience { get; } | |
| /// <summary> | |
| /// The STS subject token type based on the OAuth 2.0 token exchange spec. | |
| /// </summary> | |
| public string SubjectTokenType { get; } | |
| /// <summary> | |
| /// This is the URL for the service account impersonation request. | |
| /// If this is not set, the STS-returned access token | |
| /// should be directly used without impersonation. | |
| /// </summary> | |
| public string ServiceAccountImpersonationUrl { get; } | |
| /// <summary> | |
| /// The GCP project number to be used for Workforce Pools | |
| /// external credentials. | |
| /// </summary> | |
| /// <remarks> | |
| /// If this external account credential represents a Workforce Pool | |
| /// enabled identity and this values is not specified, then an API key needs to be | |
| /// used alongside this credential to call Google APIs. | |
| /// </remarks> | |
| public string WorkforcePoolUserProject { get; } | |
| /// <summary> | |
| /// The Client ID. | |
| /// </summary> | |
| /// <remarks> | |
| /// Client ID and Client secret are currently only required if the token info endpoint | |
| /// needs to be called with the generated GCP access token. | |
| /// When provided, STS will be called with additional basic authentication using | |
| /// ClientId as username and ClientSecret as password. | |
| /// </remarks> | |
| public string ClientId { get; } | |
| /// <summary> | |
| /// The client secret. | |
| /// </summary> | |
| /// <remarks> | |
| /// Client ID and Client secret are currently only required if the token info endpoint | |
| /// needs to be called with the generated GCP access token. | |
| /// When provided, STS will be called with additional basic authentication using | |
| /// ClientId as username and ClientSecret as password. | |
| /// </remarks> | |
| public string ClientSecret { get; } | |
| /// <summary> | |
| /// The universe domain this credential belogns to. | |
| /// Won't be null. | |
| /// </summary> | |
| public string UniverseDomain { get; } | |
| /// <summary> | |
| /// Returns true if this credential allows explicit scopes to be set | |
| /// via this library. | |
| /// Returns false otherwise. | |
| /// </summary> | |
| private protected bool SupportsExplicitScopes => true; | |
| /// <summary> | |
| /// If <see cref="ServiceAccountImpersonationUrl"/> is set, returns a <see cref="GoogleCredential"/> based on this | |
| /// one, but with <see cref="ServiceAccountImpersonationUrl"/> set to null. Otherwise returns a <see cref="GoogleCredential"/> | |
| /// based on this one. | |
| /// </summary> | |
| internal Lazy<GoogleCredential> WithoutImpersonationConfiguration { get; } | |
| /// <summary> | |
| /// If <see cref="ServiceAccountImpersonationUrl"/> is set, returns an <see cref="ImpersonatedCredential"/> | |
| /// whose source credential is <see cref="WithoutImpersonationConfiguration"/>. | |
| /// Otherwise returns null. | |
| /// </summary> | |
| internal Lazy<ImpersonatedCredential> ImplicitlyImpersonated { get; } | |
| internal ExternalAccountCredential(Initializer initializer) : base(initializer) | |
| { | |
| Audience = initializer.Audience; | |
| SubjectTokenType = initializer.SubjectTokenType; | |
| ServiceAccountImpersonationUrl = initializer.ServiceAccountImpersonationUrl; | |
| WorkforcePoolUserProject = initializer.WorkforcePoolUserProject; | |
| ClientId = initializer.ClientId; | |
| ClientSecret = initializer.ClientSecret; | |
| WithoutImpersonationConfiguration = new Lazy<GoogleCredential>(WithoutImpersonationConfigurationImpl, LazyThreadSafetyMode.ExecutionAndPublication); | |
| ImplicitlyImpersonated = new Lazy<ImpersonatedCredential>(ImplicitlyImpersonatedImpl, LazyThreadSafetyMode.ExecutionAndPublication); | |
| UniverseDomain = initializer.UniverseDomain ?? GoogleAuthConsts.DefaultUniverseDomain; | |
| } | |
| /// <summary> | |
| /// If <see cref="ServiceAccountImpersonationUrl"/> is set, returns a <see cref="GoogleCredential"/> based on this | |
| /// one, but with <see cref="ServiceAccountImpersonationUrl"/> set to null. Otherwise returns a <see cref="GoogleCredential"/> | |
| /// based on this one. | |
| /// </summary> | |
| private protected abstract GoogleCredential WithoutImpersonationConfigurationImpl(); | |
| /// <summary> | |
| /// If <see cref="ServiceAccountImpersonationUrl"/> is set, returns an <see cref="ImpersonatedCredential"/> | |
| /// whose source credential is <see cref="WithoutImpersonationConfiguration"/>. | |
| /// Otherwise returns null. | |
| /// </summary> | |
| private protected ImpersonatedCredential ImplicitlyImpersonatedImpl() | |
| { | |
| if (ServiceAccountImpersonationUrl is null) | |
| { | |
| return null; | |
| } | |
| var initializer = new ImpersonatedCredential.Initializer( | |
| ServiceAccountImpersonationUrl, ImpersonatedCredential.ExtractTargetPrincipal(ServiceAccountImpersonationUrl)) | |
| { | |
| // We copy this credential settings to the impersonated one. | |
| AccessMethod = AccessMethod, | |
| Clock = Clock, | |
| DefaultExponentialBackOffPolicy = DefaultExponentialBackOffPolicy, | |
| HttpClientFactory = HttpClientFactory, | |
| QuotaProject = QuotaProject, | |
| Scopes = Scopes | |
| }; | |
| foreach (var httpInitializer in HttpClientInitializers) | |
| { | |
| initializer.HttpClientInitializers.Add(httpInitializer); | |
| } | |
| return ImpersonatedCredential.Create(WithoutImpersonationConfiguration.Value, initializer); | |
| } | |
| /// <summary> | |
| /// Gets the subject token to be exchanged for the access token. | |
| /// </summary> | |
| protected abstract Task<string> GetSubjectTokenAsyncImpl(CancellationToken taskCancellationToken); | |
| private async Task<string> GetSubjectTokenAsync(CancellationToken taskCancellationTokne) | |
| { | |
| try | |
| { | |
| return await GetSubjectTokenAsyncImpl(taskCancellationTokne).ConfigureAwait(false); | |
| } | |
| catch (Exception ex) | |
| { | |
| throw new SubjectTokenException(this, ex); | |
| } | |
| } | |
| private protected async Task<TokenResponse> RequestStsAccessTokenAsync(CancellationToken taskCancellationToken) | |
| { | |
| StsTokenRequest request = new StsTokenRequestBuilder | |
| { | |
| Audience = Audience, | |
| GrantType = GrantType, | |
| RequestedTokenType = RequestedTokenType, | |
| Scopes = Scopes, | |
| SubjectToken = await GetSubjectTokenAsync(taskCancellationToken).ConfigureAwait(false), | |
| SubjectTokenType = SubjectTokenType, | |
| WorkforcePoolUserProject = WorkforcePoolUserProject, | |
| ClientId = ClientId, | |
| ClientSecret = ClientSecret, | |
| }.Build(); | |
| return await request | |
| .PostFormAsync(HttpClient, TokenServerUrl, request.AuthenticationHeader, Clock, Logger, taskCancellationToken) | |
| .ConfigureAwait(false); | |
| } | |
| /// <inheritdoc/> | |
| public override async Task<bool> RequestAccessTokenAsync(CancellationToken taskCancellationToken) | |
| { | |
| var impersonated = ImplicitlyImpersonated.Value; | |
| if (impersonated is null) | |
| { | |
| Token = await RequestStsAccessTokenAsync(taskCancellationToken).ConfigureAwait(false); | |
| return true; | |
| } | |
| if (await impersonated.RequestAccessTokenAsync(taskCancellationToken).ConfigureAwait(false)) | |
| { | |
| Token = impersonated.Token; | |
| return true; | |
| } | |
| return false; | |
| } | |
| /// <summary> | |
| /// Throws <see cref="InvalidOperationException"/> as <see cref="ExternalAccountCredential"/> does not | |
| /// support domain wide delegation. | |
| /// </summary> | |
| private protected IGoogleCredential WithUserForDomainWideDelegation(string user) => | |
| throw new InvalidOperationException($"{nameof(ExternalAccountCredential)} does not support Domain-Wide Delegation"); | |
| } | |
| } | |