mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02:00
b8435efc97
Fixes #3467
261 lines
8.7 KiB
JavaScript
261 lines
8.7 KiB
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("@node-red/util").log; // TODO: separate module
|
|
|
|
|
|
passport.use(strategies.bearerStrategy.BearerStrategy);
|
|
passport.use(strategies.clientPasswordStrategy.ClientPasswordStrategy);
|
|
passport.use(strategies.anonymousStrategy);
|
|
passport.use(strategies.tokensStrategy);
|
|
|
|
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','tokens','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);
|
|
}
|
|
|
|
async 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||"").replace(/\/$/,"");
|
|
if (urlPrefix.length > 0) {
|
|
urlPrefix += "/";
|
|
}
|
|
response = {
|
|
"type":"strategy"
|
|
}
|
|
if (mergedAdminAuth.strategy.autoLogin) {
|
|
response.autoLogin = true
|
|
response.loginRedirect = urlPrefix + "auth/strategy"
|
|
}
|
|
response.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);
|
|
}
|
|
}
|
|
let themeContext = await theme.context();
|
|
if (themeContext.login && themeContext.login.image) {
|
|
response.image = themeContext.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",user,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;
|
|
var verify = 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);
|
|
}
|
|
};
|
|
// Give our callback the same arity as the original one from options
|
|
if (options.verify) {
|
|
Object.defineProperty(verify, "length", { value: options.verify.length })
|
|
}
|
|
|
|
passport.use(new strategy.strategy(options, verify));
|
|
|
|
adminApp.get('/auth/strategy',
|
|
passport.authenticate(strategy.name, {session:false,
|
|
failureMessage: true,
|
|
failureRedirect: settings.httpAdminRoot
|
|
}),
|
|
completeGenerateStrategyAuth,
|
|
handleStrategyError
|
|
);
|
|
|
|
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,
|
|
failureMessage: true,
|
|
failureRedirect: settings.httpAdminRoot
|
|
}),
|
|
completeGenerateStrategyAuth,
|
|
handleStrategyError
|
|
);
|
|
|
|
}
|
|
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);
|
|
}
|
|
function handleStrategyError(err, req, res, next) {
|
|
if (res.headersSent) {
|
|
return next(err)
|
|
}
|
|
log.audit({event: "auth.login.fail.oauth",error:err.toString()});
|
|
res.redirect(settings.httpAdminRoot + '?session_message='+err.toString());
|
|
}
|
|
|
|
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
|
|
}
|