Request Signing

Block users from sending requests directly to your GraphQL API by verifying that requests have passed through Stellate first and haven't been tampered with.

How to use it

Step 1: Set up a secret in your Stellate service's configuration

This is ideally a long random string (don't worry, you won't have to type it):

import { Config } from 'stellate'

const config: Config = {
  config: {
    name: 'my-app',
    requestSigning: {
      secret: 'my-secret'
    },
  },
}
export default config

Step 2: Verify that requests arriving at your backend have been signed

import crypto from 'crypto'
import { GraphQLError } from 'graphql'

const serverHandler = async (req, res) => {
  const stellateSignature = req.headers['stellate-signature']

  if (!stellateSignature) {
    return res.status(401).json({
      errors: [new GraphQLError("You aren't authorized to request this API.")],
    })
  }

  const payload = JSON.stringify({
    query: req.body.query,
    variables: req.body.variables,
    operationName: req.body.operationName,
  })


  const values = stellateSignature.split(',')
  const obj = {
    signature: '',
    expiry: '',
  }

  values.forEach((val) => {
    if (val.startsWith('v1:')) {
      obj.signature = val.replace('v1:', '')
    } else if (val.startsWith('expiry:')) {
      obj.expiry = val.replace('expiry:', '')
    }
  })

  const sig = crypto
    .createHmac('sha256', 'my-secret')
    .update(payload)
    .digest('base64')
  
  if (
      !obj.signature ||
      !crypto.timingSafeEqual(
        Buffer.from(obj.signature, 'base64'),
        Buffer.from(sig, 'base64'),
      )
  ) {
    return res.status(401).json({
      errors: [new GraphQLError('Missmatch in the signature.')],
    })
  }

  if (Date.now() > Number(obj.expiry)) {
    return res.status(401).json({
      errors: [new GraphQLError('Signature has expired.')],
    })
  }

  // Handle request
}

How it works

The secret will be used to sign a base64 SHA-256 HMAC of a stringified version of { query: body.query, variables: body.variables, operationName: body.operationName }.

import { Config } from 'stellate'

const config: Config = {
  config: {
    name: 'my-app',
    requestSigning: {
      secret: 'my-secret'
    },
  },
}
export default config

When this configuration is pushed from then onwards you will receive an additional header with every request coming from the Stellate CDN, this header is named stellate-signature and will look like the following

v1:hash,expiry:timestamp

A few words about the properties

  • v1 is the current iteration of the hash, in the future we could have v2/... should we change the format
  • expiry is currently set 5 minutes from the moment we sign the request