From 0e534dd8ee26c4609fff2c2b466cc8e9995ece61 Mon Sep 17 00:00:00 2001 From: divlo Date: Tue, 29 Dec 2020 03:32:28 +0100 Subject: [PATCH] feat: add support for socket.io >= 3.0.0 --- oldCode/lib/UnauthorizedError.js | 15 - oldCode/lib/index.js | 301 -------------- oldCode/test/authorizer.test.js | 148 ------- oldCode/test/authorizer_namespaces.test.js | 108 ------ oldCode/test/authorizer_noqs.test.js | 59 --- .../authorizer_secret_function_noqs.test.js | 69 ---- .../authorizer_secret_function_qs.test.js | 78 ---- oldCode/test/fixture/index.js | 78 ---- oldCode/test/fixture/namespace.js | 77 ---- oldCode/test/fixture/secret_function.js | 87 ----- oldCode/test/mocha.opts | 3 - oldCode/types/index.d.ts | 86 ---- package.json | 6 +- src/__test__/authorizer.test.ts | 139 ------- src/__test__/fixture/index.ts | 57 --- src/index.ts | 367 ++---------------- 16 files changed, 43 insertions(+), 1635 deletions(-) delete mode 100644 oldCode/lib/UnauthorizedError.js delete mode 100644 oldCode/lib/index.js delete mode 100644 oldCode/test/authorizer.test.js delete mode 100644 oldCode/test/authorizer_namespaces.test.js delete mode 100644 oldCode/test/authorizer_noqs.test.js delete mode 100644 oldCode/test/authorizer_secret_function_noqs.test.js delete mode 100644 oldCode/test/authorizer_secret_function_qs.test.js delete mode 100644 oldCode/test/fixture/index.js delete mode 100644 oldCode/test/fixture/namespace.js delete mode 100644 oldCode/test/fixture/secret_function.js delete mode 100644 oldCode/test/mocha.opts delete mode 100644 oldCode/types/index.d.ts delete mode 100644 src/__test__/authorizer.test.ts delete mode 100644 src/__test__/fixture/index.ts diff --git a/oldCode/lib/UnauthorizedError.js b/oldCode/lib/UnauthorizedError.js deleted file mode 100644 index 021e75e..0000000 --- a/oldCode/lib/UnauthorizedError.js +++ /dev/null @@ -1,15 +0,0 @@ -function UnauthorizedError (code, error) { - Error.call(this, error.message) - this.message = error.message - this.inner = error - this.data = { - message: this.message, - code: code, - type: 'UnauthorizedError' - } -} - -UnauthorizedError.prototype = Object.create(Error.prototype) -UnauthorizedError.prototype.constructor = UnauthorizedError - -module.exports = UnauthorizedError diff --git a/oldCode/lib/index.js b/oldCode/lib/index.js deleted file mode 100644 index 921166b..0000000 --- a/oldCode/lib/index.js +++ /dev/null @@ -1,301 +0,0 @@ -const xtend = require('xtend') -const jwt = require('jsonwebtoken') -const UnauthorizedError = require('./UnauthorizedError') - -function noQsMethod (options) { - const defaults = { required: true } - options = xtend(defaults, options) - - return (socket) => { - 'use strict' // Node 4.x workaround - const server = this.server || socket.server - - if (!server.$emit) { - //then is socket.io 1.0 - const Namespace = Object.getPrototypeOf(server.sockets).constructor - if (!~Namespace.events.indexOf('authenticated')) { - Namespace.events.push('authenticated') - } - } - - let auth_timeout = null - if (options.required) { - auth_timeout = setTimeout(() => { - socket.disconnect('unauthorized') - }, options.timeout || 5000) - } - - socket.on('authenticate', (data) => { - if (options.required) { - clearTimeout(auth_timeout) - } - - // error handler - const onError = (err, code) => { - if (err) { - code = code || 'unknown' - const error = new UnauthorizedError(code, { - message: - Object.prototype.toString.call(err) === '[object Object]' && - err.message - ? err.message - : err - }) - - let callback_timeout - // If callback explicitly set to false, start timeout to disconnect socket - if ( - options.callback === false || - typeof options.callback === 'number' - ) { - if (typeof options.callback === 'number') { - if (options.callback < 0) { - // If callback is negative(invalid value), make it positive - options.callback = Math.abs(options.callback) - } - } - - callback_timeout = setTimeout( - () => { - socket.disconnect('unauthorized') - }, - options.callback === false ? 0 : options.callback - ) - } - - socket.emit('unauthorized', error, () => { - if (typeof options.callback === 'number') { - clearTimeout(callback_timeout) - } - socket.disconnect('unauthorized') - }) - return // stop logic, socket will be close on next tick - } - } - - const token = options.cookie - ? socket.request.cookies[options.cookie] - : data - ? data.token - : undefined - - if (!token || typeof token !== 'string') { - return onError({ message: 'invalid token datatype' }, 'invalid_token') - } - - // Store encoded JWT - socket[options.encodedPropertyName] = token - - const onJwtVerificationReady = (err, decoded) => { - if (err) { - return onError(err, 'invalid_token') - } - - // success handler - const onSuccess = () => { - socket[options.decodedPropertyName] = options.customDecoded - ? options.customDecoded(decoded) - : decoded - socket.emit('authenticated') - if (server.$emit) { - server.$emit('authenticated', socket) - } else { - //try getting the current namespace otherwise fallback to all sockets. - const namespace = - (server.nsps && socket.nsp && server.nsps[socket.nsp.name]) || - server.sockets - - // explicit namespace - namespace.emit('authenticated', socket) - } - } - - if ( - options.additional_auth && - typeof options.additional_auth === 'function' - ) { - options.additional_auth(decoded, onSuccess, onError) - } else { - onSuccess() - } - } - - const onSecretReady = (err, secret) => { - if (err || !secret) { - return onError(err, 'invalid_secret') - } - - jwt.verify(token, secret, options, onJwtVerificationReady) - } - - getSecret(socket.request, options.secret, token, onSecretReady) - }) - } -} - -function authorize (options) { - options = xtend( - { - decodedPropertyName: 'decoded_token', - encodedPropertyName: 'encoded_token' - }, - options - ) - - if ( - typeof options.secret !== 'string' && - typeof options.secret !== 'function' - ) { - throw new Error( - `Provided secret ${options.secret} is invalid, must be of type string or function.` - ) - } - - if (!options.handshake) { - return noQsMethod(options) - } - - const defaults = { - success: (socket, accept) => { - if (socket.request) { - accept() - } else { - accept(null, true) - } - }, - fail: (error, socket, accept) => { - if (socket.request) { - accept(error) - } else { - accept(null, false) - } - } - } - - const auth = xtend(defaults, options) - - return (socket, accept) => { - 'use strict' // Node 4.x workaround - let token, error - - const handshake = socket.handshake - const req = socket.request || socket - const authorization_header = (req.headers || {}).authorization - - if (authorization_header) { - const parts = authorization_header.split(' ') - if (parts.length == 2) { - const scheme = parts[0], - credentials = parts[1] - - if (scheme.toLowerCase() === 'bearer') { - token = credentials - } - } else { - error = new UnauthorizedError('credentials_bad_format', { - message: 'Format is Authorization: Bearer [token]' - }) - return auth.fail(error, socket, accept) - } - } - - // Check if the header has to include authentication - if (options.auth_header_required && !token) { - return auth.fail( - new UnauthorizedError('missing_authorization_header', { - message: 'Server requires Authorization Header' - }), - socket, - accept - ) - } - - // Get the token from handshake or query string - if (handshake && handshake.query.token) { - token = handshake.query.token - } else if (req._query && req._query.token) { - token = req._query.token - } else if (req.query && req.query.token) { - token = req.query.token - } - - if (!token) { - error = new UnauthorizedError('credentials_required', { - message: 'no token provided' - }) - return auth.fail(error, socket, accept) - } - - // Store encoded JWT - socket[options.encodedPropertyName] = token - - const onJwtVerificationReady = (err, decoded) => { - if (err) { - error = new UnauthorizedError(err.code || 'invalid_token', err) - return auth.fail(error, socket, accept) - } - - socket[options.decodedPropertyName] = options.customDecoded - ? options.customDecoded(decoded) - : decoded - - return auth.success(socket, accept) - } - - const onSecretReady = (err, secret) => { - if (err) { - error = new UnauthorizedError(err.code || 'invalid_secret', err) - return auth.fail(error, socket, accept) - } - - jwt.verify(token, secret, options, onJwtVerificationReady) - } - - getSecret(req, options.secret, token, onSecretReady) - } -} - -function getSecret (request, secret, token, callback) { - 'use strict' // Node 4.x workaround - - if (typeof secret === 'function') { - if (!token) { - return callback({ - code: 'invalid_token', - message: 'jwt must be provided' - }) - } - - const parts = token.split('.') - - if (parts.length < 3) { - return callback({ code: 'invalid_token', message: 'jwt malformed' }) - } - - if (parts[2].trim() === '') { - return callback({ - code: 'invalid_token', - message: 'jwt signature is required' - }) - } - - let decodedToken = jwt.decode(token, { complete: true }) - - if (!decodedToken) { - return callback({ code: 'invalid_token', message: 'jwt malformed' }) - } - - const arity = secret.length - if (arity == 4) { - secret(request, decodedToken.header, decodedToken.payload, callback) - } else { - // arity == 3 - secret(request, decodedToken.payload, callback) - } - } else { - callback(null, secret) - } -} - -exports.authorize = authorize -exports.UnauthorizedError = UnauthorizedError diff --git a/oldCode/test/authorizer.test.js b/oldCode/test/authorizer.test.js deleted file mode 100644 index 96d9980..0000000 --- a/oldCode/test/authorizer.test.js +++ /dev/null @@ -1,148 +0,0 @@ -const Q = require('q') -const fixture = require('./fixture') -const request = require('request') -const io = require('socket.io-client') - -describe('authorizer', () => { - //start and stop the server - before((done) => { - fixture.start({}, done) - }) - after(fixture.stop) - - describe('when the user is not logged in', () => { - it('should emit error with unauthorized handshake', (done) => { - const socket = io.connect('http://localhost:9000?token=boooooo', { - forceNew: true - }) - - socket.on('error', (err) => { - err.message.should.eql('jwt malformed') - err.code.should.eql('invalid_token') - socket.close() - done() - }) - }) - }) - - describe('when the user is logged in', () => { - before((done) => { - request.post( - { - url: 'http://localhost:9000/login', - form: { username: 'jose', password: 'Pa123' }, - json: true - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - describe('authorizer disallows query string token when specified in startup options', () => { - before((done) => { - Q.ninvoke(fixture, 'stop') - .then(() => - Q.ninvoke(fixture, 'start', { auth_header_required: true }) - ) - .done(done) - }) - - after((done) => { - Q.ninvoke(fixture, 'stop') - .then(() => Q.ninvoke(fixture, 'start', {})) - .done(done) - }) - - it('auth headers are supported', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - extraHeaders: { Authorization: 'Bearer ' + this.token } - }) - - socket - .on('connect', () => { - socket.close() - done() - }) - .on('error', done) - }) - - it('auth token in query string is disallowed', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - query: 'token=' + this.token - }) - - socket.on('error', (err) => { - err.message.should.eql('Server requires Authorization Header') - err.code.should.eql('missing_authorization_header') - socket.close() - done() - }) - }) - }) - - describe('authorizer all auth types allowed', () => { - before((done) => { - Q.ninvoke(fixture, 'stop') - .then(() => Q.ninvoke(fixture, 'start', {})) - .done(done) - }) - - it('auth headers are supported', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - extraHeaders: { Authorization: 'Bearer ' + this.token } - }) - - socket - .on('connect', () => { - socket.close() - done() - }) - .on('error', done) - }) - - it('should do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - query: 'token=' + this.token - }) - - socket - .on('connect', () => { - socket.close() - done() - }) - .on('error', done) - }) - }) - }) - - describe('unsigned token', () => { - beforeEach(() => { - this.token = - 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.' - }) - - it('should not do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - query: 'token=' + this.token - }) - - socket - .on('connect', () => { - socket.close() - done(new Error('this shouldnt happen')) - }) - .on('error', (err) => { - socket.close() - err.message.should.eql('jwt signature is required') - done() - }) - }) - }) -}) diff --git a/oldCode/test/authorizer_namespaces.test.js b/oldCode/test/authorizer_namespaces.test.js deleted file mode 100644 index 5ff4f92..0000000 --- a/oldCode/test/authorizer_namespaces.test.js +++ /dev/null @@ -1,108 +0,0 @@ -const fixture = require('./fixture/namespace') -const request = require('request') -const io = require('socket.io-client') - -describe('authorizer with namespaces', () => { - //start and stop the server - before(fixture.start) - after(fixture.stop) - - describe('when the user is not logged in', () => { - it('should be able to connect to the default namespace', (done) => { - io.connect('http://localhost:9000') - .once('hi', () => done()) - .on('error', done) - }) - - it('should not be able to connect to the admin namespace', (done) => { - io.connect('http://localhost:9000/admin') - .once('disconnect', () => done()) - .once('hi admin', () => - done( - new Error( - 'unauthenticated client was able to connect to the admin namespace' - ) - ) - ) - }) - - it('should not be able to connect to the admin_hs namespace', (done) => { - io.connect('http://localhost:9000/admin_hs') - .once('hi admin', () => - done( - new Error( - 'unauthenticated client was able to connect to the admin_hs namespace' - ) - ) - ) - .on('error', (err) => { - if (err === 'Invalid namespace') { - // SocketIO throws this error, if auth failed - return - } else if (err && err.type == 'UnauthorizedError') { - done() - } else { - done(err) - } - }) - }) - }) - - describe('when the user is logged in', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - form: { username: 'jose', password: 'Pa123' }, - json: true - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - it('should do the authentication and connect', (done) => { - io.connect('http://localhost:9000/admin', { forceNew: true }) - .on('hi admin', () => done()) - .emit('authenticate', { token: this.token }) - }) - - it('should do the authentication and connect without "forceNew"', (done) => { - io.connect('http://localhost:9000/admin', { forceNew: false }) - .on('hi admin', () => done()) - .emit('authenticate', { token: this.token }) - }) - }) - - describe('when the user is logged in via handshake', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - form: { username: 'jose', password: 'Pa123' }, - json: true - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - it('should do the handshake and connect', (done) => { - io.connect('http://localhost:9000/admin_hs', { - forceNew: true, - query: 'token=' + this.token - }).once('hi admin', () => done()) - }) - - it('should do the handshake and connect without "forceNew"', (done) => { - io.connect('http://localhost:9000/admin_hs', { - forceNew: false, - query: 'token=' + this.token - }).once('hi admin', () => done()) - }) - }) -}) diff --git a/oldCode/test/authorizer_noqs.test.js b/oldCode/test/authorizer_noqs.test.js deleted file mode 100644 index c2d92a1..0000000 --- a/oldCode/test/authorizer_noqs.test.js +++ /dev/null @@ -1,59 +0,0 @@ -const fixture = require('./fixture') -const request = require('request') -const io = require('socket.io-client') - -describe('authorizer without querystring', () => { - //start and stop the server - before((done) => { - fixture.start({ handshake: false }, done) - }) - - after(fixture.stop) - - describe('when the user is not logged in', () => { - it('should close the connection after a timeout if no auth message is received', (done) => { - io.connect('http://localhost:9000', { forceNew: true }).once( - 'disconnect', - () => done() - ) - }) - - it('should not respond echo', (done) => { - io.connect('http://localhost:9000', { forceNew: true }) - .on('echo-response', () => done(new Error('this should not happen'))) - .emit('echo', { hi: 123 }) - - setTimeout(done, 1200) - }) - }) - - describe('when the user is logged in', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - form: { username: 'jose', password: 'Pa123' }, - json: true - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - it('should do the authentication and connect', (done) => { - const socket = io.connect('http://localhost:9000', { forceNew: true }) - - socket - .on('echo-response', () => { - socket.close() - done() - }) - .on('authenticated', () => { - socket.emit('echo') - }) - .emit('authenticate', { token: this.token }) - }) - }) -}) diff --git a/oldCode/test/authorizer_secret_function_noqs.test.js b/oldCode/test/authorizer_secret_function_noqs.test.js deleted file mode 100644 index e711116..0000000 --- a/oldCode/test/authorizer_secret_function_noqs.test.js +++ /dev/null @@ -1,69 +0,0 @@ -const fixture = require('./fixture/secret_function') -const request = require('request') -const io = require('socket.io-client') - -describe('authorizer with secret function', () => { - //start and stop the server - before((done) => { - fixture.start( - { - handshake: false - }, - done - ) - }) - - after(fixture.stop) - - describe('when the user is not logged in', () => { - describe('and when token is not valid', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - json: { username: 'invalid_signature', password: 'Pa123' } - }, - (err, resp, body) => { - this.invalidToken = body.token - done() - } - ) - }) - - it('should emit unauthorized', (done) => { - io.connect('http://localhost:9000', { forceNew: true }) - .on('unauthorized', () => done()) - .emit('authenticate', { token: this.invalidToken + 'ass' }) - }) - }) - }) - - describe('when the user is logged in', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - json: { username: 'valid_signature', password: 'Pa123' } - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - it('should do the authentication and connect', (done) => { - const socket = io.connect('http://localhost:9000', { forceNew: true }) - - socket - .on('echo-response', () => { - socket.close() - done() - }) - .on('authenticated', () => { - socket.emit('echo') - }) - .emit('authenticate', { token: this.token }) - }) - }) -}) diff --git a/oldCode/test/authorizer_secret_function_qs.test.js b/oldCode/test/authorizer_secret_function_qs.test.js deleted file mode 100644 index 56476b9..0000000 --- a/oldCode/test/authorizer_secret_function_qs.test.js +++ /dev/null @@ -1,78 +0,0 @@ -const fixture = require('./fixture/secret_function') -const request = require('request') -const io = require('socket.io-client') - -describe('authorizer with secret function', () => { - //start and stop the server - before(fixture.start) - after(fixture.stop) - - describe('when the user is not logged in', () => { - it('should emit error with unauthorized handshake', (done) => { - const socket = io.connect('http://localhost:9000?token=boooooo', { - forceNew: true - }) - - socket.on('error', (err) => { - err.message.should.eql('jwt malformed') - err.code.should.eql('invalid_token') - socket.close() - done() - }) - }) - }) - - describe('when the user is logged in', () => { - beforeEach((done) => { - request.post( - { - url: 'http://localhost:9000/login', - json: { username: 'valid_signature', password: 'Pa123' } - }, - (err, resp, body) => { - this.token = body.token - done() - } - ) - }) - - it('should do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - query: 'token=' + this.token - }) - - socket - .on('connect', () => { - socket.close() - done() - }) - .on('error', done) - }) - }) - - describe('unsigned token', () => { - beforeEach(() => { - this.token = - 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.' - }) - - it('should not do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - forceNew: true, - query: 'token=' + this.token - }) - - socket - .on('connect', () => { - socket.close() - done(new Error('this shouldnt happen')) - }) - .on('error', (err) => { - socket.close() - err.message.should.eql('jwt signature is required') - done() - }) - }) - }) -}) diff --git a/oldCode/test/fixture/index.js b/oldCode/test/fixture/index.js deleted file mode 100644 index f569fc8..0000000 --- a/oldCode/test/fixture/index.js +++ /dev/null @@ -1,78 +0,0 @@ -'use strict' // Node 4.x workaround - -const express = require('express') -const http = require('http') - -const socketIo = require('socket.io') -const socketio_jwt = require('../../lib') - -const jwt = require('jsonwebtoken') -const xtend = require('xtend') -const enableDestroy = require('server-destroy') - -let sio - -exports.start = (options, callback) => { - if (typeof options == 'function') { - callback = options - options = {} - } - - options = xtend( - { - secret: 'aaafoo super sercret', - timeout: 1000, - handshake: true - }, - options - ) - - const app = express() - const server = http.createServer(app) - sio = socketIo.listen(server) - - app.use(express.json()) - app.post('/login', (req, res) => { - const profile = { - first_name: 'John', - last_name: 'Doe', - email: 'john@doe.com', - id: 123 - } - - // We are sending the profile inside the token - const token = jwt.sign(profile, options.secret, { expiresIn: 60 * 60 * 5 }) - res.json({ token: token }) - }) - - if (options.handshake) { - sio.use(socketio_jwt.authorize(options)) - - sio.sockets.on('echo', (m) => { - sio.sockets.emit('echo-response', m) - }) - } else { - sio.sockets - .on('connection', socketio_jwt.authorize(options)) - .on('authenticated', (socket) => { - socket.on('echo', (m) => { - socket.emit('echo-response', m) - }) - }) - } - - server.__sockets = [] - server.on('connection', (c) => { - server.__sockets.push(c) - }) - server.listen(9000, callback) - enableDestroy(server) -} - -exports.stop = (callback) => { - sio.close() - try { - server.destroy() - } catch (er) {} - callback() -} diff --git a/oldCode/test/fixture/namespace.js b/oldCode/test/fixture/namespace.js deleted file mode 100644 index 04c7dd0..0000000 --- a/oldCode/test/fixture/namespace.js +++ /dev/null @@ -1,77 +0,0 @@ -'use strict' // Node 4.x workaround - -const express = require('express') -const http = require('http') - -const socketIo = require('socket.io') -const socketio_jwt = require('../../lib') - -const jwt = require('jsonwebtoken') -const xtend = require('xtend') -const enableDestroy = require('server-destroy') - -let sio - -/** - * This is an example server that shows how to do namespace authentication. - * - * The /admin namespace is protected by JWTs while the global namespace is public. - */ -exports.start = (callback) => { - const options = { - secret: 'aaafoo super sercret', - timeout: 1000, - handshake: false - } - - const app = express() - const server = http.createServer(app) - sio = socketIo.listen(server) - - app.use(express.json()) - app.post('/login', (req, res) => { - const profile = { - first_name: 'John', - last_name: 'Doe', - email: 'john@doe.com', - id: 123 - } - - // We are sending the profile inside the token - const token = jwt.sign(profile, options.secret, { expiresIn: 60 * 60 * 5 }) - res.json({ token: token }) - }) - - // Global namespace (public) - sio.on('connection', (socket) => { - socket.emit('hi') - }) - - // Second roundtrip - const admin_nsp = sio.of('/admin') - - admin_nsp - .on('connection', socketio_jwt.authorize(options)) - .on('authenticated', (socket) => { - socket.emit('hi admin') - }) - - // One roundtrip - const admin_nsp_hs = sio.of('/admin_hs') - - admin_nsp_hs.use(socketio_jwt.authorize(xtend(options, { handshake: true }))) - admin_nsp_hs.on('connection', (socket) => { - socket.emit('hi admin') - }) - - server.listen(9000, callback) - enableDestroy(server) -} - -exports.stop = (callback) => { - sio.close() - try { - server.destroy() - } catch (er) {} - callback() -} diff --git a/oldCode/test/fixture/secret_function.js b/oldCode/test/fixture/secret_function.js deleted file mode 100644 index b6c55c8..0000000 --- a/oldCode/test/fixture/secret_function.js +++ /dev/null @@ -1,87 +0,0 @@ -'use strict' // Node 4.x workaround - -const express = require('express') -const http = require('http') - -const socketIo = require('socket.io') -const socketio_jwt = require('../../lib') - -const jwt = require('jsonwebtoken') -const xtend = require('xtend') -const enableDestroy = require('server-destroy') - -let sio - -exports.start = (options, callback) => { - const SECRETS = { - 123: 'aaafoo super sercret', - 555: 'other' - } - - if (typeof options == 'function') { - callback = options - options = {} - } - - options = xtend( - { - secret: (request, decodedToken, callback) => { - callback(null, SECRETS[decodedToken.id]) - }, - timeout: 1000, - handshake: true - }, - options - ) - - const app = express() - const server = http.createServer(app) - sio = socketIo.listen(server) - - app.use(express.json()) - app.post('/login', (req, res) => { - const profile = { - first_name: 'John', - last_name: 'Doe', - email: 'john@doe.com', - id: req.body.username === 'valid_signature' ? 123 : 555 - } - - // We are sending the profile inside the token - const token = jwt.sign(profile, SECRETS[123], { expiresIn: 60 * 60 * 5 }) - res.json({ token: token }) - }) - - if (options.handshake) { - sio.use(socketio_jwt.authorize(options)) - - sio.sockets.on('echo', (m) => { - sio.sockets.emit('echo-response', m) - }) - } else { - sio.sockets - .on('connection', socketio_jwt.authorize(options)) - .on('authenticated', (socket) => { - socket.on('echo', (m) => { - socket.emit('echo-response', m) - }) - }) - } - - server.__sockets = [] - server.on('connection', (c) => { - server.__sockets.push(c) - }) - - server.listen(9000, callback) - enableDestroy(server) -} - -exports.stop = (callback) => { - sio.close() - try { - server.destroy() - } catch (er) {} - - callback() -} diff --git a/oldCode/test/mocha.opts b/oldCode/test/mocha.opts deleted file mode 100644 index 81f4179..0000000 --- a/oldCode/test/mocha.opts +++ /dev/null @@ -1,3 +0,0 @@ ---require should ---reporter spec ---timeout 15000 diff --git a/oldCode/types/index.d.ts b/oldCode/types/index.d.ts deleted file mode 100644 index 57df9d9..0000000 --- a/oldCode/types/index.d.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * This module allows to authenticate socket.io connections with JWTs. - * This is especially if you do not want to use cookies in a single page application. - */ - -/// - -declare module 'socketio-jwt' { - /** - * Defines possible errors for the secret-callback. - */ - interface ISocketIOError { - readonly code: string - readonly message: string - } - - /** - * Callback gets called, if secret is given dynamically. - */ - interface ISocketCallback { - (err: ISocketIOError, success: string): void - } - - interface ISocketIOMiddleware { - (socket: SocketIO.Socket, fn: (err?: any) => void): void - } - - interface IOptions { - additional_auth?: ( - decoded: object, - onSuccess: () => void, - onError: (err: string | ISocketIOError, code: string) => void - ) => void - customDecoded?: (decoded: object) => object - - callback?: false | number - secret: - | string - | (( - request: any, - decodedToken: object, - callback: ISocketCallback - ) => void) - - encodedPropertyName?: string - decodedPropertyName?: string - auth_header_required?: boolean - handshake?: boolean - required?: boolean - timeout?: number - cookie?: string - } - - function authorize( - options: IOptions /*, onConnection: Function*/ - ): ISocketIOMiddleware - - interface UnauthorizedError extends Error { - readonly message: string - readonly inner: object - readonly data: { message: string; code: string; type: 'UnauthorizedError' } - } - - var UnauthorizedError: { - prototype: UnauthorizedError - new (code: string, error: { message: string }): UnauthorizedError - } - - /** - * This is an augmented version of the SocketIO.Server. - * It knows the 'authenticated' event and should be extended in future. - * @see SocketIO.Server - */ - export interface JWTServer extends SocketIO.Server { - /** - * The event gets fired when a new connection is authenticated via JWT. - * @param event The event being fired: 'authenticated' - * @param listener A listener that should take one parameter of type Socket - * @return The default '/' Namespace - */ - on( - event: 'authenticated' | string, - listener: (socket: SocketIO.Socket) => void - ): SocketIO.Namespace - } -} diff --git a/package.json b/package.json index af1f101..c607fd6 100644 --- a/package.json +++ b/package.json @@ -92,8 +92,6 @@ "@types/jsonwebtoken": "8.5.0", "@types/node": "14.14.16", "@types/server-destroy": "1.0.1", - "@types/socket.io": "2.1.12", - "@types/socket.io-client": "1.4.34", "axios": "0.21.1", "express": "4.17.1", "husky": "4.3.6", @@ -102,8 +100,8 @@ "rimraf": "3.0.2", "server-destroy": "1.0.1", "snazzy": "9.0.0", - "socket.io": "2.3.0", - "socket.io-client": "2.3.0", + "socket.io": "3.0.4", + "socket.io-client": "3.0.4", "ts-jest": "26.4.4", "ts-standard": "10.0.0", "typescript": "4.1.3" diff --git a/src/__test__/authorizer.test.ts b/src/__test__/authorizer.test.ts deleted file mode 100644 index 13f213f..0000000 --- a/src/__test__/authorizer.test.ts +++ /dev/null @@ -1,139 +0,0 @@ -import io from 'socket.io-client' -import { fixtureStart, fixtureStop } from './fixture/index' -import axios from 'axios' - -describe('authorizer', () => { - describe('when the user is not logged in', () => { - beforeEach((done) => { - jest.setTimeout(15_000) - fixtureStart(done) - }) - - afterEach((done) => { - fixtureStop(done) - }) - - it('should emit error with unauthorized handshake', (done) => { - const socket = io.connect('http://localhost:9000?token=boooooo') - socket.on('error', (err: any) => { - expect(err.message).toEqual('jwt malformed') - expect(err.code).toEqual('invalid_token') - socket.close() - done() - }) - }) - }) - - describe('when the user is logged in', () => { - describe('authorizer disallows query string token when specified in startup options', () => { - let token: string = '' - - beforeEach((done) => { - jest.setTimeout(15_000) - fixtureStart( - async () => { - const response = await axios.post('http://localhost:9000/login') - token = response.data.token - done() - }, - { auth_header_required: true } - ) - }) - - afterEach((done) => { - fixtureStop(done) - }) - - test('auth headers are supported', (done) => { - const socket = io.connect('http://localhost:9000', { - // @ts-expect-error - extraHeaders: { Authorization: `Bearer ${token}` } - }) - socket.on('connect', () => { - socket.close() - done() - }) - }) - - test('auth token in query string is disallowed', (done) => { - const socket = io.connect('http://localhost:9000', { - query: `token=${token}` - }) - socket.on('error', (err: any) => { - expect(err.message).toEqual('Server requires Authorization Header') - expect(err.code).toEqual('missing_authorization_header') - socket.close() - done() - }) - }) - }) - - describe('authorizer all auth types allowed', () => { - let token: string = '' - - beforeEach((done) => { - jest.setTimeout(15_000) - fixtureStart(async () => { - const response = await axios.post('http://localhost:9000/login') - token = response.data.token - done() - }) - }) - - afterEach((done) => { - fixtureStop(done) - }) - - it('auth headers are supported', (done) => { - const socket = io.connect('http://localhost:9000', { - // @ts-expect-error - extraHeaders: { Authorization: `Bearer ${token}` } - }) - socket.on('connect', () => { - socket.close() - done() - }) - }) - - it('should do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - query: `token=${token}` - }) - socket.on('connect', () => { - socket.close() - done() - }) - }) - }) - }) - - describe('unsigned token', () => { - const token = - 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.' - - beforeEach((done) => { - jest.setTimeout(15_000) - fixtureStart(done) - }) - - afterEach((done) => { - fixtureStop(done) - }) - - it('should not do the handshake and connect', (done) => { - const socket = io.connect('http://localhost:9000', { - query: `token=${token}` - }) - socket - .on('connect', () => { - socket.close() - done(new Error("this shouldn't happen")) - }) - .on('error', (err: any) => { - socket.close() - expect(err.message).toEqual('jwt signature is required') - done() - }) - }) - }) -}) diff --git a/src/__test__/fixture/index.ts b/src/__test__/fixture/index.ts deleted file mode 100644 index a95dca7..0000000 --- a/src/__test__/fixture/index.ts +++ /dev/null @@ -1,57 +0,0 @@ -import express from 'express' -import jwt from 'jsonwebtoken' -import { Server as HttpServer } from 'http' -import { Server as HttpsServer } from 'https' -import socketIo, { Server as SocketIoServer } from 'socket.io' -import enableDestroy from 'server-destroy' - -import { authorize } from '../../index' - -interface Socket { - io: null | SocketIoServer - init: (httpServer: HttpServer | HttpsServer) => void -} - -const socket: Socket = { - io: null, - init (httpServer) { - socket.io = socketIo(httpServer) - } -} - -let server: HttpServer | null = null - -export const fixtureStart = (done: any, optionsAuth: any = {}) => { - const options = Object.assign( - { - secret: 'aaafoo super sercret', - timeout: 1000, - handshake: true - }, - optionsAuth - ) - const app = express() - app.use(express.json()) - app.post('/login', (_req, res) => { - const profile = { - first_name: 'John', - last_name: 'Doe', - email: 'john@doe.com', - id: 123 - } - const token = jwt.sign(profile, options.secret, { expiresIn: 60 * 60 * 5 }) - return res.json({ token }) - }) - server = app.listen(9000, done) - socket.init(server) - socket.io?.use(authorize(options)) - enableDestroy(server) -} - -export const fixtureStop = (callback: Function) => { - socket.io?.close() - try { - server?.destroy() - } catch (err) {} - callback() -} diff --git a/src/index.ts b/src/index.ts index 3aed97b..22eae88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,9 +3,9 @@ import { Socket } from 'socket.io' class UnauthorizedError extends Error { public inner: { message: string } - public data: { message: string; code: string; type: 'UnauthorizedError' } + public data: { message: string, code: string, type: 'UnauthorizedError' } - constructor(code: string, error: { message: string }) { + constructor (code: string, error: { message: string }) { super(error.message) this.message = error.message this.inner = error @@ -18,341 +18,56 @@ class UnauthorizedError extends Error { } } -/** - * If JwtAuthOptions.secret is a function, then this is the signature of the callback function provided to that function - */ -export type JwtSecretFuncCallback = (err: Error | null, secret: string) => void +interface ExtendedError extends Error { + data?: any +} -/** - * This is a function with two args payload, and done. - * - * `request` is the original request - * `payload` is the decoded JWT payload - * `callback` is an error-first callback defined below - */ -export type JwtSecretFunc = ( - request: any, - payload: any, - callback: JwtSecretFuncCallback +type SocketIOMiddleware = ( + socket: Socket, + next: (err?: ExtendedError) => void ) => void -/** - * This is an object literal that contains options. - */ -export interface JwtAuthOptions { - auth_header_required?: boolean - secret: string | JwtSecretFunc - timeout?: number // In milliseconds to handle the second round trip. - callback?: boolean | number // To disconnect socket server-side without a client-side callback if no valid token. - decodedPropertyName?: string // Property to store the decoded token to. - handshake?: boolean // Used to trigger a single round trip authentication. - required?: boolean +interface AuthorizeOptions { + secret: string } -/** - * Defines possible errors for the secret-callback. - */ -interface ISocketIOError { - readonly code: string - readonly message: string -} - -interface AuthOptions extends JwtAuthOptions { - additional_auth?: ( - decoded: object, - onSuccess: () => void, - onError: (err: string | ISocketIOError, code: string) => void - ) => void - customDecoded?: (decoded: object) => object - encodedPropertyName: string - decodedPropertyName: string - cookie?: string -} - -type ISocketIOMiddleware = (socket: Socket, fn: (err?: any) => void) => void - -/** - * @description This function returns a middleware function for use with Socket.IO that authenticates a new connection. - * @param options is an object literal that contains options. - */ -export function authorize(authOptions: JwtAuthOptions): ISocketIOMiddleware { - const options: AuthOptions = Object.assign( - { - decodedPropertyName: 'decoded_token', - encodedPropertyName: 'encoded_token' - }, - authOptions - ) - - if ( - typeof options.secret !== 'string' && - typeof options.secret !== 'function' - ) { - throw new Error( - `Provided secret ${options.secret} is invalid, must be of type string or function.` - ) - } - - if (!options.handshake) { - return noQsMethod(options) - } - - const defaults = { - success: (socket: Socket, accept: Function) => { - if (socket.request) { - accept() - } else { - accept(null, true) - } - }, - fail: (error: Error, socket: Socket, accept: Function) => { - if (socket.request) { - accept(error) - } else { - accept(null, false) +export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => { + const { secret } = options + return (socket, next) => { + let token: string | null = null + const authorizationHeader = socket.request.headers.authorization + if (authorizationHeader != null) { + const tokenSplitted = authorizationHeader.split(' ') + if (tokenSplitted.length !== 2 || tokenSplitted[0] !== 'Bearer') { + return next( + new UnauthorizedError('credentials_bad_format', { + message: 'Format is Authorization: Bearer [token]' + }) + ) } + token = tokenSplitted[1] } - } - - const auth = Object.assign(defaults, options) - - return (socket: any, accept: Function) => { - let token: any, error: any - - const handshake = socket.handshake - const req = socket.request || socket - const authorization_header = (req.headers || {}).authorization - - if (authorization_header) { - const parts = authorization_header.split(' ') - if (parts.length === 2) { - const scheme = parts[0] - const credentials = parts[1] - - if (scheme.toLowerCase() === 'bearer') { - token = credentials - } - } else { - error = new UnauthorizedError('credentials_bad_format', { - message: 'Format is Authorization: Bearer [token]' + if (token == null) { + return next( + new UnauthorizedError('credentials_required', { + message: 'no token provided' }) - return auth.fail(error, socket, accept) - } - } - - // Check if the header has to include authentication - if (options.auth_header_required && token == null) { - return auth.fail( - new UnauthorizedError('missing_authorization_header', { - message: 'Server requires Authorization Header' - }), - socket, - accept ) } - - // Get the token from handshake or query string - if (handshake && handshake.query.token) { - token = handshake.query.token - } else if (req._query && req._query.token) { - token = req._query.token - } else if (req.query && req.query.token) { - token = req.query.token - } - - if (token == null) { - error = new UnauthorizedError('credentials_required', { - message: 'no token provided' - }) - return auth.fail(error, socket, accept) - } - // Store encoded JWT - socket = Object.assign(socket, { [options.encodedPropertyName]: token }) - - const onJwtVerificationReady = (err: any, decoded: any) => { - if (err != null) { - error = new UnauthorizedError(err.code || 'invalid_token', err) - return auth.fail(error, socket, accept) - } - - socket = Object.assign(socket, { - [options.decodedPropertyName]: - options.customDecoded != null - ? options.customDecoded(decoded) - : decoded - }) - - return auth.success(socket, accept) + socket = Object.assign(socket, { encodedToken: token }) + let payload: any + try { + payload = jwt.verify(token, secret) + } catch { + return next( + new UnauthorizedError('invalid_token', { + message: 'Unauthorized: Token is missing or invalid Bearer' + }) + ) } - - const onSecretReady = (err: any, secret: string) => { - if (err) { - error = new UnauthorizedError(err.code || 'invalid_secret', err) - return auth.fail(error, socket, accept) - } - - jwt.verify(token, secret, options as any, onJwtVerificationReady) - } - - getSecret(req, options.secret, token, onSecretReady) - } -} - -function getSecret( - request: any, - secret: any, - token: string, - callback: Function -) { - if (typeof secret === 'function') { - if (!token) { - return callback({ - code: 'invalid_token', - message: 'jwt must be provided' - }) - } - - const parts = token.split('.') - - if (parts.length < 3) { - return callback({ code: 'invalid_token', message: 'jwt malformed' }) - } - - if (parts[2].trim() === '') { - return callback({ - code: 'invalid_token', - message: 'jwt signature is required' - }) - } - - const decodedToken = jwt.decode(token, { complete: true }) as { - [key: string]: any - } - - if (decodedToken == null) { - return callback({ code: 'invalid_token', message: 'jwt malformed' }) - } - - const arity = secret.length - if (arity === 4) { - secret(request, decodedToken.header, decodedToken.payload, callback) - } else { - // arity == 3 - secret(request, decodedToken.payload, callback) - } - } else { - callback(null, secret) - } -} - -function noQsMethod(options: AuthOptions): ISocketIOMiddleware { - const defaults = { required: true } - options = Object.assign(defaults, options) - - return (socket: any) => { - let auth_timeout: NodeJS.Timeout | null = null - if (options.required) { - auth_timeout = setTimeout(() => { - socket.disconnect(true) - }, options.timeout ?? 5000) - } - - socket.on('authenticate', (data: any) => { - if (options.required && auth_timeout != null) { - clearTimeout(auth_timeout) - } - - const onError = (err: any, code: string) => { - if (err) { - code = code ?? 'unknown' - const error = new UnauthorizedError(code, { - message: - Object.prototype.toString.call(err) === '[object Object]' && - err.message - ? err.message - : err - }) - - let callback_timeout: NodeJS.Timeout | null = null - // If callback explicitly set to false, start timeout to disconnect socket - if ( - options.callback === false || - typeof options.callback === 'number' - ) { - if (typeof options.callback === 'number') { - if (options.callback < 0) { - // If callback is negative(invalid value), make it positive - options.callback = Math.abs(options.callback) - } - } - - callback_timeout = setTimeout( - () => { - socket.disconnect(true) - }, - options.callback === false ? 0 : options.callback - ) - } - - socket.emit('unauthorized', error, () => { - if ( - typeof options.callback === 'number' && - callback_timeout != null - ) { - clearTimeout(callback_timeout) - } - socket.disconnect(true) - }) - return null - } - } - - const token = options.cookie - ? socket.request.cookies[options.cookie] - : data - ? data.token - : undefined - - if (token == null || typeof token !== 'string') { - return onError({ message: 'invalid token datatype' }, 'invalid_token') - } - - // Store encoded JWT - socket = Object.assign(socket, { [options.encodedPropertyName]: token }) - - const onJwtVerificationReady = (err: any, decoded: any) => { - if (err) { - return onError(err, 'invalid_token') - } - - // success handler - const onSuccess = () => { - socket = Object.assign(socket, { - [options.decodedPropertyName]: options.customDecoded - }) - socket.emit('authenticated') - } - - if ( - options.additional_auth != null && - typeof options.additional_auth === 'function' - ) { - options.additional_auth(decoded, onSuccess, onError) - } else { - onSuccess() - } - } - - const onSecretReady = (err: any, secret: string) => { - if (err != null || secret == null) { - return onError(err, 'invalid_secret') - } - - jwt.verify(token, secret, options as any, onJwtVerificationReady) - } - - getSecret(socket.request, options.secret, token, onSecretReady) - }) + // Store decoded JWT + socket = Object.assign(socket, { decodedToken: payload }) + return next() } }