How to implement RBAC with express-graphql resolvers?

33 Views Asked by At

I'm implementing an express server using express-graphql for my React project. I have the following token being passed through validateTokenMiddleware():

{
  "http://localhost:8000/roles": [
    "admin"
  ],
  "iss": "https://<hidden>.auth0.com/",
  "sub": "auth0|<hidden>",
  "aud": [
    "http://localhost:8000",
    "https://<hidden>.auth0.com/userinfo"
  ],
  "iat": 1708694659,
  "exp": 1708781059,
  "azp": "<hidden>",
  "scope": "openid",
  "permissions": [
    "read:admin"
  ]
}

I'd like to use the permissions property to check whether a resolver can be executed. Is there a way to add decorators to resolvers or to add middleware to check the permissions of the token passed before executing a resolver? Here is the server app.js:

const express = require("express");
const bodyParser = require("body-parser");
const { graphqlHTTP } = require("express-graphql");
const cors = require("cors");

const graphqlSchema = require("./graphql/schema/index");
const graphqlResolvers = require("./graphql/resolvers/index");
const validateTokenMiddleware = require("./middleware/validate");

const app = express();

app.use(cors());
app.use(bodyParser.json());

app.use(
  "/graphql",
  validateTokenMiddleware,
  graphqlHTTP({
    schema: graphqlSchema,
    rootValue: graphqlResolvers,
    graphiql: true,
  })
);
const validateTokenMiddleware = async (req, res, next) => {
  const authHeader = req.headers.authorization;
  if (!authHeader || !authHeader.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  const token = authHeader.split(" ")[1];
  const tokenValidationResult = await isTokenValid(token);

  if (tokenValidationResult.error) {
    return res.status(401).json({ error: "Unauthorized" });
  }

  // Token is valid, add the decoded token to the request object for later use
  req.decodedToken = tokenValidationResult.decoded;
  next();
};

The schema is defined using buildSchema() from the graphql package and defined as:

const { buildSchema } = require("graphql");

module.exports = buildSchema(`
input CreateUserInput {
  id: String!
  firstName: String!
  lastName: String!
  birthdate: String!
  email: String!
  bio: String
}

input UpdateUserInput {
  id: ID!
  firstName: String
  lastName: String
  birthdate: String
  email: String
}

type User {
  _id: ID!
  firstName: String!
  lastName: String!
  birthdate: String!
  email: String!
  createdAt: String
  updatedAt: String
}

type Query {
  user(id: ID!): User
  listUsers: [User]

  ...
}

type Mutation {
  createUser(input: CreateUserInput!): User
  updateUser(input: UpdateUserInput!): User
  
  ...
}
`);

graphqlResolvers being passed to rootValue in graphqlHTTP() is an object of async resolvers. Here is an example resolver:

user: async ({ id }) => {
    try {
      const user = await User.findById(new ObjectId(id));
      return user;
    } catch (err) {
      throw err;
    }
  },
0

There are 0 best solutions below