diff --git a/README.md b/README.md index 63a7007..23a415c 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Data Source is a JS library meant to help developers access Movable Ink Data Sou - [Details on how Sorcerer determines priority](#details-on-how-sorcerer-determines-priority) - [Publishing package:](#publishing-package) - [Changelog](#changelog) + - [3.2.0](#320) - [3.1.0](#310) - [3.0.0](#300) - [2.0.0](#200) @@ -333,6 +334,10 @@ $ npm publish ## Changelog +### 3.2.0 + +- Adds RSA Signature support via `RsaToken` token utility class + ### 3.1.0 - Creates `RequestBuilder` and "Token" type utility classes diff --git a/docs/token-builder.md b/docs/token-builder.md index f063b00..4efdbe4 100644 --- a/docs/token-builder.md +++ b/docs/token-builder.md @@ -13,6 +13,7 @@ In `sorcerer`, the Token Parser will extract the tokens from the request body an - [ReplaceLargeToken](#replacelargetoken) - [SecretToken](#secrettoken) - [HmacToken](#hmactoken) + - [RsaToken](#rsatoken) - [Sha1Token](#sha1token) - [RequestBuilder](#requestbuilder) - [Generating a request payload](#generating-a-request-payload) @@ -37,6 +38,7 @@ Currently supported tokens are: - [ReplaceLargeToken](#replacelargetoken) - [SecretToken](#secrettoken) - [HmacToken](#hmactoken) +- [RsaToken](#rsatoken) - [Sha1Token](#sha1token) @@ -90,7 +92,9 @@ const tokenModel = new SecretToken(params); ``` ### HmacToken -Replaces token with an HMAC signature. +Replaces token with an HMAC signature. Used in conjunction with a `secretName` parameter which corresponds to a secret stored on a data source. + +HMAC uses symmetric encryption which means the signature requires a shared secret on the Data Source (reference via `secretName`) that the origin API will have a copy of and use to verify. **Params** - **options** (required) @@ -117,6 +121,37 @@ const params = { const tokenModel = new HmacToken(params); ``` +### RsaToken +Replaces token with an RSA signature. Used in conjunction with a `secretName` parameter which corresponds to a secret stored on a data source. + +RSA uses asymmetric encryption which means the signature requires a RSA keypair. The private key is typically stored on the Data Source (reference via `secretName`) whereas the public key is given to +the origin API's owner to use to verify requests. + +**Params** +- **options** (required) + - **stringToSign** (optional) - any string that will be used when generating an RSA signature + - **algorithm** (required)- the hashing algorithm: `sha1` , `sha256`, `md5` + - **secretName** (required) - name of the data source secret (e.g. `watson`) + - **encoding** (required) - option to encode the signature once it is generated: `hex`, `base64`, `base64url`, `base64percent` + - `base64url` produces the same result as `base64` but in addition also replaces `+` with `-` , `/` with `_` , and removes the trailing padding character `=` + - `base64percent` encodes the signature as `base64` and then also URI percent encodes it + +**Example:** + +```jsx +const params = { + name: 'rsa_sig', + options: { + stringToSign: 'some_message', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, +}; + +const tokenModel = new RsaToken(params); +``` + ### Sha1Token Replaces token with a SHA-1 signature. diff --git a/package.json b/package.json index a8dbe63..4fa159c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@movable-internal/data-source.js", - "version": "3.1.0", + "version": "3.2.0", "main": "./dist/index.js", "module": "./dist/index.es.js", "license": "MIT", diff --git a/src/index.ts b/src/index.ts index 90388ce..0dc43e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export { ReplaceLargeToken, SecretToken, HmacToken, + RsaToken, Sha1Token, } from './token-builder/types'; diff --git a/src/token-builder/types.js b/src/token-builder/types.js index 99d2663..e99e622 100644 --- a/src/token-builder/types.js +++ b/src/token-builder/types.js @@ -139,6 +139,37 @@ export class HmacToken extends TokenBase { } } +export class RsaToken extends TokenBase { + constructor(params) { + super(params); + this.type = 'rsa'; + this.rsaOptions = params.options || {}; + this.validateOptions(); + } + + toJSON() { + const json = super.toJSON(); + + return { ...json, options: this.rsaOptions }; + } + + validateOptions() { + super.validateOptions(); + + if (!ALLOWED_ALGOS.has(this.rsaOptions.algorithm)) { + this.errors.push('RSA algorithm is invalid'); + } + + if (!this.rsaOptions.secretName) { + this.errors.push('RSA secret name not provided'); + } + + if (!ALLOWED_ENCODINGS.has(this.rsaOptions.encoding)) { + this.errors.push('RSA encoding is invalid'); + } + } +} + export class Sha1Token extends TokenBase { constructor(params) { super(params); diff --git a/test/token-builder/request-builder-test.js b/test/token-builder/request-builder-test.js index ee03118..72092d3 100644 --- a/test/token-builder/request-builder-test.js +++ b/test/token-builder/request-builder-test.js @@ -5,6 +5,7 @@ import { ReplaceLargeToken, SecretToken, HmacToken, + RsaToken, Sha1Token, } from '../../src/token-builder/types'; const { test, module } = QUnit; @@ -42,6 +43,18 @@ module('RequestBuilder', function () { }; const hmacToken = new HmacToken(hmacOptions); + const rsaOptions = { + name: 'rsa_sig', + cacheOverride: 'xyz', + options: { + stringToSign: 'mystring', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, + }; + const rsaToken = new RsaToken(rsaOptions); + const sha1Token = new Sha1Token({ name: 'sha1_sig', options: { @@ -56,6 +69,7 @@ module('RequestBuilder', function () { replaceLargeToken, secretToken, hmacToken, + rsaToken, sha1Token, ]); @@ -95,6 +109,18 @@ module('RequestBuilder', function () { stringToSign: 'mystring', }, }, + { + name: 'rsa_sig', + type: 'rsa', + cacheOverride: 'xyz', + skipCache: false, + options: { + algorithm: 'sha1', + encoding: 'hex', + secretName: 'watson', + stringToSign: 'mystring', + }, + }, { name: 'sha1_sig', type: 'sha1', @@ -136,6 +162,16 @@ module('RequestBuilder', function () { }; const hmacToken = new HmacToken(hmacOptions); + const rsaOptions = { + cacheOverride: 'xyz', + options: { + stringToSign: 'mystring', + algorithm: 'ash1', + encoding: 'lex', + }, + }; + const rsaToken = new RsaToken(rsaOptions); + const sha1Token = new Sha1Token({ name: 'sha1_sig', options: { @@ -150,6 +186,7 @@ module('RequestBuilder', function () { replaceLargeToken, secretToken, hmacToken, + rsaToken, sha1Token, ]); @@ -159,7 +196,8 @@ module('RequestBuilder', function () { `token 2: ReplaceLarge token can only be used when value exceeds ${CHAR_LIMIT} character limit`, 'token 3: Missing properties for secret token: "path"', 'token 4: Missing properties for hmac token: "name", HMAC algorithm is invalid, HMAC secret name not provided, HMAC encoding is invalid', - 'token 5: SHA1 encoding is invalid, Invalid secret token passed into SHA1 tokens array', + 'token 5: Missing properties for rsa token: "name", RSA algorithm is invalid, RSA secret name not provided, RSA encoding is invalid', + 'token 6: SHA1 encoding is invalid, Invalid secret token passed into SHA1 tokens array', ]; assert.throws(function () { diff --git a/test/token-builder/types-test.js b/test/token-builder/types-test.js index 4deba1b..f4c2716 100644 --- a/test/token-builder/types-test.js +++ b/test/token-builder/types-test.js @@ -4,6 +4,7 @@ import { ReplaceLargeToken, SecretToken, HmacToken, + RsaToken, Sha1Token, CHAR_LIMIT, } from '../../src/token-builder/types'; @@ -328,6 +329,88 @@ module('HmacToken', function () { }); }); +module('RsaToken', function () { + test('can be instantiated with all options', (assert) => { + const rsaOptions = { + name: 'rsa_sig', + cacheOverride: 'xyz', + skipCache: true, + options: { + stringToSign: 'application/json\nGET\n', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, + }; + + const tokenModel = new RsaToken(rsaOptions); + + const expectedJson = { + name: 'rsa_sig', + type: 'rsa', + cacheOverride: 'xyz', + skipCache: true, + options: { + stringToSign: 'application/json\nGET\n', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, + }; + + assert.deepEqual(tokenModel.toJSON(), expectedJson); + }); + + test('gets instantiated with default options', (assert) => { + const rsaOptions = { + name: 'rsa_sig', + options: { + stringToSign: 'application/json\nGET\n', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, + }; + + const tokenModel = new RsaToken(rsaOptions); + + const expectedJson = { + name: 'rsa_sig', + type: 'rsa', + cacheOverride: null, + skipCache: false, + options: { + stringToSign: 'application/json\nGET\n', + algorithm: 'sha1', + secretName: 'watson', + encoding: 'hex', + }, + }; + + assert.deepEqual(tokenModel.toJSON(), expectedJson); + }); + + test('will include an error if instantiated with missing options', (assert) => { + const rsaOptions = { + name: 'rsa_sig', + options: { + stringToSign: 'application/json\nGET\n', + algorithm: 'invalid', + encoding: 'neo', + }, + }; + + const tokenModel = new RsaToken(rsaOptions); + + const expectedErrors = [ + 'RSA algorithm is invalid', + 'RSA secret name not provided', + 'RSA encoding is invalid', + ]; + assert.deepEqual(tokenModel.errors, expectedErrors); + }); +}); + module('Sha1Token', function () { test('can be instantiated with all options', (assert) => { const tokens = [{ name: 'secureValue', type: 'secret', path: 'mySecretPath' }];