Authenticate socket.io incoming connections with JWTs. https://www.npmjs.com/package/@thream/socketio-jwt
This repository has been archived on 2024-11-11. You can view files and clone it, but cannot push or open issues or pull requests.
Go to file
2020-12-29 00:53:45 +01:00
.github ci: add lint and build in Node.js CI 2020-12-28 23:51:29 +01:00
oldCode chore: basic structure of files to rewrite in TS 2020-12-28 16:49:31 +01:00
src fix: add better typings 2020-12-29 00:53:45 +01:00
.editorconfig chore: initial commit 2020-12-27 17:25:44 +01:00
.gitignore chore: basic structure of files to rewrite in TS 2020-12-28 16:49:31 +01:00
.npmrc chore: initial commit 2020-12-27 17:25:44 +01:00
.yarnrc chore: initial commit 2020-12-27 17:25:44 +01:00
commitlint.config.js chore: add commitlint, husky and release-it 2020-12-27 18:18:34 +01:00
LICENSE chore: initial commit 2020-12-27 17:25:44 +01:00
package.json test: add authorizer and fixture 2020-12-28 23:51:03 +01:00
README.md docs: delete changelog and blog post in readme 2020-12-27 18:08:49 +01:00
tsconfig.json chore: basic structure of files to rewrite in TS 2020-12-28 16:49:31 +01:00

Thream/socketio-jwt

Authenticate socket.io incoming connections with JWTs.

Node.js CI Dependabot badge TypeScript Standard Style Licence MIT Conventional Commits Contributor Covenant

📜 About

Authenticate socket.io incoming connections with JWTs.

This repository was originally forked from auth0-socketio-jwt & it is not intended to take any credit but to improve the code from now on.

Installation

npm install socketio-jwt

⚙️ Usage

// set authorization for socket.io
io.sockets
  .on(
    'connection',
    socketioJwt.authorize({
      secret: 'your secret or public key',
      timeout: 15000 // 15 seconds to send the authentication message
    })
  )
  .on('authenticated', (socket) => {
    //this socket is authenticated, we are good to handle more events from it.
    console.log(`hello! ${socket.decoded_token.name}`)
  })

Note: If you are using a base64-encoded secret (e.g. your Auth0 secret key), you need to convert it to a Buffer: Buffer('your secret key', 'base64')

Client side

const socket = io.connect('http://localhost:9000')
socket.on('connect', () => {
  socket
    .emit('authenticate', { token: jwt }) //send the jwt
    .on('authenticated', () => {
      //do other things
    })
    .on('unauthorized', (msg) => {
      console.log(`unauthorized: ${JSON.stringify(msg.data)}`)
      throw new Error(msg.data.type)
    })
})

One roundtrip

The previous approach uses a second roundtrip to send the jwt. There is a way you can authenticate on the handshake by sending the JWT as a query string, the caveat is that intermediary HTTP servers can log the url.

const io = require('socket.io')(server)
const socketioJwt = require('socketio-jwt')

With socket.io < 1.0:

io.set(
  'authorization',
  socketioJwt.authorize({
    secret: 'your secret or public key',
    handshake: true
  })
)

io.on('connection', (socket) => {
  console.log('hello!', socket.handshake.decoded_token.name)
})

With socket.io >= 1.0:

io.use(
  socketioJwt.authorize({
    secret: 'your secret or public key',
    handshake: true
  })
)

io.on('connection', (socket) => {
  console.log('hello!', socket.decoded_token.name)
})

For more validation options see auth0/jsonwebtoken.

Client side

Append the jwt token using query string:

const socket = io.connect('http://localhost:9000', {
  query: `token=${your_jwt}`
})

Append the jwt token using 'Authorization Header' (Bearer Token):

const socket = io.connect('http://localhost:9000', {
  extraHeaders: { Authorization: `Bearer ${your_jwt}` }
})

Both options can be combined or used optionally.

Authorization Header Requirement

Require Bearer Tokens to be passed in as an Authorization Header

Server side:

io.use(
  socketioJwt.authorize({
    secret: 'your secret or public key',
    handshake: true,
    auth_header_required: true
  })
)

Handling token expiration

Server side

When you sign the token with an expiration time (example: 60 minutes):

const token = jwt.sign(user_profile, jwt_secret, { expiresIn: 60 * 60 })

Your client-side code should handle it as below:

Client side

socket.on('unauthorized', (error) => {
  if (
    error.data.type == 'UnauthorizedError' ||
    error.data.code == 'invalid_token'
  ) {
    // redirect user to login page perhaps?
    console.log('User token has expired')
  }
})

Handling invalid token

Token sent by client is invalid.

Server side:

No further configuration needed.

Client side

Add a callback client-side to execute socket disconnect server-side.

socket.on('unauthorized', (error, callback) => {
  if (
    error.data.type == 'UnauthorizedError' ||
    error.data.code == 'invalid_token'
  ) {
    // redirect user to login page perhaps or execute callback:
    callback()
    console.log('User token has expired')
  }
})

Server side

To disconnect socket server-side without client-side callback:

io.sockets.on(
  'connection',
  socketioJwt.authorize({
    secret: 'secret goes here',
    // No client-side callback, terminate connection server-side
    callback: false
  })
)

Client side

Nothing needs to be changed client-side if callback is false.

Server side

To disconnect socket server-side while giving client-side 15 seconds to execute callback:

io.sockets.on(
  'connection',
  socketioJwt.authorize({
    secret: 'secret goes here',
    // Delay server-side socket disconnect to wait for client-side callback
    callback: 15000
  })
)

Your client-side code should handle it as below:

Client side

socket.on('unauthorized', (error, callback) => {
  if (
    error.data.type == 'UnauthorizedError' ||
    error.data.code == 'invalid_token'
  ) {
    // redirect user to login page perhaps or execute callback:
    callback()
    console.log('User token has expired')
  }
})

Getting the secret dynamically

You can pass a function instead of a string when configuring secret. This function receives the request, the decoded token and a callback. This way, you are allowed to use a different secret based on the request and / or the provided token.

Server side

const SECRETS = {
  user1: 'secret 1',
  user2: 'secret 2'
}

io.use(
  socketioJwt.authorize({
    secret: (request, decodedToken, callback) => {
      // SECRETS[decodedToken.userId] will be used as a secret or
      // public key for connection user.

      callback(null, SECRETS[decodedToken.userId])
    },
    handshake: false
  })
)

Altering the value of the decoded token

You can pass a function to change the value of the decoded token

io.on(
  'connection',
  socketIOJwt.authorize({
    customDecoded: (decoded) => {
      return 'new decoded token'
    },
    secret: 'my_secret_key',
    decodedPropertyName: 'my_decoded_token'
  })
)

io.on('authenticated', (socket) => {
  console.log(socket.my_decoded_token) // new decoded token
})

💡 Contributing

Anyone can help to improve the project, submit a Feature Request, a bug report or even correct a simple spelling mistake.

The steps to contribute can be found in the CONTRIBUTING.md file.

📄 License

MIT