Token authentication mechanism

Caution
This article is relevant for the ELMA version 3.13.24 and higher

Authorization Token Lifetime

By default, the lifetime of the authorization token AuthToken is one hour. You can adjust the lifetime of a token by editing the configuration file Settings.config, which is located in the directory …\ELMA \Web.

If a user tries to authenticate with the token, but no token is in the cache, a request will be sent to the database. The database stores information about the token’s lifetime. If the token is expired, the token entry will be automatically deleted from the database when a user tries to apply this token.  

A standard provider is used to cache the sessions.

Signing API request using the HMAC-SHA256 algorithm

Each received request is checked. It should include a secret key and a signature. If the message signature does not match, an exception will be thrown 401 Unauthorized.

Сlient-side authentication

  1. Generating a public key using the ECDHalgorithm (P256 elliptic curve), saving the private key on the client side.
  2. Sending the public key in the header Auth-Info in an uncompressed HEX-string along with the rest of the authorization credentials (for the format description, see RFC5480).
  3. Upon successful authorization, the response header Auth-Info is read. It contains the public key value in the form of an uncompressed HEX string (for the format description, see RFC5480).
  4. Generating a shared secret key based on the private client key and public server key. Hashing the result using the SHA256 algorithm.
  5. Using the resulting key as a request signature in this session.

Signature format

Signature generation algorithm:

base64(hmac-sha256(VERB + "\n"
            + NormalizedResource + "\n"
            + NormalizedQuery + "\n"
            + NormalizedHeaders + "\n"
            + CONTENT-HASH + "\n"
            + CONTENT-TYPE + "\n"
))

being:

  • VERB – method (GET, POST, PUT, etc.) in the uppercase;
  • NormalizedResource – Resource URI (for more information, please refer to the RFC3986 standard).

Example/API/REST/Entity/Load;

  • NormalizedQuery – Request URI (for more information please refer to the RFC3986 standard).

Example: Type=42302b9a-9d3c-40f9-aa78-5b7671e8732d&Id=1;

  • NormalizedHeaders – normalized headers.

           Headers are normalized according to the following algorithm:

           1. Write all headers using

           2. Sort headers by name.

           3. Remove extra spaces at the beginning and at the end of the headers.

           4. Concatenate the values of the same-name headers with a comma (',') (For more details, please refer to RFC2616)

           5. Remove all line breaks.

           6. Generate the resulting data in the format headername:headervalue

           7. Separate the headers with line feeds ('\n').

  • CONTENT-HASH – hash of the request body;
  • CONTENT-TYPE – request body type.

Requirements for the signed request:

  1. The signed string (method, headers, resource address, request parameters) must be encoded in UTF-8.
  2. Instead of the optional CONTENT-HASH, CONTENT-TYPE, there should be line feeds. If the request has no body, CONTENT-HASH is not calculated. Both parameters should be written in lowercase, CONTENT-HASH should be a hex-string expressing the value of the hash of the request body using the SHA-256
  3. An ordered list of signed headers in the Signed-Headers header. All header names must be lowercase.

An example of a string that can be signed:

"GET\n/API/REST/Entity/Load\nType=42302b9a-9d3c-40f9-aa78-5b7671e8732d&Id=1\napplicationtoken:93DA2C710A3097052F3BDB3B317CA635B62FBAA072CFDCFD061AC1F6B5FD52F203B186629CB8B52773006032436A2B343155F6C792867062CAEECD5C8AC53CED\nauthtoken:45255f51-eb4f-4763-8fed-885622499603\nwebdata-version:2.0\n\napplication/json\n"

This string is signed using the HMAC-SHA256 algorithm, and the result is encoded in Base64.

The resulting signature is added to the request in the Auth-Info header.

Using Go library

Here you can find an example of Go library.

Implementing Web Cryptography API using JavaScript

function hexToArray(hexstr) {
  if (!hexstr) {
    return new Uint8Array();
  }
  const arr = [];
  for (let i = 0, len = hexstr.length; i < len; i += 2) {
    arr.push(parseInt(hexstr.substr(i, 2), 16));
  }
  let result = new Uint8Array(arr);
  return result;
}

function bufToHex(buf) {
  return Array.from(new Uint8Array(buf))
    .map(x => ("00" + x.toString(16)).slice(-2))
    .join("");
}

