LazyStack

LzHttpClient Class

The LzHttpClient class is responsible for making signed and unsigned calls to AWS ApiGateways. There are two types of signed HttpRequests used in LazyStack calls to AWS Gateways:

The AWS "special sauce" is isolated in the LzHttpClient. Let's examine the implementation of LzHttpClient to answer the following questions:

  • How does LzHttpClass know which ApiGateway to call?
  • How does LzHttpClass know what security scheme to apply on the call?
  • How do we configure LzHttpClass to call the local WebApi? (where WebApi was generated by LazyStack)

First, take the time to read through the LzHttpClient class code.

using Microsoft.Extensions.Configuration;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net.Http;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace LazyStackAuth
{
    public class LzHttpClient : ILzHttpClient
    {
        public LzHttpClient(
            IConfiguration appConfig,
            AuthProviderCognito authProvider,
            string localApiName = null) :
#if DEBUG
        this(appConfig, authProvider, new HttpClient(GetInsecureHandler()), localApiName)
        { }
#else
        this(appConfig, methodMap, authProvider, new HttpClient(), localApiName)
        { }
#endif

        public LzHttpClient(
            IConfiguration appConfig,
            AuthProviderCognito authProvider,
            HttpClient httpClient,
            string localApiName = null)
        {
            this.httpClient = httpClient;
            this.localApiName = localApiName;
            if (!string.IsNullOrEmpty(localApiName))
                this.localApi = appConfig.GetSection($"LocalApis:{localApiName}").Get<LocalApi>();
            this.awsSettings = appConfig.GetSection("Aws").Get<AwsSettings>();
            this.authProvider = authProvider;
            this.methodMap = appConfig.GetSection("MethodMap").GetChildren().ToDictionary(x => x.Key, x => x.Value);
        }

        readonly HttpClient httpClient;
        readonly AwsSettings awsSettings;
        readonly LocalApi localApi;
        readonly string localApiName = string.Empty;
        AuthProviderCognito authProvider;
        Dictionary<string, string> methodMap;


        // Note: CallerMember is inserted as a literal by the compiler in the IL so there is no 
        // performance penalty for using it.
        public async Task<HttpResponseMessage> SendAsync(
            HttpRequestMessage requestMessage,
            HttpCompletionOption httpCompletionOption,
            CancellationToken cancellationToken,
            [CallerMemberName] string callerMemberName = null)
        {

            if (!methodMap.TryGetValue(callerMemberName, out string apiGatewayName))
                throw new Exception($"Error: {callerMemberName} not found in AwsSettings MethodMap");

            if (!awsSettings.ApiGateways.TryGetValue(apiGatewayName, out AwsSettings.Api api))
                throw new Exception($"Error: {apiGatewayName} not found in AwsSettings ApiGateways dictionary");

            var securityLevel = api.SecurityLevel;

            if (!string.IsNullOrEmpty(localApiName))
            {
                var uriBuilder = new UriBuilder(localApi.Scheme, localApi.Host, localApi.Port);

                // Issue: the AspNetCore server rejects a query with the ? encoded as %3F 
                // so the following doesn't work 
                // uriBuilder.Path = requestMessage.RequestUri.ToString(); 
                // the assignment encodes path query as %3F instead of ?
                // Here we encode the path separately and then build a
                // a new Uri from the uriBuilder and the path.
                var path = requestMessage.RequestUri.ToString(); // Unencoded
                path = Uri.EscapeUriString(path); // Encoded properly
                requestMessage.RequestUri = new Uri(uriBuilder.Uri, path);
            }
            else
            {
                var awshost = $"{api.Id}.{api.Service}.{awsSettings.Region}.{api.Host}";

                var uriBuilder = (api.Port == 443)
                    ? new UriBuilder(api.Scheme, awshost)
                    : new UriBuilder(api.Scheme, awshost, api.Port);

                var path = (!string.IsNullOrEmpty(api.Stage))
                    ? "/" + api.Stage + "/" + requestMessage.RequestUri.ToString()
                    : requestMessage.RequestUri.ToString();

                // Issue: the AspNetCore server rejects a query with the ? encoded as %3F 
                // so the following doesn't work 
                // uriBuilder.Path = requestMessage.RequestUri.ToString(); 
                // the assignment encodes path query as %3F instead of ?
                // Here we encode the path separately and then build a
                // a new Uri from the uriBuilder and the path.
                path = Uri.EscapeUriString(path); // Encoded properly
                requestMessage.RequestUri = new Uri(uriBuilder.Uri, path);
            }

            Debug.WriteLine($"requestMessage.Path {requestMessage.RequestUri.ToString()}");
            try
            {
                HttpResponseMessage response = null;
                // Note: If the call is being made against a local host then that host
                // will by default not pay any attention to the authorization header attached by 
                // the JWT or AwsSignatureVersion4 cases below. We assign the Headers
                // anyway in case you want to implement handling these headers 
                // in your local host for testing or any other purpose.
                switch (securityLevel)
                {
                    case AwsSettings.SecurityLevel.None:
                        response = await httpClient.SendAsync(
                            requestMessage,
                            httpCompletionOption,
                            cancellationToken);
                        break;

                    case AwsSettings.SecurityLevel.JWT:
                        // Use JWT Token signing process
                        requestMessage.Headers.Add("Authorization", authProvider.CognitoUser.SessionTokens.IdToken);
                        response = await httpClient.SendAsync(
                            requestMessage,
                            httpCompletionOption,
                            cancellationToken);
                        break;

                    case AwsSettings.SecurityLevel.AwsSignatureVersion4:
                        // Use full request signing process
                        // Get Temporary ImmutableCredentials :  AccessKey, SecretKey, Token
                        // This will refresh immutable credentials if necessary
                        // Calling AwsSignatureVersion4 extension method -- this signs the request message
                        var iCreds = await authProvider.Credentials.GetCredentialsAsync();

                        response = await httpClient.SendAsync(
                            requestMessage,
                            httpCompletionOption,
                            cancellationToken,
                            awsSettings.Region,
                            api.Service,
                            iCreds);
                        break;
                }
                return response;

            }
            catch (Exception e)
            {
                Debug.WriteLine($"Error: {e.Message}");
            }

            return new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);

        }
#if DEBUG        
        //https://docs.microsoft.com/en-us/xamarin/cross-platform/deploy-test/connect-to-local-web-services
        //Attempting to invoke a local secure web service from an application running in the iOS simulator 
        //or Android emulator will result in a HttpRequestException being thrown, even when using the managed 
        //network stack on each platform.This is because the local HTTPS development certificate is self-signed, 
        //and self-signed certificates aren't trusted by iOS or Android.
        public static HttpClientHandler GetInsecureHandler()
        {
            var handler = new HttpClientHandler
            {
                ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
                {
                    if (cert.Issuer.Equals("CN=localhost"))
                        return true;
                    return errors == System.Net.Security.SslPolicyErrors.None;
                }
            };
            return handler;
        }
#endif
        public void Dispose()
        {
            httpClient.Dispose();
        }
    }
}

