Signing API Requests

This page will help you understand the details of the API request signature algorithm you need to implement, and it will also provide you with a sample Postman script to assist you further.

To sign your API request, you need to:

1. Generate a signed JWS token

We strongly suggest that you use one of the existing cryptographic libraries to generate a JWS token. In our Postman example we used the "jsrsasign" library.

The JWS token must be generated using all the below input parameters:

  1. Private Key
  2. JWT Header:
    1. alg (ES256)
    2. kid (public key id)
    3. typ (JWT)
  3. JWT Payload:
    1. method (request method: GET/POST/DELETE)
    2. path (full path of the request)
    3. query (query string)
    4. sha256 (SHA256 hashed value of the request body) . This is optional and will be null for GET/DELETE requests
    5. iat (issued at time)
    6. exp (token expiry time. To avoid replay attacks, ensure this value is within 60 seconds of iat)

2. Add the JWS token to the HTTP header

To add the generated JWS token into the HTTP header you must use the Authorization header parameter, see example below.

Authorization: JWS <JWStoken>

3. Pass Wpay signature verification

The payload used to generate the signature must match the payload sent to Wpay. Any formatting issues (e.g. extra spaces, extra new lines, case mismatch) will lead to a different hash value resulting in a 401 unauthorised access error.

  • If the Signature verification fails, a 401 unauthorized error is returned.
  • If the Signature verification succeeds but the requestor does not have permission to access the API, a 403 forbidden error is returned.

Furthermore to avoid replay attacks, we verify the request against our internal server time as follows:

  • the request issued at time must be less than or equal to our internal server time
  • the request token expiry time must be greater or equal to our internal server time

If any of the above fail, a 401 unauthorized error is returned.

Sample Postman Script

This is a sample Postman pre-request script for generating and adding the JWS signature to the API request header. Note that the following environment variables need to be set before running this snippet:

  • publicKeyId - set the public key Id provided by Wpay
  • jwkPrivateKey - generate JWK key from the private key using tools like JWK Generator and set the value
// #region LOAD jsrsasign
    var navigator = {};
    var window = {};
    //load jsrsasign
    pm.sendRequest("http://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js", (err, res) => {
    pm.environment.set("jsrsasign", res.text());
    eval(pm.environment.get("jsrsasign"));
// #endregion

// #region header
    var header = 
    {
        "alg":"ES256",
        "kid": pm.environment.get("publicKeyId"),
        "typ":"JWT"
    };
// #region header

// #region Payload
        var cryptoJS = require("crypto-js");
        var hashedContent = null;
        //remove comments and trim
        var strippedData  = pm.request.body?.raw?.replace( /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).trim(); 
        if(strippedData?.length > 0) {
            //replace variables with actual values
        const resolvedBody = JSON.stringify(JSON.parse(pm.variables.replaceIn(strippedData)));
            pm.request.body.update(resolvedBody);
            //create hash from body
            hashedContent = cryptoJS.SHA256(resolvedBody).toString(cryptoJS.enc.Base64);
        }
        //sort query string
        let queryString = pm.request.url.getQueryString(); 
        if(queryString.length > 0)
        {
          let queryParams = queryString.split('&').map(param => param.split('='));
          queryParams.sort((a,b) => a[0].localeCompare(b[0]));
          queryString = queryParams.map(param => param.join('=')).join('&');
        }
        var payload = 
        {
            "method":pm.request.method,
            "path": pm.variables.replaceIn(pm.variables.replaceIn(pm.request.url).toString()),
            "query":queryString.length > 0 ? queryString: null,
            "sha256":hashedContent
        };
        addIATAndEXP();

        // This function adds the Issued At  and Expiry for the signature. This ensures that the token
        // have shorter lifetime and helps in stopping replay attacks
        function addIATAndEXP() {
            var iat = Math.floor(Date.now() / 1000);
        payload.iat = iat;
        payload.exp = iat + 60; //exp should not be more than iat + 60s
        }

//#endregion

//create signature from header and payload
const jwkPrivateKey = JSON.parse(pm.environment.get("jwkPrivateKey"));
const jwsSignature = KJUR.jws.JWS.sign("ES256", header, payload, jwkPrivateKey);

//add jwsSignature as Authorization header
 pm.request.headers.add({
     key: "Authorization",
    value: "JWS " + jwsSignature
 });
});

