From c54cf268486de419071924a05acc325bcf6770ae Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Wed, 12 Apr 2017 10:09:03 +0100 Subject: [PATCH] Add support for oauth adminAuth configs --- editor/js/user.js | 66 ++++++++++++++++++++++++++------- editor/sass/header.scss | 10 +++++ red/api/auth/index.js | 62 +++++++++++++++++++++++++++++-- red/api/auth/users.js | 29 +++++++++++---- red/api/index.js | 23 ++++++------ red/api/theme.js | 17 ++++++--- test/red/api/auth/index_spec.js | 15 +++++++- test/red/api/index_spec.js | 2 +- 8 files changed, 180 insertions(+), 44 deletions(-) diff --git a/editor/js/user.js b/editor/js/user.js index 1f9c87530..084662552 100644 --- a/editor/js/user.js +++ b/editor/js/user.js @@ -32,7 +32,7 @@ RED.user = (function() { autoOpen: false, dialogClass: "ui-dialog-no-close", modal: true, - closeOnEscape: false, + closeOnEscape: !!opts.cancelable, width: 600, resizable: false, draggable: false @@ -43,17 +43,13 @@ RED.user = (function() { dataType: "json", url: "auth/login", 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",{id:"rrr"+i,class:"form-row"}); + var row = $("
",{class:"form-row"}); $('
').appendTo(row); var input = $('').appendTo(row); @@ -112,13 +108,48 @@ RED.user = (function() { }); event.preventDefault(); }); - if (opts.cancelable) { - $("#node-dialog-login-cancel").button().click(function( event ) { - $("#node-dialog-login").dialog('destroy').remove(); + + } else if (data.type == "oauth") { + i = 0; + for (;i",{class:"form-row",style:"text-align: center"}).appendTo("#node-dialog-login-fields"); + + var loginButton = $('',{style: "padding: 10px"}).appendTo(row).click(function() { + document.location = field.url; }); + if (field.image) { + $("",{src:field.image}).appendTo(loginButton); + } else if (field.label) { + var label = $('').text(field.label); + if (field.icon) { + $('',{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.editorTheme || !RED.settings.editorTheme.hasOwnProperty("userMenu")) { - $('
  • ') + var userMenu = $('
  • ') .prependTo(".header-toolbar"); + if (RED.settings.user.image) { + $('').css({ + backgroundImage: "url("+RED.settings.user.image+")", + }).appendTo(userMenu.find("a")); + } else { + $('').appendTo(userMenu.find("a")); + } RED.menu.init({id:"btn-usermenu", options: [] diff --git a/editor/sass/header.scss b/editor/sass/header.scss index d20c6afa8..3cf25650b 100644 --- a/editor/sass/header.scss +++ b/editor/sass/header.scss @@ -279,3 +279,13 @@ span.logo { font-size: 16px; 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; +} diff --git a/red/api/auth/index.js b/red/api/auth/index.js index b6f98d38e..d1858599d 100644 --- a/red/api/auth/index.js +++ b/red/api/auth/index.js @@ -81,9 +81,22 @@ function getToken(req,res,next) { function login(req,res) { var response = {}; if (settings.adminAuth) { - response = { - "type":"credentials", - "prompts":[{id:"username",type:"text",label:"Username"},{id:"password",type:"password",label:"Password"}] + if (settings.adminAuth.type === "credentials") { + response = { + "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) { response.image = theme.context().login.image; @@ -114,5 +127,46 @@ module.exports = { return server.errorHandler()(err,req,res,next); }, 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); + } + ); + + } } diff --git a/red/api/auth/users.js b/red/api/auth/users.js index 22d389a63..88f3686cf 100644 --- a/red/api/auth/users.js +++ b/red/api/auth/users.js @@ -23,14 +23,29 @@ var users = {}; var passwords = {}; 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]; if (user) { - return when.promise(function(resolve,reject) { - bcrypt.compare(password, passwords[username], function(err, res) { - resolve(res?user:null); + if (arguments.length === 2) { + var password = arguments[1]; + 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); } @@ -51,7 +66,7 @@ function init(config) { users = {}; passwords = {}; defaultUser = null; - if (config.type == "credentials") { + if (config.type == "credentials" || config.type == "oauth") { if (config.users) { if (typeof config.users === "function") { api.get = config.users; @@ -96,6 +111,6 @@ function init(config) { module.exports = { init: init, 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(); } }; diff --git a/red/api/index.js b/red/api/index.js index 901c3cd9b..71f3cbb69 100644 --- a/red/api/index.js +++ b/red/api/index.js @@ -96,9 +96,7 @@ function init(_server,_runtime) { editorApp.get("/",ensureRuntimeStarted,ui.ensureSlash,ui.editor); editorApp.get("/icons/:module/:icon",ui.icon); theme.init(runtime); - if (settings.editorTheme) { - editorApp.use("/theme",theme.app()); - } + editorApp.use("/theme",theme.app()); editorApp.use("/",ui.editorResources); adminApp.use(editorApp); } @@ -109,14 +107,17 @@ function init(_server,_runtime) { adminApp.get("/auth/login",auth.login,errorHandler); if (settings.adminAuth) { - //TODO: all passport references ought to be in ./auth - adminApp.use(passport.initialize()); - adminApp.post("/auth/token", - auth.ensureClientSecret, - auth.authenticateClient, - auth.getToken, - auth.errorHandler - ); + if (settings.adminAuth.type === "oauth") { + auth.oauthStrategy(adminApp,settings.adminAuth.strategy); + } else if (settings.adminAuth.type === "credentials") { + adminApp.use(passport.initialize()); + adminApp.post("/auth/token", + auth.ensureClientSecret, + auth.authenticateClient, + auth.getToken, + auth.errorHandler + ); + } adminApp.post("/auth/revoke",needsPermission(""),auth.revoke,errorHandler); } if (settings.httpAdminCors) { diff --git a/red/api/theme.js b/red/api/theme.js index 6d5dba3d7..7f3e0f36d 100644 --- a/red/api/theme.js +++ b/red/api/theme.js @@ -42,6 +42,8 @@ var themeContext = clone(defaultContext); var themeSettings = null; var runtime = null; +var themeApp; + function serveFile(app,baseUrl,file) { try { var stats = fs.statSync(file); @@ -83,7 +85,7 @@ module.exports = { themeContext.version = runtime.version(); } themeSettings = null; - theme = settings.editorTheme; + theme = settings.editorTheme || {}; }, app: function() { @@ -91,17 +93,17 @@ module.exports = { var url; themeSettings = {}; - var themeApp = express(); + themeApp = express(); if (theme.page) { themeContext.page.css = serveFilesFromTheme( - themeContext.page.css, - themeApp, + themeContext.page.css, + themeApp, "/css/") themeContext.page.scripts = serveFilesFromTheme( - themeContext.page.scripts, - themeApp, + themeContext.page.scripts, + themeApp, "/scripts/") if (theme.page.favicon) { @@ -187,5 +189,8 @@ module.exports = { }, settings: function() { return themeSettings; + }, + serveFile: function(baseUrl,file) { + return serveFile(themeApp,baseUrl,file); } } diff --git a/test/red/api/auth/index_spec.js b/test/red/api/auth/index_spec.js index c40d070b8..13347ed97 100644 --- a/test/red/api/auth/index_spec.js +++ b/test/red/api/auth/index_spec.js @@ -85,7 +85,7 @@ describe("api auth middleware",function() { Users.init.restore(); }); 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) { resp.should.have.a.property("type","credentials"); resp.should.have.a.property("prompts"); @@ -100,6 +100,19 @@ describe("api auth middleware",function() { 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(); + }}); + }); }); diff --git a/test/red/api/index_spec.js b/test/red/api/index_spec.js index 8edd737d6..b1c80babb 100644 --- a/test/red/api/index_spec.js +++ b/test/red/api/index_spec.js @@ -56,7 +56,7 @@ describe("api index", function() { describe("can serve auth", function() { var mockList = [ - 'ui','nodes','flows','library','info','theme','locales','credentials' + 'ui','nodes','flows','library','info','locales','credentials' ] before(function() { mockList.forEach(function(m) {