amplify-js: Graphql's custom-auth lambda function is not invoked.

Before opening, please confirm:

JavaScript Framework

React

Amplify APIs

GraphQL API

Amplify Categories

auth

Environment information

System:
    OS: macOS 12.1
    CPU: (8) arm64 Apple M1
    Memory: 135.39 MB / 8.00 GB
    Shell: 5.8 - /bin/zsh
  Binaries:
    Node: 16.13.2 - ~/.nvm/versions/node/v16.13.2/bin/node
    Yarn: 1.22.17 - ~/.nvm/versions/node/v16.13.2/bin/yarn
    npm: 8.1.2 - ~/.nvm/versions/node/v16.13.2/bin/npm
  Browsers:
    Chrome: 97.0.4692.99
    Safari: 15.2
  npmGlobalPackages:
    @aws-amplify/cli: 7.6.9
    corepack: 0.10.0
    npm: 8.1.2
    yarn: 1.22.17


Describe the bug

i have setup the custom authentication with amplify on a model

type Module
  @model
  @auth(
    rules: [{ allow: custom }]
  ) {
  id: ID!
  name: String
}

I have also setup a lambda for this ,for verification of the token.

Am getting my jwt from cognito. It seems lambda (custom auth) is not trigged. When the header is (this is the default auth header from amplify)

Authorization: {jwt}

But when i change header

Authorization: Bearer {jwt}

Lambda is triggered when auth header used as above with Bearer.

Why is not getting trigged in the first pattern? Amplify lib is using this Authorization: {jwt} by default for network calls.

This is a problem when we are using different types of auths like

  1. Signed-in user data access
  2. User group-based data access

This auth only work when auth header is Authorization: {jwt}

Expected behavior

Custom auth lambda should be invoked for Authorization: {jwt} header type. It’s only invoked for Authorization: Bearer {jwt}.

Reproduction steps

  1. Add auth with cognito
  2. Create a schema as below
type Module
  @model
  @auth(
    rules: [{ allow: custom }]
  ) {
  id: ID!
  name: String
}
  1. Follow these steps to add a custom auth function.
  2. Making a graphql call to the respective model.

Code Snippet

query{
  listModules{
    items{
      id
      name
    }
  }
}

Log output


{
  "data": {
    "listModules": null
  },
  "errors": [
    {
      "path": [
        "listModules"
      ],
      "data": null,
      "errorType": "Unauthorized",
      "errorInfo": null,
      "locations": [
        {
          "line": 2,
          "column": 3,
          "sourceName": null
        }
      ],
      "message": "Not Authorized to access listModules on type ModelModuleConnection"
    }
  ]
}

aws-exports.js

No response

Manual configuration

No response

Additional configuration

No response

Mobile Device

No response

Mobile Operating System

No response

Mobile Browser

No response

Mobile Browser Version

No response

Additional information and screenshots

No response

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 21 (10 by maintainers)

Most upvoted comments

Hey @imdhanush sorry for the delay. Just to want to make sure I understand your use case.

Is there a reason why you’re using a Lambda to verify Cognito tokens instead of utilizing the private or group auth rules instead for both models?

If you need that much control over the authorization model, you could potentially manage all of the logic from within the lambda and set both models to custom auth.

For example, I changed my schema to this

type Module @model @auth(rules: [{ allow: custom }]) {
  id: ID!
  name: String
}

type Permission @model @auth(rules: [{ allow: custom }]) {
  id: ID!
  name: String
  module: Module @hasOne
}

Using only the custom lambda to authorize the calls, I can verify that a cognito user is making the call, check what groups they belong to, and restrict what fields, queries, mutations, etc each user can access accordingly using the deniedFields part of the response

Lambda example:

const { CognitoJwtVerifier } = require("aws-jwt-verify");

exports.handler = async (event) => {
  console.log(`event >`, JSON.stringify(event, null, 2));

  const {
    authorizationToken,
    requestContext: { apiId, accountId },
  } = event;

  let response = {
    isAuthorized: false,
    resolverContext: {
      userid: "test user",
      info: "contextual information A",
      more_info: "contextual information B",
    },
    deniedFields: [
      `Mutation.createPermission`,
      `Mutation.updatePermission`,
      `Mutation.deletePermission`,
    ],
    ttlOverride: 0,
  };

  try {
    const verifier = CognitoJwtVerifier.create({
      userPoolId: "us-east-1_UrraAvHrw",
      tokenUse: "access",
      clientId: "7gb622j3ir3oatedu8fodci6ev",
    });

    const jwt = authorizationToken.replace(/^Bearer\s/, "");

    const payload = await verifier.verify(jwt);
    console.log("Token is valid. Payload: ", payload);

    if (
      payload["cognito:groups"] &&
      payload["cognito:groups"].includes("enterprise-admins")
    ) {
      response.deniedFields = []; // allow enterprise-admins group to perform any action
    }

    response.isAuthorized = true;
  } catch (error) {
    console.log("Token not valid");
  }

  console.log(`response >`, JSON.stringify(response, null, 2));
  return response;
};

listPermissions ( valid Cognito access token )

Screen Shot 2022-02-09 at 2 16 05 AM

createPermission ( user does not belong to any group)

Screen Shot 2022-02-09 at 2 34 40 AM

