Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Graphql @include with expression

I am implementing a query which should serve some fields in the response depending on user login status.

To be specific, I want to get "pointRate" field only if $authenticationToken is passed & would want to avoid passing $authenticated in below query. The reason I want to avoid sending $authenticated is client can do mistake by sending $authenticated = true but $authenticationToken = null.

query ToDoQuery($authenticationToken: String, $authenticated: Boolean!) {
    pointRate(accessToken: $authenticationToken) @include(if: $authenticated) {
        status
    }
}
like image 346
Anand Avatar asked Aug 30 '17 11:08

Anand


People also ask

How do you pass a query variable in GraphQL playground?

Query with variables GraphQL Playground can be used like GraphiQL to send queries with variables. Here, we use the “QUERY VARIABLES” tab to add the variable username , pass it as a parameter to the GraphQL query, and use it to fetch data.

How do you pass enum in a GraphQL mutation?

You don't have to worry about sending data as enum, just send it as String otherwise you will have to face JSON error, Graphql will take care of the value you send as a string (like 'MALE' or 'FEMALE') just don't forget to mention that gender is type of Gender(which is enum) in gql mutation as I did above.

How do you create a nested query in GraphQL?

You can use the object (one-to-one) or array (one-to-many) relationships defined in your schema to make a nested query i.e. fetch data for a type along with data from a nested or related type. The name of the nested object is the same as the name of the object/array relationship configured in the console.


2 Answers

So, Actually you want to do that

i) if $authenticationToken is passed, you want to get "pointRate".

ii) and you also want to avoid passing $authenticated in subsequent queries. Because you are concern about your clients who can make some mistake like sending authenticated is true where authentication token was null.

So in generally I want to answer that if you want to handle authentication by yourself using GraphQL, at first you have to create a token, then you have to pass the token in every request or with subsequent requests. Otherwise it is not possible. Because sensitive data's will not be provided without authentication.

On the other hand, you can use session auth. You can access every data until session is closed.

If it is not satisfactory, You can read the following brief description with a scenerio like yours. I also tried to accumulate some related sample solutions for better understanding, it may clarify you more.

As GraphQL API is completely public, you can make authentication by two ways.

  1. Let the web server (e.g. express or nginx) take care of authentication.
  2. Handle authentication in GraphQL itself.

If you do authentication in the web server, you can use a standard auth package (e.g. passport.js for express) and many existing authentication methods will work out of the box. You can also add and remove methods at your liking without modifying the GraphQL schema.

If you’re implementing authentication yourself, do the followings

  • Make sure to never store passwords in clear text or a MD5 or SHA-256 hash

    • Use something like bcrypt

    • Make sure to not store your session tokens as-is on the server, you should hash them first

    • You can write a login method, which sets the context. Since mutations are executed one after the other and not in parallel, you can be sure the context is set after the login mutation:

      mutation { loginWithToken(token: "6e37a03e-9ee4-42fd-912d-3f67d2d0d852"), do_stuff(greeting: "Hello", name: "Tom"), do_more_stuff(submarine_color: "Yellow") }

    • Instead of passing in the token via header or query parameter (like JWT, OAuth, etc), we make it part of the GraphQL query. Your schema code can parse the token directly using the JWT library itself or another tool.

    • Remember to always use HTTPS when passing sensitive information :)

As parallel execution is an important for performance. and mutation and queries are executed serially, in the order given. So in most cases It is preferred to handle authentication in the web server. It’s not only more generic, but also more flexible.


Scenerio:

First go through the followings

import jwt from'express-jwt';
import graphqlHTTP from'express-graphql';
import express from'express';
import schema from'./mySchema';
const app = express();

app.use('/graphql', jwt({
  secret: 'shhhhhhared-secret',
  requestProperty: 'auth',
  credentialsRequired: false,
}));
app.use('/graphql', function(req, res, done) {
  const user = db.User.get(req.auth.sub);
  req.context = {
    user: user,
  }
  done();
});
app.use('/graphql', graphqlHTTP(req => ({
    schema: schema,
    context: req.context,
  })
));

If you check in the above section, you will get that API is not secure at all. It might try to verify the JWT but if the JWT doesn’t exist or is invalid, the request will still pass through (see credentialsRequired: false). Why? We have to allow the request to pass through because if we blocked it we would block the entire API. That means, our users wouldn’t even be able to call a loginUser mutation to get a token to authenticate themselves.


Solution#1:

Barebone example using Authenticate resolvers, not endpoints.

import { GraphQLSchema } from 'graphql';
import { Registry } from 'graphql-helpers';

// The registry wraps graphql-js and is more concise
const registry = new Registry();

