Using a Google Cloud service account and AssumeRoleWithWebIdentity to authenticate to AWS in C#
In the last post we looked at how to set up a trust policy and role in AWS so that we can use a Google ID token to authenticate to AWS. But how do we actually use this in C#?
The AWS SDK contains a class AssumeRoleWithWebIdentityCredentials – but on closer look, this class turns out to be not too useful since it expects us to provide the input token (the service account’s ID token) in a file. Assuming that out code runs on Cloud Run or on a VM instance, it would make more sense to:
- Call
GoogleCredential.GetApplicationDefaultAsync()
to get aGoogleCredential
object which automatically fetches tokens from the metadata server. - Use the
GoogleCredential
object to construct an AWS credential.
To get an ID token from a GoogleCredential
, we can call GetOidcTokenAsync,
which is part of the IOidcTokenProvider
interface. So let’s create a class that
- Accepts an
IOidcTokenProvider
object as input. - Derives from
AWSCredentials
so that it can be used in the AWS SDK.
Instead of deriving from AWSCredentials
directly, we can derive from RefreshingAWSCredentials
.
As its name suggests, RefreshingAWSCredentials
caches credentials and only refreshes them when
necessary, limiting the number of times we actually have to call AssumeRoleWithWebIdentityCredentials
.
All we have to do is implement GenerateNewCredentials
and its async counterpart GenerateNewCredentialsAsync
:
using Amazon.Runtime;
using Amazon.SecurityToken;
using Google.Apis.Auth.OAuth2;
using System.Threading;
using System.Threading.Tasks;
namespace CredentialUtils.GcpToAzure
{
public class AwsFederatedServiceAccountCredential : RefreshingAWSCredentials
{
public IOidcTokenProvider SourceCredential { get; }
public string RoleArn { get; }
public string Audience { get; }
public int DurationSeconds { get; set; }
public string RoleSessionName { get; set; }
public AwsFederatedServiceAccountCredential(
IOidcTokenProvider sourceCredential,
string audience,
string roleArn)
{
this.SourceCredential = sourceCredential;
this.Audience = audience;
this.RoleArn = roleArn;
}
protected override CredentialsRefreshState GenerateNewCredentials()
{
return GenerateNewCredentialsAsync().Result;
}
protected override async Task<CredentialsRefreshState> GenerateNewCredentialsAsync()
{
//
// (1) Get a Google-issued ID token token.
//
var idTokenSource = await this.SourceCredential.GetOidcTokenAsync(
OidcTokenOptions.FromTargetAudience(this.Audience));
var idToken = await idTokenSource.GetAccessTokenAsync();
//
// (2) Use token to assume a role and obtain temporary
// AWS credentials.
//
using (var stsClient = new AmazonSecurityTokenServiceClient())
{
var response = await stsClient.AssumeRoleWithWebIdentityAsync(
new Amazon.SecurityToken.Model.AssumeRoleWithWebIdentityRequest()
{
DurationSeconds = this.DurationSeconds,
RoleArn = this.RoleArn,
RoleSessionName = this.RoleSessionName,
WebIdentityToken = idToken
},
CancellationToken.None);
return new CredentialsRefreshState(
response.Credentials.GetCredentials(),
response.Credentials.Expiration);
}
}
}
}
We can use this class like so:
var googleCredential = await GoogleCredential.GetApplicationDefaultAsync();
var awsCredentials = new AwsFederatedServiceAccountCredential(
googleCredential,
"aws-trust-1", // audience
"arn:aws:iam::0123456789000:role/google-cloud-myapp" // role to assume (full ARN)
)
{
DurationSeconds = 900,
RoleSessionName = "test-session"
};
Now we have a convenient way to authenticate from Google Cloud to AWS and vice versa.