Now, let's examine and discuss the LzHttpClient class constructor(s) arguments:


    public LzHttpClient(
        IConfiguration appConfig,
        AuthProviderCognito authProvider,
        HttpClient httpClient,
        string localApiName = null)
    {
        this.httpClient = httpClient;
        this.localApiName = localApiName;
        if (!string.IsNullOrEmpty(localApiName))
            this.localApi = appConfig.GetSection($"LocalApis:{localApiName}").Get<LocalApi>();
        this.awsSettings = appConfig.GetSection("Aws").Get<AwsSettings>();
        this.authProvider = authProvider;
        this.methodMap = appConfig.GetSection("MethodMap").GetChildren().ToDictionary(x => x.Key, x => x.Value);
    }

It's useful to see how an instance of LzHttpStack is created to establish some context. Consider this snippet of code.

How does LzHttpClass know which ApiGateway to call?

LzHttpClient reads the "MessageMap" section of appConfig to get the MessageMap dictionary where the key is the ClientSDK method name and the value is the ApiGateway to call.

How does LzHttpClass know what security scheme to apply on the call?

The LzHttpClass reads the "Aws" section in appConfig. The AwsSettings include connection and security information for each ApiGateway used in the stack. See the AwsSettings documentation for details on this configuration.

How dow we configure LzHttpClass to call a the local WebApi?

LzHttpClient reads the "LocalApis" section of appConfg to get the local WebApi project configuration instead of ApiGateways. To enable that feature you pass the constructor the local api name in the localApiName parameter. By default, LazyStack configures two local WebApi hosts:


{
    "LocalApis": {
        "Local": {
        "Scheme": "https",
        "Host": "localhost",
        "Port": 5001
        },
        "LocalAndroid": {
        "Scheme": "https",
        "Host": "10.0.2.2",
        "Port":  5001
        }
    }
}

The LocalAndriod api is necessary when using the Andriod device emulator as the emulator includes a proxy on port 10.0.2.2 that redirects traffic to localhost.

In addition, both the iOS simulator and Android emulator require special handling for the https scheme as explained in the Microsoft documentation here.

Attempting to invoke a local secure web service from an application running in the iOS simulator or Android emulator will result in a HttpRequestException being thrown, even when using the managed network stack on each platform.This is because the local HTTPS development certificate is self-signed, and self-signed certificates aren't trusted by iOS or Android.

LzHttpClient includes the GetInsecureHandler() method to avoid this exception when calling the local WebApi.