Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Improve impersonation support. #2394

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion BACKLOG.md
Expand Up @@ -46,6 +46,5 @@ the request and want to put work into it.
- Issue [#1812 Support for limited input device flow](https://github.com/googleapis/google-api-dotnet-client/issues/1812)
- Issue [#1827 PCKE flow should no require client_secret](https://github.com/googleapis/google-api-dotnet-client/issues/1827)
- Issue [#2011 Add support for Domain-Wide Delegation using ImpersonatedCredential](https://github.com/googleapis/google-api-dotnet-client/issues/2011)
- Issue [#2159 Impersonation for gcloud credentials](https://github.com/googleapis/google-api-dotnet-client/issues/2159)
- Status: ongoing internal effort to consolidate and standardize OAuth features across language libraries. These feature requests are on the Auth team backlog.
- Action: Auth team will prioritize these issues and we'll work on them accordingly.
Expand Up @@ -15,6 +15,7 @@
<PackageReference Include="Xunit.Combinatorial" Version="1.5.25" />
<PackageReference Include="Moq" Version="4.17.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3" />
<PackageReference Include="Xunit.SkippableFact" Version="1.4.13" />
<ProjectReference Include="..\Google.Apis.Auth\Google.Apis.Auth.csproj" />
<ProjectReference Include="..\Google.Apis\Google.Apis.csproj" />
<ProjectReference Include="..\Google.Apis.Core\Google.Apis.Core.csproj" />
Expand All @@ -34,6 +35,10 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="6.0.0" />
</ItemGroup>

<ItemGroup>
<EmbeddedResource Include="OAuth2\DummyCredentialFiles\*.json" />
</ItemGroup>

<ItemGroup>
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
Expand Down

Large diffs are not rendered by default.

@@ -0,0 +1,13 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
}
}
@@ -0,0 +1,14 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
}
}
@@ -0,0 +1,14 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:aws:token-type:aws4_request",
"token_url": "https://sts.googleapis.com/v1/token",
"workforce_pool_user_project": "user_project",
"credential_source": {
"environment_id": "aws1",
"region_url": "http://169.254.169.254/latest/meta-data/placement/availability-zone",
"url": "http://169.254.169.254/latest/meta-data/iam/security-credentials",
"regional_cred_verification_url": "https://sts.{region}.amazonaws.com?Action=GetCallerIdentity&Version=2011-06-15",
"imdsv2_session_token_url": "http://169.254.169.254/latest/api/token"
}
}
@@ -0,0 +1,7 @@
{
"private_key_id": "PRIVATE_KEY_ID",
"private_key": "-----BEGIN PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJJM6HT4s6btOsfe -----END PRIVATE KEY-----",
"client_email": "CLIENT_EMAIL",
"client_id": "CLIENT_ID",
"type": "service_account"
}
@@ -0,0 +1,9 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:saml2",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"file": "/var/run/saml/assertion/token"
}
}
@@ -0,0 +1,10 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:saml2",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"file": "/var/run/saml/assertion/token"
}
}
@@ -0,0 +1,10 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:saml2",
"token_url": "https://sts.googleapis.com/v1/token",
"workforce_pool_user_project": "user_project",
"credential_source": {
"file": "/var/run/saml/assertion/token"
}
}
@@ -0,0 +1,14 @@
{
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-email:generateAccessToken",
"delegates": [
"delegate-email-1",
"delegate-email-2"
],
"source_credentials": {
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"refresh_token": "REFRESH_TOKEN",
"type": "authorized_user"
},
"type": "impersonated_service_account"
}
@@ -0,0 +1 @@
"Invalid Credentials File Contents"
@@ -0,0 +1,6 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token"
}
@@ -0,0 +1,14 @@
{
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-email-1:generateAccessToken",
"source_credentials": {
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/service-account-email-2:generateAccessToken",
"source_credentials": {
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"refresh_token": "REFRESH_TOKEN",
"type": "authorized_user"
},
"type": "impersonated_service_account"
},
"type": "impersonated_service_account"
}
@@ -0,0 +1,7 @@
{
"private_key_id": "PRIVATE_KEY_ID",
"private_key": "-----BEGIN PRIVATE KEY----- MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAJJM6HT4s6btOsfe2x4zrzrwSUtmtR37XTTi0sPARTDF8uzmXy8UnE5RcVJzEH5T2Ssz/ylX4Sl/CI4Lno1l8j9GiHJb49LSRjWe4Yx936q0Xj9H0R1HTxvjUPqwAsTwy2fKBTog+q1frqc9o8s2r6LYivUGDVbhuUzCaMJsf+x3AgMBAAECgYEAi0FTXsu/zRswAUGaViQiHjrLuU65BSHXNVjV/2fLNEKnGWGqpli68z1IXY+S2nwbUak7rnGsq9/0F6jtsW+hZbLkKXUOuuExpeC5Kd6ngWX/f2jqmhlUabiQijU9cVk7pMq8EHkRtvlosnMTUAEzempuQUPwn1PZHhmJkBvZ4lECQQDCErrxl+e3BwUDcS0yVEEmCNSG6xdXs2878b8rzbe73Mmi6SuuOLi3PU92J+j+f/MOdtYrk13mEDdYmd5dhrt5AkEAwPvDEsDT/W4y4h5ngv1awGBA5aLFE1JNWM/Gwn4D1cGpEDHKFREaBtxMDCASpHJuw8r7zUywpKhmBZcfGS37bwJANdSAKfbafLfjuhqwUJ9yGpykZm/a36aTmerp/bpn1iHdg+RtCzwMcDb/TWSwibbvsflgWmHbz657y4WSWhq+8QJAWrpCNN/ZCk2zuGDo80lfUBAwkoVat8G6wWU1oZyS+vzIGef+hLb8kHsjeZPej9eIwZ39kcBbT54oELrCkRjwGwJAQ8V2A7lTZUp8AsbVqF6rbLiiUfJMo2btGclQu4DEVyS+ymFA65tXDLUuR9EDqJYdqHNZJ5B84Z5p2prkjWTLcA\u003d\u003d-----END PRIVATE KEY-----",
"client_email": "CLIENT_EMAIL",
"client_id": "CLIENT_ID",
"type": "service_account"
}
@@ -0,0 +1,16 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"headers": {
"Metadata": "True"
},
"url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"format": {
"type": "json",
"subject_token_field_name": "access_token"
}
}
}
@@ -0,0 +1,17 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
"service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$EMAIL:generateAccessToken",
"token_url": "https://sts.googleapis.com/v1/token",
"credential_source": {
"headers": {
"Metadata": "True"
},
"url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"format": {
"type": "json",
"subject_token_field_name": "access_token"
}
}
}
@@ -0,0 +1,17 @@
{
"type": "external_account",
"audience": "//iam.googleapis.com/locations/global/workforcePools/pool/providers/oidc-google",
"subject_token_type": "urn:ietf:params:oauth:token-type:id_token",
"token_url": "https://sts.googleapis.com/v1/token",
"workforce_pool_user_project": "user_project",
"credential_source": {
"headers": {
"Metadata": "True"
},
"url": "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"format": {
"type": "json",
"subject_token_field_name": "access_token"
}
}
}
@@ -0,0 +1,6 @@
{
"client_id": "CLIENT_ID",
"client_secret": "CLIENT_SECRET",
"refresh_token": "REFRESH_TOKEN",
"type": "authorized_user"
}
Expand Up @@ -125,7 +125,7 @@ private static GoogleCredential CreateSourceCredential()
[Fact]
public void Create_InvalidSourceCredential() =>
Assert.Throws<InvalidOperationException>(() => ImpersonatedCredential.Create(
GoogleCredential.FromComputeCredential(),
GoogleCredential.FromAccessToken("fake_access_token"),
new ImpersonatedCredential.Initializer("principal")));

