Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graphql's custom-auth lambda function is not invoked. #9513

Closed
3 tasks done
thegoliathgeek opened this issue Jan 25, 2022 · 21 comments
Closed
3 tasks done

Graphql's custom-auth lambda function is not invoked. #9513

thegoliathgeek opened this issue Jan 25, 2022 · 21 comments
Assignees
Labels
bug Something isn't working GraphQL Related to GraphQL API issues Service Team Issues asked to the Service Team

Comments

@thegoliathgeek
Copy link

thegoliathgeek commented Jan 25, 2022

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

@chrisbonifacio chrisbonifacio self-assigned this Jan 25, 2022
@chrisbonifacio chrisbonifacio added API Related to REST API issues GraphQL Related to GraphQL API issues pending-triage Issue is pending triage labels Jan 25, 2022
@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jan 26, 2022

Hi @ImDhanush 👋 can you try setting the authMode of your graphql query to "AWS_LAMBDA"?

example:

const modules = await API.graphql({
  query: listModules,
  authMode: "AWS_LAMBDA",
  authToken: `{jwt}`
})

Let me know if this helps

@thegoliathgeek
Copy link
Author

Hi @chrisbonifacio
I tried the method you suggested above.
With both appsync console and library.
Still facing the same issue.

According to this doc. The auth token size must not exceed 2048 characters.
I checked my token size it's 1066.

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jan 26, 2022

You mentioned it only works if the authorization header is set to Bearer {jwt}, correct? What does your lambda response logic look like?

Also, can you share the amplify/backend/api/YOUR_PROJECT_NAME/cli-inputs.json file? I just need to see what the defaultAuthType and additionalAuthTypes look like.

@chrisbonifacio chrisbonifacio added pending-response and removed pending-triage Issue is pending triage labels Jan 26, 2022
@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jan 26, 2022

I just tried hardcoding a string on my custom lambda, didn't include Bearer on the client and was able to make the call.

Lambda logic

// This is sample code. Please update this to suite your schema

exports.handler = async (event) => {
  console.log(`event >`, JSON.stringify(event, null, 2));
  const {
    authorizationToken,
    requestContext: { apiId, accountId },
  } = event;
  const response = {
    isAuthorized: authorizationToken === "test123",
    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;
};

Unauthorized

    API.graphql({
      query: listTodos,
      authMode: "AWS_LAMBDA",
      authToken: "Bearer test123",
    })

Screen Shot 2022-01-26 at 11 45 48 AM

Authorized

    API.graphql({
      query: listTodos,
      authMode: "AWS_LAMBDA",
      authToken: "test123",
    })

Screen Shot 2022-01-26 at 11 46 39 AM

Are you sure that the logic that verifies the jwt and determines whether isAuthorized is true or false in your lambda response is correct?

@thegoliathgeek
Copy link
Author

thegoliathgeek commented Jan 26, 2022

Yes it only works when authorization header is set to Bearer {jwt}.

The response looks like this

{
    "isAuthorized": true,
    "deniedFields": [],
    "ttlOverride": 2,
    "resolverContext": {
        "app": "name"
    }
}

I tried sending Authorization: 1234556. This worked good.
Can you please try with a real jwt?

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jan 26, 2022

what is the logic that returned true for isAuthorized in the lambda function?

I did try with a real jwt (took one from Auth0) and I did not need to include Bearer, but I also was just doing a simple comparison like:

authorizationToken === "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImlXMDg3Ml8tc1hFbVA5MGEwNXdpZSJ9.eyJpc3MiOiJodHRwczovL2Rldi03dzdxNWZsNy51cy5hdXRoMC5jb20vIiwic3ViIjoiTE9iM0duRERzS2hrNzBEcFQyZTBiWXd3NHZQdWR5VlZAY2xpZW50cyIsImF1ZCI6Imh0dHBzOi8vYXdzLmFtYXpvbi5jb20iLCJpYXQiOjE2NDMyMTQwMTAsImV4cCI6MTY0MzMwMDQxMCwiYXpwIjoiTE9iM0duRERzS2hrNzBEcFQyZTBiWXd3NHZQdWR5VlYiLCJndHkiOiJjbGllbnQtY3JlZGVudGlhbHMifQ.ZQfwZJot8QSgpdpudW_ogSBz1P_ssCPkCdM6WwDFoewuE99XN9LcEN64xNs0zm9uES5hrGczYwjNJXZlyCzk5NzOpM-boAKbX1qUhQrojVuj_pxWLkVReEltRC9DtojgzqntT3cpfJsr5xDxMso_Gg1NGMCHqGbCLdGJoO87X07OXp5j1ey-LTukym5sStS_vF2VUiJY0kFgNYcRG8uWgof6F6rAbzSiqNTgW_V3UxlV9s9fCb6i5BjZEst81I5xQFOpIxd7ZKfcOSUVcmYMqW4thsu5lI5y96HGnzmGT7kTRWUI1Yzj_Xy1GZWIOSmcd1a69U0jjvBWClkZ1Xuj-A",

How are you verifying the token?

@thegoliathgeek
Copy link
Author

thegoliathgeek commented Jan 27, 2022

Am passing the token which i get from cognito after login and verify it using the keys from
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.json
if jwt is verified i return true for a isAuthorized else return false

@thegoliathgeek
Copy link
Author

This one is triggering lambda for Authorization: 123456 and i can see the logs in lambda.
Screenshot 2022-01-27 at 10 08 43 AM

This one is not triggering lambda for Authorization: {jwt} and i can't see the logs in lambda.
Screenshot 2022-01-27 at 10 09 58 AM

@chrisbonifacio
Copy link
Member

So, the 401 "You are not authorized to make this call" error is normal for when the lambda returns isAuthorized as false. The lambda is being invoked and you should be able to see that in your cloudwatch logs.

However, the graphql error saying you're not authorized to access the listModules seems off. I was able to reproduce this and while the call seems to be making it far enough to get the appsync/graphql error, isAuthorized is still being returned as false in my logs with a valid access token and I'm not sure why yet.

This is the logic in my lambda authorizer for comparison

// This is sample code. Please update this to suite your schema
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",
  });

  try {
    const payload = await verifier.verify(authorizationToken);
    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;
};

