Skip to main content

Context

A context is a representation of the incoming request and the environment. Context types define the fields and how to initialize them. A context is defined using the context keyword followed by the name of the context. Contexts, like types, include fields. For example, the following context defines AuthContext with three fields: id, email, and captcha.

context AuthContext {
@jwt("sub") id: Int
@jwt email: String
@query("checkCaptcha") captcha: Boolean
}

Each field in a context type carries an annotation that denotes the source of the value. Above, we use the @jwt annotation to specify how to initialize those. Exograph tries to coerce the value of the payload to the field's type and abandons the request if it fails.

You can use context types in a few ways:

Exograph supports several annotations to specify the source of the value of a field in a context type.

JWT Token

You may use the @jwt annotation to extract value from the JWT token specified in the Authentication header (of the form Authentication: Bearer <token>). Exograph will decode and verify the JWT token and extract the value specified in the annotation parameter from the decoded token. The incoming request will fail if the JWT token is invalid or expired.

The @jwt annotation takes a single optional argument, which denotes the key in the decoded token. In the above example, for the id field, we specify the "sub" argument to extract the "sub" key from the JWT payload. Exograph uses the field name as the key if the annotation parameter is absent. Therefore, we didn't provide the argument for the role field since the field's name matches the key in the JWT payload. In other words, the following two context fields are equivalent:

@jwt role: String
@jwt("role") role: String

The field will be set to ' null ' if the JWT token is not present in the request.

To make the @jwt annotation work, you must configure JWT authentication. See authentication for more details.

Request Header

You can use the @header annotation to specify a field in the context derived from the request header. The annotation parameter is the name of the header.

@header("X-Forwarded-For") connectingIp: String

A typical use of the @header annotation is to extract a header value, such as the API key or a captcha code. Then, coupled with the query annotation, you can use the header value to invoke a query and use the result as a context field.

You can use the @cookie annotation to extract the value of a cookie. The annotation parameter specifies the name of the cookie.

@cookie("token") token: String

Usages of the @cookie annotation are similar to the @header annotation.

Environment Variable

You can use the @env annotation to extract an environment variable. The annotation parameter specifies the name of the environment variable.

@env("MODE") isProduction: Boolean

Typically, you will use the @env context fields to implement environment-specific authorization. For example, you can use the CUSTOMER_ID environment variable to specify the customer ID for the current environment and use it with some JWT token value.

context AuthContext {
@jwt role: String
@jwt("sub") id: Int
@jwt customerId: Int
}

context CustomerContext {
@env("CUSTOMER_ID") customerId: Int
}

@access(AuthContext.customerId == CustomerContext.customerId)
...

Here, we define an access control rule that allows access only to the customer ID specified in the environment variable.

Processed Value

So far, we have seen how to extract raw values from the request and environment. However, you may want to process those values before using them in access control expressions or injected dependencies. For example, you may want to extract a header carrying an API key and decode it to get the customer ID, resulting in modularization of the logic to map the API key to the customer ID.

Exograph offers the @query annotation to process values from other contexts. The annotation takes a query name as an argument. The associated query declares other contexts as injected arguments so that the associated implementation can compute the result using those values.

Suppose you want to ensure that the user has cleared a captcha challenge. You can use the @header annotation to extract the captcha code. Let's first define the CaptchaValidatorContext context that grabs a couple of headers needed to perform the captcha validation logic.

context CaptchaValidatorContext {
@header("X-Captcha-Id") uuid: Uuid
@header("X-Captcha-Response") response: String
}

Let's also define a Captcha module with a verifyCaptcha query, which takes the CaptchaValidatorContext as an injected argument.

@deno("captcha.ts")
module Captcha {
@access(true) query verifyCaptcha(@inject context: CaptchaValidatorContext): Boolean
}

The associated TypeScript code in captcha.ts uses the header values to verify the captcha and return a boolean value:

function verifyCaptcha(context: CaptchaValidatorContext): boolean {
const { uuid, response } = context;
// verify the captcha
return true;
}

We now have all the ingredients to implement the captcha validation logic. We define the CaptchaContext context that uses the @query annotation to invoke the verifyCaptcha query and use the result as a context field.

context CaptchaContext {
@query("verifyCaptcha") isValid: Boolean
}

With this setup, it is easier to express access control expressions. For example, we can use the CaptchaContext in the @access annotation to specify that the Todo model is only accessible if the captcha is valid.

@access(CaptchaContext.isValid)
type Todo {
...
}

Currently, the @query annotation is limited to queries that only take other contexts as injected arguments and return a single primitive value. We will expand this support in the future.