Authorise Request to AWS WebSocket API Gateway using AWS_IAM
Asked Answered
O

4

11

I've set up an API Gateway using WebSocket protocol. On the '$connect' route request setting, I selected 'AWS_IAM' as the authorization method. The web app needs to make a connection to this WebSocket API after a user logged in via Cognito. How do I then authorize the WebSocket API request from the JavaScript on the web app? With the HTTP API Gateway, I can generate the signature from access key and session token, which got passed in to the request header. But I can't pass headers in a WebSocket request.

Odeen answered 29/1, 2020 at 9:56 Comment(0)
O
6

I have got an answer from AWS support. I will need to sign the wss URL. So instead of setting request headers in a HTTP request, the signature information will be passed to the url in the query string parameters. A signed wss URL looks like: wss://API_ID.execute-api.region.amazonaws.com/dev?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ACCESSKEY/20200131/region/execute-api/aws4_request&X-Amz-Date=20200131T100233Z&X-Amz-Security-Token=SECURITY_TOKEN&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATURE.

To generate the signed URL, I can use Signer.signUrl method from @aws-amplify/core library.

Odeen answered 31/1, 2020 at 16:59 Comment(3)
Hi @Odeen .. I've tried and come close, can you provide any code example?Pulsatory
Ah .. got it working now .. some of the 1st iteration code is posted below.Pulsatory
This answer seems to be good, however, it would become much better with some code example.Oftentimes
P
10

This is some example/pseudo code that works for me:

Using AWS Amplify Authenticated user:

import { w3cwebsocket as W3CWebSocket } from "websocket"
import { Auth, Signer } from "aws-amplify"

let wsClient: any = null

export const client = async () => {
  if (wsClient) return wsClient

  if ((await Auth.currentUserInfo()) === null) return wsClient

  const credentials = await Auth.currentCredentials()

  const accessInfo = {
    access_key: credentials.accessKeyId,
    secret_key: credentials.secretAccessKey,
    session_token: credentials.sessionToken,
  }

  const wssUrl = "wss://YOUR-API-ID.execute-api.REGION.amazonaws.com/dev"

  const signedUrl = Signer.signUrl(wssUrl, accessInfo)

  wsClient = new W3CWebSocket(signedUrl)

  wsClient.onerror = function () {
    console.log("[client]: Connection Error")
  }

  wsClient.onopen = function () {
    console.log("[client]: WebSocket Client Connected")
  }

  wsClient.onclose = function () {
    console.log("[client]: Client Closed")
  }

  wsClient.onmessage = function (e: any) {
    if (typeof e.data === "string") {
      console.log("Received: '" + e.data + "'")
    }
  }

  return wsClient
}

Then also using AWS Cognito needs this permission:

{
  "Action": ["execute-api:Invoke"],
  "Resource": "arn:aws:execute-api:REGION:ACCOUNT-ID-OR-WILDCARD:*/*/$connect",
  "Effect": "Allow"
}
Pulsatory answered 31/5, 2020 at 18:0 Comment(3)
Hi Rudi, in this approach, will the user be able to sign in as long as the assumed IAM is not expired? So let us say that the user signed in, and then changed the password. Will he be able to continue using the endpoint using the previously generated querystring until the generated token is expired?Oftentimes
This probably saved my some serious time. I tried using the standard Websocket library which works fine with no auth, but fails with 403 when using signed URL. ``` new WebSocket(url); << fails BUT new W3CWebSocket(url) << Good ```Aleut
@Aleut I don't use bs4 but I see this error in my lambda CloudWatch log: Runtime.ImportModuleError: Unable to import module 'app': No module named 'bs4'. Did you notice this bs4 error? API Gateway AWS_IAM authorization seems to work for me even though I am using the standard WebSocket(signedUrl)Airliah
O
6

I have got an answer from AWS support. I will need to sign the wss URL. So instead of setting request headers in a HTTP request, the signature information will be passed to the url in the query string parameters. A signed wss URL looks like: wss://API_ID.execute-api.region.amazonaws.com/dev?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ACCESSKEY/20200131/region/execute-api/aws4_request&X-Amz-Date=20200131T100233Z&X-Amz-Security-Token=SECURITY_TOKEN&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATURE.

To generate the signed URL, I can use Signer.signUrl method from @aws-amplify/core library.

Odeen answered 31/1, 2020 at 16:59 Comment(3)
Hi @Odeen .. I've tried and come close, can you provide any code example?Pulsatory
Ah .. got it working now .. some of the 1st iteration code is posted below.Pulsatory
This answer seems to be good, however, it would become much better with some code example.Oftentimes
O
0

I implemented this Dart code which signs the AWS request URLs. This is particularly helpful to connect to a IAM-secured WebSocket API Gateway.

https://github.com/MohammedNoureldin/aws-url-signer

I know that putting links in answers in discouraged, but this will not make sense to copy the whole 100 lines of code here.

The usage of my implementation will look like this:

String getSignedWebSocketUrl(
    {String apiId,
    String region,
    String stage,
    String accessKey,
    String secretKey,
    String sessionToken})
Oftentimes answered 19/4, 2021 at 21:44 Comment(0)
M
0

Code for Dean's point. Assuming we have a Websocket constructor, using AWS Amplify on the frontend, and want to protect our websocket connection:

const signedUrl = await signUrl(
  import.meta.env.VITE_WEBSOCKET_API_URL,
  'execute-api', // websocket api AWS service name
  import.meta.env.VITE_API_REGION,
);

const socket = new WebSocket(signedUrl);

// handle socket events below...

signUrl.js

import { Auth, Signer } from 'aws-amplify';

export async function signUrl(url, service, region) {
  const essentialCredentials = Auth.essentialCredentials(
    await Auth.currentCredentials(),
  );

  const credentials = {
    secret_key: essentialCredentials.secretAccessKey,
    access_key: essentialCredentials.accessKeyId,
    session_token: essentialCredentials.sessionToken,
  };

  const serviceInfo = { region, service };

  return Signer.signUrl(url, credentials, serviceInfo);
}

This will try to connect to the websocket using a signed URL.

If we try to connect with an unsigned URL, we get 403:

wscat --connect wss://<your-api-id>.execute-api.us-east-1.amazonaws.com/<your-stage>
Mccaskill answered 6/5, 2024 at 13:15 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.