[Fact]
Expand Down
Expand Up @@ -16,6 +16,7 @@

using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

Expand Down Expand Up @@ -214,6 +215,7 @@ internal GoogleCredential CreateDefaultCredentialFromJson(string json)
JsonCredentialParameters.AuthorizedUserCredentialType => new GoogleCredential(CreateUserCredentialFromParameters(credentialParameters)),
JsonCredentialParameters.ServiceAccountCredentialType => GoogleCredential.FromServiceAccountCredential(CreateServiceAccountCredentialFromParameters(credentialParameters)),
JsonCredentialParameters.ExternalAccountCredentialType => new GoogleCredential(CreateExternalCredentialFromParameters(credentialParameters)),
JsonCredentialParameters.ImpersonatedServiceAccountCredentialType => new GoogleCredential(CreateImpersonatedServiceAccountCredentialFromParameters(credentialParameters)),
_ => throw new InvalidOperationException($"Error creating credential from JSON or JSON parameters. Unrecognized credential type {credentialParameters.Type}."),
};

Expand Down Expand Up @@ -339,6 +341,28 @@ private static IGoogleCredential CreateExternalCredentialFromParameters(JsonCred
}
}

private ImpersonatedCredential CreateImpersonatedServiceAccountCredentialFromParameters(JsonCredentialParameters parameters)
{
if (parameters.Type != JsonCredentialParameters.ImpersonatedServiceAccountCredentialType
|| parameters.SourceCredential is null
|| string.IsNullOrEmpty(parameters.ServiceAccountImpersonationUrl))
{
throw new InvalidOperationException("JSON data does not represent a valid impersonated service account credential.");
}

// If source credential is of a credential type that does not support impersonation, attemting to create the
// impersonated credential will fail a few lines later.
var sourceCredential = CreateDefaultCredentialFromParameters(parameters.SourceCredential);
var maybeTargetPrincipal = ImpersonatedCredential.ExtractTargetPrincipal(parameters.ServiceAccountImpersonationUrl);
var initializer = new ImpersonatedCredential.Initializer(parameters.ServiceAccountImpersonationUrl, maybeTargetPrincipal)
{
DelegateAccounts = parameters.Delegates?.Length > 0 ? parameters.Delegates.ToList() : null,
QuotaProject = parameters.QuotaProject,
};

return ImpersonatedCredential.Create(sourceCredential, initializer);
}