function bufToBase64(buf) {
  var binary = "";
  var bytes = new Uint8Array(buf);
  var len = bytes.byteLength;
  for (var i = 0; i < len; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return window.btoa(binary);
}

// Calculation of SHA256 hash for string, returns hex string
async function sha256(message) {
  if (typeof message == "undefined" || message === null || message.length == 0)
    return "";
  const msgBuffer = new TextEncoder("utf-8").encode(message);
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
  return bufToHex(hashBuffer);
}

async function generateSharedSecret(keypair) {
  let publicKeyHex = await window.crypto.subtle
    .generateKey(
      {
        name: "ECDH",
        namedCurve: "P-256",
        hash: {
          name: "SHA-256"
        }
      },
      true,
      ["deriveKey", "deriveBits"]
    )
    .then(key => {
      keypair = key;
      return window.crypto.subtle.exportKey("raw", key.publicKey);
    })
    .then(publicKeyExported => {
      return bufToHex(publicKeyExported);
    })
    .catch(err => {
      console.log("error generating pubkey, ", err);
    });
  return publicKeyHex;
}

async function loginWithSecret(login, password) {
  //Генерация ключевой пары
  let keypair;
  let publicKeyHex = await generateSharedSecret(keypair);

  // Подготовка запроса
  let authurl =
    "http://localhost:4300/API/REST/Authorization/LoginWith?username=" + login;
  let apptoken =
    "93DA2C710A3097052F3BDB3B317CA635B62FBAA072CFDCFD061AC1F6B5FD52F203B186629CB8B52773006032436A2B343155F6C792867062CAEECD5C8AC53CED";
  let headers = new Headers();
  headers.append("ApplicationToken", apptoken);
  headers.append("Content-Type", "application/json");
  headers.append("Auth-Info", publicKeyHex); //Passes the hex string with the public key

  let req = {
    method: "POST",
    headers: headers,
    body: JSON.stringify(password)
  };

  let authResult = {};

  // Authorization getting session secret key
  let shared = await window
    .fetch(authurl, req)
    .then(response => {
      authResult.sharedKey = response.headers.get("Auth-Info");
      return response;
    })
    .then(response => response.json())
    .then(jsonResponse => {
      authResult.sessionToken = jsonResponse.SessionToken;
      authResult.authToken = jsonResponse.AuthToken;
      authResult.sharedKey = jsonResponse.SharedKey;
      return window.crypto.subtle.importKey(
        "raw",
        hexToArray(authResult.sharedKey),
        {
          name: "ECDH",
          namedCurve: "P-256"
        },
        true,
        ["deriveKey", "deriveBits"]
      );
    })
    .then(cryptoSharedKey => {
      return window.crypto.subtle.deriveBits(
        {
          name: "ECDH",
          namedCurve: "P-256",
          public: cryptoSharedKey
        },
        keypair.privateKey,
        256
      );
    })
    .then(bits => {
      return window.crypto.subtle.digest(
        {
          name: "SHA-256"
        },
        new Uint8Array(bits)
      );
    })
    .catch(err => {
      console.log("error processing auth, ", err);
    });

  authResult.sharedSecretBits = shared;

  return authResult;
}

// String signature
async function signMessage(strMessage, secret) {
  var enc = new TextEncoder();
  let data = enc.encode(strMessage);
  return window.crypto.subtle
    .importKey(
      "raw",
      secret,
      {
        name: "HMAC",
        hash: { name: "SHA-256" }
      },
      false,
      ["sign"]
    )
    .then(key => {
      return window.crypto.subtle.sign(
        {
          name: "HMAC"
        },
        key,
        data.buffer //ArrayBuffer для подписания
      );
    })
    .then(signature => {
      return bufToBase64(signature);
    })
    .catch(function(err) {
      console.log("error signing message ", err);
    });
}

// Sending a test message with a signature
async function sendTestMessage(authResult) {
  // Perparing request 
  let url =
    "http://localhost:4300/API/REST/Entity/Load?Type=42302b9a-9d3c-40f9-aa78-5b7671e8732d&Id=1";
  let apptoken =
    "93DA2C710A3097052F3BDB3B317CA635B62FBAA072CFDCFD061AC1F6B5FD52F203B186629CB8B52773006032436A2B343155F6C792867062CAEECD5C8AC53CED";
  let body = JSON.stringify("admin");
  let headers = new Headers();
  let signedHeaders = ["ApplicationToken", "WebData-Version", "AuthToken"];

  headers.append("ApplicationToken", apptoken);
  headers.append("Content-Type", "application/json");
  headers.append("WebData-Version", "2.0");
  headers.append("AuthToken", authResult.authToken);
  headers.append("Signed-Headers", signedHeaders.join(";"));

  let req = {
    method: "GET",
    headers: headers
  };

  // Preparing data for signature
  let verb = req.method.toUpperCase();
  let resourceUri = "/API/REST/Entity/Load";
  let queryUri = "Type=42302b9a-9d3c-40f9-aa78-5b7671e8732d&Id=1";
  let contentHash = await sha256(init.body);
  let contentType = "application/json";
  let headerStr = "";

  // Header Normalization
  let keys = signedHeaders.sort();
  keys.forEach(key => {
    headerStr += `${key.toLowerCase()}:${headers.get(key).trim()}\n`;
  });
  headerStr = headerStr.slice(0, -1);

  // Preparing a string request for signing 
  let signedStr = `${verb}\n${resourceUri}\n${queryUri}\n${headerStr}\n${contentHash}\n${contentType}\n`;

  let base64sign = await signMessage(signedStr, authResult.sharedSecretBits);
  headers.append("Auth-Info", base64sign);

  window
    .fetch(url, req)
    .then(response => response.json())
    .then(jsonResponse => {
      console.log(jsonResponse);
    });

The Web Cryptography API documentation is available at the following links: