New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

oniyi-http-plugin-credentials

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

oniyi-http-plugin-credentials - npm Package Compare versions

Comparing version 0.2.2 to 1.0.0

lib/apply-credentials.js

264

lib/index.js
'use strict';
// node core modules
// 3rd party modules
const _ = require('lodash');
const passport = require('passport');
const logger = require('oniyi-logger')('oniyi:http-client:plugin:attach-credentials');
const debug = require('debug')('oniyi:http-client:plugin:attach-credentials');
// internal modules
const { makeRequestParamsExtractor, getUserId } = require('./utils');
const { applyCredentials } = require('./apply-credentials');
function areCredentialsExpired(credentials, callback) {
return callback(null, credentials.expiresAt && credentials.expiresAt < Date.now());
}
const PLUGIN_NAME = 'attach-credentials';
function refreshCredentials(strategy, currentCredentials, callback) {
const params = { grant_type: 'refresh_token' };
// eslint-disable-next-line no-underscore-dangle
strategy._oauth2.getOAuthAccessToken(currentCredentials.refreshToken, params,
(err, accessToken, refreshToken, respParams) => {
if (err) {
return callback(err);
}
const defaults = {
removeUserProp: true,
userPropName: 'user',
credentialsMethodName: 'getCredentialsForProvider',
};
// @TODO: add max expiresIn and default issuedOn
const expiresIn = respParams && respParams.expires_in ? parseInt(respParams.expires_in, 10) : undefined;
const issuedOn = respParams && respParams.issued_on ? parseInt(respParams.issued_on, 10) : undefined;
const expiresAt = issuedOn + expiresIn;
// make credentials object
const credentials = {
accessToken,
refreshToken,
expiresAt,
expiresIn,
issuedOn,
tokenType: respParams.token_type || 'Bearer',
};
return callback(null, credentials);
});
}
function makeAuthParams(credentials, callback) {
return callback(null, {
auth: {
bearer: credentials.accessToken,
},
authType: 'oauth',
});
}
/**
* attachCredentialsPluginFactory
* @param {Object} pluginOptions options to define general plugin behaviour
* @param {String} pluginOptions.providerName Name of the provider that credentials should be resolved for
* @param {Boolean} [pluginOptions.removeUserProp=true] should plugin remove `user` prop from `reqParams`
* @param {String} [pluginOptions.userPropName="user"] name of the `reqParams` property that holds the `user` object
* @param {String} [pluginOptions.credentialsMethodName="getCredentialsForProvider"] name of the method on `user` object that resolves credentials for `providerName`
* @return {Object} [description]
*/
module.exports = function attachCredentialsPluginFactory(pluginOptions) {
// compile plugin options; set default values for properties that have not been provided
const options = _.defaults({}, pluginOptions, {
removeUserProp: true,
areCredentialsExpired,
refreshCredentials,
makeAuthParams,
// when the providerName ends with "-link", we'll find provider related credentials in the
// "credentials" relation. Otherwise, provider related credentials will be available in the
// "identities" relation of the user
userRelationProp: /-link$/.test(pluginOptions.providerName) ? 'credentials' : 'identities',
// name of the related document's property actually holding the credentials
credentialsProp: 'credentials',
});
const options = _.defaults({}, pluginOptions, defaults);
// options verification
if (!_.isString(options.providerName)) {
throw new TypeError(`options.providerName must be of type "String";
provided: ${options.providerName} [${typeof options.providerName}]`);
const err = new TypeError('providerName must be a "String"');
debug(err.message, { options });
throw err;
}
if (!_.isFunction(options.areCredentialsExpired)) {
throw new TypeError(`options.areCredentialsExpired must be of type "Function";
provided: [${typeof options.areCredentialsExpired}]`);
}
const {
providerName, removeUserProp, userPropName, credentialsMethodName,
} = options;
if (!_.isFunction(options.refreshCredentials)) {
throw new TypeError(`options.refreshCredentials must be of type "Function";
provided: [${typeof options.refreshCredentials}]`);
}
const extractRequestparams = makeRequestParamsExtractor(removeUserProp, userPropName);
if (!_.isFunction(options.makeAuthParams)) {
throw new TypeError(`options.makeAuthParams must be of type "Function";
provided: [${typeof options.makeAuthParams}]`);
}
return {
name: 'attach-credentials',
name: PLUGIN_NAME,
load: (req, origParams, callback) => {
// create a copy of provided request parameters
const reqParams = options.removeUserProp ? _.omit(origParams, ['user']) : _.assign({}, origParams || {});
const user = origParams.user;
// create a copy of provided request parameters (remove user prop if requested in `options`)
const reqParams = extractRequestparams(origParams);
const { [userPropName]: user } = origParams;
if (!user) {
logger.debug('No "user" prop found in request params, skipping plugin operations');
return callback(null, origParams);
debug('No "%s" prop found in request params, skipping plugin operations', userPropName, origParams);
// [bk] @TODO: should we use `reqParams` here instead?
// That would mean we potentially pass back params without
// the user prop and without credentials
callback(null, origParams);
return;
}
user[options.userRelationProp]({
where: {
provider: options.providerName,
},
}, (credentialsErr, results) => {
if (credentialsErr) {
logger.error(`Error while loading identities for user "${user.id}"`, credentialsErr);
return callback(credentialsErr);
}
if (!_.isFunction(user[credentialsMethodName])) {
const msg = `${userPropName}.${credentialsMethodName} must be a function`;
debug(msg, { user });
callback(new TypeError(msg));
return;
}
// verify results
// must be array of length === 1
if (!Array.isArray(results) || results.length !== 1) {
logger.error(`Failed to load identities for user "${user.id}"`);
logger.debug(`results is array? ${Array.isArray(results)}`);
logger.debug(`results length? ${results.length}`);
return callback(new Error(`Failed to load identities for user "${user.id}"`));
user[credentialsMethodName](providerName, reqParams, (getCredentialsError, credentials) => {
if (getCredentialsError) {
debug(
'Failed to load credentials for %s %s and provider %s',
userPropName,
getUserId(user),
providerName,
getCredentialsError
);
callback(getCredentialsError);
return;
}
const identity = results[0];
const credentials = identity[options.credentialsProp];
if (!credentials) {
logger.warn(`No credentials found for user "${user.id}" and provider "${options.providerName}"`);
return callback(null, origParams);
// [bk] @TODO: add switch to plugin options to either abort or ignore this error.
// currently we abort
const err = new Error(`No credentials found for user "${getUserId(user)}" and provider "${providerName}"`);
debug(err.message);
callback(err);
return;
}
if (!credentials.userId) {
Object.assign(credentials, { userId: user.id });
}
// must handle credentials refresh in pre-flight phase due to possible use of stream API
// with request. If client wants to send data, we can not ensure that data is still available
// when retrying.
options.areCredentialsExpired(credentials, (expiredErr, credentialsExpired) => {
if (expiredErr) {
return callback(expiredErr);
applyCredentials(reqParams, credentials, (applyCredentialsError, reqParamsWithCredentials) => {
if (applyCredentialsError) {
callback(applyCredentialsError);
return;
}
if (credentialsExpired) {
logger.warn(`credentials for user "${user.id}" and provider "${options.providerName}" are expired`);
// load the passport-strategy instance from passport
// eslint-disable-next-line no-underscore-dangle
const strategy = passport._strategy(options.providerName);
if (!strategy) {
throw new Error(`Auth provider with name "${options.providerName}" is not registered`);
}
return options.refreshCredentials(strategy, credentials, (refreshCredentialsErr, newCredentials) => {
if (refreshCredentialsErr) {
return callback(refreshCredentialsErr);
}
// store new credentials in the user's identities / credentials
identity.updateAttribute(options.credentialsProp, newCredentials,
(updateIdentityErr, updatedIdentity) => {
if (updateIdentityErr) {
return callback(updateIdentityErr);
}
logger.debug('updated user identity', updatedIdentity);
// pass newCredentials to makeAuthParams and merge return value with the modified request parameters
// explicitly using "merge" here to allow partial updates of nested object literals
options.makeAuthParams(newCredentials, (authParamsErr, authParams) => {
if (authParamsErr) {
return callback(authParamsErr);
}
_.merge(reqParams, authParams);
// finish plugin execution; hand over the modified request parameters
return callback(null, reqParams);
});
return null;
});
return null;
});
}
// when original credentials were not expired, attach them to the request parameters and finish
// plugin execution
// pass credentials to makeAuthParams and merge return value with the modified request parameters
// explicitly using "merge" here to allow partial updates of nested object literals
options.makeAuthParams(credentials, (authParamsErr, authParams) => {
if (authParamsErr) {
return callback(authParamsErr);
}
_.merge(reqParams, authParams);
// finish plugin execution; hand over the modified request parameters
return callback(null, reqParams);
});
return null;
callback(null, reqParamsWithCredentials);
});
return null;
});
return null;
},
};
};
// remember reference to the original callback function
// const originalCallback = origParams.callback;
// reqParams.callback = (requestErr, response, body) => {
// if (requestErr) {
// return originalCallback(requestErr, response, body);
// }
// // this approach lacks support of data-providing requests
// // stream API
// if (response && response.statusCode === 401) {
// return refreshCredentials(strategy, credentials, (refreshCredentialsErr, newCredentials) => {
// if (refreshCredentialsErr) {
// return originalCallback(requestErr, response, body);
// }
// // store new credentials in the user's identities / credentials
// identity.updateAttribute(options.credentialsProp, newCredentials,
// (updateIdentityErr, updatedIdentity) => {
// if (updateIdentityErr) {
// return originalCallback(requestErr, response, body);
// }
// // update request parameters with new accessToken and original callback function
// reqParams.auth = {
// bearer: newCredentials.accessToken,
// };
// reqParams.callback = originalCallback;
// // restart the request
// return callback(null, reqParams);
// });
// });
// }
// return originalCallback(requestErr, response, body);
// };
{
"name": "oniyi-http-plugin-credentials",
"version": "0.2.2",
"version": "1.0.0",
"description": "A plugin for oniyi-http-client for automatic attachment of user credentials",
"homepage": "",
"author": {
"name": "Benjamin Kroeger",
"email": "benjamin.kroeger@gmail.com",
"url": ""
},
"homepage": "https://github.com/benkroeger/oniyi-http-plugin-credentials#readme",
"author": "Benjamin Kroeger <benjamin.kroeger@gmail.com>",
"files": [
"lib"
"lib/"
],

@@ -26,25 +22,33 @@ "main": "lib/index.js",

"dependencies": {
"lodash": "^4.10.0",
"oniyi-logger": "^1.0.0",
"passport": "^0.3.2"
"debug": "^3.1.0",
"lodash": "^4.17.4"
},
"devDependencies": {
"eslint": "^3.5.0",
"eslint-config-oniyi": "^4.2.0",
"gulp": "^3.9.1",
"gulp-coveralls": "^0.1.4",
"gulp-eslint": "^2.0.0",
"gulp-exclude-gitignore": "^1.0.0",
"gulp-istanbul": "^0.10.4",
"gulp-mocha": "^2.2.0",
"gulp-nsp": "^2.4.0",
"gulp-plumber": "^1.1.0",
"lodash": "^4.15.0"
"ava": " ^0.23.0",
"eslint": "^4.10.0",
"eslint-config-oniyi": "^5.0.2",
"eslint-plugin-ava": "^4.2.2",
"nyc": "^11.2.1",
"prettier-eslint-cli": "^4.4.0",
"sinon": "^4.1.2"
},
"repository": "benkroeger/oniyi-http-plugin-credentials",
"repository": {
"type": "git",
"url": "git+https://github.com/benkroeger/oniyi-http-plugin-credentials.git"
},
"scripts": {
"prepublish": "gulp prepublish",
"test": "gulp"
"format": "prettier-eslint --write \"lib/**/*.js\" \"test/**/*.js\"",
"prelint": "npm run format",
"lint": "eslint --ignore-path .gitignore .",
"test": "ava --verbose",
"test:watch": "npm test -- --watch",
"coverage": "nyc npm test && nyc report --reporter=html"
},
"license": "MIT"
"license": "MIT",
"bugs": {
"url": "https://github.com/benkroeger/oniyi-http-plugin-credentials/issues"
},
"directories": {
"test": "test"
}
}

@@ -1,3 +0,3 @@

# oniyi-http-plugin-credentials [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-image]][daviddm-url] [![Coverage percentage][coveralls-image]][coveralls-url]
> A plugin for oniyi-http-client for automatic attachment of user credentials
# oniyi-http-plugin-credentials [![NPM version][npm-image]][npm-url] [![Dependency Status][daviddm-image]][daviddm-url]
> An async plugin for oniyi-http-client to resolve and attach credentials to request params

@@ -23,3 +23,8 @@ This plugin is designed to work with the [third-party login component](https://docs.strongloop.com/pages/releaseview.action?pageId=3836277) of [loopback](https://docs.strongloop.com/display/public/LB/LoopBack).

const pluginOptions = {};
const pluginOptions = {
providerName: 'my-auth-provider', // Name of the provider that credentials should be resolved for
removeUserProp: true, // should plugin remove `user` prop from `reqParams`
userPropName: 'user', // name of the `reqParams` property that holds the `user` object
credentialsMethodName: 'getCredentialsForProvider', // name of the method on `user` object that resolves credentials for `providerName`
};
const plugin = oniyiHttpPluginCredentials(pluginOptions);

@@ -33,14 +38,50 @@

available options are:
- **providerName**: undefined (string, required) - name of the passport-strategy to be used. *Note:* passport-strategy **must** be registered first
- **removeUserProp**: true (boolean, optional) - indicates if the `user` property should be removed from the request options
- **areCredentialsExpired**: (function, optional) - async function that checks if credentials are expired. Must take two arguments (`credentials`, `callback(err, isExpired)`)
- **refreshCredentials**: (function, optional) - async function that provides refreshed credentials. Must take three arguments (`strategy`, `currentCredentials`, `callback(err, freshCredentials)`)
- **makeAuthParams**: (function, optional) - async function that provides an object literal to be merged with request parameters. Must take two arguments (`credentials`, `callback(err, authParams)`)
- **userRelationProp**: `/-link$/.test(pluginOptions.providerName) ? 'credentials' : 'identities'`, (string, optional) - name of the relation on `req.user` that we should search for user credentials
- **credentialsProp**: 'credentials' (string, optional) - name of the property in the relation's document to be used for credentials
available options are:
- **providerName**: `undefined` (string, required) - is passed to `getCredentialsForProvider` to indicate which backend we need credentials for
- **removeUserProp**: `true` (boolean, optional) - indicates if the `user` property should be removed from the request options
- **userPropName**: `user` (string, optional) - name of the `reqParams` property that holds the `user` object
- **credentialsMethodName**: `getCredentialsForProvider` (string, optional) - name of the method on `user` object that resolves credentials for `providerName`
## How does it work?
All options of type `function` have default values that can with OAuth2 strategies.
`plugin.load()` retrieves an object with parameters (origParams) that will later be used to make an http(s) request. From there, the following flow is applied:
copy `origParams` into `reqParams`. Depending on `options.removeUserProp`, the original prop named `options.userPropName` will be omitted or included.
read prop named `options.userPropName` from `origParams` into `user`.
If `user` can not be found, abort flow and invoke callback with `origParams`.
If `user[options.credentialsMethodName]` is not a function, invoke callback with `Error`.
Invoke `user[options.credentialsMethodName]` with `options.providerName` and `reqParams` as well as a callback function.
Now `user[options.credentialsMethodName]` is supposed to resolve credentials for `user` and the authentication provider. This resolution should happen async and results be passed to our local callback (which takes `err` and `credentials` arguments).
If an error occurs, plugin flow is aborted and `err` passed to callback.
If `credentials` is falsy, plugin flow is also aborted and callback invoked with an according error.
At this point, we let `user[options.credentialsMethodName]` resolve credentials for the auth provider that this plugin instance is configured for – and no errors occurred.
Now the plugin applies `credentials` to `reqParams`. For that, `credentials.type` is mapped against a list of supported credential types. If `credentials.type` is supported, that type specific implementation is invoked with `reqParams` and `credentials.payload`.
Each credentials type expects a different layout of `credentials.payload`.
## Credentials types
### basic
Reads `username`, `password` and optionally `sendImmediately` (default: `true`) and `authType` (default: `basic`) from `payload` and injects them into `reqParams`.
Use this type when you have username and password at hand (plain)
### bearer
Reads `token` and optionally `sendImmediately` (default: `true`) and `authType` (default: `oauth`) from `payload` and injects them into `reqParams`.
Use this type when you have e.g. an OAuth2 / OIDC access token at hand
### cookie
Reads `cookie` and optionally `authType` (default: `cookie`) from `payload` and injects them into `reqParams`. The value of `cookie` is set into `reqParams.headers.cookie`. If `reqParams.headers.cookie` was not empty to begin with, value of `cookie` is appended.
Use this type when you have an authentication cookie (e.g. LtpaToken2 for IBM Websphere ApplicationServer) at hand.
### header
Reads `value` and optionally `name` (default: `authorization`) and `authType` (default: `undefined`) from `payload` and injects them into `reqParams`. The `value` is set into `reqParams.headers[name]`.
Use this type for any other form of credentials that are provided in a http header. E.g. if you only have basic credentials already base64 encoded or you're working with a custom TAI for IBM Websphere ApplicationServer where you simply pass an encrypted username to the remote host.
## License

@@ -57,3 +98,1 @@

[daviddm-url]: https://david-dm.org/benkroeger/oniyi-http-plugin-credentials
[coveralls-image]: https://coveralls.io/repos/benkroeger/oniyi-http-plugin-credentials/badge.svg
[coveralls-url]: https://coveralls.io/r/benkroeger/oniyi-http-plugin-credentials
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc