diff --git a/.env b/.env new file mode 100644 index 0000000..79cde8e --- /dev/null +++ b/.env @@ -0,0 +1,2 @@ +GITHUB_TOKEN +COVERALLS_REPO_TOKEN diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 0e3999e..14a2563 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -5,9 +5,9 @@ name: Node.js CI on: push: - branches: [ master ] + branches: [ main, dev ] pull_request: - branches: [ master ] + branches: [ main, dev ] jobs: build: @@ -17,17 +17,33 @@ jobs: strategy: matrix: node-version: [ - # 12.x, 14.x, - 16.x + 16.x, + 18.x ] # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm test + - name: Coveralls GitHub Action + uses: coverallsapp/github-action@v2 + with: + parallel: true + flag-name: run-${{ join(matrix.*, '-') }} + + finish: + needs: build + if: ${{ always() }} + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: coverallsapp/github-action@v2 + with: + parallel-finished: true + carryforward: "run-14.x,run-16.x,run-18.x" diff --git a/CHANGELOG.md b/CHANGELOG.md index 572ee2b..f3a3f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,12 @@ major.minor[.revision] 0.3.4 to 0.4). - All backwards incompatible changes are mentioned in this document. +## 0.1.3 + +2023-11-28 + +- Add `signURL` functionality. + ## 0.1.2 2021-11-19 diff --git a/README.md b/README.md index 417844e..370d4c9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ validating) dictionaries. [![NPM Version](https://img.shields.io/npm/v/skajs.svg)](https://www.npmjs.com/package/skajs) [![Supported NodeJS versions](https://img.shields.io/node/v/skajs.svg)](https://www.npmjs.com/package/skajs) [![Build Status](https://github.com/barseghyanartur/skajs/actions/workflows/node.js.yml/badge.svg)](https://github.com/barseghyanartur/skajs/actions) +[![Coverage Status](https://coveralls.io/repos/github/barseghyanartur/skajs/badge.svg?branch=main)](https://coveralls.io/github/barseghyanartur/skajs?branch=main) [![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/barseghyanartur/skajs/#License) ## Key concepts @@ -38,6 +39,7 @@ Secret Key. It's being checked whether signature is valid and not expired. ## Features +- Sign URLs. - Sign dictionaries. - Validate signed dictionaries. @@ -82,25 +84,25 @@ node examples.mjs #### Sender side -Signing dictionaries is as simple as follows. +Signing dictionaries and URLs is as simple as follows. ##### Required imports. **CommonJS** ```javascript -const { signatureToDict } = require("skajs"); +const { signatureToDict, signURL } = require("skajs"); ``` **ESM** ```javascript -import { signatureToDict } from "skajs"; +import { signatureToDict, signURL } from "skajs"; ``` ##### Sign data -**Sample usage:** +**Sample usage, sign a dictionary:** ```javascript const signatureDict = signatureToDict("user", "your-secret_key"); @@ -144,6 +146,18 @@ const signatureDict = signatureToDict( last_name: 'Doe', } ``` + +**Sample usage, sign a URL:** + +```javascript +const signedURL = signURL("user", "your-secret_key", "http://e.com/api/"); +``` + +**Sample output:** + +```javascript +'http://e.com/api/?valid_until=1378045287.0&auth_user=user&signature=YlZpLFsjUKBalL4x5trhkeEgqE8%3D' +``` **Options and defaults:** The `signatureToDict` function accepts an optional `options` argument. @@ -227,7 +241,16 @@ import { validateSignedRequestData } from "skajs"; ##### Validate signed requests Validating the signed request data. Note, that `data` value is expected to -be a dictionary; `request.GET` is given as an example. +be a dictionary; `request.POST` is given as an example. + +```javascript +validationResult = validateSignedRequestData( + request.POST, // Note, that ``request.POST`` is given as example. + "your-secret_key" +); +``` + +In case of signed URLs, it could look as follows: ```javascript validationResult = validateSignedRequestData( diff --git a/commonjs/index.js b/commonjs/index.js index 86c4f9c..c9efa50 100644 --- a/commonjs/index.js +++ b/commonjs/index.js @@ -31,6 +31,12 @@ const DEFAULT_VALID_UNTIL_PARAM = "valid_until"; */ const DEFAULT_EXTRA_PARAM = "extra"; +/** + * Suffix to add after the ``url`` and before the appended + * signature params when using ``signURL`` function/method. + */ +const DEFAULT_URL_SUFFIX = "?"; + /** * ******************************************* * *************** Helpers ***************** @@ -89,6 +95,33 @@ function defaultValueDumper(value) { return value; } +/** + * `urlencode`. + * + * @param {Object} data + * @param {boolean} quoted + * @param {Function} valueDumper + * @returns {string} + */ +function urlEncode(data, quoted = true, valueDumper = defaultValueDumper) { + console.log("data: "); + console.log(data); + + let orderedData = data; + console.log("orderedData: "); + console.log(orderedData); + + let _sorted = []; + for (const [key, value] of Object.entries(orderedData)) { + _sorted.push(`${key}=${valueDumper(value)}`); + } + let _res = _sorted.join("&"); + if (quoted) { + _res = encodeURIComponent(_res); + } + return _res; +} + /** * Sorted `urlencode`. * @@ -495,6 +528,36 @@ class RequestHelper { this.signatureCls = signatureCls; } + /** + * Signature to URL. + * + * @param {AbstractSignature} signature + * @param {string} url + * @param {string} suffix + * @param {Function} valueDumper + * @returns {{}} + */ + signatureToURL( + signature, + url, + suffix = DEFAULT_URL_SUFFIX, + valueDumper = defaultValueDumper, + ) { + let data = {}; + + data[this.signatureParam] = signature.signature; + data[this.authUserParam] = signature.authUser; + data[this.validUntilParam] = signature.validUntil; + data[this.extraParam] = dictKeys(signature.extra, true); + + let combined = { + ...data, + ...signature.extra, + }; + + return `${url}${suffix}${sortedURLEncode(combined, false, valueDumper)}`; + } + /** * Signature to dict. * @@ -717,6 +780,58 @@ function getSignatureToDictDefaults(lifetime = null) { }; } +/** + * Sign URL. + * + * @param {string} authUser + * @param {string} secretKey + * @param {string} url + * @param {Object} extra + * @param {Object} options + * @returns {{}} + */ +function signURL( + authUser, + secretKey, + url, + extra = null, + options = {} +) { + let lifetime = options["lifetime"] ?? SIGNATURE_LIFETIME; + let defaults = getSignatureToDictDefaults(lifetime); + options = { + ...defaults, + ...options + }; + let validUntil = options["validUntil"]; + let signatureParam = options["signatureParam"]; + let authUserParam = options["authUserParam"]; + let validUntilParam = options["validUntilParam"]; + let extraParam = options["extraParam"]; + let valueDumper = options["valueDumper"]; + let signatureCls = options["signatureCls"]; + + let signature = generateSignature( + authUser, + secretKey, + validUntil, + lifetime, + extra, + valueDumper, + signatureCls, + ); + + const requestHelper = new RequestHelper( + signatureParam, + authUserParam, + validUntilParam, + extraParam, + signatureCls + ); + + return requestHelper.signatureToURL(signature, url); +} + /** * Signature to dict. * @@ -828,7 +943,9 @@ exports.DEFAULT_SIGNATURE_PARAM = DEFAULT_SIGNATURE_PARAM; exports.DEFAULT_AUTH_USER_PARAM = DEFAULT_AUTH_USER_PARAM; exports.DEFAULT_VALID_UNTIL_PARAM = DEFAULT_VALID_UNTIL_PARAM; exports.DEFAULT_EXTRA_PARAM = DEFAULT_EXTRA_PARAM; +exports.DEFAULT_URL_SUFFIX = DEFAULT_URL_SUFFIX; exports.isObject = isObject; +exports.urlEncode = urlEncode; exports.sortedURLEncode = sortedURLEncode; exports.convertNumberToHex = convertNumberToHex; exports.encodeValue = encodeValue; @@ -847,6 +964,7 @@ exports.unixTimestampToDate = unixTimestampToDate; exports.getBase = getBase; exports.makeHash = makeHash; exports.generateSignature = generateSignature; +exports.signURL = signURL; exports.signatureToDict = signatureToDict; exports.validateSignedRequestData = validateSignedRequestData; exports.SignatureValidationResult = SignatureValidationResult; diff --git a/esm/index.js b/esm/index.js index c5b0e76..828ea18 100644 --- a/esm/index.js +++ b/esm/index.js @@ -31,6 +31,12 @@ const DEFAULT_VALID_UNTIL_PARAM = "valid_until"; */ const DEFAULT_EXTRA_PARAM = "extra"; +/** + * Suffix to add after the ``url`` and before the appended + * signature params when using ``signURL`` function/method. + */ +const DEFAULT_URL_SUFFIX = "?"; + /** * ******************************************* * *************** Helpers ***************** @@ -89,6 +95,33 @@ function defaultValueDumper(value) { return value; } +/** + * `urlencode`. + * + * @param {Object} data + * @param {boolean} quoted + * @param {Function} valueDumper + * @returns {string} + */ +function urlEncode(data, quoted = true, valueDumper = defaultValueDumper) { + console.log("data: "); + console.log(data); + + let orderedData = data; + console.log("orderedData: "); + console.log(orderedData); + + let _sorted = []; + for (const [key, value] of Object.entries(orderedData)) { + _sorted.push(`${key}=${valueDumper(value)}`); + } + let _res = _sorted.join("&"); + if (quoted) { + _res = encodeURIComponent(_res); + } + return _res; +} + /** * Sorted `urlencode`. * @@ -495,6 +528,36 @@ class RequestHelper { this.signatureCls = signatureCls; } + /** + * Signature to URL. + * + * @param {AbstractSignature} signature + * @param {string} url + * @param {string} suffix + * @param {Function} valueDumper + * @returns {{}} + */ + signatureToURL( + signature, + url, + suffix = DEFAULT_URL_SUFFIX, + valueDumper = defaultValueDumper, + ) { + let data = {}; + + data[this.signatureParam] = signature.signature; + data[this.authUserParam] = signature.authUser; + data[this.validUntilParam] = signature.validUntil; + data[this.extraParam] = dictKeys(signature.extra, true); + + let combined = { + ...data, + ...signature.extra, + }; + + return `${url}${suffix}${sortedURLEncode(combined, false, valueDumper)}`; + } + /** * Signature to dict. * @@ -717,6 +780,58 @@ function getSignatureToDictDefaults(lifetime = null) { }; } +/** + * Sign URL. + * + * @param {string} authUser + * @param {string} secretKey + * @param {string} url + * @param {Object} extra + * @param {Object} options + * @returns {{}} + */ +function signURL( + authUser, + secretKey, + url, + extra = null, + options = {} +) { + let lifetime = options["lifetime"] ?? SIGNATURE_LIFETIME; + let defaults = getSignatureToDictDefaults(lifetime); + options = { + ...defaults, + ...options + }; + let validUntil = options["validUntil"]; + let signatureParam = options["signatureParam"]; + let authUserParam = options["authUserParam"]; + let validUntilParam = options["validUntilParam"]; + let extraParam = options["extraParam"]; + let valueDumper = options["valueDumper"]; + let signatureCls = options["signatureCls"]; + + let signature = generateSignature( + authUser, + secretKey, + validUntil, + lifetime, + extra, + valueDumper, + signatureCls, + ); + + const requestHelper = new RequestHelper( + signatureParam, + authUserParam, + validUntilParam, + extraParam, + signatureCls + ); + + return requestHelper.signatureToURL(signature, url); +} + /** * Signature to dict. * @@ -829,7 +944,9 @@ export { DEFAULT_AUTH_USER_PARAM, DEFAULT_VALID_UNTIL_PARAM, DEFAULT_EXTRA_PARAM, + DEFAULT_URL_SUFFIX, isObject, + urlEncode, sortedURLEncode, convertNumberToHex, encodeValue, @@ -848,6 +965,7 @@ export { getBase, makeHash, generateSignature, + signURL, signatureToDict, validateSignedRequestData, SignatureValidationResult, diff --git a/package.json b/package.json index 09fb6c1..495300c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "skajs", - "version": "0.1.2", + "version": "0.1.3", "description": "Sign- and validate- data (dictionaries, strings) using symmetric-key algorithm.", "license": "MIT", "repository": "barseghyanartur/skajs", @@ -19,7 +19,7 @@ "node": ">=14" }, "scripts": { - "test": "nyc --reporter=html --reporter=text ava" + "test": "nyc --reporter=html --reporter=json --reporter=lcov --reporter=text ava" }, "ava": { "files": [ @@ -35,14 +35,14 @@ "sign URL" ], "dependencies": { - "is-obj": "^3.0.0" + "is-obj": ">=3.0.0" }, "devDependencies": { - "ava": "^3.15.0", - "axios": "^0.24.0", - "nyc": "^15.1.0", - "prettier": "^2.3.2", - "tsd": "^0.14.0", - "xo": "^0.38.2" + "ava": ">=3.15.0", + "axios": ">=0.24.0", + "nyc": ">=15.1.0", + "prettier": ">=2.3.2", + "tsd": ">=0.14.0", + "xo": ">=0.38.2" } }