API Gateway possible to pass API key in url instead of in the header?

11,169

Solution 1

The API key may not be passed in the URL. This is by design. If the API key were in the URL, then anything which can see the URL could trivially capture the API key and use it to gain unauthorized access to the API. The would include users looking at the address bar and in some cases other script code running in the browser.

Solution 2

There are a few things to address in this question so I'll break them out an answer them one-by-one:

API calls with Excel

Excel has a few different ways to fetch API data: Excel Web Queries, VB Script + Libraries, and Get and Transform (formerly called Power Query) each with its own eccentricities.

Get and Transform

Get and Transform will let you do things like add headers, use POST requests and parse JSON but is, as of the time of this writing (Jan 2020) only available on Windows and is quite frustrating to use. It also requires that manipulations are done inside of the Excel document which can be difficult to modify if copies have been made.

VBScript

In addition to requiring a different type of Excel sheet, HTTP/s calls need to be made with a system library. In Windows, this may be available but in OSX, you'll need to link to a local copy of curl which is a convenient hack but should never leave a developer machine.

Excel Web Queries

Excel Web Queries have been around for a very long time and allow you to give Excel a URL to fetch data from. You cannot change headers, make a POST request or manipulate the data in flight but it'll work for all Excel users.

Security of long-lived tokens in an URL

Yes, sending your long-lived credentials in plaintext is insecure. However, now that DNS over HTTPS is becoming more common with browsers like Firefox and Chrome looking to turn it on by default, this may not be an issue forever.

Using API Gateway to authorize with tokens in a URL

While trying to figure this out, I ran across a dozen or so posts saying that this was not possible. When API Gateway first came out, this was true and the position of the AWS team was that, due to security, it was not something they wanted to support. Now, however, not only is it possible, but it's also one of the available options when using the AWS console to create a custom authorizer.

Authorizer

module.exports = function urlTokenAuthorizer(event, context, callback) {
  callback(null, {
    principalId: "excel",
    usageIdentifierKey: event.queryStringParameters["apikey"],
    policyDocument: {
      Version: "2012-10-17",
      Statement: [
        {
          Action: "execute-api:Invoke",
          Effect: "Allow",
          Resource: event.methodArn
        }
      ]
    }
  });
};

serverless config

service: serverless-secured-api

provider:
  name: aws
  runtime: nodejs12.x
  # The serverless framework will automatically create a usage plan and 
  # associate the key with it. If you're not using serverless, make sure you
  # have created a usage plan for your API and that the key is attached. 
  # Otherwise, you will get { message: forbidden } 
  apiKeys:
    - my-key
  # According to the AWS docs on Lambda Authorizers, "If the API uses a usage
  # plan (the apiKeySource is set to AUTHORIZER), the Lambda authorizer
  # function must return one of the usage plan's API keys as the
  # usageIdentifierKey property value."
  #
  # I did not find that to be true, for some reason. When you add an `apiKey`
  # in serverless, it creates a usage plan and assiciates it with that plan.
  # No matter what I did, nothing worked until I forced the apiKeySourceType
  # to AUTHORIZER. Without this line, every request will return:
  # { message: Forbidden }
  #
  # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-output.html
  apiGateway:
    apiKeySourceType: AUTHORIZER

functions:
  urlTokenAuthorizer:
    handler: handler.urlTokenAuthorizer
  getMedications:
    handler: handler.getMedications
    events:
      - http:
          path: get-medications
          method: GET
          private: true
          authorizer:
            name: urlTokenAuthorizer
            resultTtlInSeconds: 0
            # Configure your authorizer to look in the querystring for the key.
            # If it does not find a value here, the authorizer will not fire.
            identitySource: method.request.querystring.apikey
            # The default type is token which won't return information about 
            # the request. You can read more about token vs request authorizers
            # in the AWS docs here. 
            # https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-lambda-authorizer-input.html
            type: request

Solution 3

Although it might not be recommended, you can pass the API key in the query string by creating your own API Gateway lambda authorizer. Create a lambda function with the following contents:

exports.handler = function(event, context, callback) {
    callback(null, {
        principalId: "x-api-key",
        usageIdentifierKey: event.queryStringParameters["x-api-key"],
        policyDocument: {
            Version: "2012-10-17",
            Statement: [{
                Action: "execute-api:Invoke",
                Effect: "Allow",
                Resource: event.methodArn
            }]
        }
    });
};

