aws-amplify / amplify-js

A declarative JavaScript library for application development using cloud services.
https://docs.amplify.aws/lib/q/platform/js
Apache License 2.0
9.44k stars 2.13k forks source link

Unable to connect to Events API with IAM authentication #14001

Closed dreamorosi closed 1 week ago

dreamorosi commented 1 week ago

Before opening, please confirm:

JavaScript Framework

Not applicable

Amplify APIs

GraphQL API

Amplify Version

v6

Amplify Categories

api

Backend

None

Environment information

``` # Put output below this line System: OS: macOS 14.7.1 CPU: (12) arm64 Apple M2 Pro Memory: 100.64 MB / 32.00 GB Shell: 5.9 - /bin/zsh Binaries: Node: 20.12.2 - ~/.local/state/fnm_multishells/83883_1731329339080/bin/node Yarn: 1.22.22 - ~/.local/state/fnm_multishells/83883_1731329339080/bin/yarn npm: 10.5.0 - ~/.local/state/fnm_multishells/83883_1731329339080/bin/npm pnpm: 9.6.0 - ~/.local/state/fnm_multishells/83883_1731329339080/bin/pnpm Browsers: Chrome: 130.0.6723.117 Safari: 18.1 npmPackages: @aws-sdk/client-location: ^3.687.0 => 3.687.0 @aws/amazon-location-utilities-auth-helper: ^1.2.0 => 1.2.0 @types/react: ^18.3.12 => 18.3.12 @types/react-dom: ^18.3.1 => 18.3.1 @vitejs/plugin-react: ^4.3.3 => 4.3.3 aws-amplify: ^6.8.0 => 6.8.0 aws-amplify/adapter-core: undefined () aws-amplify/analytics: undefined () aws-amplify/analytics/kinesis: undefined () aws-amplify/analytics/kinesis-firehose: undefined () aws-amplify/analytics/personalize: undefined () aws-amplify/analytics/pinpoint: undefined () aws-amplify/api: undefined () aws-amplify/api/server: undefined () aws-amplify/auth: undefined () aws-amplify/auth/cognito: undefined () aws-amplify/auth/cognito/server: undefined () aws-amplify/auth/enable-oauth-listener: undefined () aws-amplify/auth/server: undefined () aws-amplify/data: undefined () aws-amplify/data/server: undefined () aws-amplify/datastore: undefined () aws-amplify/in-app-messaging: undefined () aws-amplify/in-app-messaging/pinpoint: undefined () aws-amplify/push-notifications: undefined () aws-amplify/push-notifications/pinpoint: undefined () aws-amplify/storage: undefined () aws-amplify/storage/s3: undefined () aws-amplify/storage/s3/server: undefined () aws-amplify/storage/server: undefined () aws-amplify/utils: undefined () maplibre-gl: ^4.7.1 => 4.7.1 () react: ^18.3.1 => 18.3.1 react-dom: ^18.3.1 => 18.3.1 react-map-gl: ^7.1.7 => 7.1.7 vite: ^5.4.11 => 5.4.11 npmGlobalPackages: corepack: 0.25.2 npm: 10.5.0 ```

Describe the bug

When using the aws-amplify/data with an Events API set up with IAM authentication, the connection fails with a NonRetryableError.

Expected behavior

The Websocket should work with IAM authentication.

Reproduction steps

import React, { useState, useEffect, useRef } from "react";
import { Amplify } from 'aws-amplify';
import { ConsoleLogger } from 'aws-amplify/utils';
ConsoleLogger.LOG_LEVEL = 'DEBUG';
import { fetchAuthSession } from 'aws-amplify/auth'
import { events } from 'aws-amplify/data';
import { createRoot } from "react-dom/client";
import Map from "react-map-gl/maplibre";
import { withIdentityPoolId } from "@aws/amazon-location-utilities-auth-helper";

import 'maplibre-gl/dist/maplibre-gl.css';
import "./index.css";

Amplify.configure({
  Auth: {
    Cognito: {
      region: "eu-west-1",
      identityPoolId: "eu-west-1: xxxxxxxxxx",
      allowGuestAccess: true,
    }
  },
  API: {
    Events: {
      endpoint: "https://api-id.appsync-api.eu-west-1.amazonaws.com/event",
      region: "eu-west-1",
      defaultAuthMode: "iam"
    }
  },
  region: "eu-west-1",
});

