Webhook

Introduction

A webhook is an HTTP request, triggered by an event in a source system and sent to a destination system, often with a payload of data. Webhooks are automated, in other words they are automatically sent out when their event is fired in the source system.

This provides a way for one system (the source) to "speak" (HTTP request) to another system (the destination) when an event occurs, and share information (request payload) about the event that occurred.

Annoto backend is the source and will send messages to the destination platform.

Webhook Security

Annoto will include a secure signature in all the webhook requests so that the destination platform can verify that the webhook request was generated by Annoto, and not from some server acting like Annoto.

Since the signature is specific to each and every webhook request, it also helps to validate that the message wasn’t intercepted and modified by someone in between the destination and the source (i.e. Man-in-the-middle attack).

We will send webhook messages only to valid TLS 1.2 and up (Https) endpoint.

Security Header

Each Webhook message will include a signature header:

X-Annoto-JWS - A JSON Web Signature (RFC-7797) signed using the Annoto JSON Web Key Set (JWKS / RFC-7517).

The X-Annoto-JWS signature is a JSON Web Signature (JWS) and has a detached payload. It is a string that looks like JWS_PROTECTED_HEADER..JWS_SIGNATURE

JSON Web Signature (JWS) represents content secured with digital signatures or Message Authentication Codes (MACs) using JSON-based [RFC7159] data structures. The JWS cryptographic mechanisms provide integrity protection for an arbitrary sequence of octets.

The signature generation works as follows:

  • Webhook message is generated (e.g LTI outcomes)

  • The payload is signed using one of the private keys from Anntoo's JSON Web Key Set (e.g. kid: PylK7Us1ntafSdT9RxJ3q

  • The JWS protected header will contain the following standard properties:

    • kid - The key used to sign the request. This can be looked up in the JWKS

    • alg - Will be RS256

  • The detached JWS will be added as the X-Annoto-JWS header to the Webhook request.

  • The HTTP Request is sent using POST method to the Webhook destination endpoint.

Verifying a Webhook Payload

To verify that a webhook did actually come from Annoto, you need to compare the JSON Web Signature (JWS) from the X-Annoto-JWS header with the JSON body of the request:

  1. Ensure the X-Annoto-JWS header exists. If it doesn't exist, this request didn't come from Annoto.

  2. Look up the Annoto JWKS at the URL provided by Annoto. This contains the public keys, and should have a kid that matches the kid of the JWS_PROTECTED_HEADER

  3. Use a JWT library for your programming language to verify the body matches the signature.

    To implement the verification, some languages may require you to build URL variant of Base64 Encode of the JWS Payload (the webhook body) in order to verify the JWS.

    The JWS signature uses a detached payload, so it is of the form JWS_PROTECTED_HEADER..JWS_SIGNATURE

as per RFC the kid for an individual JWK is immutable, and therefore it is safe and recommended to cache individual JWK's by their kid indefinitely (a proper JWT library will probably do that for you).

Example libraries that support RFC-7797 and JWKS, and simplify verifying a JWS:

Pseudo PHP example code

$jws = getRequestHeader('X-Annoto-JWS');
$encodedPayload = urlsafeB64Encode($rawRequestPayload);
list($header, $signature) = explode('..', $jws);
$fullJWT = implode('.', [$header, $encodedPayload, $signature]);
$publicJwk = getPublicJwkFromAnnotoJwksUrl();
validateJwt($fullJWT, $publicJwk);

Webhook Messages

Webhook messages are POST requests with JSON body:

export interface IWebhookMessagePayload<T extends WebhookType = WebhookTytpe> {
  type: T;
  data: IWebhookDataMap[T];
  /**
   * Unique identifier
   */
  id: string;
  /**
   * Strictly increasing timestamp formatted using ISO 8601 with a sub-second precision.
   */
  timestamp: string;
  /**
   * id of the host
   */
  hostId: string;
}

export interface IWebhookDataMap {
  ltiOutcome: IWebhookLtiOutcomeData;
}

export type WebhookType = keyof IWebhookDataMap;

LTI Outcomes

Annoto supports managing the LTI 1.3 Assignment and Grades via third party LTI 1.3 Tool. Webhook serving as a proxy medium.

Webhook messages JSON body:

export interface IWebhookMessagePayload {
  type: 'ltiOutcome';
  data: IWebhookLtiOutcomeData;
  /**
   * Unique identifier
   */
  id: string;
  /**
   * Strictly increasing timestamp formatted using ISO 8601 with a sub-second precision.
   */
  timestamp: string;
}

/**
 * Webhook for proxy of LTI Outcomes.
 * LTI 1.3/1.1 Service Message Response should be returned as
 * Response to the Webhook API as is, including:
 * Response code
 * Link http header (RFC8288) (For LTI 1.3)
 * Content-Type header
 * Response body
 */
export interface IWebhookLtiOutcomeData {
  /**
   * LTI version Used
   */
  version: '1.3' | '1.1';
  /**
   * Platform service URL to where to send the Service Message
   */
  serviceUrl: string;
  /**
   * The request method to use for sending the payload to the serviceUrl
   */
  method: RequestMethodType;
  /**
   * Headers to use for the service message
   * such as Content-Type
   */
  headers: Record<string, string>;
  /**
   * Scopes for the Authorization token
   * Required for LTI 1.3.
   */
  scope?: Lti13AGSScopeType[];
  /**
   * JSON Payload (Body) for the Service Message to be forwarded as is
   * can be empty for example for GET lineitem request
   * object for LTI 1.3
   * xml string for LTI 1.1
   */
  payload?: object | string;
  /**
   * LTI 1.3 Tool deployment client_id
   */
  clientId?: string;
}

export type RequestMethodType = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
export type Lti13AGSScopeType =
    'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem' |
    'https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly' |
    'https://purl.imsglobal.org/spec/lti-ags/scope/score' |
    'https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly';

Last updated