createPermission (user belongs to enterprise-admins group)

Screen Shot 2022-02-09 at 2 36 58 AM

Let me know if this helps

@arundna Just a heads up: we had “Bearer” in front of our OIDC tokens for lambda auth for the last 2 months as a workaround but it randomly stopped working today and started to return the error “Not authorized to perform x on type Query”. Changing the prefix to anything else caused it to start working again. Is “Bearer” a reserved word for something else now in AppSync or Amplify? I don’t see this documented anywhere but Bearer no longer works for us.

@thegoliathgeek Apologies for the delayed response on this one. @chrisbonifacio worked with our AppSync team and we’ve made changes to our AppSync documentation to clarify the scenario and also added the workaround section - Circumventing SigV4 and OIDC token authorization limitations

https://docs.aws.amazon.com/appsync/latest/devguide/security-authz.html#aws-lambda-authorization

I’ll go ahead and close this out, but feel free to open a new issue if you have further questions.

Hey @chrisbonifacio , Yes I’ll move all models to custom auth. Thank you 😃.

Hey @chrisbonifacio Thank you for this. But can this be fixed? So that we don’t have to use Bearer in the auth header for custom-auth.

type Module @model @auth(rules: [{ allow: custom }]) {
  id: ID!
  name: String
}

type Permission
  @model
  @auth(
    rules: [
      { allow: groups, groups: ["enterprise-admins"] }
      { allow: private, operations: [read] }
    ]
  ) {
  id: ID!
  name: String
  module: Module @hasOne
  create: Boolean
  read: Boolean
  update: Boolean
  delete: Boolean
}

For the above schema i’ll make a nested query like this

query{
 listPermissions{
  items{
    id
    name
    module{
      id
      name
    }
  }
}
}

As both models have different authorization rules. Permission model requires Authorization: {jwt} Module model (custom-auth) requires Authorization: Bearer {jwt}

Screenshot 2022-01-28 at 8 15 26 AM
output
{
  "data": {
    "listPermissions": {
      "items": [
        {
          "id": "d7cb9f3b-eb62-4fc6-a0a7-80426142c4cd",
          "name": "editors_permission",
          "module": null
        }
      ]
    }
  },
  "errors": [
    {
      "path": [
        "listPermissions",
        "items",
        0,
        "module"
      ],
      "data": null,
      "errorType": "Unauthorized",
      "errorInfo": null,
      "locations": [
        {
          "line": 6,
          "column": 5,
          "sourceName": null
        }
      ],
      "message": "Not Authorized to access module on type Module"
    }
  ]
}

Now only Permission module is accessible. If we pass auth header with Bearer {jwt} Screenshot 2022-01-28 at 8 20 28 AM

The whole query fails.

So, after digging into this further. It seems you were right - the requests without Bearer in the auth header are being auto-rejected by AppSync. Had to turn on my AppSync logs too see that (which I don’t recommend, as it can have unexpected increases on your bill).

So, I’m not quite sure why a jwt without Bearer gets auto-rejected when a regular string like "test12345" doesn’t - still trying to figure that out.

But, I also found that not including an auth token in your API.graphql call with an authMode of AWS_LAMBDA will result in an error.

Screen Shot 2022-01-27 at 3 22 30 PM

So, Amplify wouldn’t have automatically made the request with its usual auth header anyway. So, I don’t think this is an issue or bug, at least not with the JS library.

You’ll have to adjust your lambda to remove the Bearer part of the auth header before verifying it but once you do that, just include it in your call and the lambda should work like you’d expect.

My lambda logic

const { CognitoJwtVerifier } = require("aws-jwt-verify");

exports.handler = async (event) => {
  console.log(`event >`, JSON.stringify(event, null, 2));
  const {
    authorizationToken,
    requestContext: { apiId, accountId },
  } = event;

  let isAuthorized = false;

  const verifier = CognitoJwtVerifier.create({
    userPoolId: "us-east-1_UrraAvHrw",
    tokenUse: "access",
    clientId: "7gb622j3ir3oatedu8fodci6ev",
  });

  // have to include "Bearer" in the auth header for the lambda to be invoked, but we'll remove it here to verify the token
  const jwt = authorizationToken.replace(/^Bearer\s/, ""); 

  try {
    const payload = await verifier.verify(jwt);
    console.log("Token is valid. Payload: ", payload);
    isAuthorized = true;
  } catch (error) {
    console.log("Token not valid");
  }

  const response = {
    isAuthorized,
    resolverContext: {
      userid: "test user",
      info: "contextual information A",
      more_info: "contextual information B",
    },
    deniedFields: [
      `arn:aws:appsync:${process.env.AWS_REGION}:${accountId}:apis/${apiId}/types/Event/fields/comments`,
      `Mutation.createEvent`,
    ],
    ttlOverride: 300,
  };

  console.log(`response >`, JSON.stringify(response, null, 2));
  return response;
};

Client-side API GraphQL query

const res = await API.graphql({
  query: listTodos,
  authMode: "AWS_LAMBDA",
  authToken: `Bearer ${(await Auth.currentSession())
    .getAccessToken()
    .getJwtToken()}`,
});

Result: Screen Shot 2022-01-27 at 3 20 11 PM

🙌