const identityPoolId = "eu-west-1:xxxxxxxxxx";
const region = "eu-west-1";
const styleName = "Standard"; // Standard, Monochrome, Hybrid, or Satellite

const authHelper = await withIdentityPoolId(identityPoolId);

const App = () => {
  const [myEvents, setMyEvents] = useState([]);
  const channelRef = useRef(null)

  useEffect(() => {
    const connectAndSubscribe = async () => {
            console.log('Connecting to channel')
      if (channelRef.current) {
        return;
      }
      try {
        const channel = await events.connect('/default/channel')
    console.log('Channel subscribed')
    channelRef.current = channel

        channel.subscribe({
      next: (data) => {
            console.log('received', data)            
          },
      error: (err) => console.log(err),
        })
      } catch (e) {
         console.log('Error connecting to channel: ', e)
      }
     }

    connectAndSubscribe();

    return () => {
            if (channelRef.current) {
                channelRef.current.close()
            }
        }
  }, []);

  return (
    <Map
      initialViewState={{
        longitude: -123.1169,
        latitude: 49.2824,
        zoom: 16,
      }}
      style={{ width: "100vw", height: "100vh" }}
      mapStyle={`https://maps.geo.${region}.amazonaws.com/v2/styles/${styleName}/descriptor`}
      transformRequest={authHelper.getMapAuthenticationOptions().transformRequest}
    />
  );
}

createRoot(document.getElementById("root")).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

Code Snippet

// Put your code below this line.

Log output

See logs below ⬇️ ⬇️ ⬇️ ⬇️ ⬇️

``` // Put your logs below this line [DEBUG] 34:13.758 AWSAppSyncRealTimeProvider Auth - Authenticating with "iam" [DEBUG] 34:13.759 CognitoCredentialsProvider - Clearing out in-memory credentials [DEBUG] 34:13.880 AWSAppSyncEventsProvider - Establishing retryable connection [DEBUG] 34:13.880 retryUtil - bound attempt #1 with this vars: ["wss://gasctn76kraldllcpmqms44pyu.appsync-realtime-api.eu-west-1.amazonaws.com/event/realtime","header-eyJhY2NlcH ...... [DEBUG] 34:13.880 AWSAppSyncEventsProvider - Establishing WebSocket connection to wss://gasctn76kraldllcpmqms44pyu.appsync-realtime-api.eu-west-1.amazonaws.com/event/realtime [DEBUG] 34:14.119 AWSAppSyncEventsProvider - subscription message from AWS AppSyncRealTime: {"errors":[{"errorType":"BadRequestException","message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details.\n\nThe Canonical String for this request should have been\n'POST\n/event\n\naccept:application/json, text/javascript\ncontent-encoding:amz-1.0\ncontent-type:application/json; charset=UTF-8\nhost:api-id.appsync-api.eu-west-1.amazonaws.com\nx-amz-date:20241111T173413Z\nx-amz-security-token:xxxxxxxxxxxxx\n\naccept;content-encoding;content-type;host;x-amz-date;x-amz-security-token\nxxxxxxxxxxxxxx'\n\nThe String-to-Sign should have been\n'AWS4-HMAC-SHA256\n20241111T173413Z\n20241111/eu-west-1/appsync/aws4_request\n714896dcf56b33c34cec13ee8137ebc0a5c35895af0ac92924fd111a4ccab2e4'\n","errorCode":400}],"type":"connection_error"} [DEBUG] 34:14.120 retryUtil - error on bound NonRetryableError: BadRequestException at AWSWebSocketProvider._establishConnection (aws-amplify_data.js?v=b49bb864:7873:17) at async chunk-2MQQKA2O.js?v=21653013:1073:17 DEBUG] 34:14.120 retryUtil - bound non retryable error NonRetryableError: BadRequestException at AWSWebSocketProvider._establishConnection (aws-amplify_data.js?v=b49bb864:7873:17) at async chunk-2MQQKA2O.js?v=21653013:1073:17 chunk-2MQQKA2O.js?v=21653013:114 [DEBUG] 34:14.120 AWSAppSyncEventsProvider - Connection exited with NonRetryableError: BadRequestException at AWSWebSocketProvider._establishConnection (aws-amplify_data.js?v=b49bb864:7873:17) at async chunk-2MQQKA2O.js?v=21653013:1073:17 chunk-2MQQKA2O.js?v=21653013:107 [DEBUG] 34:14.121 AWSAppSyncEventsProvider {err: NonRetryableError: BadRequestException at AWSWebSocketProvider._establishConnection (http://loc…} ``` The header, once base64 decoded has the following JSON (redacted): ```json { "accept": "application/json, text/javascript", "content-encoding": "amz-1.0", "content-type": "application/json; charset=UTF-8", "host": "api-id.appsync-api.eu-west-1.amazonaws.com", "x-amz-date": "20241111T173413Z", "x-amz-security-token": "xxxxxxxx", "authorization": "AWS4-HMAC-SHA256 Credential=ASIAXxxxxxxxxxxxxxx/20241111/eu-west-1/appsync/aws4_request, SignedHeaders=accept;content-encoding;content-type;host;x-amz-date;x-amz-security-token, Signature=5ecfxxxxxxxxxxxxxxxx" } ```

