diff --git a/.gitignore b/.gitignore index 3c3629e..49d7ee0 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,51 @@ +# Mac Resource files +._* +*.DS_Store + +# Archives +*.gz + +# Cache and build output directories +.tscache +dist + +# CI Test results +testresults.xml + +# Logs +logs +*.log +npm-debug.log* + +# Runtime data +pids +*.pid +*.seed + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules +jspm_packages + +# Optional npm cache directory +.npm + +# Optional REPL history +.node_repl_history diff --git a/Makefile b/Makefile index 753c847..03a96dc 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ test: - clear && mocha --recursive --reporter spec --slow 1 - + clear && npm run build && mocha --recursive --reporter spec --slow 1 + coveralls: istanbul cover ./node_modules/mocha/bin/_mocha --report lcovonly -- -R spec --recursive && cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage diff --git a/index.js b/index.js new file mode 100644 index 0000000..5fd4e17 --- /dev/null +++ b/index.js @@ -0,0 +1,2 @@ +module.exports = require('./dist/oauth').OAuth; +module.exports.default = module.exports; diff --git a/package.json b/package.json index 3d2075e..61a18eb 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,11 @@ "version": "3.0.0", "description": "OAuth 1.0a Request Authorization for Node and Browser.", "scripts": { + "build": "tsc --project .", + "lint": "tslint --project tsconfig.json --type-check", "test": "make test" }, - "main": "src/oauth.js", + "main": "index.js", "repository": "https://github.com/whs/node-oauth-1.0a.git", "keywords": [ "oauth", @@ -14,17 +16,33 @@ "authorize", "signature", "nonce", - "consumer" + "consumer", + "typescript" ], "license": "MIT", "devDependencies": { - "mocha": "~2.4.5", - "chai": "~3.5.0", - "request": "~2.69.0", - "istanbul": "^0.4.2", - "coveralls": "^2.10.0" + "@types/chai": "^3.5.0", + "@types/istanbul": "^0.4.29", + "@types/mocha": "^2.2.40", + "@types/node": "^7.0.12", + "@types/randomstring": "^1.1.5", + "@types/request": "0.0.42", + "chai": "^3.5.0", + "coveralls": "^2.13.0", + "istanbul": "^0.4.5", + "mocha": "^3.2.0", + "request": "^2.81.0", + "tslint": "^5.1.0", + "typescript": "^2.2.2" }, "dependencies": { - "randomstring": "^1.1.4" - } + "randomstring": "^1.1.5" + }, + "engines": { + "node": ">= 6" + }, + "files": [ + "dist", + "index.js" + ] } diff --git a/src/oauth.js b/src/oauth.js deleted file mode 100644 index d3ffd51..0000000 --- a/src/oauth.js +++ /dev/null @@ -1,359 +0,0 @@ -'use strict'; - -const querystring = require('querystring'); -const randomstring = require('randomstring'); - -const Signer = require('./signer'); -const Utils = require('./utils'); - -/** - * OAuth 1.0a signature generator - * - * @example Setup - * let OAuth = require('node-oauth-1.0a'); - * let request = require('request'); - * - * let oauth = new OAuth({ - * consumer: { - * public: '', - * secret: '', - * } - * }); - * let request_data = { - * url: 'https://api.twitter.com/1/statuses/update.json?include_entities=true', - * method: 'POST', - * data: { - * status: 'Hello Ladies + Gentlemen, a signed OAuth request!' - * } - * }; - * let token = { - * public: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', - * secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' - * }; - * - * @example Sending in POST body with request library - * let formData = Object.assign( - * {}, - * request_data.data, - * oauth.authorize(request_data, token) - * ); - * request({ - * url: request_data.url, - * method: request_data.method, - * form: oauth.buildQueryString(formData) - * }, function(error, response, body) { - * // Process data - * }); - * - * @example Sending in Authorization header with request library - * request({ - * url: request_data.url, - * method: request_data.method, - * form: oauth.buildQueryString(request_data.data), - * headers: { - * Authorization: oauth.getHeader(request_data, token) - * } - * }, function(error, response, body) { - * // Process data - * }); - * - */ -class OAuth{ - /** - * @param {Object} opts - * @param {Object} opts.consumer Consumer token (required) - * @param {string} opts.consumer.public Consumer key (public key) - * @param {string} opts.consumer.private Consumer secret - * @param {number} [opts.nonce_length=32] Length of nonce (oauth_nonce) - * @param {string} [opts.signature_method="HMAC-SHA1"] Signing algorithm - * Supported algorithm: - * - `HMAC-SHA1` - * - `PLAINTEXT` - * - `HMAC-SHA256` - * - * Note that `HMAC-256` is non-standard. - * @param {string} [opts.version=1.0] OAuth version (oauth_version) - * @param {boolean} [opts.last_ampersand=true] Whether to append trailing - * ampersand to signing key - */ - constructor(opts){ - opts = opts || {} - - if(!opts.consumer) { - throw new Error('consumer option is required'); - } - - this._opts = Object.assign({ - nonce_length: 32, - signature_method: 'HMAC-SHA1', - version: '1.0', - last_ampersand: true, - parameter_seperator: ', ', - }, opts); - } - - /** - * Sign a string with key - * @private - * @param {string} str String to sign - * @param {string} key HMAC key - * @return {string} Signed string in base64 format - */ - _sign(str, key){ - if(!this._signer){ - // Cache the signer - this._signer = this._getSigner(this._opts.signature_method); - } - return this._signer(str, key); - } - - /** - * Retrieve a signing algorithm by algorithm name - * @private - * @param {string} type Algorithm name - * @throws {Error} Algorithm is not supported - * @return {Function} Algorithm implementation - */ - _getSigner(type){ - if(Signer[type]){ - return Signer[type]; - }else{ - let supported = JSON.stringify(Object.keys(Signer)); - throw new Error(`Hash type ${type} not supported. Supported: ${supported}`); - } - } - - /** - * Create OAuth signing data for attaching to request body - * @param {Object} request - * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) - * @param {string} request.url URL - * @param {Object} request.data Post data as a key, value map - * - * @param {Object} [token={}] User token - * @param {string} [token.key] Token public key - * @param {string} [token.secret] Token secret key - * - * @return {Object} OAuth signing data. You probably want to put this in your POST body - * - * @example - * let request = { - * method: 'POST', - * url: 'https://api.twitter.com/1.1/statuses/update.json', - * data: { - * status: 'Hello, world!' - * } - * }; - * let token = { - * public: '', - * private: '' - * }; - * let oauth_data = oauth.authorize(request, token); - * console.log(oauth_data); - * - * @example Example response - * { - * "oauth_consumer_key": "xvz1evFS4wEEPTGEFPHBog", - * "oauth_nonce": "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", - * "oauth_signature_method": "HMAC-SHA1", - * "oauth_timestamp": 1318622958, - * "oauth_version": "1.0", - * "oauth_token": "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", - * "oauth_signature": "tnnArxj06cWHq44gCs1OSKk/jLY=" - * } - */ - authorize(request, token){ - token = token || {}; - - let oauth_data = this._getOAuthData(token); - oauth_data.oauth_signature = this.getSignature(request, token.secret, oauth_data); - - return oauth_data; - } - - /** - * Create OAuth Authorization header - * @param {Object} request - * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) - * @param {string} request.url URL - * @param {Object} request.data Post data as a key, value map - * - * @param {Object} [token={}] User token - * @param {string} [token.key] Token public key - * @param {string} [token.secret] Token secret key - * - * @return {string} Authorization header value - */ - getHeader(request, token){ - let oauth_data = this.authorize(request, token); - return Utils.toHeader(oauth_data, this._opts.parameter_seperator); - } - - /** - * Format OAuth signing data for sending via HTTP Header - * @param {Object} oauth_data OAuth signing data as returned from - * {@link OAuth#authorize} - * @return {Object} Headers required to sign the request - * @deprecated This method is preserved for backward compatibility with - * oauth-1.0a. New implementors should use {@link OAuth#getHeader} instead. - */ - toHeader(oauth_data){ - return { - Authorization: Utils.toHeader(oauth_data, this._opts.parameter_seperator) - }; - } - - /** - * Create oauth_signature from request. Usually you probably want to use - * {@link OAuth#authorize} instead. - * @param {Object} request - * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) - * @param {string} request.url URL - * @param {Object} request.data Post data as a key, value map - * - * @param {Object} [token={}] User token - * @param {string} [token.key] Token public key - * @param {string} [token.secret] Token secret key - * - * @param {Object} oauth_data - * @param {string} oauth_data.oauth_consumer_key Consumer key - * @param {string} oauth_data.oauth_nonce Nonce string - * @param {string} oauth_data.oauth_signature_method Signing algorithm name - * (only for building signing string, the actual signing algorithm is set by - * class {@link OAuth#constructor} arguments) - * @param {number} oauth_data.oauth_timestamp Current time in seconds - * @param {string} oauth_data.oauth_version OAuth version (should be 1.0) - * - * @return {string} Value of oauth_signature field - */ - getSignature(request, token, oauth_data){ - return this._sign( - this._getBaseString(request, oauth_data), - this._getSigningKey(token) - ); - } - - /** - * Create new OAuth data - * @private - * @param {Object} [token] User token - * @param {string} token.public Token public key - * @return {Object} OAuth data without oauth_signature - * - * @example Example response - * { - * "oauth_consumer_key": "xvz1evFS4wEEPTGEFPHBog", - * "oauth_nonce": "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", - * "oauth_signature_method": "HMAC-SHA1", - * "oauth_timestamp": 1318622958, - * "oauth_version": "1.0", - * "oauth_token": "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", - * } - */ - _getOAuthData(token){ - let oauth_data = { - oauth_consumer_key: this._opts.consumer.public, - oauth_nonce: this._getNonce(), - oauth_signature_method: this._opts.signature_method, - oauth_timestamp: this._getTimeStamp(), - oauth_version: this._opts.version - }; - - if(token && token.public){ - oauth_data.oauth_token = token.public; - } - - return oauth_data; - } - - /** - * Build authorization string to sign. - * - * An authorization string composes of HTTP method name, base URL and - * parameters (both request parameters and OAuth data) - * @private - * @param {Object} request Request object - * @param {Object} oauth_data OAuth parameters - * @return {string} Authorization string - */ - _getBaseString(request, oauth_data){ - let out = [ - request.method.toUpperCase(), - Utils.getBaseUrl(request.url), - Utils.getParameterString(request, oauth_data) - ]; - - return out.map(item => Utils.percentEncode(item)) - .join('&'); - } - - /** - * Build a query string similar to {@link querystring.encode}, but - * escape things correctly per OAuth spec - * - * @param {Object} data Object to encode as query string - * @return {string} Query string object - */ - buildQueryString(data){ - data = Utils.toSortedMap(data); - - return Utils.stringifyQueryMap(data, '&', '=', { - encodeURIComponent: Utils.percentEncode - }); - } - - /** - * Build signing key. - * - * A signing key composes of consumer secret and, - * optionally, user token secret. - * - * This method will append trailing ampersand when `token_secret` is unset - * if `last_ampersand` constructor option is set. - * - * @private - * @param {string} [token_secret] User token secret - * @return {string} Signing Key - */ - _getSigningKey(token_secret){ - let out = [ - this._opts.consumer.secret - ]; - - if(this._opts.last_ampersand || token_secret){ - out.push(token_secret || ''); - } - - return out.map(item => Utils.percentEncode(item)) - .join('&'); - } - - /** - * Create nonce. - * - * Nonce is a random string to prevent replaying of requests. - * - * Default nonce length is 32 characters, and can be specified by - * `nonce_length` constructor option. - * - * @private - * @return {string} Nonce string - */ - _getNonce(){ - return randomstring.generate({ - length: this._opts.nonce_length || 32, - charset: 'alphanumeric' - }); - } - - /** - * Create timestamp from current time - * @private - * @return {number} Current time in seconds - */ - _getTimeStamp(){ - return parseInt(new Date().getTime()/1000, 10); - } -} - -module.exports = OAuth; diff --git a/src/oauth.ts b/src/oauth.ts new file mode 100644 index 0000000..e80a657 --- /dev/null +++ b/src/oauth.ts @@ -0,0 +1,395 @@ +import * as querystring from "querystring"; +import * as randomstring from "randomstring"; + +import {Signer, SignerType} from "./signer"; +import Utils from "./utils"; + +/** + * OAuth 1.0a signature generator + * + * @example Setup + * let OAuth = require('node-oauth-1.0a'); + * let request = require('request'); + * + * let oauth = new OAuth({ + * consumer: { + * public: '', + * secret: '', + * } + * }); + * let request_data = { + * url: 'https://api.twitter.com/1/statuses/update.json?include_entities=true', + * method: 'POST', + * data: { + * status: 'Hello Ladies + Gentlemen, a signed OAuth request!' + * } + * }; + * let token = { + * public: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', + * secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' + * }; + * + * @example Sending in POST body with request library + * let formData = Object.assign( + * {}, + * request_data.data, + * oauth.authorize(request_data, token) + * ); + * request({ + * url: request_data.url, + * method: request_data.method, + * form: oauth.buildQueryString(formData) + * }, function(error, response, body) { + * // Process data + * }); + * + * @example Sending in Authorization header with request library + * request({ + * url: request_data.url, + * method: request_data.method, + * form: oauth.buildQueryString(request_data.data), + * headers: { + * Authorization: oauth.getHeader(request_data, token) + * } + * }, function(error, response, body) { + * // Process data + * }); + * + */ +export class OAuth { + private _opts: OAuthOpts; + private _signer: Function; + + /** + * @param {Object} opts + * @param {Object} opts.consumer Consumer token (required) + * @param {string} opts.consumer.public Consumer key (public key) + * @param {string} opts.consumer.private Consumer secret + * @param {number} [opts.nonce_length=32] Length of nonce (oauth_nonce) + * @param {string} [opts.signature_method="HMAC-SHA1"] Signing algorithm + * Supported algorithms: + * - `HMAC-SHA1` + * - `PLAINTEXT` + * - `HMAC-SHA256` + * + * Note that `HMAC-256` is non-standard. + * @param {string} [opts.version=1.0] OAuth version (oauth_version) + * @param {boolean} [opts.last_ampersand=true] Whether to append trailing + * ampersand to signing key + */ + constructor(opts: Partial = {}) { + if(!opts.consumer) { + throw new Error('consumer option is required'); + } + + this._opts = Object.assign({ + nonce_length: 32, + signature_method: 'HMAC-SHA1', + version: '1.0', + last_ampersand: true, + parameter_separator: ', ', + }, opts, {consumer: opts.consumer}); + } + + /** + * Sign a string with key + * @private + * @param {string} str String to sign + * @param {string} key HMAC key + * @return {string} Signed string in base64 format + */ + _sign(str: string, key: string) { + if (!this._signer) { + // Cache the signer + this._signer = this._getSigner(this._opts.signature_method); + } + return this._signer(str, key); + } + + /** + * Retrieve a signing algorithm by algorithm name + * @private + * @param {SignerType} type Algorithm name + * @throws {Error} Algorithm is not supported + * @return {Function} Algorithm implementation + */ + _getSigner(type: SignerType) { + if (Signer[type]) { + return Signer[type]; + } else { + let supported = JSON.stringify(Object.keys(Signer)); + throw new Error(`Hash type ${type} not supported. Supported: ${supported}`); + } + } + + /** + * Create OAuth signing data for attaching to request body + * @param {Object} request + * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) + * @param {string} request.url URL + * @param {Object} request.data Post data as a key, value map + * + * @param {Object} [token={}] User token + * @param {string} [token.key] Token public key + * @param {string} [token.secret] Token secret key + * + * @return {Object} OAuth signing data. You probably want to put this in your POST body + * + * @example + * let request = { + * method: 'POST', + * url: 'https://api.twitter.com/1.1/statuses/update.json', + * data: { + * status: 'Hello, world!' + * } + * }; + * let token = { + * public: '', + * private: '' + * }; + * let oauth_data = oauth.authorize(request, token); + * console.log(oauth_data); + * + * @example Example response + * { + * "oauth_consumer_key": "xvz1evFS4wEEPTGEFPHBog", + * "oauth_nonce": "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", + * "oauth_signature_method": "HMAC-SHA1", + * "oauth_timestamp": 1318622958, + * "oauth_version": "1.0", + * "oauth_token": "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", + * "oauth_signature": "tnnArxj06cWHq44gCs1OSKk/jLY=" + * } + */ + authorize(request: RequestOpts, token?: Token) { + token = token || {}; + + let oauth_data: OAuthData = this._getOAuthData(token); + oauth_data.oauth_signature = this.getSignature(request, token.secret, oauth_data); + + return oauth_data; + } + + /** + * Create OAuth Authorization header + * @param {Object} request + * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) + * @param {string} request.url URL + * @param {Object} request.data Post data as a key, value map + * + * @param {Object} [token={}] User token + * @param {string} [token.key] Token public key + * @param {string} [token.secret] Token secret key + * + * @return {string} Authorization header value + */ + getHeader(request: RequestOpts, token: Token) { + let oauth_data = this.authorize(request, token); + return Utils.toHeader(oauth_data, this._opts.parameter_separator); + } + + /** + * Format OAuth signing data for sending via HTTP Header + * @param {Object} oauth_data OAuth signing data as returned from + * {@link OAuth#authorize} + * @return {Object} Headers required to sign the request + * @deprecated This method is preserved for backward compatibility with + * oauth-1.0a. New implementors should use {@link OAuth#getHeader} instead. + */ + toHeader(oauth_data: OAuthData) { + return { + Authorization: Utils.toHeader(oauth_data, this._opts.parameter_separator) + }; + } + + /** + * Create oauth_signature from request. Usually you probably want to use + * {@link OAuth#authorize} instead. + * @param {Object} request + * @param {string} request.method HTTP Method name (eg. `GET`, `POST`, `PUT`) + * @param {string} request.url URL + * @param {Object} request.data Post data as a key, value map + * + * @param {string} token signing token + * + * @param {Object} oauth_data + * @param {string} oauth_data.oauth_consumer_key Consumer key + * @param {string} oauth_data.oauth_nonce Nonce string + * @param {string} oauth_data.oauth_signature_method Signing algorithm name + * (only for building signing string, the actual signing algorithm is set by + * class {@link OAuth#constructor} arguments) + * @param {number} oauth_data.oauth_timestamp Current time in seconds + * @param {string} oauth_data.oauth_version OAuth version (should be 1.0) + * + * @return {string} Value of oauth_signature field + */ + getSignature(request: RequestOpts, token: string | undefined, oauth_data: OAuthData) { + return this._sign( + this._getBaseString(request, oauth_data), + this._getSigningKey(token) + ); + } + + /** + * Create new OAuth data + * @private + * @param {Object} [token] User token + * @param {string} token.public Token public key + * @return {Object} OAuth data without oauth_signature + * + * @example Example response + * { + * "oauth_consumer_key": "xvz1evFS4wEEPTGEFPHBog", + * "oauth_nonce": "kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg", + * "oauth_signature_method": "HMAC-SHA1", + * "oauth_timestamp": 1318622958, + * "oauth_version": "1.0", + * "oauth_token": "370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb", + * } + */ + _getOAuthData(token: Token): OAuthData { + let oauth_data: OAuthData = { + oauth_consumer_key: this._opts.consumer.public, + oauth_nonce: this._getNonce(), + oauth_signature_method: this._opts.signature_method, + oauth_timestamp: this._getTimeStamp(), + oauth_version: this._opts.version, + }; + + if (token && token.public) { + oauth_data.oauth_token = token.public; + } + + return oauth_data; + } + + /** + * Build authorization string to sign. + * + * An authorization string composes of HTTP method name, base URL and + * parameters (both request parameters and OAuth data) + * @private + * @param {Object} request Request object + * @param {Object} oauth_data OAuth parameters + * @return {string} Authorization string + */ + _getBaseString(request: RequestOpts, oauth_data: any) { + let out = [ + request.method.toUpperCase(), + Utils.getBaseUrl(request.url), + Utils.getParameterString(request, oauth_data) + ]; + + return out.map(item => Utils.percentEncode(item)) + .join('&'); + } + + /** + * Build a query string similar to {@link querystring.encode}, but + * escape things correctly per OAuth spec + * + * @param {Object} data Object to encode as query string + * @return {string} Query string object + */ + buildQueryString(data: any) { + data = Utils.toSortedMap(data); + + return Utils.stringifyQueryMap(data, '&', '=', { + encodeURIComponent: Utils.percentEncode + }); + } + + /** + * Build signing key. + * + * A signing key composes of consumer secret and, + * optionally, user token secret. + * + * This method will append trailing ampersand when `token_secret` is unset + * if `last_ampersand` constructor option is set. + * + * @private + * @param {string} [token_secret] User token secret + * @return {string} Signing Key + */ + _getSigningKey(token_secret?: string) { + let out = [ + this._opts.consumer.secret + ]; + + if (this._opts.last_ampersand || token_secret) { + out.push(token_secret || ''); + } + + return out.map(item => Utils.percentEncode(item)) + .join('&'); + } + + /** + * Create nonce. + * + * Nonce is a random string to prevent replaying of requests. + * + * Default nonce length is 32 characters, and can be specified by + * `nonce_length` constructor option. + * + * @private + * @return {string} Nonce string + */ + _getNonce() { + return randomstring.generate({ + length: this._opts.nonce_length || 32, // tslint:disable-line + charset: 'alphanumeric' + }); + } + + /** + * Create timestamp from current time + * @private + * @return {number} Current time in seconds + */ + _getTimeStamp() { + return Math.floor(Date.now() / 1000); // tslint:disable-line + } +} + +export default OAuth; // tslint:disable-line + +export interface OAuthOpts { + consumer: OAuthConsumer; + nonce_length: number; + signature_method: SignerType; + version: string; + last_ampersand: boolean; + parameter_separator: string; +} + +export interface OAuthConsumer { + public: string; // consumer key + secret: string; // shared secret +} + +export interface RequestOpts { + method: string; // HTTP method + url: string; // URL + data?: any; // Post data as a key, value map +} + +export interface OAuthData { + oauth_consumer_key: string; // Consumer key + oauth_nonce: string; // Nonce string + // Signing algorithm name + // (only for building signing string, the actual signing algorithm is set by + // class {@link OAuth#constructor} arguments) + oauth_signature_method: SignerType; + oauth_timestamp: number; // Current Unix time in seconds + oauth_version: string; // OAuth version + oauth_signature?: string; // Signature of the request data + oauth_token?: string; +} + +export interface Token { + key?: string; // Token key + public?: string; // Token public key + secret?: string; // Token secret +} diff --git a/src/signer/hmac-sha1.js b/src/signer/hmac-sha1.js deleted file mode 100644 index eda41f3..0000000 --- a/src/signer/hmac-sha1.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -module.exports = (base_string, key) => { - return crypto.createHmac('sha1', key).update(base_string).digest('base64') -}; diff --git a/src/signer/hmac-sha1.ts b/src/signer/hmac-sha1.ts new file mode 100644 index 0000000..5ca22db --- /dev/null +++ b/src/signer/hmac-sha1.ts @@ -0,0 +1,12 @@ +import * as crypto from "crypto"; + +/** + * Sign message + * @param {string} base_string message string + * @param {string} key signing key + */ +export function sign(base_string: string, key: string) { + return crypto.createHmac('sha1', key).update(base_string).digest('base64'); +} + +export default sign; // tslint:disable-line diff --git a/src/signer/hmac-sha256.js b/src/signer/hmac-sha256.js deleted file mode 100644 index 1a109d8..0000000 --- a/src/signer/hmac-sha256.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -const crypto = require('crypto'); - -module.exports = (base_string, key) => { - return crypto.createHmac('sha256', key).update(base_string).digest('base64') -}; \ No newline at end of file diff --git a/src/signer/hmac-sha256.ts b/src/signer/hmac-sha256.ts new file mode 100644 index 0000000..d8b1708 --- /dev/null +++ b/src/signer/hmac-sha256.ts @@ -0,0 +1,12 @@ +import * as crypto from "crypto"; + +/** + * Sign message + * @param {string} base_string message string + * @param {string} key signing key + */ +export function sign(base_string: string, key: string) { + return crypto.createHmac('sha256', key).update(base_string).digest('base64'); +} + +export default sign; // tslint:disable-line diff --git a/src/signer/index.js b/src/signer/index.js deleted file mode 100644 index 5aba8a7..0000000 --- a/src/signer/index.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = { - 'HMAC-SHA1': require('./hmac-sha1'), - 'HMAC-SHA256': require('./hmac-sha256'), - 'PLAINTEXT': require('./plaintext'), -}; diff --git a/src/signer/index.ts b/src/signer/index.ts new file mode 100644 index 0000000..a97a73d --- /dev/null +++ b/src/signer/index.ts @@ -0,0 +1,12 @@ +import signSha1 from "./hmac-sha1"; +import signSha256 from "./hmac-sha256"; +import signPlaintext from "./plaintext"; + +export const Signer = Object.freeze<{[key: string]: Function}>({ + 'HMAC-SHA1': signSha1, + 'HMAC-SHA256': signSha256, + PLAINTEXT: signPlaintext, +}); + +export default Signer; // tslint:disable-line +export type SignerType = "HMAC-SHA1" | "HMAC-SHA256" | "PLAINTEXT"; diff --git a/src/signer/plaintext.js b/src/signer/plaintext.js deleted file mode 100644 index fb38602..0000000 --- a/src/signer/plaintext.js +++ /dev/null @@ -1,5 +0,0 @@ -'use strict'; - -module.exports = (base_string, key) => { - return key; -}; diff --git a/src/signer/plaintext.ts b/src/signer/plaintext.ts new file mode 100644 index 0000000..5e4d506 --- /dev/null +++ b/src/signer/plaintext.ts @@ -0,0 +1,10 @@ +/** + * Plaintext signing method just returns the key. + * @param {string} base_string message string + * @param {string} key signing key + */ +export function sign(base_string: string, key: string) { + return key; +} + +export default sign; // tslint:disable-line diff --git a/src/utils.js b/src/utils.js deleted file mode 100644 index ba77be3..0000000 --- a/src/utils.js +++ /dev/null @@ -1,141 +0,0 @@ -'use strict'; - -const querystring = require('querystring'); -const url = require('url'); - -/** - * @private - */ -const Utils = { - /** - * Escape string according to [OAuth 1.0 section 3.6]{@link https://tools.ietf.org/html/rfc5849#section-3.6} - * @param {String} str String to encode - * @return {String} Encoded string - */ - percentEncode: (str) => { - return encodeURIComponent(str) - .replace(/\!/g, '%21') - .replace(/\*/g, '%2A') - .replace(/\'/g, '%27') - .replace(/\(/g, '%28') - .replace(/\)/g, '%29'); - }, - - /** - * Build OAuth Authorization header - * @param {Object} oauth_data - * @param {string} [separator=", "] Separator between items - * @return {String} Authorization header string - */ - toHeader: (oauth_data, separator) => { - separator = separator || ', '; - oauth_data = Utils.toSortedMap(oauth_data); - - let params = []; - - // encode each items as key="value" - for(let item of oauth_data){ - let key = Utils.percentEncode(item[0]); - let value = Utils.percentEncode(item[1]); - params.push(`${key}="${value}"`); - } - - let joinedParams = params.join(separator); - - return `OAuth ${joinedParams}`; - }, - - /** - * Build parameter string part of the signing string. - * - * Parameter string consists of all request parameters and OAuth data - * sorted by key alphabetically. - * - * @param {Object} request - * @param {Object} oauth_data - * @return {Object} string Parameter string - */ - getParameterString: (request, oauth_data) => { - let parsedUrl = url.parse(request.url, true); - let data = Object.assign({}, parsedUrl.query, request.data || {}, oauth_data); - data = Utils.toSortedMap(data); - - return Utils.stringifyQueryMap(data, '&', '=', { - encodeURIComponent: Utils.percentEncode - }); - }, - - /** - * Build query string from {@link Map} - * - * This method should be the same as {@link querystring#stringify} - * but accept a `Map` instead of {@link Object} - * - * @param {Map.} obj Input - * @param {string} [sep="&"] Separator between items - * @param {string} [eq="="] Separator between key and value - * @param {Object} [options] - * @param {Function} [options.encodeURIComponent=encodeURIComponent] - * Key and value escaping algorithm - * @return {string} Query string - */ - stringifyQueryMap: (obj, sep, eq, options) => { - sep = sep || '&'; - eq = eq || '='; - options = Object.assign({ - encodeURIComponent: encodeURIComponent - }, options); - - let out = []; - - for(let item of obj){ - if(!Array.isArray(item[1])){ - item[1] = [item[1]]; - } - item[1].sort(); - - let key = options.encodeURIComponent(item[0]); - for(let value of item[1]){ - // if value is an array, repeat the key multiple time - value = options.encodeURIComponent(value); - out.push(`${key}${eq}${value}`); - } - } - - return out.join(sep); - }, - - /** - * Strip query string from URL - * - * @param {String} url URL to strip - * @return {String} Stripped URL - */ - getBaseUrl: (url) => { - return url.split('?')[0]; - }, - - /** - * Return a ES6 Map with same key/value pairs as object. - * - * Iterating over this map would yield key/value pairs in alphabetical - * order of keys. - * - * @param {Object} object Object to sort - * @return {Map} - */ - toSortedMap: (object) => { - let keys = Object.keys(object); - keys.sort(); - - let out = new Map(); - - for(let key of keys){ - out.set(key, object[key]); - } - - return out; - }, -}; - -module.exports = Utils; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..a38a93f --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,136 @@ +import * as querystring from "querystring"; +import * as url from "url"; + +/** + * @private + */ +export namespace Utils { + /** + * Escape string according to [OAuth 1.0 section 3.6]{@link https://tools.ietf.org/html/rfc5849#section-3.6} + * @param {String} str String to encode + * @return {String} Encoded string + */ + export function percentEncode(str: string) { + return encodeURIComponent(str).replace(/[!'()*]/g, (c) => + `%${c.charCodeAt(0).toString(16).toUpperCase()}` // tslint:disable-line + ); + } + + /** + * Build OAuth Authorization header + * @param {Object} oauth_data + * @param {string} [separator=", "] Separator between items + * @return {String} Authorization header string + */ + export function toHeader(oauth_data: any, separator?: string) { + separator = separator || ', '; + oauth_data = Utils.toSortedMap(oauth_data); + + let params = []; + + // encode each items as key="value" + for(let item of oauth_data){ + let key = Utils.percentEncode(item[0]); + let value = Utils.percentEncode(item[1]); + params.push(`${key}="${value}"`); + } + + let joinedParams = params.join(separator); + + return `OAuth ${joinedParams}`; + } + + /** + * Build parameter string part of the signing string. + * + * Parameter string consists of all request parameters and OAuth data + * sorted by key alphabetically. + * + * @param {Object} request + * @param {Object} oauth_data + * @return {Object} string Parameter string + */ + export function getParameterString(request: any, oauth_data: any) { + let parsedUrl = url.parse(request.url, true); + let data = Object.assign({}, parsedUrl.query, request.data || {}, oauth_data); + data = Utils.toSortedMap(data); + + return Utils.stringifyQueryMap(data, '&', '=', { + encodeURIComponent: Utils.percentEncode + }); + } + + /** + * Build query string from {@link Map} + * + * This method should be the same as {@link querystring#stringify} + * but accept a `Map` instead of {@link Object} + * + * @param {Object} obj Input + * @param {string} [sep="&"] Separator between items + * @param {string} [eq="="] Separator between key and value + * @param {Object} [options] + * @param {Function} [options.encodeURIComponent=encodeURIComponent] + * Key and value escaping algorithm + * @return {string} Query string + */ + export function stringifyQueryMap(obj: any, sep: string, eq: string, options: any) { + sep = sep || '&'; + eq = eq || '='; + options = Object.assign({ + encodeURIComponent: encodeURIComponent + }, options); + + let out = []; + + for(let item of obj){ + if(!Array.isArray(item[1])) { + item[1] = [item[1]]; + } + item[1].sort(); + + let key = options.encodeURIComponent(item[0]); + for(let value of item[1]){ + // if value is an array, repeat the key multiple time + value = options.encodeURIComponent(value); + out.push(`${key}${eq}${value}`); + } + } + + return out.join(sep); + } + + /** + * Strip query string from URL + * + * @param {String} url URL to strip + * @return {String} Stripped URL + */ + export function getBaseUrl(url: string) { + return url.split('?')[0]; + } + + /** + * Return a ES6 Map with same key/value pairs as object. + * + * Iterating over this map would yield key/value pairs in alphabetical + * order of keys. + * + * @param {Object} object Object to sort + * @return {Map} + */ + export function toSortedMap(object: any) { + let keys = Object.keys(object); + keys.sort(); + + let out = new Map(); + + for(let key of keys){ + out.set(key, object[key]); + } + + return out; + } +} + +export default Utils; // tslint:disable-line diff --git a/test/options/parameter_seperator.js b/test/options/parameter_seperator.js index 94909e9..f3ce9a4 100644 --- a/test/options/parameter_seperator.js +++ b/test/options/parameter_seperator.js @@ -8,7 +8,7 @@ if(typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') { expect = chai.expect; } -describe("parameter_seperator option", function() { +describe("parameter_separator option", function() { describe("default (', ')", function() { var oauth = generateTest({ consumer: { @@ -41,12 +41,12 @@ describe("parameter_seperator option", function() { public: 'xvz1evFS4wEEPTGEFPHBog', secret: 'kAcSOqF21Fu85e7zjz7ZN2U4ZRhfV3WpwPAoE3Z7kBw' }, - parameter_seperator: '-' + parameter_separator: '-', }); var token = { public: '370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb', - secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE' + secret: 'LswwdoUaIvS8ltyTt5jkRh4J50vUPVVHtR2YPi5kE', }; var request = { @@ -54,11 +54,15 @@ describe("parameter_seperator option", function() { method: 'POST', data: { status: 'Hello Ladies + Gentlemen, a signed OAuth request!' - } + }, }; it("header should be correct", function() { - expect(oauth.toHeader(oauth.authorize(request, token))).to.have.property('Authorization', 'OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog"-oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg"-oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D"-oauth_signature_method="HMAC-SHA1"-oauth_timestamp="1318622958"-oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb"-oauth_version="1.0"'); + const authData = oauth.authorize(request, token); + const authHeader = oauth.toHeader(authData); + const prop = "Authorization"; + const expectedVal = 'OAuth oauth_consumer_key="xvz1evFS4wEEPTGEFPHBog"-oauth_nonce="kYjzVBB8Y0ZFabxSWbWovY3uYSQ2pTgmZeNu2VS4cg"-oauth_signature="tnnArxj06cWHq44gCs1OSKk%2FjLY%3D"-oauth_signature_method="HMAC-SHA1"-oauth_timestamp="1318622958"-oauth_token="370773112-GmHxMAgYyLbNEtIKZeRNFsMKPR9EyMZeS9weJAEb"-oauth_version="1.0"'; + expect(authHeader).to.have.property(prop, expectedVal); }); }); }); diff --git a/test/options/signer.js b/test/options/signer.js index f7fec30..b844d06 100644 --- a/test/options/signer.js +++ b/test/options/signer.js @@ -1,11 +1,9 @@ -'use strict'; - let expect; //Node.js if(typeof(module) !== 'undefined' && typeof(exports) !== 'undefined') { expect = require('chai').expect; - var Signer = require('../../src/signer'); + var Signer = require('../../dist/signer').default; } else { //Browser expect = chai.expect; } diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..98a3729 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "inlineSourceMap": true, + "lib": ["DOM", "ES2016", "ES6", "ScriptHost"], + "module": "commonjs", + "noImplicitAny": true, + "preserveConstEnums": true, + "removeComments": false, + "strictNullChecks": true, + "target": "ES2016" + }, + "include": [ + "**/*" + ] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..6cd4449 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compileOnSave": true, + "compilerOptions": { + "declaration": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "importHelpers": true, + "inlineSourceMap": true, + "lib": ["DOM", "ES2016", "ES6", "ScriptHost"], + "module": "commonjs", + "noImplicitAny": true, + "outDir": "dist", + "preserveConstEnums": true, + "removeComments": false, + "strictNullChecks": true, + "target": "ES2016" + }, + "include": [ + "src/**/*" + ] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..c742bc0 --- /dev/null +++ b/tslint.json @@ -0,0 +1,89 @@ +{ + "rules": { + "adjacent-overload-signatures": true, + "align": [ + true, + "statements" + ], + "ban": [ + true, + [ "_", "forEach" ], + [ "_", "map" ], + [ "_", "filter" ], + [ "_", "each" ] + ], + "class-name": true, + "comment-format": [ true, "check-space" ], + "completed-docs": [false, "classes", "functions", "methods"], + "curly": true, + "cyclomatic-complexity": true, + "eofline": true, + "forin": true, + "indent": [ true, "spaces" ], + "jsdoc-format": true, + "label-position": true, + "max-classes-per-file": [true, 1], + "max-line-length": [ true, 140 ], + "new-parens": true, + "no-angle-bracket-type-assertion": false, + "no-any": false, + "no-arg": true, + "no-bitwise": false, + "no-conditional-assignment": true, + "no-consecutive-blank-lines": false, + "no-construct": true, + "no-debugger": true, + "no-default-export": true, + "no-duplicate-variable": true, + "no-empty": false, + "no-eval": true, + "no-for-in-array": true, + "no-inferrable-types": false, + "no-internal-module": true, + "no-magic-numbers": true, + "no-namespace": [false, "allow-declarations"], + "no-null-keyword": false, + "no-reference": false, + "no-require-imports": true, + "no-shadowed-variable": true, + "no-string-literal": false, + "no-string-throw": true, + "no-switch-case-fall-through": true, + "no-trailing-whitespace": true, + "no-unsafe-finally": true, + "no-unused-expression": true, + "no-var-keyword": true, + "no-var-requires": true, + "object-literal-key-quotes": [true, "as-needed"], + "one-line": [ true, "check-open-brace", "check-whitespace" ], + "only-arrow-functions": [ true, "allow-declarations", "allow-named-functions" ], + "prefer-for-of": true, + "radix": true, + "semicolon": [true, "always"], + "switch-default": true, + "trailing-comma": [ true, { "singleline": "never" } ], + "triple-equals": [ false, "allow-null-check", "allow-undefined-check" ], + "typedef": [ + true, + "parameter", + "property-declaration" + ], + "typeof-compare": true, + "use-isnan": true, + "variable-name": [ + false, + "allow-leading-underscore", + "allow-pascal-case", + "ban-keywords", + "check-format" + ] + }, + "whitespace": [ + true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] +}