Skip to content

Commit f5f0221

Browse files
authored
docs: add guide for signing Admin API requests (#3807)
* docs: add guide for signing Admin API requests * Update signing-admin-api-requests.mdx Code snippet fixes from Blair * Prettier, linting, etc. Casing fix pnpm format and pnpm lint
1 parent 32eff2e commit f5f0221

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

packages/documentation/astro.config.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,10 @@ export default defineConfig({
315315
label: 'GraphQL Admin APIs',
316316
link: '/apis/graphql/admin-api-overview'
317317
},
318+
{
319+
label: 'Signing Admin API requests',
320+
link: '/apis/graphql/signing-admin-api-requests'
321+
},
318322
{
319323
label: 'Backend Admin API',
320324
link: '/apis/graphql/backend',
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
---
2+
title: Signing Admin API requests
3+
---
4+
5+
import { Tabs, TabItem } from '@astrojs/starlight/components'
6+
import { LinkOut, Badge } from '@interledger/docs-design-system'
7+
8+
Rafiki requires every request to the Backend Admin API to include a valid cryptographic signature.
9+
10+
This signature authenticates the caller and ensures the request body has not been altered in transit.
11+
12+
## Signature overview
13+
14+
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.
15+
16+
All signed requests include two headers: the `signature` header and the `tenant-id` header.
17+
18+
### `signature` header
19+
20+
The `signature` header authenticates the request payload. Rafiki uses this value to verify the request's integrity.
21+
22+
```bash title="signature header"
23+
signature: t=<timestamp>, v<version>=<digest>
24+
```
25+
26+
- `t=<timestamp>`: The UNIX timestamp (in seconds) when the signature was generated.
27+
- `v<version>=<digest>`: The versioned HMAC SHA-256 signature digest. The default version is v1.
28+
29+
### `tenant-id` header
30+
31+
```bash title="tenant-id header"
32+
tenant-id: <OPERATOR_TENANT_ID>
33+
```
34+
35+
- `<OPERATOR_TENANT_ID>`: The unique UUID v4 identifying the tenant or operator.
36+
37+
Rafiki uses the signature to authenticate the tenant or operator and prevent replay attacks.
38+
39+
## How signing works
40+
41+
To protect the Admin API from unauthorized or replayed requests, each client request must include a digital signature that Rafiki can verify. By generating a signature before sending your request, you allow Rafiki to confirm who sent it and ensure the payload matches what was originally signed.
42+
43+
Follow these steps to create a valid signature.
44+
45+
### Generate the timestamp
46+
47+
Each signature includes a timestamp representing when it was created. Rafiki uses this value to confirm the request is recent and reject requests outside the configured TTL window (30 seconds by default).
48+
49+
In JavaScript, generate it with `Date.now()`.
50+
51+
### Prepare the request body
52+
53+
To ensure you and Rafiki sign the same data, serialize the GraphQL request consistently.
54+
55+
Use a canonicalization method that orders keys predictably (for example, <LinkOut href="https://www.npmjs.com/package/json-canonicalize">`json‑canonicalize`</LinkOut>) applied to the `query`, `variables`, and `operationName` fields.
56+
57+
### Build the payload string
58+
59+
Combine the timestamp and canonicalized request body with a period (.). This string forms the message to be signed.
60+
61+
```bash
62+
<timestamp>.<canonicalized_request_body>
63+
```
64+
65+
### Create the HMAC digest
66+
67+
Generate the digest using the tenant's or operator's API secret as the key and the payload string as the message. The output should be a hexadecimal string.
68+
69+
### Attach signature headers
70+
71+
Include the generated values in your request headers:
72+
73+
```bash
74+
signature: t=<timestamp>, v<version>=<digest>
75+
tenant-id: <OPERATOR_TENANT_ID>
76+
```
77+
78+
The version number (`v1` by default) corresponds to the configured `ADMIN_API_SIGNATURE_VERSION` environment variable.
79+
80+
Rafiki reconstructs the same payload internally and validates the digest, timestamp, and tenant ID before processing the request.
81+
82+
## Example implementation
83+
84+
Below is an example in JavaScript to sign an Admin API request:
85+
86+
```js title="Signing Admin API request example"
87+
import { createHmac } from 'crypto'
88+
import { canonicalize } from 'json-canonicalize'
89+
import { gql } from '@apollo/client'
90+
import { print } from 'graphql/language/printer'
91+
92+
const timestamp = Date.now()
93+
const version = process.env.ADMIN_API_SIGNATURE_VERSION
94+
95+
const GET_ASSET = gql`
96+
query GetAsset($id: String!) {
97+
asset(id: $id) {
98+
id
99+
code
100+
scale
101+
}
102+
}
103+
`
104+
105+
const requestBody = {
106+
query: print(GET_ASSET), // converts `DocumentNode` to string
107+
variables: { id: 'asset-id-here' },
108+
operationName: 'GetAsset'
109+
}
110+
111+
// Canonicalize ensures both client and server produce identical JSON strings
112+
// by sorting object keys deterministically and normalizing whitespace.
113+
const payload = `${timestamp}.${canonicalize(requestBody)}`
114+
const hmac = createHmac('sha256', process.env.ADMIN_API_SECRET)
115+
hmac.update(payload)
116+
const digest = hmac.digest('hex')
117+
118+
headers['signature'] = `t=${timestamp}, v${version}=${digest}`
119+
headers['tenant-id'] = process.env.OPERATOR_TENANT_ID
120+
```
121+
122+
### Configuration reference
123+
124+
<div class="overflow-table">
125+
126+
| Environment variable | Description | Default |
127+
| ----------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
128+
| `ADMIN_API_SIGNATURE_VERSION` | The version of the HMAC SHA-256 request-signing algorithm used by the Backend Admin API. | `1` |
129+
| `ADMIN_API_SECRET` | Operator API secret used to sign Backend Admin API requests (HMAC SHA‑256). Set to a strong, random value. Synced to the operator tenant on startup. ||
130+
| `OPERATOR_TENANT_ID` | The unique identifier of the operator. Must be a UUID v4 generated by the operator. ||
131+
132+
</div>
133+
134+
## Signature validation
135+
136+
When Rafiki receives a signed Admin API request, it automatically rebuilds the same payload and verifies the HMAC digest against the operator's configured secret.
137+
138+
The request is accepted only if the following conditions are met:
139+
140+
- The signature digest matches
141+
- The timestamp is within the allowed TTL window
142+
- The tenant ID is recognized
143+
144+
If any check fails, Rafiki rejects the request before executing any GraphQL operation.
145+
146+
For details on how Rafiki validates incoming requests from its own services, see [Verify webhook signatures](/integration/requirements/webhook-events/#verify-webhook-signatures)

packages/documentation/src/content/docs/integration/requirements/webhook-events.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,8 @@ function verifyWebhookSignature(request: Request): boolean {
194194
}
195195
```
196196

197+
To learn how to sign requests you send to the Admin API, see [Signing Admin API requests](/apis/graphql/signing-admin-api-requests).
198+
197199
## Event handling
198200

199201
### Asynchronous handling

0 commit comments

Comments
 (0)