Invoking Lambda through the AWS API Gateway with PowerShell

The ability to invoke Lamda functions via REST based web API is incredibly powerful.  As the API gateway is relatively new functionality, the documentation surrounding REST requests and PowerShell is sparse with official documentation sometimes confusing and appearing to be contradictory.

The attached PowerShell routine can be used to make signed GET and POST requests to an API advertised Lambda script.

The script accepts a JSON payload as a text string.  With a PUT, properties can be directly referenced within Lambda using directly using a call such as: “event['name']”

Currently there are quirks with using a JSON mapping template and GET requests to Lambda – a workaround can be read at:    https://forums.aws.amazon.com/thread.jspa?messageID=663593&tstart=0#663593

The script also demonstrates the Signature version 4 signing process for AWS API Gateway usage.

########################
# SUPPORTING FUNCTIONS

<#  
   .Synopsis
     Retrieves the current Universal time in required format for AWS
#>
function UniversalTime{
    $curdate = Get-Date
    $UniversalTime = ($curdate.ToUniversalTime().Year).ToString("0000") `
    + ($curdate.ToUniversalTime().Month).ToString("00") `
    + ($curdate.ToUniversalTime().Day).ToString("00") + "T" `
    + ($curdate.ToUniversalTime().Hour).ToString("00") `
    + ($curdate.ToUniversalTime().Minute).ToString("00") `
    + ($curdate.ToUniversalTime().Second).ToString("00") + "Z"

    return $UniversalTime
 }

<#  
   .Synopsis
     Retrieves the current Universal time in short-date format used by AWS
#>
function ShortDate{

    $curdate = Get-Date
    $Shortdate = ($curdate.ToUniversalTime().Year).ToString("0000") `
    + ($curdate.ToUniversalTime().Month).ToString("00") `
    + ($curdate.ToUniversalTime().Day).ToString("00") 

    return $Shortdate
 }



<#  
   .Synopsis
     Retrieves an SHA hash of a string as required by AWS Signature 4 
#>

function [string]sha($message) {
   $sha256 = new-object -TypeName System.Security.Cryptography.SHA256Managed
   $utf8   = new-object -TypeName System.Text.UTF8Encoding
   $hash   = [System.BitConverter]::ToString($sha256.ComputeHash($utf8.GetBytes($message)))
   return $hash.replace('-','').toLower()
}

<#  
   .Synopsis
     HMACSHA256 signing function used in the construction of a "Signature 4 " request
#>
function [byte[]]hmacSHA256([byte[]]$key, [string]$message) {
   $hmacsha = New-Object System.Security.Cryptography.HMACSHA256
   $hmacsha.key = $key
   return $hmacsha.ComputeHash([Text.Encoding]::UTF8.GetBytes($message))
}

<#  
   .Synopsis
    The AWS Signature version 4 creation routine
#>
function GetSignatureKey([String]$AWSAccessKey,[String]$shortdate,[String]$AWSRegion,[String]$AWSService){
   $kSecret            = [System.Text.Encoding]::UTF8.GetBytes("AWS4"+$AWSAccessKey)
   $kDate              = hmacSHA256 $kSecret $shortdate 
   $kRegion            = hmacSHA256 $kDate $AWSRegion 
   $kService           = hmacSHA256 $kRegion $AWSService 
   $kSigningKey        = hmacSHA256 $kService "aws4_request" 
   return $kSigningKey
}

###########
# Main
# Used for constructing a POST request for the AWS API Gateway

 <#
  .Synopsis
    Submits a request to the AWS API Gateway and retrieves the results

   .Description
    This function demonstrates using PowerShell to submit REST based requests to 
    Amazon's AWS API gateway using Signature 4 signing.

    There are quirks with using GET requests, Mapping Templates and Lambda functions 
    through the API Gateway. A workaround can be read:
    https://forums.aws.amazon.com/thread.jspa?messageID=663593&tstart=0#663593

    POST requests bypass the need for mappins templates.
    
    The example correctly shows the use of the AWS signature version 4.

   .Example
    Get-WebResponse -Method POST `
                -EndpointURI "https://sacvo76l6b.execute-api.ap-northeast-1.amazonaws.com/prod/ExamplePostFunction" `
                -AWSAccessID "MyAccessID" `
                -AWSAccessKey "MySecRetAccEssKey" `
                -Payload $ExamplePayload 
   .Example
    Get-WebResponse -Method "GET" `
                -EndpointURI "https://sacvo76l6b.execute-api.ap-northeast-1.amazonaws.com/prod/ExampleGetFunction?name=Bernie&address=Washington" `
                -AWSAccessID "MyAccessID" `
                -AWSAccessKey "MySecRetAccEssKey"


   .Parameter EndpointURI
      The Fully Qualified endpoint URL shown attached to a Lambda function
   .Parameter AWSAccessID 
      The Access Key ID created for a user account under IAM / Security Credentials  
   .Parameter AWSAccessKey 
      The secret Access Key ID created for a user account under IAM / Security Credentials 
   .Parameter Payload 
      A text string representing the body or payload of the request - typically JSON
   .Inputs
    [string]
   .OutPuts
    [string]
   .Notes
    NAME:  Get-WebResponse
    AUTHOR: Laurie Rhodes
    LASTEDIT: 13/02/2016
    KEYWORDS:
   .Link
     Http://www.laurierhodes.info
 #Requires -Version 4.5
 #>
function Get-WebResponse{
[CmdletBinding()]
param(
      [Parameter(Mandatory = $true,
		          Position = 0,
                  HelpMessage="The method of web request such as POST, GET")]
      [string]
      [ValidateNotNullOrEmpty()]
      $Method,

      [Parameter(Mandatory = $true,
		           Position = 1,
                   HelpMessage="the AWS endpoint URI to query")]
      [string]
      [ValidateNotNullOrEmpty()]
      $EndpointURI,

      [Parameter(Mandatory = $false,
		           Position = 2,
                   HelpMessage="The access ID of a User authorised to use the web service")]
      [string]
      [ValidateNotNullOrEmpty()]
      $AWSAccessID,

      [Parameter(Mandatory = $false,
		           Position = 3,
                   HelpMessage="The Secret AWS Access Key")]
      [string]
      [ValidateNotNullOrEmpty()]
      $AWSAccessKey,

      [Parameter(Mandatory = $false,
		           Position = 4,
                   HelpMessage="The Payload / Body of a POST request")]
      [string]
      #[ValidateNotNullOrEmpty()]
      $Payload

) #end param


#eg endpoint https://sacvo76l6b.execute-api.ap-northeast-1.amazonaws.com/prod/ExampleFunction
$EndpointURI = $EndpointURI.replace("https://","")

# The AWSHost is the DNS name from the request which contains
# the personal Domain ID, AWS Region and AWS Service details
$AWShost          = $EndpointURI.Substring(0,($EndpointURI.IndexOf("/")))
$hostarray        = $AWShost.Split(".")
$AssignedDomainID = $hostarray[0]
$AWSService       = $hostarray[1]
$AWSRegion        = $hostarray[2]

#post requests have an empty string as a payload
if($Method -eq "GET"){
     $Payload ="" 
   }

#The canonical Request & any Request Query are determined from the remainder of the URI
if ($EndpointURI.Contains("?")){
   #The URI contains a URI and Query
   $URIParams      = $EndpointURI.Substring(($EndpointURI.IndexOf("/")),($EndpointURI.Length -$EndpointURI.IndexOf("/") ))
   $CanonicalURI   = $URIParams.Substring(0,($URIParams.IndexOf("?") ))
   $CanonicalQuery = ($URIParams.Substring(($URIParams.IndexOf("?")),($URIParams.Length -$URIParams.IndexOf("?") ))).Replace("?","")
   
}
else
{
  #The URI does not contain a query string
  $CanonicalURI    = $EndpointURI.Substring(($EndpointURI.IndexOf("/")),($EndpointURI.Length -$EndpointURI.IndexOf("/") ))
}



$shortdate      = Shortdate
$universaltime  = UniversalTime
$fullHostname   = "$($AssignedDomainID).$($AWSService).$($AWSRegion).amazonaws.com"
$URI            =  "https://$($AssignedDomainID).$($AWSService).$($AWSRegion).amazonaws.com$($CanonicalURI)"
$PayloadBytes   = ([System.Text.Encoding]::UTF8.GetBytes($Payload)).Length 



write-host "AWSAccessID      = $($AWSAccessID)"  
write-host "AWSAccessKey     = $($AWSAccessKey)"         
write-host "AssignedDomainID = $($AssignedDomainID)"     
write-host "AWSService       = $($AWSService)"           
write-host "AWSRegion        = $($AWSRegion)"           
write-host "CanonicalURI     = $($CanonicalURI)"        
write-host "CanonicalQuery   = $($CanonicalQuery)"      
write-host "RequestMethod    = $($Method)"       


#Query String URI Safe
#http utility doesnt produce the results expected by AWS

$CanonicalQuery = $CanonicalQuery.Replace("=","%3D")
$CanonicalQuery = $CanonicalQuery.Replace("&","%26")


############################################
#  Create the Canonical Request
#
#

$CanonicalRequest = "" 
$CanonicalRequest = $Method +"`n"
$CanonicalRequest = $CanonicalRequest + $CanonicalURI +"`n"
if(!$CanonicalQuery){ #no query string
    $CanonicalRequest = $CanonicalRequest + $CanonicalQuery  +"`n"
}
else #query string exists
{
$CanonicalRequest = $CanonicalRequest + $CanonicalQuery +"=" +"`n"
}
#### Canonical Headers and values
   #Different AWS Services have different Header requirements
   # all have some common headers

   $CanonicalHeaderHashTable = @{} 
   $CanonicalHeaderHashTable.Add("host",$fullHostname)
   $CanonicalHeaderHashTable.Add("x-amz-date",$universaltime)

   # Add additional headers as required
   # API Gateway Service
   if($AWSService -eq "execute-api"){
      if($Method -eq "POST"){
          write-host "POST Request received"
          $CanonicalHeaderHashTable.Add("content-length",$($PayloadBytes))
          $CanonicalHeaderHashTable.Add("content-type","application/json")
          
      }
   }


   $cHeaderList = $CanonicalHeaderHashTable.GetEnumerator() | Sort Name| % {
       [string]$newheader = ""
       $newheader = ($_.Key) +":" + $_.Value 
       $newheader = $newheader.Trim()
       $newheader = $newheader.Replace("`r","")
       $CanonicalRequest = $CanonicalRequest + $newheader +"`n"
      }
 
$CanonicalRequest = $CanonicalRequest +"`n"

   #Each key from the Canonical Header HashTable needs to be listed
   [System.Collections.ArrayList]$SignedHeadersArray = @()
    
   $CanonicalHeaderHashTable.GetEnumerator() | Sort Name| % {
         $null = $SignedHeadersArray.Add($_.Key)
       }
      
   $SignedHeadersList = ($SignedHeadersArray -join ";") 
   $CanonicalRequest = $CanonicalRequest + $SignedHeadersList +"`n"

    $CanonicalRequest = $CanonicalRequest + $(sha $Payload) 




##### Create the Signed String
   # Add Algorithm
   $Signedstring = "AWS4-HMAC-SHA256" +"`n"
   #  Add UTC Request Date
   $Signedstring = $Signedstring +$universaltime + "`n"
   #  Add CredentialScope
   $Signedstring = $Signedstring +    "$($shortdate)/$($AWSRegion)/$($AWSService)/aws4_request" + "`n"
   #  Add Canonical Request Hash
   $Signedstring = $Signedstring + $(sha $CanonicalRequest)# +"`n"



############################
# Sign the Created 'Signed String'


$kSigningKey = GetSignatureKey $AWSAccessKey $shortdate $AWSRegion $AWSService
# HexEncoded version is signature

$encSignedString    = hmacSHA256 $kSigningKey $Signedstring 
[string]$hexstring = [System.BitConverter]::ToString($encSignedString )
$signature = $hexstring.replace("-","")
$signature = $signature.ToLower()


 
############################
# Add Headers for dispatching web request
#
#
$headers = New-Object "System.Collections.Generic.Dictionary[[String],[String]]"
$headers.Clear()

$CanonicalHeaderHashTable.GetEnumerator() | Sort Name| % {
       if($_.Key -eq "host"){
         # Host Key is derived from the domain name - it can't be set separately
       }
       else{ 
          $headers.Add(($_.Key),($_.value)) 
       }
      }

      # API Gateway Service
      # The authorization key can't be signed but must still be added to theheader
      if($AWSService -eq "execute-api"){
            $headers.Add("authorization", "AWS4-HMAC-SHA256 Credential=$($AWSAccessID)/$($shortdate)/$($AWSRegion)/$($AWSService)/aws4_request,SignedHeaders=$($SignedHeadersList),Signature=$($signature)")
      }

   if($Method -eq "POST"){
      $Result = Invoke-RestMethod -Uri $URI -Body $Payload -Method $Method -Headers $Headers 
    }

   if($Method -eq "GET"){
      write-host $CanonicalQuery
      write-host "$($URI)?$($CanonicalQuery)"
      $Result = Invoke-RestMethod -Uri ("$($URI)?$($CanonicalQuery)") -Method $Method -Headers $Headers 
    }

write-host ""

return $Result

} #end Get-Webresponse