Authorization (draft)

These are draft docs for features that will be released in 20.07.0 and in beta releases before then. They don’t apply to the 20.03.x release series.

Dgraph GraphQL comes with inbuilt authorization. It allows you to annotate your schema with rules that determine who can access or mutate what data.

Firstly, let’s get some concepts defined. There are two important concepts in what’s often called ‘auth’.

  • authentication : who are you; and
  • authorization : what are you allowed to do.

Dgraph GraphQL deals with authorization, but is completely flexible about how your app does authentication. You could authenticate your users with a cloud service like OneGraph or Auth0, use some social sign in options, or write bespoke code. The connection between Dgraph and your authentication mechanism is a signed JWT - you tell Dgraph, for example, the public key of the JWT signer and Dgraph trusts JWTs signed by the corresponding private key.

With an authentication mechanism set up, you then annotate your schema with the @auth directive to define your authorization rules, attach details of your authentication provider to the last line of the schema, and pass the schema to Dgraph. So your schema will follow this pattern.

type A @auth(...) {
    ...
}

type B @auth(...) {
    ...
}

# Authorization HEADER CLAIMS-KEY ALGORITHM KEY
  • HEADER is the header in which requests will send the signed JWT
  • CLAIMS-KEY is the key inside the JWT that contains the claims relevant to Dgraph auth
  • ALGORITHM can be either HS256 or RS256, and
  • KEY is the string value of the key (newlines replaced with \n) wrapped in ""

Valid examples look like

# Authorization X-My-App-Auth https://my.app.io/jwt/claims HS256 "secretkey"

for HMAC-SHA256 JWT with symmetric cryptography (the signing key and verification key are the same), and like

# Authorization X-My-App-Auth https://my.app.io/jwt/claims RS256 "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"

for RSA Signature with SHA-256 asymmetric cryptography (the JWT is signed with the private key and Dgraph checks with the public key).

Both cases expect the JWT to be in a header X-My-App-Auth and expect the JWT to contain custom claims object "https://my.app.io/jwt/claims": { ... } with the claims used in authorization rules.

Note: authorization is in beta and some aspects may change - for example, it’s possible that the method to specify the header, key, etc. will move into the /admin updateGQLSchema mutation that sets the schema. Some features are also in active improvement and development - for example, auth is supported an on types, but interfaces (and the types that implement them) don’t correctly support auth in the current beta.

The @auth directive

Given an authentication mechanism and signed JWT, it’s the @auth directive that tells Dgraph how to apply authorization. The directive can be used on any type (that isn’t a @remote type) and specifies the authorization for query as well as add, update and delete mutations.

In each case, @auth specifies rules that Dgraph applies during queries and mutations. Those rules are expressed in exactly the same syntax as GraphQL queries. Why? Because the authorization you add to your app is about the graph of your application, so graph rules make sense. It’s also the syntax you already know about, you get syntax help from GraphQL tools in writing such rules, and it turns out to be exactly the kinds of rules Dgraph already knows how to evaluate.

Here’s how the rules work.

Authorization rules

A valid type and rule looks like the following.

type Todo @auth(
  query: { rule: """
    query ($USER: String!) { 
      queryTodo(filter: { owner: { eq: $USER } } ) { 
        id 
      } 
    }"""
  }
){
  id: ID!
  text: String! @search(by: [term])
  owner: String!
}

Here we define a type Todo, that’s got an id, the text of the todo and the username of the owner of the todo. What todos can a user query? Any Todo that the query rule would also return.

The query rule in this case expects the JWT to contain a claim "USER": "..." giving the username of the logged in user, and says: you can query any todo that has your username as the owner.

This rule is applied automatically at query time. For example, the query

query {
  queryTodo {
    id
    text
  }
}

will return only the todo’s where owner equals amit, when Amit is logged in and only the todos owned by nancy when she’s logged into your app.

Similarly,

query {
  queryTodo(filter: { text: { anyofterms: "graphql"}}, first: 10, order: { asc: text }) {
    id
    text
  }
}

will return the first ten todos, ordered ascending by title of the user that made the query.

This means your frontend doesn’t need to be sensitive to the auth rules. Your app can simply query for the todos and that query behaves properly depending on who’s logged in.

In general, an auth rule should select a field that’s expected to exist at the inner most field, often that’s the ID or @id field. Auth rules are run in a mode that requires all fields in the rule to find a value in order to succeed.

Graph traversal in auth rules

Often authorization depends not on the object being queried, but on the connections in the graph that object has or doesn’t have. Because the auth rules are graph queries, they can express very powerful graph search and traversal.

For a simple todo app, it’s more likely that you’ll have types like this:

type User {
  username: String! @id
  todos: [Todo]
}

type Todo {
  id: ID!
  text: String!
  owner: User
}

This means your auth rule for todos will depend not on a value in the todo, but on checking which owner it’s linked to. This means our auth rule must make a step further into the graph to check who the owner is.

query ($USER: String!) { 
  queryTodo {
    owner(filter: { username: { eq: $USER } } ) { 
      username
    } 
  } 
}

You can express a lot with these kinds of graph traversals. For example, multitenancy rules can express that you can only see an object if it’s linked (through what ever graph search you define) to the organization you were authenticated from. That means your app can split data per customer easily.

You can also express rules that can be administered by the app itself. You might define type Role and enum Privileges that can have values like VIEW, ADD, etc. and state in your auth rules that a user needs to have a role with particular privileges to query/add/update/delete and those roles can then be allocated inside the app. For example, in an app about project management, when a project is created the admin can decide which users have view or edit permission, etc.

Role Based Access Control

As well as rules that relate a user’s claims to a graph traversal, role based access control rules are also possible. These rules relate a claim in the JWT to a known value.

