JWT token security

Both the front end communication and the lifecycle events use JWT tokens for data exchange. To ensure data integrity, we sign the payload according to RFC 7519. The signature can be verified using public certificates.

JOSE compliant header

The token header contains the 'jku' claim, which holds a URL linking to the public key that should be used to verify the signature.
For front end communication the Plug-In Adapter library has a few built-in certificates for fast evaluation of the most used public keys (coyocloud and coyostaging), but can download missing public keys on demand from this URL. If a plug-in has a back end, it can use the same mechanism to download missing / unknown keys when verifying passed front end communication tokens or lifecycle events.

jku URL whitelist

To prevent malicious attacks a plug-in MUST check the public key download URL ('jku' header claim) against a whitelist of known sites:

Adding key download URLs and keys to the Plug-In Adapter library manually

The Plug-In Adapter library allows to add custom download URLs and / or public keys for signature validation.
Please note that the protocol MUST be secured by TLS using HTTPS.
You can add multiple keys for the same issuer regex, which can be useful when there is a transition to a new public key and you want to cover both.

import {JwkStore} from '@coyoapp/plugin-adapter';

const JWK_REGEX = /^https:\/\/some.site$/i; // match 'iss' issuer claim
const JWK = {
  alg: {
    name: 'RSASSA-PKCS1-v1_5',
    hash: { name: 'SHA-256' }
  },
  key: {
    kty: 'RSA',
    n:
      '6pEgjiM-gu6T7HFeI_OWxr7YftcYqFVHnpMcAsW510Nqn4citmM6XvOMlagovI5c3MoSHvfpRPuHqNpR' +
      'HX_ZpOlbn0NDCJFsOaKQPmffaQSaeRdHsVmXgFAJ3Y7cUSW4NqJAZV71GwMzOu7B7db1htF-kPvb0lN0' +
      '18EXrWBFO0nkHYGi_7vUedwbAu-hOaMh1028Yiwl2dcZW3X9htE6RhXY_B4PofiipOGtklotm424b0Gh' +
      'ccGdIz7cBapxNX1_QLc3C4QatpWt8Hs4QWH0iJMRpdFpuOxkZ8d6c6aqbUglgXLrEICT3ZWrBOFZCVom' +
      'NYyYv321BdfLfnSIT4bIQQ',
    e: 'AQAB',
    alg: 'RS256',
    use: 'sig'
  }
};

// call the next statement from within some initialisation function after you instantiated
// the PluginAdapter
const jwkStore = JwkStore.getInstance();
// add jku download URL
jwkStore.addHost('https://pubkeys.some.site');
// or add a key directly, the regex must match the issuer
jwkStore.addJwk(JWK_REGEX, JWK);
// or add multiple keys for one environment
jwkStore.addJwk(JWK_REGEX, JWK1, JWK2, JWK3);

Let the backend do the validation

In case your plug-in has a backend you will probably have to solve the following issues:

  • secure your frontend-to-backend communication
  • verify lifecycle event tokens

A token verification requires CPU intensive cryptography. Currently it only consists of a simple signature verification, but this could be replaced by certificate chains in the future. Verifying certificate chains not only requires even more CPU time but it is also not that easy with init tokens using javascript in the browser (e.g. with the Plug-In Adapter library), as the browser's crypto engine just doesn't support it natively but only with additional libraries. A backend on the other hand can do that easier, cache certificates and most likely also runs on well equipped hardware.

A viable approach then is to move the init token verification to the backend and return a plug-in backend access token for the plug-in frontend in exchange.

The Plug-In Adapter library supports the suppression of the validation when calling the init method with a boolean flag as the third parameter. In the following pseudo code example the Plug-In Adapter library gets initialised and token validation is set to false. The init token response is then forwarded to the plug-in backend, in this case using the popular 'axios' library. Its response could carry a backend access token, the format and usage is completely up to the plug-in implementation.

import {PluginAdapter} from '@coyoapp/plugin-adapter';
import axios from 'axios';

const PLUGIN_BACKEND_INIT = '/auth/init';

const adapter = new PluginAdapter();
adapter.init(false, undefined, false)
  .then(initResponse => {
    return axios.post(PLUGIN_BACKEND_INIT, {
      token: initResponse.token
    })
  })
  .then(accessToken => {
    // store backend access token for upcoming requests
  })
  .catch(err => {
    // do error handling
  });