Skip to content

Instantly share code, notes, and snippets.

@NeilBostrom
Last active December 4, 2020 10:30
Show Gist options
  • Save NeilBostrom/cab8b9275e39bb90ecf8e06ab980664b to your computer and use it in GitHub Desktop.
Save NeilBostrom/cab8b9275e39bb90ecf8e06ab980664b to your computer and use it in GitHub Desktop.
AWS AWS4Signer HttpClientHandler implementation with GraphQL client example
static async Task Main(string[] args)
{
var options = new GraphQLHttpClientOptions
{
EndPoint = new Uri("https://countries.trevorblades.com/"),
HttpMessageHandler = new AWS4SignerMessageHandler(
new AmazonAppSyncClient(),
"--awsAccessKeyId--",
"--awsSecretAccessKey--")
};
var graphQLClient = new GraphQLHttpClient(options, new NewtonsoftJsonSerializer());
var graphQLResponse = await graphQLClient.SendQueryAsync<DataResponse>(new GraphQL.GraphQLRequest(
@"{
countries {
code,
name
}
}"));
if (graphQLResponse.Errors?.Any() == true)
throw new Exception(string.Join(Environment.NewLine, graphQLResponse.Errors.Select(e => e.Message)));
var data = graphQLResponse.Data;
Console.WriteLine($"Country count: {data.Countries.Count}");
}
public class DataResponse
{
public List<Country> Countries { get; set; }
}
public class Country
{
public string Code { get; set; }
public string Name { get; set; }
}
public class AWS4SignerMessageHandler : HttpClientHandler
{
private readonly AmazonServiceClient _client;
private readonly string _awsAccessKeyId;
private readonly string _awsSecretAccessKey;
private readonly string _sessionToken;
public AWS4SignerMessageHandler(AmazonServiceClient client, string awsAccessKeyId, string awsSecretAccessKey, string sessionToken = null)
{
_client = client;
_awsAccessKeyId = awsAccessKeyId;
_awsSecretAccessKey = awsSecretAccessKey;
_sessionToken = sessionToken;
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_sessionToken != null)
request.Headers.Add(HeaderKeys.XAmzSecurityTokenHeader, _sessionToken);
var signingRequest = new AmazonHttpRequestSigning(_client, request)
{
Content = await request.Content.ReadAsByteArrayAsync()
};
new AWS4Signer().Sign(signingRequest, _client.Config, null, _awsAccessKeyId, _awsSecretAccessKey);
foreach (var header in signingRequest.Headers)
{
if (header.Key != HeaderKeys.HostHeader && header.Key != HeaderKeys.XAmzSecurityTokenHeader && header.Key != HeaderKeys.UserAgentHeader)
request.Headers.TryAddWithoutValidation(header.Key, header.Value);
}
return await base.SendAsync(request, cancellationToken);
}
}
public class AmazonHttpRequestSigning : IRequest
{
public AmazonHttpRequestSigning(AmazonServiceClient client, HttpRequestMessage request)
{
HttpMethod = request.Method.Method;
Endpoint = new Uri(request.RequestUri.AbsoluteUri[..request.RequestUri.AbsoluteUri.LastIndexOf(request.RequestUri.AbsolutePath)]);
ResourcePath = request.RequestUri.AbsolutePath;
Headers = request.Headers.ToDictionary(e => e.Key, e => e.Value.FirstOrDefault());
AlternateEndpoint = client.Config.RegionEndpoint;
AuthenticationRegion = client.Config.AuthenticationRegion;
}
public Uri Endpoint { get; set; }
public string DeterminedSigningRegion { get; set; }
public RegionEndpoint AlternateEndpoint { get; set; }
public IDictionary<string, string> Headers { get; }
public IDictionary<string, string> SubResources { get; }
public string AuthenticationRegion { get; set; }
public string HttpMethod { get; set; }
public string ResourcePath { get; set; }
public bool UseQueryString { get; set; }
public bool UseChunkEncoding { get; set; }
public Stream ContentStream { get; set; }
public byte[] Content { get; set; }
public bool? DisablePayloadSigning { get; set; }
public string OverrideSigningServiceName { get; set; }
public IDictionary<string, string> PathResources { get; set; }
public int MarshallerVersion { get; set; }
#region Not Implemented
public string RequestName => throw new NotImplementedException();
public IDictionary<string, string> Parameters => throw new NotImplementedException();
public ParameterCollection ParameterCollection => throw new NotImplementedException();
public bool SetContentFromParameters { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public long OriginalStreamPosition { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public string ServiceName => throw new NotImplementedException();
public AmazonWebServiceRequest OriginalRequest => throw new NotImplementedException();
public string HostPrefix { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public bool Suppress404Exceptions { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public AWS4SigningResult AWS4SignerResult { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public string CanonicalResourcePrefix { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public bool UseSigV4 { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public void AddPathResource(string key, string value) => throw new NotImplementedException();
public void AddSubResource(string subResource) => throw new NotImplementedException();
public void AddSubResource(string subResource, string value) => throw new NotImplementedException();
public string ComputeContentStreamHash() => throw new NotImplementedException();
public string GetHeaderValue(string headerName) => throw new NotImplementedException();
public bool HasRequestBody() => throw new NotImplementedException();
public bool IsRequestStreamRewindable() => throw new NotImplementedException();
public bool MayContainRequestBody() => throw new NotImplementedException();
#endregion
}
@sudipkhansema4
Copy link

Hi Neil,
Thanks for your post. I just inquisitive to know , the awsAccessKeyId and awsSecretAccessKey you are using for which user and what are the supported role for him.

@NeilBostrom
Copy link
Author

Fraid I don't have that code kicking around anymore. The project I was on took a different turn and this code didn't live long enough. I'm pretty sure the policy was something along the lines of:

{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Effect": "Allow",
         "Action": [
            "appsync:GraphQL"
         ],
         "Resource": [
            "arn:aws:appsync:us-west-2:123456789012:apis/YourGraphQLApiId/*"
         ]
      }
   ]
}

Quoted from https://docs.aws.amazon.com/appsync/latest/devguide/security.html

@joseph-adam
Copy link

Hi Neil,

Interested to know if the reference above if the GraphQLClient above is the .net GraphQL client lib (https://github.com/graphql-dotnet/graphql-dotnet) or is this from somewhere else? If I understand correctly, by signing the requests we can use the graphql client lib to connect to aws appsync? Thank you for any pointers.

@NeilBostrom
Copy link
Author

@joseph-adam That is correct! It is indeed using this GraphQLClient. Yep, this handler adds AWS signing support to the GraphQLClient library to enable you to use it again AWS AppSync.

@joseph-adam
Copy link

Thank you Neil!

@NeilBostrom
Copy link
Author

Turns out my gist was a little out of date with the latest libraries. Updated gist to match GraphQL.Client 3.2.0 and AWSSDK.AppSync 3.5.1.23

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment