Skip to content

Conversation

@brad-dow
Copy link
Contributor

@brad-dow brad-dow commented Jan 23, 2026

Description of changes

Fixes #3690

This update adds a new "Signing Admin API requests" guide that explains how a client can generate and include HMAC SHA-256 signatures when calling the Backend Admin API.

The guide includes the following:

  • Step-by-step instructions for creating valid request signatures
  • JavaScript code example provided by Max
  • Configuration reference table for relevant environment variables
  • A brief explanation of how Rafiki validates these signed requests
  • Cross-links to/from "Verify webhook signatures" page in case the reader ended up in the wrong, but similar content

Required

  • Used LinkOut component on external links
  • Reviewed Vale errors and made changes where appropriate
  • Ran Prettier
  • Previewed updates in Netlify
  • Received SME and/or peer approval if updates are significant
  • Included a "fixes #" comment

Conditional

  • Ensured sequence diagrams follow our style guide
  • Included code samples where appropriate
  • Updated related READMEs

@netlify
Copy link

netlify bot commented Jan 23, 2026

Deploy Preview for brilliant-pasca-3e80ec ready!

Name Link
🔨 Latest commit 58a9e23
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/6973b6663199610007359519
😎 Deploy Preview https://deploy-preview-3807--brilliant-pasca-3e80ec.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions github-actions bot added the pkg: documentation Changes in the documentation package. label Jan 23, 2026
@github-actions
Copy link

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 39.47
  • Iterations/s: 13.17
  • Failed Requests: 0.00% (0 of 2373)
📜 Logs

> performance@1.0.0 run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test "-k" "-q" "--vus" "4" "--duration" "1m"

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 857 kB 14 kB/s
     data_sent......................: 1.8 MB 30 kB/s
     http_req_blocked...............: avg=6.93µs   min=2.4µs    med=5.57µs   max=642.02µs p(90)=6.69µs   p(95)=7.16µs  
     http_req_connecting............: avg=638ns    min=0s       med=0s       max=567.18µs p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=100.61ms min=6.43ms   med=82.74ms  max=655.37ms p(90)=175.46ms p(95)=191.67ms
       { expected_response:true }...: avg=100.61ms min=6.43ms   med=82.74ms  max=655.37ms p(90)=175.46ms p(95)=191.67ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2373
     http_req_receiving.............: avg=101.48µs min=27.66µs  med=87.74µs  max=2.13ms   p(90)=128.69µs p(95)=160.61µs
     http_req_sending...............: avg=36.59µs  min=9.86µs   med=29.75µs  max=1.28ms   p(90)=42.37µs  p(95)=56.26µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=100.47ms min=6.27ms   med=82.6ms   max=655.29ms p(90)=175.33ms p(95)=191.56ms
     http_reqs......................: 2373   39.473887/s
     iteration_duration.............: avg=303.2ms  min=202.95ms med=288.31ms max=1.16s    p(90)=364.76ms p(95)=430.18ms
     iterations.....................: 792    13.174597/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

Copy link
Contributor

@BlairCurrey BlairCurrey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dont see any technical accuracy problems. Just the one tenant-id casing issue and a suggestion for the js example which is really more for Max.


Each request is HMAC‑signed with SHA‑256 using the tenant’s or operator’s API secret. This signature ensures that Rafiki can verify where the request originated and confirm that the payload data matches what was originally signed.

All signed requests include two headers: the `signature` header and the `tenant-Id` header.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
All signed requests include two headers: the `signature` header and the `tenant-Id` header.
All signed requests include two headers: the `signature` header and the `tenant-id` header.

Comment on lines +87 to +103
const timestamp = Date.now()
const version = process.env.ADMIN_API_SIGNATURE_VERSION

const { query, variables, operationName } = request
const formattedRequest = {
variables,
operationName,
query: print(query)
}

const payload = `${timestamp}.${canonicalize(formattedRequest)}`
const hmac = createHmac('sha256', process.env.ADMIN_API_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')

headers['signature'] = `t=${timestamp}, v${version}=${digest}`
headers['tenant-id'] = process.env.OPERATOR_TENANT_ID
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mkurapov thoughts on making what the request looks like more explicit? I have some familiarity with our implementation (which this comes from) but I still had to stop and think about what formattedRequest really is, other than "some GQL request in whatever form we need here". What I find unclear currently:

  • request is not defined anywhere. We can kinda reason about what we destructure off of it but we dont know exactly. Is query a string, object, some special gql class instance? (it's the latter)
  • print is not defined anywhere

In addition to making it more explicit, it might be useful to use less libraries. This js implementation is an example that should illustrate the concept in general. Someone might need to do this in go, rust etc. I figure the less magic happening from third party libraries the better. And for that matter, someone in js might not be using the same gql libraries.

What I would probably suggest is the more explicit one with less 3rd party libraries:

import { createHmac } from "crypto";
import { canonicalize } from "json-canonicalize";

const timestamp = Date.now();
const version = process.env.ADMIN_API_SIGNATURE_VERSION;

const requestBody = {
  query: `
    query GetAsset($id: String!) {
      asset(id: $id) {
        id
        code
        scale
      }
    }
  `,
  variables: { id: "asset-id-here" },
  operationName: "GetAsset",
};

// Canonicalize ensures both client and server produce identical JSON strings
// by sorting object keys deterministically and normalizing whitespace.
const payload = `${timestamp}.${canonicalize(requestBody)}`;                                                                     

const hmac = createHmac('sha256', process.env.ADMIN_API_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')

headers['signature'] = `t=${timestamp}, v${version}=${digest}`
headers['tenant-id'] = process.env.OPERATOR_TENANT_ID

Or the more explicit version of what we already have with print and the gql tagged function:

import { createHmac } from 'crypto'
import { canonicalize } from 'json-canonicalize'
import { gql } from '@apollo/client'
import { print } from 'graphql/language/printer'

const timestamp = Date.now()
const version = process.env.ADMIN_API_SIGNATURE_VERSION

const GET_ASSET = gql`
  query GetAsset($id: String!) {
    asset(id: $id) {
      id
      code
      scale
    }
  }
`

const requestBody = {
  query: print(GET_ASSET),  // converts `DocumentNode` to string
  variables: { id: 'asset-id-here' },
  operationName: 'GetAsset'
}

// Canonicalize ensures both client and server produce identical JSON strings
// by sorting object keys deterministically and normalizing whitespace.
const payload = `${timestamp}.${canonicalize(requestBody)}`
const hmac = createHmac('sha256', process.env.ADMIN_API_SECRET)
hmac.update(payload)
const digest = hmac.digest('hex')

headers['signature'] = `t=${timestamp}, v${version}=${digest}`
headers['tenant-id'] = process.env.OPERATOR_TENANT_ID

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pkg: documentation Changes in the documentation package.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs: Create guide for signing requests to the Admin API

4 participants