/// <summary>
/// Returns platform-specific well known credential file path. This file is created by
/// <a href="https://cloud.google.com/sdk/gcloud/reference/auth/login">gcloud auth login</a>
Expand Down
Expand Up @@ -235,7 +235,7 @@ private protected ImpersonatedCredential ImplicitlyImpersonatedImpl()
}

var initializer = new ImpersonatedCredential.Initializer(
ServiceAccountImpersonationUrl, ExtractTargetPrincipal(ServiceAccountImpersonationUrl))
ServiceAccountImpersonationUrl, ImpersonatedCredential.ExtractTargetPrincipal(ServiceAccountImpersonationUrl))
{
// We copy this credential settings to the impersonated one.
AccessMethod = AccessMethod,
Expand All @@ -252,26 +252,6 @@ private protected ImpersonatedCredential ImplicitlyImpersonatedImpl()


return ImpersonatedCredential.Create(WithoutImpersonationConfiguration.Value, initializer);

// Tries to extract the target principal ID from the impersonation URL which is possible if the URL looks like
// https://host/segment-1/.../segment-n/target-principal-ID:generateAccessToken.
// It's OK if we can't though as for fetching the impersonated access token we have the impersonation URL as a whole.
// It's just a nice to have, as we may get extra operations, that we don't use now, but might use in the future.
// Regardless we don't expose the resulting impersonated credential so users won't be affected by any of this.
static string ExtractTargetPrincipal(string url)
{
int start = url.LastIndexOf("/") + 1;
if (start == 0 || start >= url.Length)
{
return null;
}
int afterEnd = url.IndexOf($":{GoogleAuthConsts.IamAccessTokenVerb}");
if (afterEnd == -1 || afterEnd <= start)
{
return null;
}
return url.Substring(start, afterEnd - start);
}
}

/// <summary>
Expand Down
38 changes: 34 additions & 4 deletions Src/Support/Google.Apis.Auth/OAuth2/ImpersonatedCredential.cs
Expand Up @@ -171,13 +171,18 @@ internal static ImpersonatedCredential Create(GoogleCredential sourceCredential,
}
if (!(sourceCredential.UnderlyingCredential is ServiceAccountCredential
|| sourceCredential.UnderlyingCredential is UserCredential
|| sourceCredential.UnderlyingCredential is ExternalAccountCredential))
|| sourceCredential.UnderlyingCredential is ExternalAccountCredential
|| sourceCredential.UnderlyingCredential is ComputeCredential))
{
throw new InvalidOperationException($"Only {nameof(ServiceAccountCredential)}, {nameof(UserCredential)} and {nameof(ExternalAccountCredential)} support impersonation.");
throw new InvalidOperationException(
$"Only {nameof(ServiceAccountCredential)}," +
$"{nameof(UserCredential)}, " +
$"{nameof(ExternalAccountCredential)} " +
$"and {nameof(ComputeCredential)} support impersonation.");
}
if (sourceCredential.UnderlyingCredential is ExternalAccountCredential externalCred && externalCred.ServiceAccountImpersonationUrl is string)
{
throw new InvalidOperationException($"Only {nameof(ExternalAccountCredential)}s that have no impersonation conigured via service_account_impersonation_url support explicit impersonation.");
throw new InvalidOperationException($"Only {nameof(ExternalAccountCredential)}s that have no impersonation configured via service_account_impersonation_url support explicit impersonation.");
}

// We ourselves modify the client supplied initializer, so let's make a copy first.
Expand Down Expand Up @@ -287,9 +292,34 @@ private void ThrowIfCustomTokenUrl()
{
if (HasCustomTokenUrl)
{
// We never expose an ImpersonatedCredential with a custom token URL, users will never see this.
// If the impersonated credential has a custom access token URL we don't know how the OIDC token and blob signing
// tokens may look like, so we cannot support those operations.
// For TPC, regional endpoints, etc. we only need to change the definition of custom, which at the moment is
// everything different of GoogleAuthConsts.IamAccessTokenEndpointFormatString.
throw new InvalidOperationException("Operation not supported when a custom access token URL has been specified.");
}
}

/// <summary>
/// Attempts to extract the target principal ID from the impersonation URL which is possible if the URL looks like
/// https://host/segment-1/.../segment-n/target-principal-ID:generateAccessToken.
/// It's OK if we can't though as for fetching the impersonated access token we have the impersonation URL as a whole.
/// It's just a nice to have, as the user may be able to execute extra operations with the impersonated credential, like
/// signing a blob of fetching its OIDC token.
/// </summary>
internal static string ExtractTargetPrincipal(string url)
{
int start = url.LastIndexOf("/") + 1;
if (start == 0 || start >= url.Length)
{
return null;
}
int afterEnd = url.IndexOf($":{GoogleAuthConsts.IamAccessTokenVerb}");
if (afterEnd == -1 || afterEnd <= start)
{
return null;
}
return url.Substring(start, afterEnd - start);
}
}
}