GraphQL RBAC without JWT Roles
Tags: graphql, security, database, webdev
Published: 2022-01-03 05:12:02

Before I continue from my last post [GraphQL Auth Without Middleware](https://dev.to/verneleem/graphql-auth-without-middleware-37c6), a question came up:

> _Is it possible to use Role Based Access Control (RBAC) in [Dgraph Authorization rules](https://dgraph.io/docs/graphql/authorization/directive/), when my authentication and JWT provider is not capable of putting roles into the JWT given to clients?_

Yes, this is possible. Before I give a brief work around to enable this with a full example, I want to first show another reason why someone might want to not put user roles into the JWT.

If you are familiar with [JWT](https://jwt.io/)s, you will know that a JWT is secure only in that it is signed with a private key. This signature on the JWT validates that the payload is authenticated, but it does not prevent anyone else from decoding the payload. It does prevent JWT modification without access to the signing private key. The kicker though is that under most implementation you cannot revoke JWT tokens once issued.

> _Once issued, access tokens and ID tokens cannot be revoked in the same way as cookies with session IDs for server-side sessions._

> _As a result, tokens should be issued for relatively short periods, and then refreshed periodically if the user remains active._

> —[Auth0 Docs](https://auth0.com/docs/security/tokens/revoke-tokens)

So then let's play out a situation: A admin comes to the sight and is issued a JWT with a role of admin giving him full control. Even if this JWT is only short lived 24 hours, let's say this specific JWT is compromised is some way. Now how do you handle this?

- Do you change the private key which would also invalidate ALL of the other users tokens?
- Do you change all of your auth rules to use a new role and trash that old role?

Remember time is ticking and you have a hacker out there with a full access token. You want to block them NOW, but you cannot just stop your service for all other users.

There really isn't a good solution. There are some other hack around solutions such as [How do I revoke JWT everytime a new token is geneated using django-graphql](https://stackoverflow.com/questions/63135623/how-do-i-revoke-jwt-everytime-a-new-token-generated-using-django-graphql-jwt), but nothing that works really well because a user may actually want to be signed into multiple devices at the same time, and this hack around would prevent that from being allowed.

## So my solution is: never put the role into the JWT.

How then can you use a role in my auth rules if it is not in the JWT? You just need to create a path between all of your data needing RBAC to all of your users and put their role in the database. By keeping the role in the db instead of the JWT you can grant/revoke access without the user needing to reauthenticate and without the need to revoke compromised JWTs.

Linking all types needing RBAC to all your users sounds like a daunting task, but let me show you how easy it is.

Let's start with three types: User, Pages, Comments. To set this linking up, we will add a 4th type `Link` that will be this linking node.

```GraphQL
type User {
  username: String! @id
  comments: [Comment]
  link: Link!
}
type Page {
  id: ID
  title: String! @search
  content: String
  comments: [Comment]
  link: Link!
}
type Comment {
  id: ID
  comment: String! @search
  by: User!
  for: Page! @hasInverse(field: "comments")
  link: Link!
}
type Link {
  id: String! @id
  users: [Link] @hasInverse(field: "link")
}
```

Purposefully, the Link `id` is of type `String` instead of `ID` and it has the `@id` directive. This will make it a unique string id to help us create something we can easily reference later on.

After deploying this GraphQL schema to Dgraph, we need to create a single node for this linking node and then lock it down so no new nodes for this type will be created later. We can create a node by using the `addLink` mutation:

```GraphQL
mutation {
  addLink(input: [{id: "link"}]) {
    link {
      id
    }
  }
}
```

Very simply this created a single Link node with the id equal to `"link"`. If you go back up and look at the schema, you will notice that every type links to the type `Link` through the field `link`, but the link node only has a field connecting it inversely back to users. Also by making the `link` field a required field, every user, page, and comment must have a link. We want all of them to use the same link. So the next step is going to disable any future links from being created by the GraphQL endpoint. We do this by first disabling the mutations to add, update, and delete links, and then we create an auth rule on the Link type that will always be false, in essence never allowing a parent to create a new child link, but forcing it to use the existing link already created. We don't want to expose the root level queries for the Link type either, so we will also disable those from being generated by Dgraph.

```GraphQL
type User {
  id: ID!
  username: String! @id
  comments: [Comment]
  link: Link!
}
type Page {
  id: ID
  title: String! @search
  content: String
  comments: [Comment]
  link: Link!
}
type Comment {
  id: ID
  comment: String! @search
  by: User!
  for: Page! @hasInverse(field: "comments")
  link: Link!
}
type Link @generate(
  query: {
    get: false,
    query: false,
    aggregate: false
  },
  mutation: {
    add: false,
    update: false,
    delete: false
  }
) @auth(
  add: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" },
  update: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" },
  delete: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" }
) {
  id: String! @id
  users: [Link] @hasInverse(field: "link")
}
```

Deploying this GraphQL schema to Dgraph (after following the steps above) will leave us with a single Link node that is immutable. The auth rules `{$ALWAYS: { eq: \"_NEVER_\" }}` is a trick I use that makes the schema rules easy to read, and should always equate to false as long as the JWT does not contain a claim for `ALWAYS: "_NEVER_"` which should not be there unless you specifically craft it that way by your authentication jwt generator.

Now that we have this in place, we can start adding users, pages, and comments. Here are a few mutations to get you started:

```GraphQL
mutation addFooBar {
  addUser(input: [
    { username: "foo", link: { id: "link" } },
    { username: "bar", link: { id: "link" } }
  ]) {
    user {
      id
      username
    }
  }
}
mutation addNewPageWithComments {
  addPage(input: [
    {
      title: "Dgraph makes GraphQL Easy",
      content: "Did you know you can do role-less RBAC with Dgraph GraphQL?",
      link: { id: "link" },
      comments: [
        {
          comment: "I never thought this would be possible!",
          by: { username: "bar" },
          link: { id: "link" }
        }, {
          comment: "This has made my life so much easier.",
          by: { username: "foo" },
          link: { id: "link" }
        }
      ] },
  ]) {
    page {
      id
      title
      content
      comments {
        id
        comment
        by {
          id
          username
        }
      }
    }
  }
}
```

Now that we have a GraphQL API with our singular link node, 2 users, 1 page, and 2 comments, let's add user roles and auth rules to do the following:

- Only let an `Editor` edit pages.
- Only let an `Admin` delete pages.
- Let an `Admin` delete any comment.
- Let a user delete their own comment.
- Only let a `Subscriber` add a new comment.

```GraphQL

type User {
  id: ID!
  username: String! @id
  roles: [Role] @search
  comments: [Comment]
  link: Link!
}
enum Role {
  Admin
  Editor
  Author
  Subscriber
  Disabled
}
type Page @auth(
  # Only let an Editor edit pages
  update: { rule: """
    query ($username: String!) {
      queryPage {
        link {
          users(filter: {
            username: { eq: $username },
            roles: { eq: Editor }
          }) {
            id
          }
        }
      }
    }
  """ },
  # Only let an Admin delete pages
  delete: { rule: """
    query ($username: String!) {
      queryPage {
        link {
          users(filter: {
            username: { eq: $username },
            roles: { eq: Admin }
          }) {
            id
          }
        }
      }
    }
  """ }
) {
  id: ID
  title: String! @search
  content: String
  comments: [Comment]
  link: Link!
}
type Comment @auth(
  delete: { OR: [
    # Let an Admin delete any comment
    { rule: """
      query ($username: String!) {
        queryComment {
          link {
            users(filter: {
              username: { eq: $username },
              roles: { eq: Admin }
            }) {
              id
            }
          }
        }
      }
    """ },
    # Let a user delete their own comment
    { rule: """
      query ($username: String!) {
        queryComment {
          by(filter: {
            username: { eq: $username },
          }) {
            id
          }
        }
      }
    """ },
  ] },
  add: { rule: """
    query ($username: String!) {
      queryComment {
        link {
          users(filter: {
            username: { eq: $username },
            roles: { eq: Subscriber }
          }) {
            id
          }
        }
      }
    }
  """ }
) {
  id: ID
  comment: String! @search
  by: User!
  for: Page! @hasInverse(field: "comments")
  link: Link!
}
type Link @generate(
  query: {
    get: false,
    query: false,
    aggregate: false
  },
  mutation: {
    add: false,
    update: false,
    delete: false
  }
) @auth(
  add: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" },
  update: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" },
  delete: { rule: "{$ALWAYS: { eq: \"_NEVER_\" }}" }
) {
  id: String! @id
  users: [Link] @hasInverse(field: "link")
}
# Dgraph.Authorization {"VerificationKey":"your-256-bit-secret","Header":"X-My-App-Auth","Namespace":"https://dev.to/verneleem/example","Algo":"HS256"}
```

> You will need to setup an authentication service to generate JWTs for your users with their `username` in the claim. See [Dgraph Docs](https://dgraph.io/docs/graphql/authorization/authorization-overview/#dgraphauthorization-parameters) for more information. To aide in this process we created some JWTs you can use below.

This new schema with auth rules covers just the rules listed above. You will want to flesh out this schema with more rules to meet your own rule set for your application needs.

After deploying this GraphQL schema to Dgraph GraphQL, you will mostly likely want to add some roles to users. Here are a few mutation blocks to help do this:

```
mutation makeFooSubscriber {
  updateUser(input: {
    filter: {
      username: { eq: "foo" }
    }
    set: {
      roles: [Subscriber]
    }
  }) {
    user {
      id
      username
      roles
    }
  }
}
mutation makeBarEditor {
  updateUser(input: {
    filter: {
      username: { eq: "bar" }
    }
    set: {
      roles: [Editor]
    }
  }) {
    user {
      id
      username
      roles
    }
  }
}
mutation addBazAdmin {
  addUser(input: [{
    username: "baz",
    roles: [Admin],
    link: { id: "link" }
  }]) {
    user {
      id
      username
      roles
    }
  }
}
```

After making the mutations adding roles to your users and adding the new user `baz`, you can then use the correlating JWTs below sending them in the header of the GraphQL request in the `X-My-App-Auth` key.

foo: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDExNjgwMDAsImV4cCI6NDEwMjQ0NDgwMCwiaXNzIjoiand0LmlvIiwiaHR0cHM6Ly9kZXYudG8vdmVybmVsZWVtL2V4YW1wbGUiOnsidXNlcm5hbWUiOiJmb28ifX0.zoCgwCRKGATrL_9sw180Xb1X2PnsH-y62AuTeKKQ5MU`
bar: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDExNjgwMDAsImV4cCI6NDEwMjQ0NDgwMCwiaXNzIjoiand0LmlvIiwiaHR0cHM6Ly9kZXYudG8vdmVybmVsZWVtL2V4YW1wbGUiOnsidXNlcm5hbWUiOiJiYXIifX0.Vk_ihPY-QZfAjD5r5HB4Z8T9aaCoO9bPYV3aU5XCx5c`
baz: `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE2NDExNjgwMDAsImV4cCI6NDEwMjQ0NDgwMCwiaXNzIjoiand0LmlvIiwiaHR0cHM6Ly9kZXYudG8vdmVybmVsZWVtL2V4YW1wbGUiOnsidXNlcm5hbWUiOiJiYXoifX0.W4A_QDJyqeir7FZgHCkJLadoPI__-ZMIDpo2yQb9q4c`

Pick one of these JWTs and try to do the following tasks

- Add a new Comment
- Delete all comments
- Delete a Page
- Edit a Page

Hope this helps you see how you can accomplish the feat of Role Based Access Control without sacrificing your roles into your tokens.

What is great about this, is that is you update a user's role you can use the same token above and it will immediately resolve the rules for their new role. No longer will you have to force users to sign out and sign back in to grant additional roles. Also the best benefit in terms of security, is that if a user becomes compromised, you can immediately revoke the role for that specific user without waiting for them to destroying their token or even worse invalidating all users tokens to make the application secure.

Photo by <a href="https://unsplash.com/@claybanks?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Clay Banks</a> on <a href="https://unsplash.com/s/photos/around?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>