-
Notifications
You must be signed in to change notification settings - Fork 105
docs: add guide for signing Admin API requests #3807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
✅ Deploy Preview for brilliant-pasca-3e80ec ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
🚀 Performance Test ResultsTest Configuration:
Test Metrics:
📜 Logs |
BlairCurrey
left a comment
There was a problem hiding this 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. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| 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. |
| 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 |
There was a problem hiding this comment.
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:
requestis 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)printis 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_IDOr 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
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:
Required
Conditional