How do you pass Authorization header through API Gateway to HTTP endpoint?

Nick picture Nick · Jan 19, 2016 · Viewed 23.7k times · Source

I have an API behind an AWS API Gateway that needs to use the Authorization header for processing. I have unfortunately been unable to pass this to the backend for processing.

I have tried creating the Authorization HTTP Request Header in my Method Request and then creating the corresponding Authorization HTTP Header in my Integration Request (Authorization is mapped from method.request.header.Authorization in this case). I log all of the headers that the backend receives, and from the log, I can see other headers that I have listed in the Integration Request but not Authorization.

I have also tried creating a mapping template with Content-Type application/json and the template defined as

  {
      "AccountID": "$context.identity.accountId",
      "Caller": "$context.identity.caller",
      "User": "$context.identity.user",
      "Authorization": "$input.params().header.get('Authorization')",
      "UserARN": "$context.identity.userArn"
  }

Yet, the backend logs show that there is still no Authorization header nor any Authorization field in the JSON body. I also cannot see the user's ARN. I have seen other examples and threads where users have mentioned accessing the Authorization field on the event object that is passed into a Lambda function, but I am not using a Lambda function.

I have made sure to Deploy the API Gateway in both scenarios.

Does anyone know if there is some way I can pass the Authorization header through the API Gateway to my HTTP endpoint? Is there an alternative way to access the API caller's user name or ID?


Edit - Here's a snippet of the code I'm using to hit the API Gateway:

String awsAccessKey = "myaccesskey";
String awsSecretKey = "mysecretkey";

URL endpointUrl;
try {
    endpointUrl = new URL("https://<host>/<path>/<to>/<resource>?startDate=20151201&endDate=20151231");
} catch(Exception e) {
    throw new RuntimeException("Unable to parse service endpoint: " + e.getMessage());
}

Date now = new Date();

SimpleDateFormat sdf1 = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
sdf1.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateTS = sdf1.format(now);

String headerNames = "host;x-amz-date";
String queryParameters = "endDate=20151231&startDate=20151201";

String canonicalRequest = "GET\n" +
        "/<path>/<to>/<resource>\n" +
        queryParameters + "\n" +
        "host:<host>\n" +
        "x-amz-date:" + dateTS + "\n" +
        "\n" +
        headerNames + "\n" +
        "<sha256 hash for empty request body>";

System.out.println(canonicalRequest);

SimpleDateFormat sdf2 = new SimpleDateFormat("yyyyMMdd");
sdf2.setTimeZone(new SimpleTimeZone(0, "UTC"));
String dateStr = sdf2.format(now);
String scope =  dateStr + "/us-east-1/execute-api/aws4_request";
String stringToSign =
        "AWS4-HMAC-SHA256\n" +
               dateTS + "\n" +
               scope + "\n" +
               "hex encoded hash of canonicalRequest";

System.out.println(stringToSign);

byte[] kSecret = ("AWS4" + awsSecretKey).getBytes();
byte[] kDate = HmacSHA256(dateStr, kSecret);
byte[] kRegion = HmacSHA256("us-east-1", kDate);
byte[] kService = HmacSHA256("execute-api", kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
byte[] signature = HmacSHA256(stringToSign, kSigning);
String credentialsAuthorizationHeader = "Credential=" + awsAccessKey + "/" + scope;
String signedHeadersAuthorizationHeader = "SignedHeaders=" + headerNames;
String signatureAuthorizationHeader = "Signature=" + "hex encoded signature";
String authorization = "AWS4-HMAC-SHA256 "
        + credentialsAuthorizationHeader + ", "
        + signedHeadersAuthorizationHeader + ", "
        + signatureAuthorizationHeader;

Map<String, String> headers = new HashMap<String, String>();
headers.put("x-amz-date", dateTS);
headers.put("Host", endpointUrl.getHost());
headers.put("Authorization", authorization);
headers.put("Content-Type", "application/json");

HttpURLConnection connection = null;
try {
    connection = (HttpURLConnection) endpointUrl.openConnection();
    connection.setRequestMethod("GET");

    for (String headerKey : headers.keySet()) {
        connection.setRequestProperty(headerKey, headers.get(headerKey));
    }
    connection.setUseCaches(false);
    connection.setDoInput(true);
    connection.setDoOutput(true);

    InputStream is;
    try {
        is = connection.getInputStream();
    } catch (IOException e) {
        is = connection.getErrorStream();
    }

    BufferedReader rd = new BufferedReader(new InputStreamReader(is));
    String line;
    StringBuffer response = new StringBuffer();
    while ((line = rd.readLine()) != null) {
        response.append(line);
        response.append('\r');
    }
    rd.close();
    System.out.println(response.toString());
} catch (Exception e) {
    throw new RuntimeException("Error: " + e.getMessage(), e);
} finally {
    if (connection != null) {
        connection.disconnect();
    }
}

This is good enough to authenticate successfully and hit the HTTP endpoint on the backend.

Answer

Bob Kinney picture Bob Kinney · Jan 20, 2016

As noted in comments, the Authorization header includes incomplete information for you to establish who the user is, so I wouldn't recommend going this route. Additionally, if AWS_IAM auth is enabled, the Authorization header will be consumed by API Gateway.

If AWS_IAM auth is enabled and the signature is supplied correctly, the $context.identity parameters should reflect the credentials used to sign the request.

If you use the test invoke feature in the console, do you see the context fields being filled in?

Update: I'm unable to reproduce this issue. I have an API with the following mapping template:

#set($path = $input.params().path)
#set($qs = $input.params().querystring)
{
    "resource-path": "$context.resourcePath",
    "http-method": "$context.httpMethod",
    "identity": {
        #foreach($key in $context.identity.keySet())
            "$key": "$context.identity.get($key)"
        #if($foreach.hasNext), #end
        #end
    },
    "params": {
        #foreach($key in $path.keySet())
            "$key": "$path.get($key)"
        #if($foreach.hasNext), #end
        #end
    },
    "query": {
        #foreach($key in $qs.keySet())
            "$key": "$qs.get($key)"
        #if($foreach.hasNext), #end
        #end
    },
    "body": $input.json('$')
}

And a lambda function that simply spits back the input as output. When I sign the request and invoke the API, I get back the expected results:

{
  "resource-path":"/iam",
  "http-method":"GET",
  "identity":{ 
    "cognitoIdentityPoolId":"",
    "accountId":"xxxxxxxx",
    "cognitoIdentityId":"",
    "caller":"AIDXXXXXXXXXXX,
    "apiKey":"",
    "sourceIp":"54.xx.xx.xx",
    "cognitoAuthenticationType":"",
    "cognitoAuthenticationProvider":"",
    "userArn":"arn:aws:iam::xxxxxxxx:user/hackathon",
    "userAgent":"Java/1.8.0_31",
    "user":"AIDXXXXXXXXXXXXXX"
  },
  "params":{},
  "query":{},
  "body":{}
}