Search code examples
aws-lambdagraphqlamazon-cognitoamazon-iamaws-appsync

AppSync with AWS4 and mixed authentication type (UserPools and IAM)


I have an AppSync GraphQL API defined. The API is using Cognito UserPools as primary authentication. This part works fine.

I am trying to execute the request from the Nodejs Lambda function using axios and aws4. The mutation I am trying to hit is configure something like this in my schema (omitting most of the schema):

AppSyncSchema:
    Type: AWS::AppSync::GraphQLSchema
    Properties:
      ApiId: !GetAtt MyApi.ApiId
      Definition: |
        type Something {
          someId: ID!
        }
        input SomethingInput {
          someId: ID!
        }
        type Mutation {
          doSomething(input: SomethingInput!): Something @aws_iam
        }
        

I have configure the Lambda function execution role to have appsync:GraphQL permission:

"Action": [
  "appsync:GraphQL"
],
"Resource": "arn:aws:appsync:eu-west-2:xxxx:apis/yyyy/*",
"Effect": "Allow",
"Sid": "AllowAppSyncExecution"

From different articles online, I have put together some request that my Nodejs Typescript function then tries to execute:

 const doSomething = `mutation doSomething($input: SomethingInput!) {
    doSomething(input: $input) {
      someId
    }
  }`;

  const data = {
    operationName: 'doSomething',
    query: doSomething,
    variables: {
      input: { someId: 'abc' },
    },
  };
  const body = JSON.stringify(data);

  const signOptions = {
    method: 'POST',
    url: 'https://zzzz.appsync-api.eu-west-2.amazonaws.com/graphql, 
    host: 'zzzz.appsync-api.eu-west-2.amazonaws.com',
    path: '/graphql',
    region: process.env.AWS_REGION,
    service: 'appsync',
    headers: {
      'content-type': 'application/json',
    },
    body, // used by aws4
    data, // used by axios
  };

  const creds = {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
    sessionToken: process.env.AWS_SESSION_TOKEN,
  };

  const signed = aws4.sign(signOptions, creds);
  delete signed.headers.Host;
  delete signed.headers['Content-Length'];

  console.log(signed);
  console.log('Got signed headers');

  try {
    await axios(signed);
  } catch (err) {
    console.error('Fialed to execute AppSync request', err);
    throw err;
  }

I am always getting a Request failed with status code 401 response back. The AppSync logs don't reveal much more:

{
    "logType": "RequestSummary",
    "requestId": "11111111",
    "graphQLAPIId": "some id",
    "statusCode": 401,
    "latency": 7079000
}

What am I missing in configuring this correctly? Is there something I need to add to allow AWS IAM only for a specific mutation or do you see any issue with the way I am trying to execute the request?

These are the articles I was referencing:

manually-triggering-appsync-apollo-subscriptions

calling-amazon-api-gateway-authenticated-methods-with-axios-and-aws4


Solution

  • Got it working. I had to add additional authentication provider to the AWS::AppSync::GraphQLApi. In CloudFormation, this looks like an additional property:

      Type: AWS::AppSync::GraphQLApi
        Properties:
          Name: !Ref MyApi
          AuthenticationType: AMAZON_COGNITO_USER_POOLS
          UserPoolConfig:
            UserPoolId: !Ref MyPool
            AwsRegion: !Ref MyRegion
            DefaultAction: ALLOW
          AdditionalAuthenticationProviders:
            - AuthenticationType: AWS_IAM
    

    After making this work, I got a response from AppSync GraphQL but it contained GraphQL errors for all fields in the response object:

    Not Authorized to access someId on type Something

    To get around this I had to also allow IAM on this type in the GraphQL Schema:

    type Something @aws_cognito_user_pools @aws_iam {
      someId: ID!
    }