5

I'm currently in the process of implementing a subscription mutation within AWS Lambda using AppSync. I want to use IAM and avoid using any other type of AUTH mechanism as I'm calling it within the AWS stack. Unfortunately, I'm receiving the following 403 error:

(Excerpt from an SQS' CloudWatch log)

 {
    "errorMessage": "Response not successful: Received status code 403",
    "name": "ServerError",
    "errorType": "UnrecognizedClientException",
    "message": "The security token included in the request is invalid."
 }

I've tried following these to no avail, but I don't know what I'm missing:

Here's the code that I'm currently calling it from:

import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
import "cross-fetch/polyfill";

/**
 *
 */

AWS.config.update({
  region: Config.region,
});

export class AppSyncClient {
  client: AWSAppSyncClient<any>;
  constructor() {
    if (!env.APPSYNC_ENDPOINT) {
      throw new Error("APPSYNC_ENDPOINT not defined");
    }

    /**
     * We create the AppSyncClient with the AWS_IAM
     * authentication.
     */
    this.client = new AWSAppSyncClient({
      url: env.APPSYNC_ENDPOINT,
      region: Config.region,
      auth: {
        credentials: AWS.config.credentials!,
        type: "AWS_IAM",
      },
      disableOffline: true,
    });
  }

  /**
   * Sends a mutation on the AppSync Client
   * @param mutate The Mutation that will be sent with the variables.
   * @returns
   */
  sendMutation(mutate: Mutation) {
    const mutation = mutations[mutate.type] as any;
    const variables = mutate.variables;
    console.log("Sending the mutation");
    console.log("Variables is ", JSON.stringify(variables));
    return this.client.mutate({
      mutation,
      fetchPolicy: "network-only",
      variables,
    });
  }
}

Here's the current IAM from the Lambda SQS:

{
    "Statement": [
        {
            "Action": [
                "appsync:GraphQL"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:appsync:us-east-2:747936726382:apis/myapi"
            ]
        }
    ],
    "Version": "2012-10-17"
}

I know it is not an IAM problem from the lambda, because I've tried momentarily giving it full access, and I still got the 403 error.

I've also verified that AppSync has the IAM permission configured (as an additional provider).

Do you guys have any ideas? I'm impressed that this is a ghost topic with such little configuraiton references.

Jose A
  • 10,053
  • 11
  • 75
  • 108

1 Answers1

8

I finally nailed it. I went and re-read for third time Adrian Hall's post, and it did lead me to the solution.

Please note that I installed the AWS AppSync client which is not needed but simplifies the process (otherwise you'd have to sign the URL yourself. For that see Adrian Hall's post).

There are a couple of things:

  • You need to polyfill "fetch" by including either cross-fetch (Otherwise you're going to get hit by Invariant Violation from the Apollo Client which AppSync internally uses).
  • You need to pass the lambda's internal IAM credentials (Which I didn't even know existed) to the configuration portion of the AppSyncClient.
  • You need to add the proper permission to the IAM role of the lambda, in this case: ["appsync:GraphQL"] for the action.

Here's some code:

This is the AppSync code.

// The code is written in TypeScript.
// https://adrianhall.github.io/cloud/2018/10/26/backend-graphql-trigger-appsync/
// https://www.edwardbeazer.com/using-appsync-client-from-lambda/
import { env } from "process";
import { Config, env as Env } from "../../../../shared";
// This is such a bad practice
import AWS from "aws-sdk";
import { AWSAppSyncClient } from "aws-appsync";
import { Mutation, mutations } from "./mutations/";
// Very important, otherwise it won't work!!! You'll have Invariant Violation 
// from Apollo Client.
import "cross-fetch/polyfill";

/**
 *
 */
AWS.config.update({
  region: Config.region,
  credentials: new AWS.Credentials(
    env.AWS_ACCESS_KEY_ID!,
    env.AWS_SECRET_ACCESS_KEY!,
    env.AWS_SESSION_TOKEN!
  ),
});

export class AppSyncClient {
  client: AWSAppSyncClient<any>;
  constructor() {
    // Your AppSync endpoint - The Full URL.
    if (!Env.APPSYNC_ENDPOINT) {
      throw new Error("APPSYNC_ENDPOINT not defined");
    }

    /**
     * We create the AppSyncClient with the AWS_IAM
     * authentication.
     */
    this.client = new AWSAppSyncClient({
      url: Env.APPSYNC_ENDPOINT,
      region: Config.region,
      auth: {
        credentials: AWS.config.credentials!,
        type: "AWS_IAM",
      },
      disableOffline: true,
    });
  }

  /**
   * Sends a mutation on the AppSync Client
   * @param mutate The Mutation that will be sent with the variables.
   * @returns
   */
  // The mutation is a object that holds the mutation in 
  // the `gql` tag. You can ommit this part. 
  sendMutation(mutate: Mutation) {
    const mutation = mutations[mutate.type] as any;
    const variables = mutate.variables;
    // This is the important part.
    return this.client.mutate({
      mutation,
      // Specify "no-cache" in the policy. 
      // network-only won't work.
      fetchPolicy: "no-cache",
      variables,
    });
  }
}

We need to enable IAM in the AppSync authorization mechanism. Yes, it is possible to have multiple Authentication enabled. I'm currently using OPEN_ID and IAM simultaneously.

https://us-east-2.console.aws.amazon.com/appsync/home?region=us-east-2#/myappsync-id/v1/settings

enter image description here

Here's the Lambda's IAM policy that executes the GQL:

{
    "Statement": [
        {
            "Action": [
                "appsync:GraphQL"
            ],
            "Effect": "Allow",
            "Resource": [
                "arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/*"
           
            ]
        }
    ],
    "Version": "2012-10-17"
}

You can further restrict here in the following fashion: arn:${Partition}:appsync:${Region}:${Account}:apis/${GraphQLAPIId}/types/${TypeName}/fields/${FieldName}

arn:aws:appsync:us-east-2:747936726382:apis/ogolfgja65edlmhkcpp3lcmwli/types/Mutation/field/myCustomField"

Note, we need to better restrict this as we are currently giving it entire access to the API.

In your .gql file (AppSync GraphQL schema), add the @aws_iam directive to the mutation that is being used to send the subscriptions to, in order to restrict access from the front-end.

  type Mutation {
  addUsersMutationSubscription(
    input: AddUsersSagaResultInput!
  ): AddUsersSagaResult @aws_iam
}
Jose A
  • 10,053
  • 11
  • 75
  • 108