registry.createType(`
  type User {
    id: ID!
    username: String!
  }
`;

registry.createType(`
  type Query {
    me: User
  }
`, {
  me: (parent, args, context, info) => {
    if (context.user) {
      return context.user;
    }
    throw new Error('User is not logged in (or authenticated).');
  },
};

const schema = new GraphQLSchema({
  query: registry.getType('Query'),
});

By the time the request gets to our Query.me resolver, the server middleware has already tried to authenticate the user and fetch the user object from the database. In our resolver, we can then check the graphql context for the user (we set the context in our server.js file) and if one exists then return it else throw an error.

Note: you could just as easily return null instead of throwing an error and I would actually recommend it.

Solution#2:

Use functional Composition(middleware based) of express-graphql

import { GraphQLSchema } from 'graphql';
import { Registry } from 'graphql-helpers';
// See an implementation of compose https://gist.github.com/mlp5ab/f5cdee0fe7d5ed4e6a2be348b81eac12
import { compose } from './compose';

const registry = new Registry();

/**
* The authenticated function checks for a user and calls the next function in the composition if
* one exists. If no user exists in the context then an error is thrown.
*/
const authenticated =
  (fn: GraphQLFieldResolver) =>
  (parent, args, context, info) => {
    if (context.user) {
      return fn(parent, args, context, info);
    }
    throw new Error('User is not authenticated');
  };

/*
* getLoggedInUser returns the logged in user from the context.
*/
const getLoggedInUser = (parent, args, context, info) => context.user;

registry.createType(`
  type User {
    id: ID!
    username: String!
  }
`;

registry.createType(`
  type Query {
    me: User
  }
`, {
  me: compose(authenticated)(getLoggedInUser)
};

const schema = new GraphQLSchema({
  query: registry.getType('Query'),
});

The above code will work exactly the same as the first snippet. Instead of checking for the user in our main resolver function, we have created a highly reusable and testable middleware function that achieves the same thing. The immediate impact of this design may not be apparent yet but think about what would happen if we wanted to add another protected route as well as log our resolver running times. With our new design its as simple as:

const traceResolve =
  (fn: GraphQLFieldResolver) =>
  async (obj: any, args: any, context: any, info: any) => {
    const start = new Date().getTime();
    const result = await fn(obj, args, context, info);
    const end = new Date().getTime();
    console.log(`Resolver took ${end - start} ms`);
    return result;
  };

registry.createType(`
  type Query {
    me: User
    otherSecretData: SecretData
  }
`, {
  me: compose(traceResolve, authenticated)(getLoggedInUser)
  otherSecretData: compose(traceResolve, authenticated)(getSecretData)
};

Using this technique will help you build more robust GraphQL APIs. Function composition is a great solution for authentication tasks but you can also use it for logging resolvers, cleaning input, massaging output, and much more.

Solution#3:

A decent solution is to factor out data fetching into a separate layer and do the authorization check there. Below is an example that follows the principles outlined above. It’s for a query that fetches all todo lists that a user can see.

For the following query,

{
  allLists {
    name
  }
}

Don’t do this:

//in schema.js (just the essential bits)
allLists: {
  resolve: (root, _, ctx) => {
    return sql.raw("SELECT * FROM lists WHERE owner_id is NULL or owner_id = %s", ctx.user_id);
  }
}

Instead, I suggest you do this:

// in schema.js (just the essential bits)
allLists: {
  resolve: (root, _, ctx) => {
    //factor out data fetching
    return DB.Lists.all(ctx.user_id)
      .then( lists => {
        //enforce auth on each node
        return lists.map(auth.List.enforce_read_perm(ctx.user_id) );
      });
  }
}
//in DB.js 
export const DB = {
  Lists: {
    all: (user_id) => {
      return sql.raw("SELECT id FROM lists WHERE owner_id is NULL or owner_id = %s, user_id);
    }
  }
}
//in auth.js
export const auth = {
  List: {
   enforce_read_perm: (user_id) => {
     return (list) => {
       if(list.owner_id !== null && list.owner_id !== user_id){
         throw new Error("User not authorized to read list");
       } else {
         return list;
       }
     }
   }
}

You may think that the DB.Lists.all function is already enforcing permissions, but the way I see it it’s just trying not to fetch too much data, the permissions themselves are enforced not on each node separately. That way you have the auth checks in one place and can be sure that they will be applied consistently, even if you fetch data in many different places.

Solution#4:

Auth flow can be done in many different ways.

i) basic auth, 
ii) session auth, or
iii) token auth.

As your issue is according to token auth, I would like to meet you with Scaphold which one uses token authentication. Everything we do, whether it be logging a user into Scaphold or logging your user into your app, we use tokens to manage a user's auth status. The auth flow works like this:

a) User logs in with username and password.

b) The GraphQL server verifies the user in the database against his / her hashed password.

c) If successful, the server returns a JSON Web Token (JWT) that is a Base64 encoded token with an expiration date. This is the authentication token.

d) To use the authentication token, your future requests should include the authentication token in the header as

{ Authorization: 'Bearer' + [Auth_Token] }

Now, each time the server (perhaps Node Express) sees the token in the header, it will parse out the token, verify it, and in the GraphQL world, save the identified user in the context for use in the rest of the application. The user is now logged in.

For more, you can learn more about @include in this tutorial: https://github.com/mugli/learning-graphql/blob/master/4.%20Querying%20with%20Directives.md#include

For learning step by step graphql authentication, you can go through this tutorial: GraphQL Authentication

Resource Link:

  1. Authentication with GraphQL
  2. A guide to authentication in GraphQL
  3. Best practices for GraphQL security
like image 51
SkyWalker Avatar answered Oct 05 '22 06:10

SkyWalker


I don't think this is possible since you cannot convert an (empty) String to a Boolean in GraphQL.

Also, some advice from the official GraphQL docs:

Delegate authorization logic to the business logic layer

like image 32
zwippie Avatar answered Oct 05 '22 06:10

zwippie