awslabs / aws-mobile-appsync-sdk-js

JavaScript library files for Offline, Sync, Sigv4. includes support for React Native
Apache License 2.0
919 stars 266 forks source link

realtime-subscription-handshake-link: BadRequestException #619

Closed successx closed 2 years ago

successx commented 3 years ago

Do you want to request a feature or report a bug? Bug

What is the current behavior? Subscription over web-socket fail for IAM authorization. Receive following error when debugging _handleIncomingSubscriptionMessage. { errorType: "BadRequestException", message: "The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method....} If the current behavior is a bug, please provide the steps to reproduce and if possible a minimal demo of the problem. Successfully initialized handshake with the same signature using IAM auth. The above error only receives at _handleIncomingSubscriptionMessage. Probably because websocket signature is different.

What is the expected behavior? https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html Looks like realtime-subscription-handshake-link sign header using "body" key instead of "data" key for IAM authorization from the documentation. However, change "body" to "data" to sign header throw signature mismatch when initializing handshake.

Which versions and which environment (browser, react-native, nodejs) / OS are affected by this issue? Did this work in previous versions?

For the moment, I revert back to use mqtt. It works fine so far, but the deprecation is coming soon. Love to be able to use web socket ASAP.

Cheers,

pmuller commented 3 years ago

Same issue here with:

"@apollo/client": "3.3.13",
"aws-appsync-auth-link": "3.0.4",
"aws-appsync-subscription-link": "3.0.6",
"aws-sdk": "2.873.0",

AppSync no longer supports MQTT on newly created GraphQL API. Would be great to fix IAM auth for websockets!

EdouardLauret commented 3 years ago

Hi, Same issue here, I got the same error from IAM auth since I removed my old appsync instance and redeploy it.

    "@apollo/client": "^3.3.13",
    "aws-appsync-auth-link": "^3.0.4",
    "aws-appsync-subscription-link": "^3.0.6",

I need to generate my graphql client depending on the authentication type (IAM / Cognito) to let users log from Federated Entites like Google, either by username/pass. AWSAppsyncClient doesn't let me create 2 instances because of conflict issues. So I chosed ApolloClient.

const createApolloClient = authType => {

    const httpLink = new HttpLink({
        uri: APIConfiguration.aws_appsync_graphqlEndpoint,
    });

    const cognitoAuth = {
        type: APIConfiguration.aws_appsync_authenticationTypeCognito,
        jwtToken: APIConfiguration.aws_jwToken
    };
    const iamAuth = {
        type: APIConfiguration.aws_appsync_authenticationTypeIAM,
        credentials: () => Auth.currentCredentials()
    };

    return new ApolloClient({
        cache: new InMemoryCache(),
        defaultOptions: {
            watchQuery: { fetchPolicy: 'no-cache', errorPolicy: 'ignore' },
            query: { fetchPolicy: 'no-cache', errorPolicy: 'all' }
        },
        connectToDevTools: process.env.APP_STAGE === 'dev',
        link: from([
            createAuthLink({
                url: APIConfiguration.aws_appsync_graphqlEndpoint,
                region: APIConfiguration.aws_appsync_region,
                auth: authType === 'COGNITO'
                    ? cognitoAuth
                    : iamAuth
            }),
            split(op => {
                const { operation } = op.query.definitions[0];

                if (operation === 'subscription') {
                    return false;
                }

                return true;
            }, httpLink, createSubscriptionHandshakeLink(
                {
                    url: APIConfiguration.aws_appsync_graphqlEndpoint,
                    region: APIConfiguration.aws_appsync_region,
                    auth: authType === 'COGNITO'
                        ? cognitoAuth
                        : iamAuth
                },
                httpLink
            ))
        ]),
    });
}

with

export const APIConfiguration = {
    aws_appsync_graphqlEndpoint: process.env.GRAPHQL_ENDPOINT,
    aws_appsync_region: process.env.AWS_REGION,
    aws_appsync_authenticationTypeIAM: "AWS_IAM",
    aws_appsync_authenticationTypeCognito: "AMAZON_COGNITO_USER_POOLS",
    aws_jwToken: async () => {
        const session = await Amplify.Auth.currentSession();
        return session.getIdToken().getJwtToken();
    },
};

@successx how did you revert back to mqtt ? I'm very intrigued ... As I use Serverless config file to deploy, it deployed a new appsync instance, I can't find where I can set the protocol to use for subscriptions (pure websocket / mqtt over websocket)

Edit : When I log with username/password, I got a Connection closed error from my subscription with no clue

successx commented 3 years ago

Hm, I didn't realise app sync already deprecate mqtt. For us, simply use createSubscriptionHandshakeLink(url) does the trick for mqtt. Not sure if it works for your auth cases. For anyone waiting for the fix from aws, I've forked a stripped down version with IAM auth over websocket subscription only (link). We've been using it for the web for a while in our monorepo. Feel free to add your own logging solution to improve it.

pmuller commented 3 years ago

@successx How did you fix it?

I've read diff -ru aws-mobile-appsync-sdk-js/packages/aws-appsync-subscription-link aws-appsync-subscription-link but as there are lots of changes, I did not understand what's the fix there.

Eveliyse commented 3 years ago

After a load of comparison between @successx's changes and the original code. The change that "fixed" the issue for me was to comment out this line: https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts#L104

This fixes the issue because these graphql_headers seem to be used here to create the "start subscription" message: https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts#L256

but is not used here to initialize the websocket connection: https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/master/packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts#L366

Which results in a mismatch of headers and thus a Bad Request

I believe the fix isn't to comment out the line I originally linked but to actually pass through the graphql_headers to _initializeWebSocketConnection so that the headers for initializing the websocket connection actually match the subsequent "start subscription" message.

I've made a PR here https://github.com/awslabs/aws-mobile-appsync-sdk-js/pull/633

Jonymul commented 3 years ago

I've been having the same issue whereby the signed headers appear to mismatch those included in the GraphQL message payload. As @Eveliyse 's PR (#633) indicates, the removal of graphql_headers seems to resolve the issue in question. Thanks for the investigation!

Edit: This change appears to remove any custom headers which are set in a prior link. So if no custom headers are required, the above fix works. But care should be taken when your connection requires custom headers also be set.

import { setContext } from "@apollo/link-context";

const linkParams = {
  url: "",
  region: "",
  auth: {
    type: AUTH_TYPE.AWS_IAM,
    credentials: {}, // IAM credentials
  } as AuthOptions,
};

const httpAndWSCompositeLink = ApolloLink.from([
  setContext((request, previousContext) => ({
    headers: {
      ...previousContext.headers,
      "x-my-header": "foo-bar",
    },
  })),
  createAuthLink(linkParams),
  createSubscriptionHandshakeLink(linkParams),
]);
chrislim commented 3 years ago

I commented on PR #633 , aside from removing the headers, it appears that the actual intent may have been to return an object with headers:

instead of: graphql_headers: () => (headers), it could be: graphql_headers: () => ({headers}),

UPDATE: eh I think having the headers as an attribute of an object may have worked as a fluke. attempting to expand the attributes of the headers fails, so the original solution is preferable.

chrislim commented 3 years ago

I ended up directly modifying the prototype method so that it won't send graphql_headers to unblock me.

// HACK since this PR {@link https://github.com/awslabs/aws-mobile-appsync-sdk-js/pull/633/files} has not been accepted
// after several months, we go ahead and modify the method to drop the graphql_headers that are causing subscription problems.
const {
  AppSyncRealTimeSubscriptionHandshakeLink,
} = require('aws-appsync/node_modules/aws-appsync-subscription-link/lib/realtime-subscription-handshake-link');
const oldStartSubscription =
  AppSyncRealTimeSubscriptionHandshakeLink.prototype
    ._startSubscriptionWithAWSAppSyncRealTime;
AppSyncRealTimeSubscriptionHandshakeLink.prototype._startSubscriptionWithAWSAppSyncRealTime =
  function (a) {
    if (a.options) {
      delete a.options.graphql_headers;
    }
    return oldStartSubscription.call(this, a);
  };
beckleyc commented 2 years ago

I don't think this issue is fixed. I upgraded to aws-appsync 4.1.2 which includes aws-appsync-subscription-link 2.2.5, and I'm still getting the same error: The request signature we calculated does not match the signature you provided... Can anyone else confirm/deny?

kenmckaba commented 2 years ago

If I replace my realtime-subscription-handshake-link.ts with the fixed one, the problem goes away. https://github.com/awslabs/aws-mobile-appsync-sdk-js/blob/apollov2/packages/aws-appsync-subscription-link/src/realtime-subscription-handshake-link.ts

I don't think this issue is fixed. I upgraded to aws-appsync 4.1.2 which includes aws-appsync-subscription-link 2.2.5, and I'm still getting the same error: The request signature we calculated does not match the signature you provided... Can anyone else confirm/deny?

aws-appsync-subscription-link@2.2.5 doesn't have the fix. The PR has been merged to apollov2 but not released yet.

Is there an ETA on the release?

david-mcafee commented 2 years ago

@beckleyc - see @kenmckaba's comment above. 2.2.5 is not the latest version for use with Apollo V2. Can you let me know what Apollo versions you are using, and perhaps give examples of how you are setting up the client? We recently updated our README to make this more clear, but open to suggestions if it's still ambiguous.

If you are using Apollo V3 (and this is the suggested approach), then you will NOT want to use aws-appsync to create your client. Instead, you'll want to use the latest versions of the subscription and / or auth links, and follow the configuration listed here: https://github.com/awslabs/aws-mobile-appsync-sdk-js#using-authorization-and-subscription-links-with-apollo-client-v3-no-offline-support

However, if you are using Apollo V2, you can still use the aws-appsync package, but you'll need to use the minor versions that we recently published. For Apollo V2, that's version 2.2.6 for the subscription link (see the apolloV2 tag listed under "Versions": https://www.npmjs.com/package/aws-appsync-subscription-link, and version 2.0.7 for the auth link, also listed under the apolloV2 tag: https://www.npmjs.com/package/aws-appsync-auth-link).

If you have followed the above and are still running into an issue, please let me know and we can re-open this issue.

@kenmckaba - the fixes are released, please let me know if you encounter further issues!