1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00

Add support for oauth adminAuth configs

This commit is contained in:
Nick O'Leary 2017-04-12 10:09:03 +01:00
parent bfb548636e
commit c54cf26848
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
8 changed files with 180 additions and 44 deletions

View File

@ -32,7 +32,7 @@ RED.user = (function() {
autoOpen: false, autoOpen: false,
dialogClass: "ui-dialog-no-close", dialogClass: "ui-dialog-no-close",
modal: true, modal: true,
closeOnEscape: false, closeOnEscape: !!opts.cancelable,
width: 600, width: 600,
resizable: false, resizable: false,
draggable: false draggable: false
@ -43,17 +43,13 @@ RED.user = (function() {
dataType: "json", dataType: "json",
url: "auth/login", url: "auth/login",
success: function(data) { success: function(data) {
if (data.type == "credentials") { var i=0;
var i=0;
if (data.type == "credentials") {
if (data.image) {
$("#node-dialog-login-image").attr("src",data.image);
} else {
$("#node-dialog-login-image").attr("src","red/images/node-red-256.png");
}
for (;i<data.prompts.length;i++) { for (;i<data.prompts.length;i++) {
var field = data.prompts[i]; var field = data.prompts[i];
var row = $("<div/>",{id:"rrr"+i,class:"form-row"}); var row = $("<div/>",{class:"form-row"});
$('<label for="node-dialog-login-'+field.id+'">'+field.label+':</label><br/>').appendTo(row); $('<label for="node-dialog-login-'+field.id+'">'+field.label+':</label><br/>').appendTo(row);
var input = $('<input style="width: 100%" id="node-dialog-login-'+field.id+'" type="'+field.type+'" tabIndex="'+(i+1)+'"/>').appendTo(row); var input = $('<input style="width: 100%" id="node-dialog-login-'+field.id+'" type="'+field.type+'" tabIndex="'+(i+1)+'"/>').appendTo(row);
@ -112,13 +108,48 @@ RED.user = (function() {
}); });
event.preventDefault(); event.preventDefault();
}); });
if (opts.cancelable) {
$("#node-dialog-login-cancel").button().click(function( event ) { } else if (data.type == "oauth") {
$("#node-dialog-login").dialog('destroy').remove(); i = 0;
for (;i<data.prompts.length;i++) {
var field = data.prompts[i];
var row = $("<div/>",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields");
var loginButton = $('<a href="#"></a>',{style: "padding: 10px"}).appendTo(row).click(function() {
document.location = field.url;
}); });
if (field.image) {
$("<img>",{src:field.image}).appendTo(loginButton);
} else if (field.label) {
var label = $('<span></span>').text(field.label);
if (field.icon) {
$('<i></i>',{class: "fa fa-2x "+field.icon, style:"vertical-align: middle"}).appendTo(loginButton);
label.css({
"verticalAlign":"middle",
"marginLeft":"8px"
});
}
label.appendTo(loginButton);
}
loginButton.button();
} }
} }
dialog.dialog("open"); if (opts.cancelable) {
$("#node-dialog-login-cancel").button().click(function( event ) {
$("#node-dialog-login").dialog('destroy').remove();
});
}
var loginImageSrc = data.image || "red/images/node-red-256.png";
$("#node-dialog-login-image").load(function() {
dialog.dialog("open");
}).attr("src",loginImageSrc);
} }
}); });
} }
@ -170,8 +201,15 @@ RED.user = (function() {
if (RED.settings.user) { if (RED.settings.user) {
if (!RED.settings.editorTheme || !RED.settings.editorTheme.hasOwnProperty("userMenu")) { if (!RED.settings.editorTheme || !RED.settings.editorTheme.hasOwnProperty("userMenu")) {
$('<li><a id="btn-usermenu" class="button hide" data-toggle="dropdown" href="#"><i class="fa fa-user"></i></a></li>') var userMenu = $('<li><a id="btn-usermenu" class="button hide" data-toggle="dropdown" href="#"></a></li>')
.prependTo(".header-toolbar"); .prependTo(".header-toolbar");
if (RED.settings.user.image) {
$('<span class="user-profile"></span>').css({
backgroundImage: "url("+RED.settings.user.image+")",
}).appendTo(userMenu.find("a"));
} else {
$('<i class="fa fa-user"></i>').appendTo(userMenu.find("a"));
}
RED.menu.init({id:"btn-usermenu", RED.menu.init({id:"btn-usermenu",
options: [] options: []

View File

@ -279,3 +279,13 @@ span.logo {
font-size: 16px; font-size: 16px;
color: #fff; color: #fff;
} }
#btn-usermenu .user-profile {
background-position: center center;
background-repeat: no-repeat;
background-size: contain;
display: inline-block;
width: 40px;
height: 35px;
vertical-align: middle;
}

View File

@ -81,9 +81,22 @@ function getToken(req,res,next) {
function login(req,res) { function login(req,res) {
var response = {}; var response = {};
if (settings.adminAuth) { if (settings.adminAuth) {
response = { if (settings.adminAuth.type === "credentials") {
"type":"credentials", response = {
"prompts":[{id:"username",type:"text",label:"Username"},{id:"password",type:"password",label:"Password"}] "type":"credentials",
"prompts":[{id:"username",type:"text",label:"Username"},{id:"password",type:"password",label:"Password"}]
}
} else if (settings.adminAuth.type === "oauth") {
response = {
"type":"oauth",
"prompts":[{type:"button",label:settings.adminAuth.strategy.label, url:"/auth/oauth"}]
}
if (settings.adminAuth.strategy.icon) {
response.prompts[0].icon = settings.adminAuth.strategy.icon;
}
if (settings.adminAuth.strategy.image) {
response.prompts[0].image = theme.serveFile('/login/',settings.adminAuth.strategy.image);
}
} }
if (theme.context().login && theme.context().login.image) { if (theme.context().login && theme.context().login.image) {
response.image = theme.context().login.image; response.image = theme.context().login.image;
@ -114,5 +127,46 @@ module.exports = {
return server.errorHandler()(err,req,res,next); return server.errorHandler()(err,req,res,next);
}, },
login: login, login: login,
revoke: revoke revoke: revoke,
oauthStrategy: function(adminApp,strategy) {
var session = require('express-session');
adminApp.use(session({
secret: 'keyboard cat', // TODO: pull this out
resave: false,
saveUninitialized:false
}));
//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(token, tokenSecret, 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:profile.id});
done(null,false);
}
});
}
));
adminApp.get('/auth/oauth', passport.authenticate(strategy.name));
adminApp.get('/auth/oauth/callback',
passport.authenticate(strategy.name, {session:false, failureRedirect: '/' }),
function(req, res) {
var tokens = req.user.tokens;
delete req.user.tokens;
// Successful authentication, redirect home.
res.redirect('/?access_token='+tokens.accessToken);
}
);
}
} }

View File

@ -23,14 +23,29 @@ var users = {};
var passwords = {}; var passwords = {};
var defaultUser = null; var defaultUser = null;
function authenticate(username,password) { function authenticate() {
var username;
if (arguments.length === 2) {
username = arguments[0];
} else {
username = arguments[0].username;
}
var user = users[username]; var user = users[username];
if (user) { if (user) {
return when.promise(function(resolve,reject) { if (arguments.length === 2) {
bcrypt.compare(password, passwords[username], function(err, res) { var password = arguments[1];
resolve(res?user:null); return when.promise(function(resolve,reject) {
bcrypt.compare(password, passwords[username], function(err, res) {
resolve(res?user:null);
});
}); });
}); } else {
// Try to extract common profile information
if (arguments[0].hasOwnProperty('photos') && arguments[0].photos.length > 0) {
user.image = arguments[0].photos[0].value;
}
return when.resolve(user);
}
} }
return when.resolve(null); return when.resolve(null);
} }
@ -51,7 +66,7 @@ function init(config) {
users = {}; users = {};
passwords = {}; passwords = {};
defaultUser = null; defaultUser = null;
if (config.type == "credentials") { if (config.type == "credentials" || config.type == "oauth") {
if (config.users) { if (config.users) {
if (typeof config.users === "function") { if (typeof config.users === "function") {
api.get = config.users; api.get = config.users;
@ -96,6 +111,6 @@ function init(config) {
module.exports = { module.exports = {
init: init, init: init,
get: function(username) { return api.get(username) }, get: function(username) { return api.get(username) },
authenticate: function(username,password) { return api.authenticate(username,password) }, authenticate: function() { return api.authenticate.apply(null, arguments) },
default: function() { return api.default(); } default: function() { return api.default(); }
}; };

View File

@ -96,9 +96,7 @@ function init(_server,_runtime) {
editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor);
editorApp.get("/icons/:module/:icon",ui.icon); editorApp.get("/icons/:module/:icon",ui.icon);
theme.init(runtime); theme.init(runtime);
if (settings.editorTheme) { editorApp.use("/theme",theme.app());
editorApp.use("/theme",theme.app());
}
editorApp.use("/",ui.editorResources); editorApp.use("/",ui.editorResources);
adminApp.use(editorApp); adminApp.use(editorApp);
} }
@ -109,14 +107,17 @@ function init(_server,_runtime) {
adminApp.get("/auth/login",auth.login,errorHandler); adminApp.get("/auth/login",auth.login,errorHandler);
if (settings.adminAuth) { if (settings.adminAuth) {
//TODO: all passport references ought to be in ./auth if (settings.adminAuth.type === "oauth") {
adminApp.use(passport.initialize()); auth.oauthStrategy(adminApp,settings.adminAuth.strategy);
adminApp.post("/auth/token", } else if (settings.adminAuth.type === "credentials") {
auth.ensureClientSecret, adminApp.use(passport.initialize());
auth.authenticateClient, adminApp.post("/auth/token",
auth.getToken, auth.ensureClientSecret,
auth.errorHandler auth.authenticateClient,
); auth.getToken,
auth.errorHandler
);
}
adminApp.post("/auth/revoke",needsPermission(""),auth.revoke,errorHandler); adminApp.post("/auth/revoke",needsPermission(""),auth.revoke,errorHandler);
} }
if (settings.httpAdminCors) { if (settings.httpAdminCors) {

View File

@ -42,6 +42,8 @@ var themeContext = clone(defaultContext);
var themeSettings = null; var themeSettings = null;
var runtime = null; var runtime = null;
var themeApp;
function serveFile(app,baseUrl,file) { function serveFile(app,baseUrl,file) {
try { try {
var stats = fs.statSync(file); var stats = fs.statSync(file);
@ -83,7 +85,7 @@ module.exports = {
themeContext.version = runtime.version(); themeContext.version = runtime.version();
} }
themeSettings = null; themeSettings = null;
theme = settings.editorTheme; theme = settings.editorTheme || {};
}, },
app: function() { app: function() {
@ -91,7 +93,7 @@ module.exports = {
var url; var url;
themeSettings = {}; themeSettings = {};
var themeApp = express(); themeApp = express();
if (theme.page) { if (theme.page) {
@ -187,5 +189,8 @@ module.exports = {
}, },
settings: function() { settings: function() {
return themeSettings; return themeSettings;
},
serveFile: function(baseUrl,file) {
return serveFile(themeApp,baseUrl,file);
} }
} }

View File

@ -85,7 +85,7 @@ describe("api auth middleware",function() {
Users.init.restore(); Users.init.restore();
}); });
it("returns login details - credentials", function(done) { it("returns login details - credentials", function(done) {
auth.init({settings:{adminAuth:{}},log:{audit:function(){}}}) auth.init({settings:{adminAuth:{type:"credentials"}},log:{audit:function(){}}})
auth.login(null,{json: function(resp) { auth.login(null,{json: function(resp) {
resp.should.have.a.property("type","credentials"); resp.should.have.a.property("type","credentials");
resp.should.have.a.property("prompts"); resp.should.have.a.property("prompts");
@ -100,6 +100,19 @@ describe("api auth middleware",function() {
done(); done();
}}); }});
}); });
it("returns login details - oauth", function(done) {
auth.init({settings:{adminAuth:{type:"oauth",strategy:{label:"test-oauth",icon:"test-icon"}}},log:{audit:function(){}}})
auth.login(null,{json: function(resp) {
resp.should.have.a.property("type","oauth");
resp.should.have.a.property("prompts");
resp.prompts.should.have.a.lengthOf(1);
resp.prompts[0].should.have.a.property("type","button");
resp.prompts[0].should.have.a.property("label","test-oauth");
resp.prompts[0].should.have.a.property("icon","test-icon");
done();
}});
});
}); });

View File

@ -56,7 +56,7 @@ describe("api index", function() {
describe("can serve auth", function() { describe("can serve auth", function() {
var mockList = [ var mockList = [
'ui','nodes','flows','library','info','theme','locales','credentials' 'ui','nodes','flows','library','info','locales','credentials'
] ]
before(function() { before(function() {
mockList.forEach(function(m) { mockList.forEach(function(m) {