/** * 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. **/ 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 = null 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(runtime) { settings = runtime.settings; log = runtime.log; if (settings.adminAuth) { var mergedAdminAuth = Object.assign({}, settings.adminAuth, settings.adminAuth.module); Users.init(mergedAdminAuth); Tokens.init(mergedAdminAuth,runtime.storage); strategies.init(runtime); } } 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 ); adminApp.get('/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 }