Allow Comms websocket auth to be done via token header

Fixes #2642
This commit is contained in:
Nick O'Leary 2020-07-09 19:07:51 +01:00
parent 32163d5f21
commit 1df2f5e96a
No known key found for this signature in database
GPG Key ID: 4F2157149161A6C9
4 changed files with 74 additions and 28 deletions

View File

@ -123,38 +123,57 @@ AnonymousStrategy.prototype.authenticate = function(req) {
}); });
} }
function authenticateUserToken(req) {
return new Promise( (resolve,reject) => {
var token = null;
var tokenHeader = Users.tokenHeader();
if (Users.tokenHeader() === null) {
// No custom user token provided. Fail the request
reject();
return;
} else if (Users.tokenHeader() === 'authorization') {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') {
token = req.headers.authorization.split(' ')[1];
}
} else {
token = req.headers[Users.tokenHeader()];
}
if (token) {
Users.tokens(token).then(function(user) {
if (user) {
resolve(user);
} else {
reject();
}
});
} else {
reject();
}
});
}
function TokensStrategy() { function TokensStrategy() {
passport.Strategy.call(this); passport.Strategy.call(this);
this.name = 'tokens'; this.name = 'tokens';
} }
util.inherits(TokensStrategy, passport.Strategy); util.inherits(TokensStrategy, passport.Strategy);
TokensStrategy.prototype.authenticate = function(req) { TokensStrategy.prototype.authenticate = function(req) {
var self = this; authenticateUserToken(req).then(user => {
var token = null; this.success(user,{scope:user.permissions});
if (Users.tokenHeader() === 'authorization') { }).catch(err => {
if (req.headers.authorization && req.headers.authorization.split(' ')[0] === 'Bearer') { this.fail(401);
token = req.headers.authorization.split(' ')[1]; });
}
} else {
token = req.headers[Users.tokenHeader()];
}
if (token) {
Users.tokens(token).then(function(admin) {
if (admin) {
self.success(admin,{scope:admin.permissions});
} else {
self.fail(401);
}
});
} else {
self.fail(401);
}
} }
module.exports = { module.exports = {
bearerStrategy: bearerStrategy, bearerStrategy: bearerStrategy,
clientPasswordStrategy: clientPasswordStrategy, clientPasswordStrategy: clientPasswordStrategy,
passwordTokenExchange: passwordTokenExchange, passwordTokenExchange: passwordTokenExchange,
anonymousStrategy: new AnonymousStrategy(), anonymousStrategy: new AnonymousStrategy(),
tokensStrategy: new TokensStrategy() tokensStrategy: new TokensStrategy(),
authenticateUserToken: authenticateUserToken
} }

View File

@ -61,7 +61,7 @@ var api = {
authenticate: authenticate, authenticate: authenticate,
default: getDefaultUser, default: getDefaultUser,
tokens: getDefaultUser, tokens: getDefaultUser,
tokenHeader: "authorization" tokenHeader: null
} }
function init(config) { function init(config) {
@ -111,6 +111,8 @@ function init(config) {
api.tokens = config.tokens; api.tokens = config.tokens;
if (config.tokenHeader && typeof config.tokenHeader === "string") { if (config.tokenHeader && typeof config.tokenHeader === "string") {
api.tokenHeader = config.tokenHeader.toLowerCase(); api.tokenHeader = config.tokenHeader.toLowerCase();
} else {
api.tokenHeader = "authorization";
} }
} }
} }

View File

@ -21,6 +21,7 @@ var log = require("@node-red/util").log; // TODO: separate module
var Tokens; var Tokens;
var Users; var Users;
var Permissions; var Permissions;
var Strategies;
var server; var server;
var settings; var settings;
@ -44,6 +45,7 @@ function init(_server,_settings,_runtimeAPI) {
Tokens.onSessionExpiry(handleSessionExpiry); Tokens.onSessionExpiry(handleSessionExpiry);
Users = require("../auth/users"); Users = require("../auth/users");
Permissions = require("../auth/permissions"); Permissions = require("../auth/permissions");
Strategies = require("../auth/strategies");
} }
function handleSessionExpiry(session) { function handleSessionExpiry(session) {
@ -63,17 +65,18 @@ function generateSession(length) {
return token.join(""); return token.join("");
} }
function CommsConnection(ws) { function CommsConnection(ws, user) {
this.session = generateSession(32); this.session = generateSession(32);
this.ws = ws; this.ws = ws;
this.stack = []; this.stack = [];
this.user = null; this.user = user;
this.lastSentTime = 0; this.lastSentTime = 0;
var self = this; var self = this;
log.audit({event: "comms.open"}); log.audit({event: "comms.open"});
log.trace("comms.open "+self.session); log.trace("comms.open "+self.session);
var pendingAuth = (settings.adminAuth != null); var preAuthed = !!user;
var pendingAuth = !this.user && (settings.adminAuth != null);
if (!pendingAuth) { if (!pendingAuth) {
addActiveConnection(self); addActiveConnection(self);
@ -199,8 +202,8 @@ function start() {
var commsPath = settings.httpAdminRoot || "/"; var commsPath = settings.httpAdminRoot || "/";
commsPath = (commsPath.slice(0,1) != "/" ? "/":"") + commsPath + (commsPath.slice(-1) == "/" ? "":"/") + "comms"; commsPath = (commsPath.slice(0,1) != "/" ? "/":"") + commsPath + (commsPath.slice(-1) == "/" ? "":"/") + "comms";
wsServer = new ws.Server({ noServer: true }); wsServer = new ws.Server({ noServer: true });
wsServer.on('connection',function(ws) { wsServer.on('connection',function(ws, request, user) {
var commsConnection = new CommsConnection(ws); var commsConnection = new CommsConnection(ws, user);
}); });
wsServer.on('error', function(err) { wsServer.on('error', function(err) {
log.warn(log._("comms.error-server",{message:err.toString()})); log.warn(log._("comms.error-server",{message:err.toString()}));
@ -209,8 +212,26 @@ function start() {
server.on('upgrade', function upgrade(request, socket, head) { server.on('upgrade', function upgrade(request, socket, head) {
const pathname = url.parse(request.url).pathname; const pathname = url.parse(request.url).pathname;
if (pathname === commsPath) { if (pathname === commsPath) {
if (Users.tokenHeader() !== null && request.headers[Users.tokenHeader()]) {
// The user has provided custom token handling. For the websocket,
// the token could be provided in two ways:
// - as an http header (only possible with a reverse proxy setup)
// - passed over the connected websock in an auth packet
// If the header is present, verify the token. If not, use the auth
// packet over the connected socket
//
Strategies.authenticateUserToken(request).then(user => {
wsServer.handleUpgrade(request, socket, head, function done(ws) {
wsServer.emit('connection', ws, request, user);
});
}).catch(err => {
log.audit({event: "comms.auth.fail"});
socket.destroy();
})
return
}
wsServer.handleUpgrade(request, socket, head, function done(ws) { wsServer.handleUpgrade(request, socket, head, function done(ws) {
wsServer.emit('connection', ws, request); wsServer.emit('connection', ws, request, null);
}); });
} }
// Don't destroy the socket as other listeners may want to handle the // Don't destroy the socket as other listeners may want to handle the

View File

@ -155,6 +155,10 @@ describe("api/auth/users", function() {
}); });
describe('#get',function() { describe('#get',function() {
it("returns null for tokenHeader", function() {
should.not.exist(Users.tokenHeader());
});
it('delegates get user',function(done) { it('delegates get user',function(done) {
Users.get('dave').then(function(user) { Users.get('dave').then(function(user) {
try { try {