serverless / serverless-graphql

Serverless GraphQL Examples for AWS AppSync and Apollo
https://www.serverless.com
MIT License
2.72k stars 363 forks source link

GraphQL Endpoint Authentication and Authorization #96

Open cdelgadob opened 7 years ago

cdelgadob commented 7 years ago

I am wondering how are you implementing authorization on a graphql endpoint in AWS.

My idea is to use Cognito Groups and match them with allowed queries/mutation per group in a custom authorizer lambda function.

Is this something reasonable?

It is important that the graphql lambda function is not hit by any unauthorized request.

cdelgadob commented 7 years ago

Hi, Before my post obviously I didn't read all the threads about it. I guess the question now is: what is the status of this? I can't find any doc/post/code/example... ? Thanks

Carlos

dalerka commented 7 years ago

I'm also interested in this and welcome any advice. Here's some relevant discussion.

sid88in commented 7 years ago

@dalerka @cdelgadob its a valid concern. Looks like AWS provides different ways to authenticate AWS API Gateway http://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-control-access-to-api.html

We can design/prioritize this in coming days.

sid88in commented 7 years ago

Also some info here http://dev.apollodata.com/react/auth.html

sid88in commented 6 years ago

@nikgraf I have implemented Cognito User Pool Authentication on my local branch. Will create a PR on this soon. 2 Problems trying to solve now after authenticating my endpoints : 1) how to pass token to graphiQL endpoint 2) my resolver now has a rest wrapper and this rest endpoint is also secured, so my POST request to graphql endpoint must pass authorization header token value to resolver and another rest endpoint

sid88in commented 6 years ago

well 1) got resolved :) I added chrome plugin to add auth header (now client can generate token which is valid for 1 hour and then set this in the chrome header) and graphiQL works!

Looking into 2)

sid88in commented 6 years ago

OK resolved 1) and 2)

unfortunately there is no serverless plugin to mock cognito https://github.com/dherault/serverless-offline/issues/264

sid88in commented 6 years ago

OK great! this works https://github.com/dherault/serverless-offline/issues/118

buggy commented 6 years ago

I think it's import to consider authentication and authorization as two different problems.

Authentication is how someone proves who they are to the GraphQL server. The most common approach here is to include an Authorization header with a token (for example a JWT).

My personal feeling on using authorizers (either custom or Cognito) with the API GW is that they're great in a REST environment where I can tell from the URL who should be able to access it but completely inappropriate for GraphQL. For example: I can protect POST, PUT, DELETE to /pages and /page/:n but allow unauthenticated access to GET /pages and /pages/:n using authorizers

With GraphQL everything sits behind a single API endpoint. If you put an authorizer on it then the entire GraphQL API ceases to be publicly available which is a problem if you want to allow self registration, login, password reset or other functionality for anonymous users while restricting other operations/information. Using the previous example I should be able to request pages anonymously with only the mutations to create, update and delete pages restricted.

For authorization Facebook seems to recommend doing it in the business logic or the resolver. This approach allows partial failures where a user may be authorized to access some information but not all. The GraphQL spec itself talks about partial errors "A response may contain both a partial response as well as encountered errors in the case that an error occurred on a field which was replaced with null." See http://facebook.github.io/graphql/October2016/#sec-Response

Typically I put authentication into the handler. It will validate the token from the Authorization header and lookup the relevant user. The user information is then put into the request context that is available to the resolvers. This allows the resolvers to restrict access to information or to pass user information on to the business logic. It also frees the resolvers from authentication as they are told who the user is.

sid88in commented 6 years ago

@buggy thanks for your valuable inputs. I completely agree that authentication and authorization as two different problems and must be handled differently (also explained here https://dev-blog.apollodata.com/a-guide-to-authentication-in-graphql-e002a4039d1 and https://dev-blog.apollodata.com/auth-in-graphql-part-2-c6441bcc4302). https://github.com/serverless/serverless-graphql-apollo/pull/130 deals with Authentication and NOT authorization. We certainly need additional work to implement Authorization. Also implementing Authentication in graphql is evaluated in the posts above where it can either be passed as JWT token or handled in graphql layer itself (with its own downsides). Another one is that for a given use case you might not want to expose your schema to the outside world using graphiqL (if by default users are authenticated to access endpoint but not authorized to access resources).

Regarding your observations - "If you put an authorizer on it then the entire GraphQL API ceases to be publicly available which is a problem if you want to allow self registration, login, password reset or other functionality for anonymous users while restricting other operations/information." I would say yes but again it depends on the use cases we are dealing with. For instance, in internal use cases where authorization/signup is handled in a separate layer which makes API call to graphql there might be no signups etc. In other cases Cognito can sign up the user and add him/her to the user pool along with different use cases https://github.com/aws/amazon-cognito-identity-js but it could very well be handled at the resolver layer. We need to think more! but yeah I guess this is just a beginning of a good discussion.

nikgraf commented 6 years ago

What I have done so far is:

I used a JWT to authenticate users. The JWT is placed in the authorization header, which is parsed in the graphQL handler. The auth token is then passed into the graphQL root and therefor accessible in every resolvers. Resolvers (can be at the GraphQL type level or in a remove Lambda function are responsible) to check if this auth token is authorized to execute the resolver query or mutation. This is all done without AWS cognito.

danbruder commented 6 years ago

@nikgraf I love that approach because it doesn't tie you to an underlying service. Can you post an example?

sid88in commented 6 years ago

@danbruder in the mean time you might want to look at few examples as open PR's for the same. In addition Ryan gave a wonderful talk at GraphQL Conference this year https://github.com/chenkie/graphql-auth

danbruder commented 6 years ago

@sid88in thanks a lot!

cdelgadob commented 6 years ago

@danbruder, @sid88in, I have just shared my implementation for GraphQL authorization, which performs check for the operation being called (mutation or query) and for the data which is queried afterwards. I has some domain-specific variables, but in general maybe can serve as an inspiration. It doesn't use Cognito at all, just tables from dynamodb where we store the user roles for the entity being queried and the roles needed to access and modify the data. Maybe I had it completely wrong, or maybe not, anyway it seems to work so far. Here is the code, please ask any question or make any comment you think it's constructive ;) https://gist.github.com/cdelgadob/7fd2d900ef575257905c2ebe61acf781

