cloud-red
Version:
Harnessing Serverless for your cloud integration needs
280 lines (259 loc) • 7.87 kB
JavaScript
/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
/**
* @mixin @node-red/editor-api_auth
*/
var passport = require('passport');
var oauth2orize = require('oauth2orize');
var strategies = require('./strategies');
var Tokens = require('./tokens');
var Users = require('./users');
var permissions = require('./permissions');
var theme = require('../editor/theme');
var settings = null;
var log = require('../../../util').log; // TODO: separate module
passport.use(strategies.bearerStrategy.BearerStrategy);
passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy);
passport.use(strategies.anonymousStrategy);
var server = oauth2orize.createServer();
server.exchange(
oauth2orize.exchange.password(strategies.passwordTokenExchange)
);
function init(_settings, storage) {
settings = _settings;
if (settings.adminAuth) {
var mergedAdminAuth = Object.assign(
{},
settings.adminAuth,
settings.adminAuth.module
);
Users.init(mergedAdminAuth);
Tokens.init(mergedAdminAuth, storage);
}
}
/**
* Returns an Express middleware function that ensures the user making a request
* has the necessary permission.
*
* @param {String} permission - the permission required for the request, such as `flows.write`
* @return {Function} - an Express middleware
* @memberof @node-red/editor-api_auth
*/
function needsPermission(permission) {
return function(req, res, next) {
if (settings && settings.adminAuth) {
return passport.authenticate(['bearer', 'anon'], { session: false })(
req,
res,
function() {
if (!req.user) {
return next();
}
if (permissions.hasPermission(req.authInfo.scope, permission)) {
return next();
}
log.audit({ event: 'permission.fail', permissions: permission }, req);
return res.status(401).end();
}
);
} else {
next();
}
};
}
function ensureClientSecret(req, res, next) {
if (!req.body.client_secret) {
req.body.client_secret = 'not_available';
}
next();
}
function authenticateClient(req, res, next) {
return passport.authenticate(['oauth2-client-password'], { session: false })(
req,
res,
next
);
}
function getToken(req, res, next) {
return server.token()(req, res, next);
}
function login(req, res) {
var response = {};
if (settings.adminAuth) {
var mergedAdminAuth = Object.assign(
{},
settings.adminAuth,
settings.adminAuth.module
);
if (mergedAdminAuth.type === 'credentials') {
response = {
type: 'credentials',
prompts: [
{ id: 'username', type: 'text', label: 'user.username' },
{ id: 'password', type: 'password', label: 'user.password' }
]
};
} else if (mergedAdminAuth.type === 'strategy') {
var urlPrefix =
settings.httpAdminRoot === '/' ? '' : settings.httpAdminRoot;
response = {
type: 'strategy',
prompts: [
{
type: 'button',
label: mergedAdminAuth.strategy.label,
url: urlPrefix + 'auth/strategy'
}
]
};
if (mergedAdminAuth.strategy.icon) {
response.prompts[0].icon = mergedAdminAuth.strategy.icon;
}
if (mergedAdminAuth.strategy.image) {
response.prompts[0].image = theme.serveFile(
'/login/',
mergedAdminAuth.strategy.image
);
}
}
if (theme.context().login && theme.context().login.image) {
response.image = theme.context().login.image;
}
}
res.json(response);
}
function revoke(req, res) {
var token = req.body.token;
// TODO: audit log
Tokens.revoke(token).then(function() {
log.audit({ event: 'auth.login.revoke' }, req);
if (
settings.editorTheme &&
settings.editorTheme.logout &&
settings.editorTheme.logout.redirect
) {
res.json({ redirect: settings.editorTheme.logout.redirect });
} else {
res.status(200).end();
}
});
}
function completeVerify(profile, done) {
Users.authenticate(profile).then(function(user) {
if (user) {
Tokens.create(user.username, 'node-red-editor', user.permissions).then(
function(tokens) {
log.audit({
event: 'auth.login',
username: user.username,
scope: user.permissions
});
user.tokens = tokens;
done(null, user);
}
);
} else {
log.audit({
event: 'auth.login.fail.oauth',
username: typeof profile === 'string' ? profile : profile.username
});
done(null, false);
}
});
}
function genericStrategy(adminApp, strategy) {
var crypto = require('crypto');
var session = require('express-session');
var MemoryStore = require('memorystore')(session);
adminApp.use(
session({
// As the session is only used across the life-span of an auth
// hand-shake, we can use a instance specific random string
secret: crypto.randomBytes(20).toString('hex'),
resave: false,
saveUninitialized: false,
store: new MemoryStore({
checkPeriod: 86400000 // prune expired entries every 24h
})
})
);
//TODO: all passport references ought to be in ./auth
adminApp.use(passport.initialize());
adminApp.use(passport.session());
var options = strategy.options;
passport.use(
new strategy.strategy(options, function() {
var originalDone = arguments[arguments.length - 1];
if (options.verify) {
var args = Array.from(arguments);
args[args.length - 1] = function(err, profile) {
if (err) {
return originalDone(err);
} else {
return completeVerify(profile, originalDone);
}
};
options.verify.apply(null, args);
} else {
var profile = arguments[arguments.length - 2];
return completeVerify(profile, originalDone);
}
})
);
adminApp.get(
'/auth/strategy',
passport.authenticate(strategy.name, {
session: false,
failureRedirect: settings.httpAdminRoot
}),
completeGenerateStrategyAuth
);
var callbackMethodFunc = adminApp.get;
if (/^post$/i.test(options.callbackMethod)) {
callbackMethodFunc = adminApp.post;
}
callbackMethodFunc.call(
adminApp,
'/auth/strategy/callback',
passport.authenticate(strategy.name, {
session: false,
failureRedirect: settings.httpAdminRoot
}),
completeGenerateStrategyAuth
);
}
function completeGenerateStrategyAuth(req, res) {
var tokens = req.user.tokens;
delete req.user.tokens;
// Successful authentication, redirect home.
res.redirect(settings.httpAdminRoot + '?access_token=' + tokens.accessToken);
}
module.exports = {
init: init,
needsPermission: needsPermission,
ensureClientSecret: ensureClientSecret,
authenticateClient: authenticateClient,
getToken: getToken,
errorHandler: function(err, req, res, next) {
//TODO: audit log statment
//console.log(err.stack);
//log.log({level:"audit",type:"auth",msg:err.toString()});
return server.errorHandler()(err, req, res, next);
},
login: login,
revoke: revoke,
genericStrategy: genericStrategy
};