0
votes

I currently have a Node.js back-end running Express with Passport.js for authentication and am attempting to switch to GraphQL with Apollo Server. My goal is to implement the same authentication I am using currently, but cannot figure out how to leave certain resolvers public while enabling authorization for others. (I have tried researching this question extensively yet have not been able to find a suitable solution thus far.)

Here is my code as it currently stands:

My JWT Strategy:

const opts = {};
opts.jwtFromRequest = ExtractJwt.fromAuthHeaderAsBearerToken();
opts.secretOrKey = JWT_SECRET;

module.exports = passport => {
  passport.use(
    new JwtStrategy(opts, async (payload, done) => {
      try {
        const user = await UserModel.findById(payload.sub);
        if (!user) {
          return done(null, false, { message: "User does not exist!" });
        }
        done(null, user);
      } catch (error) {
        done(err, false);
      }
    })
  );
}

My server.js and Apollo configuration: (I am currently extracting the bearer token from the HTTP headers and passing it along to my resolvers using the context object):

const apollo = new ApolloServer({
  typeDefs,
  resolvers,
  context: async ({ req }) => {
    let authToken = "";

    try {
      if (req.headers.authorization) {
        authToken = req.headers.authorization.split(" ")[1];
      }
    } catch (e) {
      console.error("Could not fetch user info", e);
    }

    return {
      authToken
    };
  }
});

apollo.applyMiddleware({ app });

And finally, my resolvers:

exports.resolvers = {
  Query: {
    hello() {
      return "Hello world!";
    },
    async getUserInfo(root, args, context) {
      try {
        const { id } = args;
        let user = await UserModel.findById(id);
        return user;
      } catch (error) {
        return "null";
      }
    },
    async events() {
      try {
        const eventsList = await EventModel.find({});
        return eventsList;
      } catch (e) {
        return [];
      }
    }
  }
};

My goal is to leave certain queries such as the first one ("hello") public while restricting the others to requests with valid bearer tokens only. However, I am not sure how to implement this authorization in the resolvers using Passport.js and Passport-JWT specifically (it is generally done by adding middleware to certain endpoints, however since I would only have one endpoint (/graphql) in this example, that option would restrict all queries to authenticated users only which is not what I am looking for. I have to perform the authorization in the resolvers somehow, yet not sure how to do this with the tools available in Passport.js.)

Any advice is greatly appreciated!

1

1 Answers

2
votes

I would create a schema directive to authorized query on field definition and then use that directive wherever I want to apply authorization. Sample code :

class authDirective extends SchemaDirectiveVisitor {
    visitObject(type) {
        this.ensureFieldsWrapped(type);
        type._requiredAuthRole = this.args.requires;
    }

    visitFieldDefinition(field, details) {
        this.ensureFieldsWrapped(details.objectType);
        field._requiredAuthRole = this.args.requires;
    }

    ensureFieldsWrapped(objectType) {
        // Mark the GraphQLObjectType object to avoid re-wrapping:
        if (objectType._authFieldsWrapped) return;
        objectType._authFieldsWrapped = true;

        const fields = objectType.getFields();

        Object.keys(fields).forEach(fieldName => {
            const field = fields[fieldName];
            const {
                resolve = defaultFieldResolver
            } = field;
            field.resolve = async function (...args) {
                // your authorization code 
                return resolve.apply(this, args);
            };
        });
    }

}

And declare this in type definition

directive @authorization(requires: String) on OBJECT | FIELD_DEFINITION

map schema directive in your schema

....
resolvers,
schemaDirectives: {
authorization: authDirective
}

Then use it on your api end point or any object

Query: {
    hello { ... }
    getuserInfo():Result @authorization(requires:authToken) {...} 
    events():EventResult @authorization(requires:authToken) {...} 
  };