/*
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
{
///
/// Exception thrown when the subject token cannot be obtained for a given
/// external account credential.
///
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)
{ }
}
///
/// Base class for external account credentials.
///
///
/// 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
/// Security requirements when using credential configurations from an external source
/// for more details.
///
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";
///
/// Initializer for .
///
new public class Initializer : ServiceCredential.Initializer
{
///
/// The STS audience which contains the resource name for the
/// workload identity pool or the workforce pool
/// and the provider identifier in that pool.
///
public string Audience { get; }
///
/// The STS subject token type based on the OAuth 2.0 token exchange spec.
///
public string SubjectTokenType { get; }
///
/// 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.
///
public string ServiceAccountImpersonationUrl { get; set; }
///
/// The GCP project number to be used for Workforce Identity Pools
/// external credentials.
///
///
/// 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.
///
public string WorkforcePoolUserProject { get; set; }
///
/// The Client ID.
///
///
/// 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.
///
public string ClientId { get; set; }
///
/// The client secret.
///
///
/// 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.
///
public string ClientSecret { get; set; }
///
/// The universe domain this credential belongs to.
/// May be null, in which case the default universe domain will be used.
///
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;
}
}
///
/// The STS audience which contains the resource name for the
/// workload identity pool or the workforce pool
/// and the provider identifier in that pool.
///
public string Audience { get; }
///
/// The STS subject token type based on the OAuth 2.0 token exchange spec.
///
public string SubjectTokenType { get; }
///
/// 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.
///
public string ServiceAccountImpersonationUrl { get; }
///
/// The GCP project number to be used for Workforce Pools
/// external credentials.
///
///
/// 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.
///
public string WorkforcePoolUserProject { get; }
///
/// The Client ID.
///
///
/// 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.
///
public string ClientId { get; }
///
/// The client secret.
///
///
/// 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.
///
public string ClientSecret { get; }
///
/// The universe domain this credential belogns to.
/// Won't be null.
///
public string UniverseDomain { get; }
///
/// Returns true if this credential allows explicit scopes to be set
/// via this library.
/// Returns false otherwise.
///
private protected bool SupportsExplicitScopes => true;
///
/// If is set, returns a based on this
/// one, but with set to null. Otherwise returns a
/// based on this one.
///
internal Lazy WithoutImpersonationConfiguration { get; }
///
/// If is set, returns an
/// whose source credential is .
/// Otherwise returns null.
///
internal Lazy 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(WithoutImpersonationConfigurationImpl, LazyThreadSafetyMode.ExecutionAndPublication);
ImplicitlyImpersonated = new Lazy(ImplicitlyImpersonatedImpl, LazyThreadSafetyMode.ExecutionAndPublication);
UniverseDomain = initializer.UniverseDomain ?? GoogleAuthConsts.DefaultUniverseDomain;
}
///
/// If is set, returns a based on this
/// one, but with set to null. Otherwise returns a
/// based on this one.
///
private protected abstract GoogleCredential WithoutImpersonationConfigurationImpl();
///
/// If is set, returns an
/// whose source credential is .
/// Otherwise returns null.
///
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);
}
///
/// Gets the subject token to be exchanged for the access token.
///
protected abstract Task GetSubjectTokenAsyncImpl(CancellationToken taskCancellationToken);
private async Task GetSubjectTokenAsync(CancellationToken taskCancellationTokne)
{
try
{
return await GetSubjectTokenAsyncImpl(taskCancellationTokne).ConfigureAwait(false);
}
catch (Exception ex)
{
throw new SubjectTokenException(this, ex);
}
}
private protected async Task 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);
}
///
public override async Task 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;
}
///
/// Throws as does not
/// support domain wide delegation.
///
private protected IGoogleCredential WithUserForDomainWideDelegation(string user) =>
throw new InvalidOperationException($"{nameof(ExternalAccountCredential)} does not support Domain-Wide Delegation");
}
}