The sections below provide an explanation for each part of this Postman script.

Library

Postman needs a library to generate the JWS signature. This script uses the jsrsasign library, and the below part of the script is used to load this library in Postman. This library is not needed to be loaded for every API request.

Other libraries/packages are available to generate the signatures for other environments (Java/node) . E.g. npm: jose

// #region LOAD jsrsasign
    var navigator = {};
    var window = {};
    //load jsrsasign
    pm.sendRequest("http://kjur.github.io/jsrsasign/jsrsasign-latest-all-min.js", (err, res) => {
    pm.environment.set("jsrsasign", res.text());
    eval(pm.environment.get("jsrsasign"));
// #endregion

Header

This part of the script is used to create the JWT Header values:

  • algorithm is ES256 (Elliptic Curve Digital Signature Algorithm with the P-256 curve and the SHA-256 hash function)
  • publicKeyId is the Wpay generated public key Id
  • typ is the Token type. In this case, it will be JWT
// #region header
    var header = 
    {
        "alg":"ES256",
        "kid": pm.environment.get("publicKeyId"),
        "typ":"JWT"
    };
// #region header

Payload

This part of the script is used to create the JWT Payload that contains the following values:

  • method - request method (GET/POST/DELETE) from pm.request.method
  • path - full path of the request url after resolving any variable names
  • query - query parameter string should be sorted alphabetically in the format bar=1&baz=2&foo=3
  • sha256 - SHA256 hash value for the request body
    • Loads crypto-js library that will be used to generate SHA256 hash value
    • Removes any comments and trims the request body
    • replaces all the variable names with actual values (pm.variables.replaceIn)
    • updates the request body with the resolved request body pm.request.body.update(resolvedBody)
    • generates the SHA256 value
  • iat - Token Issued At timestamp. This should be in Unix epoch (Unix timestamp)
  • exp - Token expiry timestamp. This should not be more than iat + 60s.
// #region Payload
        var cryptoJS = require("crypto-js");
        var hashedContent = null;
        //remove comments and trim
        var strippedData  = pm.request.body?.raw?.replace( /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g, (m, g) => g ? "" : m).trim(); 
        if(strippedData?.length > 0) {
            //replace variables with actual values
        const resolvedBody = JSON.stringify(JSON.parse(pm.variables.replaceIn(strippedData)));
            pm.request.body.update(resolvedBody);
            //create hash from body
            hashedContent = cryptoJS.SHA256(resolvedBody).toString(cryptoJS.enc.Base64);
        }
        //sort query string
        let queryString = pm.request.url.getQueryString(); 
        if(queryString.length > 0)
        {
          let queryParams = queryString.split('&').map(param => param.split('='));
          queryParams.sort((a,b) => a[0].localeCompare(b[0]));
          queryString = queryParams.map(param => param.join('=')).join('&');
        }
        var payload = 
        {
            "method":pm.request.method,
            "path": pm.variables.replaceIn(pm.variables.replaceIn(pm.request.url).toString()),
            "query":queryString.length > 0 ? queryString: null,
            "sha256":hashedContent
        };
        addIATAndEXP();
        // This function adds the Issued At  and Expiry for the signature. This ensures that the token
        // have shorter lifetime and helps in stopping replay attacks
        function addIATAndEXP() {
            var iat = Math.floor(Date.now() / 1000);
        payload.iat = iat;
        payload.exp = iat + 60; //exp should not be more than iat + 60s
        }
//#endregion

Signature

This part of the script is used to generate the JWS signature using header, payload and jwkPrivateKey (using the KJUR library).

jwkPrivateKey is the JWK format of the private key that was generated at the beginning.

The generated signature is added as the Authorization header for each request.

//create signature from header and payload
const jwkPrivateKey = JSON.parse(pm.environment.get("jwkPrivateKey"));
const jwsSignature = KJUR.jws.JWS.sign("ES256", header, payload, jwkPrivateKey);
//add jwsSignature as Authorization header
 pm.request.headers.add({
     key: "Authorization",
    value: "JWS " + jwsSignature
 });
});