aws-exports.js

No response

Manual configuration

Amplify.configure({
  Auth: {
    Cognito: {
      region: "eu-west-1",
      identityPoolId: "eu-west-1:xxxxxxxxxxxxxxxxxxx",
      allowGuestAccess: true,
    }
  },
  API: {
    Events: {
      endpoint: "https://api-id.appsync-api.eu-west-1.amazonaws.com/event",
      region: "eu-west-1",
      defaultAuthMode: "iam"
    }
  },
  region: "eu-west-1",
});

Additional configuration

Resources:
  WebsocketAPI:
    Type: AWS::AppSync::Api
    Properties:
      EventConfig:
        AuthProviders:
          - AuthType: AWS_IAM
        ConnectionAuthModes:
          - AuthType: AWS_IAM
        DefaultPublishAuthModes:
          - AuthType: AWS_IAM
        DefaultSubscribeAuthModes:
          - AuthType: AWS_IAM
      Name: trackerAssetWebsocketAPI

  WebsocketAPINamespace:
    Type: AWS::AppSync::ChannelNamespace
    Properties:
      ApiId: !GetAtt WebsocketAPI.ApiId
      Name: asset-tracker

  IdentityPoolEC8A1A0D:
    Type: AWS::Cognito::IdentityPool
    Properties:
      AllowUnauthenticatedIdentities: true
      CognitoIdentityProviders: []

  IdentityPoolAuthenticatedRole42131CF5:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                'cognito-identity.amazonaws.com:aud': !Ref IdentityPoolEC8A1A0D
              ForAnyValue:StringLike:
                'cognito-identity.amazonaws.com:amr': authenticated
      Description: !Join ['', ['Default Authenticated Role for Identity Pool ', !GetAtt IdentityPoolEC8A1A0D.Name]]

  IdentityPoolUnauthenticatedRole68AEFF8B:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Federated: cognito-identity.amazonaws.com
            Action: sts:AssumeRoleWithWebIdentity
            Condition:
              StringEquals:
                'cognito-identity.amazonaws.com:aud': !Ref IdentityPoolEC8A1A0D
              ForAnyValue:StringLike:
                'cognito-identity.amazonaws.com:amr': unauthenticated
      Description: !Join ['', ['Default Unauthenticated Role for Identity Pool ', !GetAtt IdentityPoolEC8A1A0D.Name]]

  IdentityPoolDefaultRoleAttachmentD81AFC39:
    Type: AWS::Cognito::IdentityPoolRoleAttachment
    Properties:
      IdentityPoolId: !Ref IdentityPoolEC8A1A0D
      Roles:
        authenticated: !GetAtt IdentityPoolAuthenticatedRole42131CF5.Arn
        unauthenticated: !GetAtt IdentityPoolUnauthenticatedRole68AEFF8B.Arn

  websocketServicePolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Action:
              - appsync:EventSubscribe
              - appsync:EventConnect
              - appsync:EventPublish
            Resource:
              - !Join 
                - ''
                - - 'arn:aws:appsync:'
                  - !Ref AWS::Region
                  - ':'
                  - !Ref AWS::AccountId
                  - ':apis/'
                  - !GetAtt WebsocketAPI.ApiId
                  - '/*'
      PolicyName: websocketServicePolicy
      Roles:
        - !Ref IdentityPoolUnauthenticatedRole68AEFF8B

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

dreamorosi commented 1 week ago

aws-amplify@6.8.1 fixed the issue.