Skip to main content

Overview

In a typical Exograph application, the model declares a context to capture the user's identity and uses it to specify access control rules, etc. Earlier, when discussing the context concept, we briefly looked at the @jwt annotation. Let's take a closer look at this annotation and how to configure JWT authentication.

This section will explore how to set up symmetric and OpenID authentication. We will also explore how to test your authentication logic in the GraphQL Playground.

The @jwt annotation

Consider the following claims encoded as a JWT token and passed in the Authorization header in the form Bearer <token>:

{
"sub": "1234567890",
"name": "Jordan Taylor",
"role": "admin",
"email": "[email protected]"
}

In the Exograph definition, you can capture any of these claims using the @jwt annotation. For example, you may want to know the user's id (typically available as the sub field in a JWT token) and use it to implement access control rules such as "a user can only access only their todos". Similarly, you may want to capture the role to implement rules such as "admin users can access any todos". The following context definition will do the job:

context AuthContext {
@jwt sub: string
@jwt role: string
}

By default, Exograph assumes that the claim name is the same as the context field name. Thus, the sub field is assumed to be the sub claim, and the role field is assumed to be the role claim. You can explicitly specify the claim field name by providing it as the argument to the @jwt annotation. For example, since the sub field is (typically) the user's id, we may want to rename it to id as follows:

context AuthContext {
@jwt("sub") id: string
@jwt role: string
}

If the JWT token contains nested claims, you can access them using the dot notation. For example, if the JWT token contains the sub as a direct claim and the role as a nested claim under a user field such as:

{ 
"sub": "1234567890",
"user": {
"role": "admin"
}
}

You can access them in the context as follows (note the use of user.role):

context AuthContext {
@jwt("sub") id: string
@jwt("user.role") role: string
}

Once we have the context, we can use it in access control rules, as default values for fields, and as injected arguments to queries, mutations, and interceptors defined in Deno modules.

Using in access control rules

We explored access control rules in the access control for Postgres and for Deno section. We will explore it from the @jwt annotation's perspective here.

Assume you have a Todo type with a userId field and the AuthContext defined earlier.

@postgres
module TodoDatabase {
type Todo {
@pk id: Int = autoIncrement()
title: String
completed: Boolean
userId: String
}
}

To implement the "a user can only access only their todos" rule as follows, you can attach the following access control rule to the Todo type.

@postgres
module TodoDatabase {
@access(self.userId == AuthContext.id)
type Todo {
@pk id: Int = autoIncrement()
title: String
completed: Boolean
userId: String
}
}

This rule specifies that to query or mutate a Todo, its userId field must be the same as the AuthContext's id field. You will get an error if you try to query or mutate a Todo for another user.

If later you want to implement the "admin users can access any todos" rule, you can do so by adding || AuthContext.role == "admin" to the access control rule:

@postgres
module TodoDatabase {
@access(self.userId == AuthContext.id || AuthContext.role == "admin")
type Todo {
@pk id: Int = autoIncrement()
title: String
completed: Boolean
userId: String
}
}

With this rule, if the user is an admin, the expression will evaluate to true, and the user can access any Todo.

Using in default values

With the earlier Todo definition, if you wanted to create a new Todo, you would need to specify the userId field.

mutation {
createTodo(
data: { title: "Buy milk", completed: false, userId: "1234567890" }
) {
id
title
completed
userId
}
}

This is not ideal since the user's id is already available in the AuthContext. We can use the AuthContext to set the userId field to the user's id as follows:

@postgres
module TodoDatabase {
@access(self.userId == AuthContext.id || AuthContext.role == "admin")
type Todo {
@pk id: Int = autoIncrement()
title: String
completed: Boolean
userId: String = AuthContext.id
}
}

With the default value specified, you can omit the userId field when creating a new Todo.

mutation {
createTodo(data: { title: "Buy milk", completed: false }) {
id
title
completed
userId
}
}

However, an admin user can still create a Todo for another user by explicitly specifying the userId field.

Using in Deno modules

Besides access control rules, you may want to capture the user's identity as an argument to a Deno query, mutation, or interceptor. For example, you may want to log who performed a mutation. You can add the AuthContext as an argument to the interceptor.

@deno("log-mutations.js")
module LogMutations {
interceptor logMutations(operation: Operation, context: AuthContext)
}
export function logMutations(operation: Operation, context: AuthContext) {
const userId = context.id;
const operationName = operation.name();
const argsString = JSON.stringify(operation.args);

console.log(
`User ${userId} performed ${operationName} with arguments ${argsString}`
);
}