Reading previous posts my approach can be in the same line as @nikgraf suggests

olistic commented 5 years ago

Conversation is old, but sharing my two cents. Regarding this:

With GraphQL everything sits behind a single API endpoint. If you put an authorizer on it then the entire GraphQL API ceases to be publicly available [...].

– @buggy

In GraphQL, everything sits behind a single endpoint, right. But what if that same everything sat behind two different endpoints? For example:

POST /graphql
POST /graphql/authed

Then you can apply Lambda Authorizers (custom or Cognito) only to the /authed endpoint and still get the benefits of it while not forcing the entire API to be restricted.

Some things to consider if using this approach:

  1. You'd add the authenticated user to the GraphQL context only when requestContext.authorizer is present in your Lambda's handler (but you don't have to worry about token verification because the Lambda Authorizer took care of that for you ;-)).

Using Apollo Server, that'd look something like this:

const { ApolloServer } = require('apollo-server-lambda');

const User = require('../models/User');
const schema = require('../schema');

const getUser = async ({ requestContext: { authorizer } }) => {
  if (authorizer) {
    const {
      claims: { sub: id },
    } = authorizer;
    const user = await User.getById(id);
    if (user) {
      return user;
    }
  }

  return null;
};

const server = new ApolloServer({
  schema,
  context: async ({ event }) => ({
    user: await getUser(event),
  }),
});

exports.handler = server.createHandler();
  1. The presence of the authenticated user in the GraphQL context must be checked at the resolver level for those fields that are not public.

I'm using a GraphQL middleware for this, with the excellent graphql-shield package. I defined a rule like this:

const isAuthenticated = rule()(async (parent, args, { user }) => {
  if (user !== null) {
    return true;
  }

  return new AuthenticationError('You must be logged in');
});

And I apply it to those fields that require authentication.

  1. The consumers of your API have to send requests to /graphql/authed instead of /graphql when they include the Authorization header in the request.

I'm using the Apollo Client, which allows to implement this pretty easily with the apollo-link-context package.

import { setContext } from 'apollo-link-context';

import Auth from '../services/Auth';

const authLink = setContext(async (request, { headers }) => {
  const token = await Auth.getToken();
  if (token) {
    return {
      headers: {
        ...headers,
        Authorization: `Bearer ${token}`,
      },
      uri: 'https://your.api.com/graphql/authed',
    };
  }
});

export default authLink;

I hope this can be useful for someone looking for a solution to this problem. And please let me know if you find any flaws in this approach!

dukuo commented 5 years ago

As @olistic said, I would really recommend as well checking GraphQL Shield for the authorization side of this topic, it's a very easy to use permissions layer based on middlewares and boolean logic.

cdelgadob commented 5 years ago

Thanks Dilip! Looks very interesting. How would this work with a multi tenant app?

El 16 dic 2018, a las 6:26, Dilip notifications@github.com escribió:

I would recommend checking GraphQL Shield for the authorization side of this topic, it's a very easy to use permissions layer based on middlewares and boolean logic.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

dukuo commented 5 years ago

Great question! My first thought is to package the auth rules in a separate dependency but that would cause some issues like duplication of types maybe. So instead of giving an uninformed opinion i'm digging into it now with separate functions. Will post something when I have the answer.

MeixnerTobias commented 5 years ago

Thanks Dilip! Looks very interesting. How would this work with a multi tenant app? El 16 dic 2018, a las 6:26, Dilip @.***> escribió: I would recommend checking GraphQL Shield for the authorization side of this topic, it's a very easy to use permissions layer based on middlewares and boolean logic. — You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub, or mute the thread.

@cdelgadob @dukuo In a multi-tenant settings you can go down 2 paths: Use customer header e.g. x-your-app-client-id to identify users/apps based on their client id. or Bake the client information into the token such as client id and permissions so the token stores which tenant it belongs too and user's claims

If you use Apollo server in Node you could use the context function in constructor to pass user permission down to middleware (graphql-shield) or resolvers. Then in graphql-shield you can define a function such as isOwner based on the token context.

dukuo commented 5 years ago

I might have confused terms, I was thinking on keeping authorization + authentication across multiple serverless functions! Speaking of which, @MeixnerTobias solution on how to implement headers or baking client id in the token (the later sounding not so safe IMO) would apply as well.

Has anyone played with schema stitching across serverless functions ?