aws / aws-sdk-js-v3

Modularized AWS SDK for JavaScript.
Apache License 2.0
3.12k stars 578 forks source link

SignatureV4 Presign without Session Token #3417

Closed kazkansouh closed 9 months ago

kazkansouh commented 2 years ago

Describe the bug

From sigv4-create-canonical-request.html it has the following:

For some services, you must include the X-Amz-Security-Token query parameter in the canonical (signed) query string. For other services, you add the X-Amz-Security-Token parameter at the end, after you calculate the signature. For details, see the API reference documentation for that service.

Unfortunately, the @aws-sdk/signature-v4 does not support this feature during pre-sign. It assumes that if the session token is present it will always add the X-Amz-Security-Token canonical query string. This means its not possible to generate a pre-signed URL for some services. E.g. iotdevicegateway is one example of such a service.

A configuration option should be added to the presign function to support this feature.

Your environment

SDK version number

@aws-sdk/signature-v4@3.54.0

Is the issue in the browser/Node.js/ReactNative?

Observed in browser/React - but should affect all.

Details of the browser/Node.js/ReactNative version

Paste output of npx envinfo --browsers or node -v or react-native -v

$ npx envinfo --browsers
  Browsers:
    Chrome: 99.0.4844.51
    Firefox: 91.7.0esr

Steps to reproduce

Generate temporary credentials with sts and then use the SignatureV4.presign function to generate a URL for the iotdevicegateway service. This will result in a request that is rejected with a 403 HTTP error.

Below is some code demonstrating this:

import { SignatureV4 } from '@aws-sdk/signature-v4'
import { Sha256 } from '@aws-crypto/sha256-js'
import { HttpRequest } from '@aws-sdk/protocol-http'
import { formatUrl } from '@aws-sdk/util-format-url'

const signer = new SignatureV4({
  credentials,
  region: 'eu-west-2',
  service: 'iotdevicegateway',
  sha256: Sha256,
})
const req = new HttpRequest({
  protocol: 'wss',
  hostname: 'xxxxxxxxxxxxxx-ats.iot.eu-west-2.amazonaws.com',
  path: '/mqtt',
  method: 'GET',
  headers: { host: 'xxxxxxxxxxxxxx-ats.iot.eu-west-2.amazonaws.com' },
})
signer.presign(req, { expiresIn: 3600 }).then((req) => {
  console.log(req)
  console.log(formatUrl(req))
}

In the above credentials is obtained using @aws-sdk/credential-providers. Importantly, the credential obtained has sessionToken set.

Observed behavior

Pre-signed request is rejected with HTTP 403 - forbidden.

Expected behavior

Pre-signed request is is accepted.

Additional context

After looking at the source code, there is a workaround by removing the sessionToken from the credential and then adding it back in after the request has been pre-signed. See the following code:

credentials().then((credentials) => {
  const signer = new SignatureV4({
    credentials: { ...credentials, sessionToken: undefined },
    region: 'eu-west-2',
    service: 'iotdevicegateway',
    sha256: Sha256,
  })
  const req = new HttpRequest({
    protocol: 'wss',
    hostname: 'xxxxxxxxxxxxxx-ats.iot.eu-west-2.amazonaws.com',
    path: '/mqtt',
    method: 'GET',
    headers: { host: 'xxxxxxxxxxxxxx-ats.iot.eu-west-2.amazonaws.com' },
  })
  signer.presign(req, { expiresIn: 3600 }).then((req) => {
    if (
      typeof credentials.sessionToken === 'string' &&
      typeof req.query === 'object'
    ) {
      req.query['X-Amz-Security-Token'] = credentials.sessionToken
    }
    console.log(req)
    console.log(formatUrl(req))
  }
}

While this works, it is not ideal as it relies on internal behaviour of the SDK.

huntharo commented 2 years ago

@kazkansouh - I tried reproducing this issue using the new Lambda URLs that support IAM signing.

I was able to submit signed and presigned requests without issue without using your workaround (which I had tried but it failed due to a temporary bug with query strings being passed to Lambda URLs when signing was enabled).

I'm curious if you could try sending a request with your code (without the workaround on session token) to a Lambda URL that you have deployed. If that works then the issue may be more a bug (perhaps one that they have to keep now for backwards compatibility) or a spec difference used by IOT that the libraries currently do not support well.

I created a repo here that deploys a Lambda URL that requires IAM auth and a CLI tool that signs/presigns the requests and sends them, showing that there is no issue when the target is something other than IOT:

https://github.com/huntharo/lambda-url-signing

kazkansouh commented 2 years ago

Hello @huntharo, thankyou for the info.

I've taken a look at your code but not run it. Just to help clarify the comment in your code:

The underlying issue is that different AWS API endpoints can expect the SigV4 to be computed slightly differently w.r.t. how the the session token is handled. Probably because the services are built by different teams and they like to keep backwards compatibility. If it works for you with the lambda service you should be fine. I have only experienced the issue with iotdevicegateway, but other services could also follow the same interpretation - which is explicitly allowed in the SigV4 documentation.

This bug post was a suggestion that the presign function supports both interpretations.

RanVaknin commented 2 years ago

Hi @kazkansouh , Thanks for reaching out.

That behavior is indeed odd. While I research this, can you give me some more context on the use case for this iotdevicegateway? Is this your code or some boilerplate borrowed from somewhere? I'm not familiar with that specific service name. From what I can see that service name pops up in things related to the IOT AWS SDK, have you tried using that specific SDK to make this request?

kazkansouh commented 2 years ago

Hello @RanVaknin. To answer your questions. I used this to presign the WSS URL for the MQTT broker in AWS IoT Core. This was within an SPA to view/control IoT devices. Session tokens were obtained from Cognito along with policies that limit their access on the MQTT service. Originally I looked at IoT SDK but it was not appropriate as it is intended for end devices and not an SPA/control app. Further, the IoT JS SDK is designed for node and makes use of c libraries along with lots of additional features for dealing with items like device shadows which is not needed in the control application.

I am not the only person to come across this need, see below:

There are more links, but hopefully you get the idea. In reality, the required change to the presign function would be minimal and be backwards compatible. Adding a optional Boolean flag to enable the alternative SigV4 interpretation and two simple if statements around handling the session token is all that is needed.

github-actions[bot] commented 9 months ago

Greetings! We’re closing this issue because it has been open a long time and hasn’t been updated in a while and may not be getting the attention it deserves. We encourage you to check if this is still an issue in the latest release and if you find that this is still a problem, please feel free to comment or open a new issue.

github-actions[bot] commented 8 months ago

This thread has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs and link to relevant comments in this thread.