Cloudwatch logs

Screen Shot 2022-01-27 at 1 20 25 PM

I've reached out to the team to investigate this further. Might be something I'm just missing or unaware of here.

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jan 27, 2022

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

🙌

@thegoliathgeek
Copy link
Author

thegoliathgeek commented Jan 28, 2022

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.

@chrisbonifacio chrisbonifacio added bug Something isn't working Service Team Issues asked to the Service Team and removed pending-response labels Jan 31, 2022
@chrisbonifacio
Copy link
Member

chrisbonifacio commented Feb 3, 2022

Hey @ImDhanush apologies for the delay. So, I spoke to the AppSync team and while this is not exactly a bug, because the service is working as intended, it is something that we should document better.

When the service is authorizing credentials, all it knows is the auth types configured for an API and the credentials provided. It’ll check what the credentials look like against what an auth type expects to try to infer what auth type is in use. Because a cognito token is what was provided and a cognito provider is associated with the api, cognito auth is what was inferred.

So, nesting two models with different auth modes, especially lambda as we saw in this case will result in a partial result and Unauthorized error. You will have to query for the records separately so that you can set the correct authMode and include the Bearer key in your authToken (the word "Bearer isn't actually required, any other additional characters or prefix/suffix should technically work, it's just so that Cognito doesn't confuse the request to be restricted to Cognito User Pools because it recognizes a cognito token and we make sure the Lambda is invoked). You'll still have to remove the Bearer key or any additional characters in the auth header to make the jwt valid to your verifier.

@thegoliathgeek
Copy link
Author

Hey, @chrisbonifacio thank you for this.
Nested query with different auth modes is a much-needed requirement for me.
So are there any alternatives to this?

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Feb 9, 2022

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

@thegoliathgeek
Copy link
Author

thegoliathgeek commented Feb 10, 2022

Hey @chrisbonifacio ,
Yes I'll move all models to custom auth.
Thank you :).

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Apr 8, 2022

@ImDhanush scratch that, this workaround is actually for a separate issue, I confused the issue that was opened on the CLI repo to be the same as this one. The workaround for this particular issue is still the same as I mentioned - adding a prefix other than "Bearer" to the token and removing it in the lambda. And each request can still only have one type of auth mode, otherwise only partial results will be returned.

I opened an internal service ticket for the AppSync team and they are going to add this to their documentation.

Deleting my previous comment. Apologies for the confusion!

@arundna
Copy link
Member

arundna commented Jul 12, 2022

@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.

@arundna arundna closed this as completed Jul 12, 2022
@JoshuaWarejko
Copy link

JoshuaWarejko commented Jul 15, 2022

@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.

@mewtlu
Copy link

mewtlu commented Jul 20, 2022

We've suddenly been having the same issue as JoshuaWarejko as of the last week, spent significant time trying to diagnose the cause of the problem across each of our environments and it has turned out to be this problem, seemingly due to something being silently changed within AppSync/Amplify?
Very agonized to have wasted so much time diagnosing such a problem, but it seems like we now have a workaround by using a separate authorization prefix than Bearer in the request and in our custom authorizer function.

@chrisbonifacio
Copy link
Member

chrisbonifacio commented Jul 26, 2022

@JoshuaWarejko @mewtlu Hello and apologies for the delay. The AppSync team reached out to me to confirm that "Bearer" is indeed a reserved prefix for OIDC now as defined by the OIDC spec.

@JoshuaWarejko
Copy link

@chrisbonifacio Thanks for the update, good to know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working GraphQL Related to GraphQL API issues Service Team Issues asked to the Service Team
Projects
None yet
Development

No branches or pull requests

5 participants