From e328a83a18dbc225beaec90ab4b96221cfe1fc54 Mon Sep 17 00:00:00 2001 From: divlo Date: Tue, 29 Dec 2020 01:55:44 +0100 Subject: [PATCH] fix: temporary fix ts errors by using any type --- src/__test__/authorizer.test.ts | 6 +- src/__test__/fixture/index.ts | 2 +- src/index.ts | 333 +++++++++++++++++++------------- 3 files changed, 199 insertions(+), 142 deletions(-) diff --git a/src/__test__/authorizer.test.ts b/src/__test__/authorizer.test.ts index 0b43320..13f213f 100644 --- a/src/__test__/authorizer.test.ts +++ b/src/__test__/authorizer.test.ts @@ -46,7 +46,7 @@ describe('authorizer', () => { test('auth headers are supported', (done) => { const socket = io.connect('http://localhost:9000', { - // @ts-ignore + // @ts-expect-error extraHeaders: { Authorization: `Bearer ${token}` } }) socket.on('connect', () => { @@ -86,7 +86,7 @@ describe('authorizer', () => { it('auth headers are supported', (done) => { const socket = io.connect('http://localhost:9000', { - // @ts-ignore + // @ts-expect-error extraHeaders: { Authorization: `Bearer ${token}` } }) socket.on('connect', () => { @@ -108,7 +108,7 @@ describe('authorizer', () => { }) describe('unsigned token', () => { - let token = + const token = 'eyJhbGciOiJub25lIiwiY3R5IjoiSldUIn0.eyJuYW1lIjoiSm9obiBGb28ifQ.' beforeEach((done) => { diff --git a/src/__test__/fixture/index.ts b/src/__test__/fixture/index.ts index e4d6337..a95dca7 100644 --- a/src/__test__/fixture/index.ts +++ b/src/__test__/fixture/index.ts @@ -14,7 +14,7 @@ interface Socket { const socket: Socket = { io: null, - init(httpServer) { + init (httpServer) { socket.io = socketIo(httpServer) } } diff --git a/src/index.ts b/src/index.ts index ea34484..3aed97b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,11 @@ import jwt from 'jsonwebtoken' +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 @@ -17,134 +18,70 @@ class UnauthorizedError extends Error { } } -function noQsMethod (options: any) { - const defaults = { required: true } - options = Object.assign(defaults, options) +/** + * 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 - return (socket: any) => { - const server = socket.server +/** + * 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 +) => void - let auth_timeout: any = null - if (options.required) { - auth_timeout = setTimeout(() => { - socket.disconnect('unauthorized') - }, options.timeout || 5000) - } - - socket.on('authenticate', (data: any) => { - if (options.required) { - clearTimeout(auth_timeout) - } - - const onError = (err: any, code: any) => { - 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: any - // 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 null - } - } - - 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: any, decoded: any) => { - 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: any, secret: any) => { - if (err || !secret) { - return onError(err, 'invalid_secret') - } - - jwt.verify(token, secret, options, onJwtVerificationReady) - } - - getSecret(socket.request, options.secret, token, onSecretReady) - }) - } +/** + * 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 } -export function authorize (options: any) { - options = Object.assign( +/** + * 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' }, - options + authOptions ) if ( @@ -161,14 +98,14 @@ export function authorize (options: any) { } const defaults = { - success: (socket: any, accept: Function) => { + success: (socket: Socket, accept: Function) => { if (socket.request) { accept() } else { accept(null, true) } }, - fail: (error: Error, socket: any, accept: Function) => { + fail: (error: Error, socket: Socket, accept: Function) => { if (socket.request) { accept(error) } else { @@ -180,7 +117,6 @@ export function authorize (options: any) { const auth = Object.assign(defaults, options) return (socket: any, accept: Function) => { - 'use strict' // Node 4.x workaround let token: any, error: any const handshake = socket.handshake @@ -189,7 +125,7 @@ export function authorize (options: any) { if (authorization_header) { const parts = authorization_header.split(' ') - if (parts.length == 2) { + if (parts.length === 2) { const scheme = parts[0] const credentials = parts[1] @@ -205,7 +141,7 @@ export function authorize (options: any) { } // Check if the header has to include authentication - if (options.auth_header_required && !token) { + if (options.auth_header_required && token == null) { return auth.fail( new UnauthorizedError('missing_authorization_header', { message: 'Server requires Authorization Header' @@ -224,7 +160,7 @@ export function authorize (options: any) { token = req.query.token } - if (!token) { + if (token == null) { error = new UnauthorizedError('credentials_required', { message: 'no token provided' }) @@ -232,17 +168,20 @@ export function authorize (options: any) { } // Store encoded JWT - socket[options.encodedPropertyName] = token + socket = Object.assign(socket, { [options.encodedPropertyName]: token }) const onJwtVerificationReady = (err: any, decoded: any) => { - if (err) { + if (err != null) { error = new UnauthorizedError(err.code || 'invalid_token', err) return auth.fail(error, socket, accept) } - socket[options.decodedPropertyName] = options.customDecoded - ? options.customDecoded(decoded) - : decoded + socket = Object.assign(socket, { + [options.decodedPropertyName]: + options.customDecoded != null + ? options.customDecoded(decoded) + : decoded + }) return auth.success(socket, accept) } @@ -253,14 +192,19 @@ export function authorize (options: any) { return auth.fail(error, socket, accept) } - jwt.verify(token, secret, options, onJwtVerificationReady) + jwt.verify(token, secret, options as any, onJwtVerificationReady) } getSecret(req, options.secret, token, onSecretReady) } } -function getSecret (request: any, secret: any, token: any, callback: Function) { +function getSecret( + request: any, + secret: any, + token: string, + callback: Function +) { if (typeof secret === 'function') { if (!token) { return callback({ @@ -282,14 +226,16 @@ function getSecret (request: any, secret: any, token: any, callback: Function) { }) } - const decodedToken: any = jwt.decode(token, { complete: true }) + const decodedToken = jwt.decode(token, { complete: true }) as { + [key: string]: any + } - if (!decodedToken) { + if (decodedToken == null) { return callback({ code: 'invalid_token', message: 'jwt malformed' }) } const arity = secret.length - if (arity == 4) { + if (arity === 4) { secret(request, decodedToken.header, decodedToken.payload, callback) } else { // arity == 3 @@ -299,3 +245,114 @@ function getSecret (request: any, secret: any, token: any, callback: Function) { 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) + }) + } +}