Request Signing
Overview
Request signing is a crucial security measure in the NOAH Business API, ensuring the integrity and authenticity of each request. In the prod
environment, request signing is mandatory, while in the Sandbox environment, it is conditionally required based on your API Key configuration.
Request signing involves constructing a JWT (JSON Web Token) that encapsulates specific claims about the request. This JWT is then signed using your Request Signing Private Key and included in the Api-Signature
header of your API requests. Our API verifies this signature to authenticate and authorize the request.
When is Request Signing Required?
- Prod Environment:
- Request signing is compulsory for all API requests.
- Sandbox Environment:
- Request signing is required only if the API Key was generated with an associated Request Signing Public Key. This facilitates easier testing with tools like Postman.
Constructing the JWT
To sign your requests, follow these steps:
- Create the JWT Payload:
- Include the necessary claims as outlined below.
- Sign the JWT:
- Use your Request Signing Private Key to sign the JWT with the ES384 algorithm. (ES256 can also be used but we recommend ES384)
- Include the JWT in the Header:
- Add the signed JWT to the
Api-Signature
header of your API request.
- Add the signed JWT to the
JWT Claims
The following claims are verified by the NOAH API:
Claim | Description | Required |
---|---|---|
aud | The audience for which this JWT is intended, in all cases should be https://api.noah.com . | Yes |
iat | Issued At time as a Unix timestamp in seconds. | Yes |
exp | Expiration time, must be within 15 minutes from iat . | Yes |
method | HTTP method of the request (e.g., GET, POST). | Yes |
path | URL path of the request (e.g., /v1/transactions ). | Yes |
queryParams | A map of query parameters. Must be included if the request uses query parameters; otherwise, omit. | Conditional |
bodyHash | Hex-encoded SHA256 hash of the exact request body. Must be included if the request has a body; otherwise, omit. | Conditional |
Example JWT Payload with Params:
{
"aud": "https://api.noah.com",
"iat": 1719820231,
"exp": 1719820831,
"method": "GET",
"path": "/v1/transactions",
"queryParams": {
"PageSize": 20,
"SortDirection": "ASC"
}
}
Example JWT Payload with Body Hash:
{
"aud": "https://api.noah.com",
"iat": 1719820231,
"exp": 1719820831,
"method": "POST",
"path": "/v1/checkout/buy",
"bodyHash": "96f9ab7b05d9ef0bdf3e46e6b83351c1fb2cf2675a5cb6b79f9bf9af390fb9b0"
}
Request Signing Key Generation
If you require a new key, an ES384 private/public key pair can be generated using OpenSSL in command line as follows:
For ES384 (recommended):
openssl ecparam -name secp384r1 -genkey -noout -out private-key.pem
openssl ec -in private-key.pem -pubout -out public-key.pem
Only the public key is used when creating API Keys and can be shared as needed.
Keep your private key secure. It provides full access to our APIs for initiating financial operations and must be protected against unauthorized access. Do not expose it in client-side code, repositories, or unencrypted storage.
Do not share private keys between sandbox
and prod
environments
Request Signing Example
The example functions below demonstrate how to make a simple api client using Axios where the createJwt
function is responsible for generating a signed JSON Web Token (JWT) that authenticates your API requests.
Why Exact Bytes Matter
A critical aspect of the createJwt
function is the hashing of the request body using SHA-256. It is imperative that the same byte sequence used to compute the hash is exactly what’s sent in your actual HTTP request payload. Any discrepancy, be it differences in encoding, whitespace, or data formatting, will result in a mismatched hash. A mismatch will lead to authentication failures, as our API relies on the hash to validate the request’s integrity. This verification enables us to ensure that the payload has not been tampered with during transit.
import crypto from 'crypto';
import type { AxiosResponse } from 'axios';
import axios from 'axios';
import jwt from 'jsonwebtoken';
const client = axios.create({ baseURL: 'https://api.sandbox.noah.com' });
/**
* Creates a JWT token for authenticating API requests.
*
* @param opts - Options for JWT creation.
* @param opts.body - A buffer made from the body of the request. Important to use the exact same body buffer in the request.
* @param opts.method - The HTTP method of the request, e.g., GET, POST, PUT, DELETE.
* @param opts.path - The path of the request, e.g., /api/v1/customers.
* @param opts.privateKey - The private key used to sign the JWT, in PEM format.
* @param opts.queryParams - The query parameters of the request.
* @returns A signed JWT token as a string.
*/
export async function createJwt(opts: {
body: Buffer | undefined;
method: string;
path: string;
privateKey: string;
queryParams: object | undefined;
}): Promise<string> {
const { body, method, path, privateKey, queryParams } = opts;
let bodyHash;
if (body) {
bodyHash = crypto.createHash('sha256').update(body).digest('hex');
}
const payload = {
bodyHash,
method,
path,
queryParams,
};
// ES384 is recommended but the algorithm can also be ES256
const token = jwt.sign(payload, privateKey, {
algorithm: 'ES384',
audience: 'https://api.noah.com',
// use a short expiry time, less than 15m
expiresIn: '5m',
});
return token;
}
/**
* Makes a GET request to the specified API endpoint with a signed JWT in the Api-Signature header.
*
* @param opts - Options for the GET request.
* @param opts.path - The path of the request, e.g., /api/v1/customers.
* @param opts.privateKey - The private key used to sign the JWT, in PEM format.
* @param opts.queryParams - The query parameters of the request.
* @returns A promise that resolves to the Axios response.
* @throws Will throw an error if the request fails.
*/
export async function get(opts: {
path: string;
privateKey: string;
queryParams?: Record<string, string | number>;
}): Promise<AxiosResponse> {
const { path, privateKey, queryParams } = opts;
try {
const signature = await createJwt({
body: undefined,
method: 'GET',
path,
privateKey,
queryParams,
});
const response = await client.get(path, {
headers: { 'Api-Signature': signature },
params: queryParams,
});
return response;
} catch (err: unknown) {
console.error(err);
throw err;
}
}
/**
* Makes a POST request to the specified API endpoint with a signed JWT in the Api-Signature header.
*
* @param opts - Options for the POST request.
* @param opts.data - The data to be sent in the body of the request.
* @param opts.path - The path of the request, e.g., /api/v1/customers.
* @param opts.privateKey - The private key used to sign the JWT, in PEM format.
* @returns A promise that resolves to the Axios response.
* @throws Will throw an error if the request fails.
*/
export async function post(opts: {
data: object;
path: string;
privateKey: string;
}): Promise<AxiosResponse> {
const { data, path, privateKey } = opts;
try {
const body = Buffer.from(JSON.stringify(data));
const signature = await createJwt({
body,
method: 'POST',
path,
privateKey,
queryParams: undefined,
});
const response = await client.post(path, body, {
headers: { 'Api-Signature': signature, 'Content-Type': 'application/json' },
});
return response;
} catch (err: unknown) {
console.error(err);
throw err;
}
}
/**
* Makes a PUT request to the specified API endpoint with a signed JWT in the Api-Signature header.
*
* @param opts - Options for the PUT request.
* @param opts.data - The data to be sent in the body of the request.
* @param opts.path - The path of the request, e.g., /api/v1/customers.
* @param opts.privateKey - The private key used to sign the JWT, in PEM format.
* @returns A promise that resolves to the Axios response.
* @throws Will throw an error if the request fails.
*/
export async function put(opts: {
data: object;
path: string;
privateKey: string;
}): Promise<AxiosResponse> {
const { data, path, privateKey } = opts;
try {
const body = Buffer.from(JSON.stringify(data));
const signature = await createJwt({
body,
method: 'PUT',
path,
privateKey,
queryParams: undefined,
});
const response = await client.put(path, body, {
headers: { 'Api-Signature': signature, 'Content-Type': 'application/json' },
});
return response;
} catch (err: unknown) {
console.error(err);
throw err;
}
}
Example Request Payloads
Given a data
object as below, when we pass it to one of our put
or post
methods...
const data = {
CryptoCurrency: 'USDC_TEST',
CustomerID: 'noah-1235',
ExternalID: 'noah-funds-124',
FiatAmount: '100',
FiatCurrency: 'USD',
LineItems: [
{
Description: '100 USDT for use in noah.com',
Quantity: '1',
TotalFiatAmount: '100',
UnitFiatAmount: '100',
},
],
Nonce: 'nonce-987654321',
PaymentMethodCategory: 'Bank',
ReturnURL: 'https://noah.com',
};
...then the resulting JSON body that needs to be used is shown below (note the lack of whitespace). It is critical that this exact body is used for your request payload. This is achieved in our functions above by using the same Buffer
for the createJwt
function and the post
and put
methods.
{"CryptoCurrency":"USDC_TEST","CustomerID":"noah-1235","ExternalID":"noah-funds-124","FiatAmount":"100","FiatCurrency":"USD","LineItems":[{"Description":"100 USDT for use in noah.com","Quantity":"1","TotalFiatAmount":"100","UnitFiatAmount":"100"}],"Nonce":"nonce-987654321","PaymentMethodCategory":"Bank","ReturnURL":"https://noah.com"}
For reference, the bodyHash
from the above will be:
f5d7c7d38825cb5701e20342e4b0ca47dfb2006a91d3dd6a847da86a78a8380b