The code above basically just maps the API key from the query parameter called x-api-key. API Gateway will take care of validating the API key and responding with 403 Forbidden if it is invalid.

Solution 4

This lambda function solves the problem when the client calling the API cannot pass apikey in the header, for example, this is a webhook of a third-party service.

We cannot read apikey in the body of a POST request, since the lambda authorizer does not have access to the request body.

Two solutions remain:

  1. Read apikey as a querystring parameter (the most dangerous way)
  2. Read apikey as the "password" field of the Basic Auth header, ignore the "user" field in this case. Yes, this is a dirty hack, but it works if the client supports Basic Auth

We use both solutions and add the ability to read apikey in the header (for clients who can send headers).

The read priority for apikey is as follows (In case the client sends apikey in three ways at once):

  1. In the header "x-api-key"
  2. In the "password" field Basic Auth
  3. In the querystring parameter "x-api-key"    An authorizer of type Request must be created for this function. Do not use caching, otherwise get error 401 see https://docs.aws.amazon.com/apigateway/api-reference/resource/authorizer/#identitySource The error is due to the need to fill in three parameters at once

If caching is indispensable, you will only have to choose one of three apikey read options. I did not find another solution

function parseApiKey(str) {
    if (!str)
      return;
    var m1 = str.match(/Basic (.+)/i);
    if (!m1)
        return;
    var decodeStr = Buffer.from(m1[1], 'base64').toString();
    var m2 = decodeStr.match(/(.*):(.+)/);
    if (m2) {
        return m2[2];
    }
}

exports.handler = function (event, context, callback) {
    callback(null, {
        'principalId': "Any value",
        'policyDocument': {
            'Version': '2012-10-17',
            'Statement': [{
                    'Action': 'execute-api:Invoke',
                    'Effect': 'Allow',
                    'Resource': event.methodArn
                }]
        },
        'usageIdentifierKey': 
                event.headers["x-api-key"] 
                || parseApiKey(event.headers["Authorization"]) 
                || event.queryStringParameters["x-api-key"] 
                || ""
    });
};
Share:
11,169
sky
Author by

sky

Updated on July 06, 2022

Comments

  • sky
    sky almost 2 years

    To access AWS API Gateway using the aws generated "API KEY", one must pass they key as a 'x-api-key' header. I know you can do this by 'curl', 'wget', postman and programmatically.

    Question: Is there any way the key can be passed as part of a url so that folks who do not have curl/wget/postman etc can call it using just the browser? In other words, is there a way to create a url such as following to perform api-key auth?

    https://<api-key>@www.aws-api-gw-url.com/path/to/get_my_data
    

    or

    https://www.aws-api-gw-url.com/path/to/get_my_data?x-api-key=<api-key>
    

    I didn't see any way to do this in the official docs or after searching the web. I also tried various combinations unsuccessfully.

  • sky
    sky over 7 years
    Thanks for the clear reply. While I agree with the concerns, in my business scenario, this is an acceptable risk, as my data I am trying to distribute is not that critical. By now allowing this scenario, all browser users are automatically excluded unless 1) I implement a custom basic auth via lambda (not good), or 2) just don't protect the API via a key (and therefore not able to do any API throttling/measurements). I wanted to give API Gateway RESTFul url to business people to load a csv directly into Excel but protected by API key so I could do billing/throttling,etc.
  • Mark B
    Mark B over 7 years
    What is "not good" about custom auth via Lambda? That seems like a perfectly good solution to your problem.
  • sky
    sky over 7 years
    "not good" is additional implementation + complexity, additional latency, and I will probably make mistakes, not able to manage keys thru aws UX, not sure how throttling/metering will work with API gateway. And its not really more secure because custom basic auth allows user:pwd@domain/path already - which could be captured per @miked in a url. I dont know of any other ways to allow browser/excel user to access API gateway url.
  • Adam
    Adam about 4 years
    For anyone else struggling to get this working, ensure that your authorizer is using identity source context: "apiId", that authorizer caching is disabled, and that the API Key Source is set to "AUTHORIZER" under API Settings.
  • Baer
    Baer about 4 years
    This answer no longer valid. This is a supported use case with a Lambda custom authorizer.