googleapis / google-api-dotnet-client

Google APIs Client Library for .NET
https://developers.google.com/api-client-library/dotnet
Apache License 2.0
1.35k stars 526 forks source link

Support for workload identity federation #2033

Closed wvanderdeijl closed 2 years ago

wvanderdeijl commented 2 years ago

We run on GCP and have several other parties that do not run on GCP and need to interact with our GCP infrastructure. We tend to use the Workload Identity Federation feature of GCP (see https://cloud.google.com/iam/docs/using-workload-identity-federation) to allow them access based on their AWS, Azure, or OIDC credentials. We love that feature as it removes the need to manage secrets.

However, it seems this identity federation is supported by most google client libraries, but not the dotnet one. Is it just a matter of me being unable to find this in the documentation? Otherwise, please consider this a feature request to add support for federated credentials.

See https://github.com/googleapis/google-auth-library-nodejs#workload-identity-federation for how Identity Federation is supported by the nodejs GCP clients. They use the new gcloud iam workload-identity-pools create-cred-config cli tool to create a json that can be used in the GOOGLE_APPLICATION_CREDENTIALS environment variable to have the client libraries automatically use identity federation. Perhaps this can be an inspiration on how to offer this in the dotnet eco system.

jskeet commented 2 years ago

I'm transferring this issue to google-api-dotnet-client as that's where the auth library lives. I'll then assign it to Amanda, who knows this area better than I do.

amanda-tarafa commented 2 years ago

This feature is already beeing considered for implementation in the .NET Auth client library. The hope is that we have it ready sometime this quarter but that's still a very soft commitment. Please bear with us in the meantime. I'm sorry I don't have a better answer for you at the moment.

wvanderdeijl commented 2 years ago

Thanks for confirming this is being worked on. We can manage for now by doing raw http calls from the C# code, but it is good to know we can drop this in a couple of weeks/months and replace it with proper client library support. That would make life so much easier

sopelt commented 2 years ago

Hi @jskeet and @amanda-tarafa, do you have some info on how this scenario will be supported and when? Or is there even a branch/PR to look at? Do you see any downsides with the workaround described in #2076 or can we live with that until this is supported? The only alternative we could think of is replacing the whole FirebaseApp before the token expires ...

Thanks.

jskeet commented 2 years ago

I'll leave this for @amanda-tarafa to reply to - but please be aware that that may not be until Monday due to vacations.

amanda-tarafa commented 2 years ago

I just started going through requirements last week but I can't see anything wrong with your approach, it's similar to what I currently have in mind.

I will almost certainly have at least a draft PR at the end of next week and then we can release soon after merge. So not long to go before we support it.

amanda-tarafa commented 2 years ago

Just as an update, I've made some advances, but it's still going to be a couple of weeks before I've got a PR ready for review.

cudders commented 2 years ago

Hi @amanda-tarafa do you have a timeframe on when this will be released please, we're looking to use AWS IAM roles? Thanks for working on this btw, WIF is such a great feature.

amanda-tarafa commented 2 years ago

@cudders I'm hoping that by the end of September we are in a good place for releasing, but that's a soft commitment. I'm sorry it has taken so long, but as always, it's a matter of some other priorities getting on the way.

javeedashraf commented 2 years ago

Hi @amanda-tarafa Any updates on the Release. Also do we have any Docs for the SDK Changes ?

amanda-tarafa commented 2 years ago

File and URL sourced external credentials are already implemented, I'm working on implementing AWS external credentials at the moment. We'll most likely release a beta of Google.Apis.Auth once the AWS change is in, probably late this week or very early next week.

There's no specific .NET library documentation that I've added, as you will use these credentials via ADC most likely or build one from a JSON configuration the same as you can do with any other type of credential. I can build up documentation if specific issues arise. You mostly need to look at Workload Identity Federation and Workforce Identity Federation for information on how to configure each.

iabdelkareem commented 1 year ago

Hi @amanda-tarafa ,

Following this issue I was trying to use identity federation from AWS to GCP. The idea was to assume a role in AWS and use this role's credentials to call GCP through identity federation. But I couldn't get the solution to work and always end-up with this error message.

Not sure if it's because of something in my implementation or a bug in the SDK because we could get similar flow to work in python but not in .NET. I included the code I used below. Appreciate your assistance.

Code

    [LambdaSerializer(typeof(JsonSerializer))]
    public async Task<IResult> Handle(ILambdaContext context)
    {
        Environment.SetEnvironmentVariable("AWS_DEFAULT_REGION", "eu-west-1");
        var options = ServiceProvider.GetRequiredService<AWSOptions>();
        var configuration = ServiceProvider.GetRequiredService<IConfiguration>();

        var credentials = await AssumeRole(options, configuration);

        var projectId = "mathem-ml-datahem-test";
        var awsCredentialsProvider = new StaticCredentialsProvider(new AwsCredentials() { Token = credentials.SessionToken, SecretKey = credentials.SecretAccessKey, AccessKey = credentials.AccessKeyId });
        var httpClient = new GcpAwsHttpClientFactory(awsCredentialsProvider);

        var googleCredential = (await GetGcpCredentials(configuration));

        var bigQueryClient = await new BigQueryClientBuilder()
        {
            Credential = googleCredential,
            HttpClientFactory = httpClient,
            ProjectId = projectId,
            ApplicationName = "test-app"
        }.BuildAsync();

        var response = await bigQueryClient.ExecuteQueryAsync("<QUERY>", Array.Empty<BigQueryParameter>());
        var rows = await response.ReadPageAsync(100);
        return Results.Ok(rows.Rows);
    }

    private static async Task<Credentials> AssumeRole(AWSOptions options, IConfiguration configuration)
    {
        var awsSecurityTokenServiceClient = new AmazonSecurityTokenServiceClient(options.Region);

        var assumeRoleReq = new AssumeRoleRequest()
        {
            DurationSeconds = 900,
            RoleSessionName = $"<Session-Name>",
            RoleArn = "<ARN>"
        };

        var assumeRoleRes = await awsSecurityTokenServiceClient.AssumeRoleAsync(assumeRoleReq);

        var assumeRoleClient = new AmazonSecurityTokenServiceClient(assumeRoleRes.Credentials);
        var callerIdentity = await assumeRoleClient.GetCallerIdentityAsync(new GetCallerIdentityRequest(), CancellationToken.None);

        return assumeRoleRes.Credentials;
    }

    private static async Task<GoogleCredential> GetGcpCredentials(IConfiguration configuration)
    {
       // The credentials are loaded from AWS parameter store to the environment variables
        var gcpCredentialsJson = configuration.GetValue<string>("GcpCredentials");

        return GoogleCredential
            .FromJson(gcpCredentialsJson);
    }

// This will sign the request as described here
// https://docs.aws.amazon.com/general/latest/gr/signing_aws_api_requests.html
public class GcpAwsHttpClientFactory : HttpClientFactory
{
    private readonly ICredentialsProvider _credentialsProvider;

    public GcpAwsHttpClientFactory(ICredentialsProvider credentialsProvider)
    {
        _credentialsProvider = credentialsProvider;
    }

    protected override HttpMessageHandler CreateHandler(CreateHttpClientArgs args)
    {
        var gcpHttpClient = base.CreateHandler(args);
        var handler = new AwsSignedHttpMessageHandler()
        {
            InnerHandler = gcpHttpClient
        };

        return handler;
    }
}

Error

{
  "errorType": "SubjectTokenException",
  "errorMessage": "An error occurred while attempting to obtain the subject token for AwsExternalAccountCredential",
  "stackTrace": [
    "at Google.Apis.Auth.OAuth2.ExternalAccountCredential.GetSubjectTokenAsync(CancellationToken taskCancellationTokne)",
    "at Google.Apis.Auth.OAuth2.ExternalAccountCredential.RequestStsAccessTokenAsync(CancellationToken taskCancellationToken)",
    "at Google.Apis.Auth.OAuth2.ExternalAccountCredential.RequestAccessTokenAsync(CancellationToken taskCancellationToken)",
    "at Google.Apis.Auth.OAuth2.TokenRefreshManager.RefreshTokenAsync()",
    "at Google.Apis.Auth.TaskExtensions.<>c__DisplayClass0_0`1.<<WithCancellationToken>g__ImplAsync|0>d.MoveNext()",
    "--- End of stack trace from previous location ---",
    "at Google.Apis.Auth.OAuth2.TokenRefreshManager.GetAccessTokenForRequestAsync(CancellationToken cancellationToken)",
    "at Google.Apis.Auth.OAuth2.ServiceCredential.GetAccessTokenWithHeadersForRequestAsync(String authUri, CancellationToken cancellationToken)",
    "at Google.Apis.Auth.OAuth2.ServiceCredential.InterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)",
    "at Google.Apis.Http.ConfigurableMessageHandler.CredentialInterceptAsync(HttpRequestMessage request, CancellationToken cancellationToken)",
    "at Google.Apis.Http.ConfigurableMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)",
    "at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)",
    "at Google.Apis.Auth.OAuth2.Requests.ImpersonationTokenRequestExtensions.ExecuteAsync(ImpersonationRequest request, HttpClient httpClient, String url, CancellationToken cancellationToken)",
    "at Google.Apis.Auth.OAuth2.Requests.ImpersonationTokenRequestExtensions.ExecuteAsync(ImpersonationRequest request, HttpClient httpClient, String url, IClock clock, ILogger logger, CancellationToken cancellationToken)",
    "at Google.Apis.Auth.OAuth2.ImpersonatedCredential.RequestAccessTokenAsync(CancellationToken taskCancellationToken)",
    "at Google.Apis.Auth.OAuth2.ExternalAccountCredential.RequestAccessTokenAsync(CancellationToken taskCancellationToken)",
    "at Google.Apis.Auth.OAuth2.TokenRefreshManager.RefreshTokenAsync()",
    "at Google.Apis.Auth.OAuth2.TokenRefreshManager.<GetAccessTokenForRequestAsync>g__LogException|10_0(Task task)",
    "at lambda_method1(Closure , Stream , ILambdaContext , Stream )",
    "at Amazon.Lambda.RuntimeSupport.Bootstrap.UserCodeLoader.Invoke(Stream lambdaData, ILambdaContext lambdaContext, Stream outStream) in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/UserCodeLoader.cs:line 145",
    "at Amazon.Lambda.RuntimeSupport.HandlerWrapper.<>c__DisplayClass8_0.<GetHandlerWrapper>b__0(InvocationRequest invocation) in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/HandlerWrapper.cs:line 56",
    "at Amazon.Lambda.RuntimeSupport.LambdaBootstrap.InvokeOnceAsync(CancellationToken cancellationToken) in /src/Repo/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs:line 176"
  ],
  "cause": {
    "errorType": "HttpRequestException",
    "errorMessage": "Connection refused (169.254.169.254:80)",
    "stackTrace": [
      "at Google.Apis.Http.ConfigurableMessageHandler.SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)",
      "at System.Net.Http.HttpClient.<SendAsync>g__Core|83_0(HttpRequestMessage request, HttpCompletionOption completionOption, CancellationTokenSource cts, Boolean disposeCts, CancellationTokenSource pendingRequestsCts, CancellationToken originalCancellationToken)",
      "at Google.Apis.Auth.OAuth2.AwsExternalAccountCredential.AwsMetadataServerClient.FetchMetadataAsync(String metadataUrl)",
      "at Google.Apis.Auth.OAuth2.AwsExternalAccountCredential.AwsSecurityCredentials.<>c__DisplayClass15_0.<<MaybeFromMetadataAsync>g__BuildSecurityCredentialsEndpointAsync|0>d.MoveNext()",
      "--- End of stack trace from previous location ---",
      "at Google.Apis.Auth.OAuth2.AwsExternalAccountCredential.AwsSecurityCredentials.MaybeFromMetadataAsync(AwsMetadataServerClient metadataClient, String credentialUrl)",
      "at Google.Apis.Auth.OAuth2.AwsExternalAccountCredential.AwsSecurityCredentials.FetchAsync(AwsMetadataServerClient metadataClient, String credentialUrl)",
      "at Google.Apis.Auth.OAuth2.AwsExternalAccountCredential.GetSubjectTokenAsyncImpl(CancellationToken taskCancellationToken)",
      "at Google.Apis.Auth.OAuth2.ExternalAccountCredential.GetSubjectTokenAsync(CancellationToken taskCancellationTokne)"
    ],
    "cause": {
      "errorType": "SocketException",
      "errorMessage": "Connection refused",
      "stackTrace": [
        "at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.ThrowException(SocketError error, CancellationToken cancellationToken)",
        "at System.Net.Sockets.Socket.AwaitableSocketAsyncEventArgs.System.Threading.Tasks.Sources.IValueTaskSource.GetResult(Int16 token)",
        "at System.Net.Sockets.Socket.<ConnectAsync>g__WaitForConnectWithCancellation|277_0(AwaitableSocketAsyncEventArgs saea, ValueTask connectTask, CancellationToken cancellationToken)",
        "at System.Net.Http.HttpConnectionPool.ConnectToTcpHostAsync(String host, Int32 port, HttpRequestMessage initialRequest, Boolean async, CancellationToken cancellationToken)"
      ]
    }
  }
}
amanda-tarafa commented 1 year ago

@iabdelkareem plese create a new issue with all this detail on it and I'll reply there.

iabdelkareem commented 1 year ago

@iabdelkareem plese create a new issue with all this detail on it and I'll reply there.

Thanks ^^ I've created the issue #2250