For example, perhaps only someone logged in with the ADMIN role is allowed to delete users. For that, we might expect the JWT to contain a claim "ROLE": "ADMIN", and can thus express a rule that only allows users with the ADMIN claim to delete.

type User @auth(
  delete: { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
) { 
  username: String! @id
  todos: [Todo]
}

Not all claims need to be present in all JWTs. For example, if the ROLE claim isn’t present in a JWT, any rule that relies on ROLE simply evaluates to false. As well as simplifying your JWTs (e.g. not all users need a role if it doesn’t make sense to do so), this means you can also simply disallow some queries and mutations. If you know that your JWTs never contain the claim DENIED, then a rule such as

type User @auth(
  delete: { rule:  "{$DENIED: { eq: \"DENIED\" } }"}
) { 
  ...
}

can never be true and this would prevent users ever being deleted.

and, or & not

Rules can be combined with the logical connectives and, or and not, so a permission can be a mixture of graph traversals and role based rules.

In the todo app, you can express, for example, that you can delete a Todo if you are the author, or are the site admin.

type Todo @auth(
  delete: { or: [ 
    { rule:  "query ($USER: String!) { ... }" }, # you are the author graph query
    { rule:  "{$ROLE: { eq: \"ADMIN\" } }" }
  ]}
)

Public Data

Many apps have data that can be accessed by anyone, logged in or not. That also works nicely with Dgraph auth rules.

For example, in Twitter, StackOverflow, etc. you can see authors and posts without being signed it - but you’d need to be signed in to add a post. With Dgraph auth rules, if a type doesn’t have, for example, a query auth rule or the auth rule doesn’t depend on a JWT value, then the data can be accessed without a signed JWT.

For example, the todo app might allow anyone, logged in or not, to view any author, but not make any mutations unless logged in as the author or an admin. That would be achieved by rules like the following.

type User @auth(
  # no query rule
  add: { rule:  "{$ROLE: { eq: \"ADMIN\" } }" },
  update: ...
  delete: ...
) {
  username: String! @id
  todos: [Todo]
}

Maybe some todos can be marked as public and users you aren’t logged in can see those.

type Todo @auth(
  query: { or: [
    # you are the author 
    { rule: ... },
    # or, the todo is marked as public
    { rule: """query { 
      queryTodo(filter: { isPublic: { eq: true } } ) { 
        id 
      } 
    }"""}
  ]}
) { 
  ...
  isPublic: Boolean
}

Because the rule doesn’t depend on a JWT value, it can be successfully evaluated for users who aren’t logged in.

Ensuring that requests are from an authenticated JWT, and no further restrictions, can be done by arranging the JWT to contain a value like "isAuthenticated": "true". For example,

type User @auth(
  query: { rule:  "{$isAuthenticated: { eq: \"true\" } }" },
) {
  username: String! @id
  todos: [Todo]
}

specifies that only authenticated users can query other users.

Mutations

Mutations with auth work similarly to query. However, mutations involve a state change in the database, so it’s important to understand when the rules are applied and what they mean.

Add

Rules for add authorization state that the rule must hold of nodes created by the mutation data once committed to the database.

For example, a rule such as:

type Todo @auth(
  add: { rule: """
    query ($USER: String!) { 
      queryTodo {
        owner(filter: { username: { eq: $USER } } ) { 
          username
        } 
      } 
    }"""
  }
){{
  id: ID!
  text: String!
  owner: User
}

type User {
  username: String! @id
  todos: [Todo]
}

states that if you add a new todo, then that new todo must be a todo that satisfies the add rule, in this case saying that you can only add todos with yourself as the author.

Delete

Delete rules filter the nodes that can be deleted. A user can only ever delete a subset of the nodes that the delete rules allow.

For example, this rule states that a user can delete a todo if they own it, or they have the ADMIN role.

type Todo @auth(
  delete: { or: [ 
    { rule: """
      query ($USER: String!) { 
        queryTodo {
          owner(filter: { username: { eq: $USER } } ) { 
            username
          } 
        } 
      }"""
    },
    { rule:  "{$ROLE: { eq: \"ADMIN\" } }"}
  ]}
){{
  id: ID!
  text: String! @search(by: [term])
  owner: User
}

type User {
  username: String! @id
  todos: [Todo]
}

So a mutation like:

mutation {
  deleteTodo(filter: { text: { anyofterms: "graphql" } }) {
    numUids    
  }
}

for most users would delete their own posts that contain the term “graphql”, but wouldn’t affect any other user’s todos; for an admin, it would delete any users posts that contain “graphql”

For add, what matters is the resulting state of the database, for delete it’s the state before the delete occurs.

Update

Updates have both a before and after state that can be important for auth.

For example, consider a rule stating that you can only update your own todos. If evaluated in the database before the mutation, like the delete rules, it would prevent you updating anyone elses todos, but does it stop you updating your own todo to have a different owner. If evaluated in the database after the mutation occurs, like for add rules, it would stop setting the owner to another user, but would it prevent editing other’s posts.

Currently, Dgraph evaluates update rules before the mutation. Our auth support is still in beta and we may extend this for example to make the update rule an invariant of the mutation, or enforce pre and post conditions, or even allow custom logic to validate the update data.

Update and Add

Update mutations can also insert new data. For example, you might allow a mutation that runs an update mutation to add a new todo.

mutation {
  updateUser(input: {
    filter: { username: { eq: "aUser" }},
    set: { todos: [ { text: "do this new todo"} ] }
  }) {
    ...
  }
}

Such a mutation updates a user’s todo list by inserting a new todo. It would have to satisfy the rules to update the author and the rules to add a todo. If either fail, the mutation has no effect.