├── .commitlintrc.json ├── .editorconfig ├── .eslintrc.json ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── BUG.md │ ├── DOCUMENTATION.md │ ├── FEATURE_REQUEST.md │ ├── IMPROVEMENT.md │ └── QUESTION.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── build.yml │ ├── lint.yml │ ├── release.yml │ └── test.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .lintstagedrc.json ├── .markdownlint-cli2.jsonc ├── .npmrc ├── .prettierrc.json ├── .releaserc.json ├── .swcrc ├── .vscode ├── extensions.json └── settings.json ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── src ├── UnauthorizedError.ts ├── __test__ │ ├── authorize.test.ts │ └── fixture │ │ └── index.ts ├── authorize.ts └── index.ts └── tsconfig.json /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { "extends": ["@commitlint/config-conventional"] } 2 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information see: https://editorconfig.org/ 2 | 3 | root = true 4 | 5 | [*] 6 | indent_style = space 7 | indent_size = 2 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["conventions", "prettier"], 3 | "plugins": ["prettier", "import", "unicorn"], 4 | "parser": "@typescript-eslint/parser", 5 | "parserOptions": { 6 | "project": "./tsconfig.json" 7 | }, 8 | "env": { 9 | "node": true 10 | }, 11 | "rules": { 12 | "prettier/prettier": "error", 13 | "import/extensions": ["error", "always"], 14 | "unicorn/prefer-node-protocol": "error" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🐛 Bug Report" 3 | about: "Report an unexpected problem or unintended behavior." 4 | title: "[Bug]" 5 | labels: "bug" 6 | --- 7 | 8 | 12 | 13 | ## Steps To Reproduce 14 | 15 | 1. Step 1 16 | 2. Step 2 17 | 18 | ## The current behavior 19 | 20 | ## The expected behavior 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/DOCUMENTATION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "📜 Documentation" 3 | about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)." 4 | title: "[Documentation]" 5 | labels: "documentation" 6 | --- 7 | 8 | 9 | 10 | ## Documentation 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ## Proposal 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "✨ Feature Request" 3 | about: "Suggest a new feature idea." 4 | title: "[Feature]" 5 | labels: "feature request" 6 | --- 7 | 8 | 9 | 10 | ## Description 11 | 12 | 13 | 14 | ## Describe the solution you'd like 15 | 16 | 17 | 18 | ## Describe alternatives you've considered 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/IMPROVEMENT.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🔧 Improvement" 3 | about: "Improve structure/format/performance/refactor/tests of the code." 4 | title: "[Improvement]" 5 | labels: "improvement" 6 | --- 7 | 8 | 9 | 10 | ## Type of Improvement 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ## Proposal 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/QUESTION.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "🙋 Question" 3 | about: "Further information is requested." 4 | title: "[Question]" 5 | labels: "question" 6 | --- 7 | 8 | ### Question 9 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # What changes this PR introduce? 4 | 5 | ## List any relevant issue numbers 6 | 7 | ## Is there anything you'd like reviewers to focus on? 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: "Build" 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | build: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4.0.0" 14 | 15 | - name: "Setup Node.js" 16 | uses: "actions/setup-node@v3.8.1" 17 | with: 18 | node-version: "20.x" 19 | cache: "npm" 20 | 21 | - name: "Install dependencies" 22 | run: "npm clean-install" 23 | 24 | - name: "Build" 25 | run: "npm run build" 26 | 27 | - run: "npm run build:typescript" 28 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: "Lint" 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | lint: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4.0.0" 14 | 15 | - name: "Setup Node.js" 16 | uses: "actions/setup-node@v3.8.1" 17 | with: 18 | node-version: "20.x" 19 | cache: "npm" 20 | 21 | - name: "Install dependencies" 22 | run: "npm clean-install" 23 | 24 | - run: 'npm run lint:commit -- --to "${{ github.sha }}"' 25 | - run: "npm run lint:editorconfig" 26 | - run: "npm run lint:markdown" 27 | - run: "npm run lint:eslint" 28 | - run: "npm run lint:prettier" 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | 7 | jobs: 8 | release: 9 | runs-on: "ubuntu-latest" 10 | permissions: 11 | contents: "write" 12 | issues: "write" 13 | pull-requests: "write" 14 | id-token: "write" 15 | steps: 16 | - uses: "actions/checkout@v4.0.0" 17 | 18 | - name: "Setup Node.js" 19 | uses: "actions/setup-node@v3.8.1" 20 | with: 21 | node-version: "20.x" 22 | cache: "npm" 23 | 24 | - name: "Install dependencies" 25 | run: "npm clean-install" 26 | 27 | - name: "Build Package" 28 | run: "npm run build" 29 | 30 | - run: "npm run build:typescript" 31 | 32 | - name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies" 33 | run: "npm audit signatures" 34 | 35 | - name: "Release" 36 | run: "npm run release" 37 | env: 38 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 39 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: "Test" 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | pull_request: 7 | branches: [master, develop] 8 | 9 | jobs: 10 | test: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v4.0.0" 14 | 15 | - name: "Setup Node.js" 16 | uses: "actions/setup-node@v3.8.1" 17 | with: 18 | node-version: "20.x" 19 | cache: "npm" 20 | 21 | - name: "Install dependencies" 22 | run: "npm clean-install" 23 | 24 | - name: "Build" 25 | run: "npm run build" 26 | 27 | - name: "Test" 28 | run: "npm run test" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .npm 6 | 7 | # production 8 | build 9 | .swc 10 | 11 | # testing 12 | coverage 13 | .nyc_output 14 | 15 | # debug 16 | npm-debug.log* 17 | 18 | # IDEs and editors 19 | /.idea 20 | .project 21 | .classpath 22 | .c9/ 23 | *.launch 24 | .settings/ 25 | *.sublime-workspace 26 | 27 | # IDE - VSCode 28 | .vscode/* 29 | !.vscode/settings.json 30 | !.vscode/tasks.json 31 | !.vscode/launch.json 32 | !.vscode/extensions.json 33 | 34 | # misc 35 | .DS_Store 36 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:commit -- --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | npm run build 6 | npm run build:typescript 7 | -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": ["editorconfig-checker"], 3 | "*.{js,jsx,ts,tsx}": ["prettier --write", "eslint --fix"], 4 | "*.{json,jsonc,yml,yaml}": ["prettier --write"], 5 | "*.{md,mdx}": ["prettier --write", "markdownlint-cli2 --fix"] 6 | } 7 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "config": { 3 | "extends": "markdownlint/style/prettier", 4 | "relative-links": true, 5 | "default": true, 6 | "MD033": false 7 | }, 8 | "globs": ["**/*.{md,mdx}"], 9 | "ignores": ["**/node_modules"], 10 | "customRules": ["markdownlint-rule-relative-links"] 11 | } 12 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | provenance=true 3 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false 3 | } 4 | -------------------------------------------------------------------------------- /.releaserc.json: -------------------------------------------------------------------------------- 1 | { 2 | "branches": ["master"], 3 | "plugins": [ 4 | [ 5 | "@semantic-release/commit-analyzer", 6 | { 7 | "preset": "conventionalcommits" 8 | } 9 | ], 10 | [ 11 | "@semantic-release/release-notes-generator", 12 | { 13 | "preset": "conventionalcommits" 14 | } 15 | ], 16 | "@semantic-release/npm", 17 | "@semantic-release/github" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /.swcrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "jsc": { 4 | "parser": { 5 | "syntax": "typescript", 6 | "dynamicImport": true 7 | }, 8 | "target": "esnext" 9 | }, 10 | "module": { 11 | "type": "es6" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig", 4 | "esbenp.prettier-vscode", 5 | "dbaeumer.vscode-eslint", 6 | "davidanson.vscode-markdownlint" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib", 3 | "typescript.preferences.importModuleSpecifierEnding": "js", 4 | "editor.defaultFormatter": "esbenp.prettier-vscode", 5 | "prettier.configPath": ".prettierrc.json", 6 | "editor.formatOnSave": true, 7 | "editor.codeActionsOnSave": { 8 | "source.fixAll": true 9 | }, 10 | "eslint.options": { "ignorePath": ".gitignore" } 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | - Demonstrating empathy and kindness toward other people 21 | - Being respectful of differing opinions, viewpoints, and experiences 22 | - Giving and gracefully accepting constructive feedback 23 | - Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | - Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | - The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | - Trolling, insulting or derogatory comments, and personal or political attacks 33 | - Public or private harassment 34 | - Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | - Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][mozilla coc]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][faq]. Translations are available 126 | at [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 130 | [mozilla coc]: https://github.com/mozilla/diversity 131 | [faq]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 💡 Contributing 2 | 3 | Thanks a lot for your interest in contributing to **Thream/socketio-jwt**! 🎉 4 | 5 | ## Code of Conduct 6 | 7 | **Thream** has adopted the [Contributor Covenant](https://www.contributor-covenant.org/) as its Code of Conduct, and we expect project participants to adhere to it. Please read [the full text](./CODE_OF_CONDUCT.md) so that you can understand what actions will and will not be tolerated. 8 | 9 | ## Open Development 10 | 11 | All work on **Thream** happens directly on [GitHub](https://github.com/Thream). Both core team members and external contributors send pull requests which go through the same review process. 12 | 13 | ## Types of contributions 14 | 15 | - Reporting a bug. 16 | - Suggest a new feature idea. 17 | - Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...). 18 | - Improve structure/format/performance/refactor/tests of the code. 19 | 20 | ## Pull Requests 21 | 22 | - **Please first discuss** the change you wish to make via [issue](https://github.com/Thream/socketio-jwt/issues) before making a change. It might avoid a waste of your time. 23 | 24 | - Ensure your code respect linting. 25 | 26 | - Make sure your **code passes the tests**. 27 | 28 | If you're adding new features to **Thream/socketio-jwt**, please include tests. 29 | 30 | ## Commits 31 | 32 | The commit message guidelines adheres to [Conventional Commits](https://www.conventionalcommits.org/) and [Semantic Versioning](https://semver.org/) for releases. 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Auth0, Inc. () and Thream contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Thream/socketio-jwt

2 | 3 |

4 | Authenticate socket.io incoming connections with JWTs. 5 |

6 | 7 |

8 | ⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself. 9 |

10 | 11 |

12 | 13 | Licence MIT 14 | Contributor Covenant 15 |
16 | 17 | 18 | 19 |
20 | Conventional Commits 21 | semantic-release 22 | npm version 23 |

24 | 25 | ## 📜 About 26 | 27 | Authenticate socket.io incoming connections with JWTs. 28 | 29 | This repository was originally forked from [auth0-socketio-jwt](https://github.com/auth0-community/auth0-socketio-jwt) and it is not intended to take any credit but to improve the code from now on. 30 | 31 | ## Prerequisites 32 | 33 | - [Node.js](https://nodejs.org/) >= 16.0.0 34 | - [Socket.IO](https://socket.io/) >= 3.0.0 35 | 36 | ## 💾 Install 37 | 38 | **Note:** It is a package that is recommended to use/install on both the client and server sides. 39 | 40 | ```sh 41 | npm install --save @thream/socketio-jwt 42 | ``` 43 | 44 | ## ⚙️ Usage 45 | 46 | ### Server side 47 | 48 | ```ts 49 | import { Server } from "socket.io" 50 | import { authorize } from "@thream/socketio-jwt" 51 | 52 | const io = new Server(9000) 53 | io.use( 54 | authorize({ 55 | secret: "your secret or public key", 56 | }), 57 | ) 58 | 59 | io.on("connection", async (socket) => { 60 | // jwt payload of the connected client 61 | console.log(socket.decodedToken) 62 | const clients = await io.sockets.allSockets() 63 | if (clients != null) { 64 | for (const clientId of clients) { 65 | const client = io.sockets.sockets.get(clientId) 66 | client?.emit("messages", { message: "Success!" }) 67 | // we can access the jwt payload of each connected client 68 | console.log(client?.decodedToken) 69 | } 70 | } 71 | }) 72 | ``` 73 | 74 | ### Server side with `jwks-rsa` (example) 75 | 76 | ```ts 77 | import jwksClient from "jwks-rsa" 78 | import { Server } from "socket.io" 79 | import { authorize } from "@thream/socketio-jwt" 80 | 81 | const client = jwksClient({ 82 | jwksUri: "https://sandrino.auth0.com/.well-known/jwks.json", 83 | }) 84 | 85 | const io = new Server(9000) 86 | io.use( 87 | authorize({ 88 | secret: async (decodedToken) => { 89 | const key = await client.getSigningKeyAsync(decodedToken.header.kid) 90 | return key.getPublicKey() 91 | }, 92 | }), 93 | ) 94 | 95 | io.on("connection", async (socket) => { 96 | // jwt payload of the connected client 97 | console.log(socket.decodedToken) 98 | // You can do the same things of the previous example there... 99 | }) 100 | ``` 101 | 102 | ### Server side with `onAuthentication` (example) 103 | 104 | ```ts 105 | import { Server } from "socket.io" 106 | import { authorize } from "@thream/socketio-jwt" 107 | 108 | const io = new Server(9000) 109 | io.use( 110 | authorize({ 111 | secret: "your secret or public key", 112 | onAuthentication: async (decodedToken) => { 113 | // return the object that you want to add to the user property 114 | // or throw an error if the token is unauthorized 115 | }, 116 | }), 117 | ) 118 | 119 | io.on("connection", async (socket) => { 120 | // jwt payload of the connected client 121 | console.log(socket.decodedToken) 122 | // You can do the same things of the previous example there... 123 | // user object returned in onAuthentication 124 | console.log(socket.user) 125 | }) 126 | ``` 127 | 128 | ### `authorize` options 129 | 130 | - `secret` is a string containing the secret for HMAC algorithms, or a function that should fetch the secret or public key as shown in the example with `jwks-rsa`. 131 | - `algorithms` (default: `HS256`) 132 | - `onAuthentication` is a function that will be called with the `decodedToken` as a parameter after the token is authenticated. Return a value to add to the `user` property in the socket object. 133 | 134 | ### Client side 135 | 136 | ```ts 137 | import { io } from "socket.io-client" 138 | import { isUnauthorizedError } from "@thream/socketio-jwt/build/UnauthorizedError.js" 139 | 140 | // Require Bearer Token 141 | const socket = io("http://localhost:9000", { 142 | auth: { token: `Bearer ${yourJWT}` }, 143 | }) 144 | 145 | // Handling token expiration 146 | socket.on("connect_error", (error) => { 147 | if (isUnauthorizedError(error)) { 148 | console.log("User token has expired") 149 | } 150 | }) 151 | 152 | // Listening to events 153 | socket.on("messages", (data) => { 154 | console.log(data) 155 | }) 156 | ``` 157 | 158 | ## 💡 Contributing 159 | 160 | Anyone can help to improve the project, submit a Feature Request, a bug report or even correct a simple spelling mistake. 161 | 162 | The steps to contribute can be found in the [CONTRIBUTING.md](./CONTRIBUTING.md) file. 163 | 164 | ## 📄 License 165 | 166 | [MIT](./LICENSE) 167 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@thream/socketio-jwt", 3 | "version": "0.0.0-development", 4 | "type": "module", 5 | "public": true, 6 | "description": "Authenticate socket.io incoming connections with JWTs.", 7 | "license": "MIT", 8 | "main": "build/index.js", 9 | "types": "build/index.d.ts", 10 | "files": [ 11 | "build", 12 | "!**/*.test.js", 13 | "!**/*.test.d.ts", 14 | "!**/*.map" 15 | ], 16 | "engines": { 17 | "node": ">=16.0.0", 18 | "npm": ">=9.0.0" 19 | }, 20 | "publishConfig": { 21 | "access": "public", 22 | "provenance": true 23 | }, 24 | "keywords": [ 25 | "socket", 26 | "socket.io", 27 | "jwt" 28 | ], 29 | "author": "Théo LUDWIG ", 30 | "repository": { 31 | "type": "git", 32 | "url": "https://github.com/Thream/socketio-jwt" 33 | }, 34 | "bugs": { 35 | "url": "https://github.com/Thream/socketio-jwt/issues" 36 | }, 37 | "homepage": "https://github.com/Thream/socketio-jwt#readme", 38 | "scripts": { 39 | "build": "rimraf ./build && swc ./src --out-dir ./build", 40 | "build:dev": "swc ./src --out-dir ./build --watch", 41 | "build:typescript": "tsc", 42 | "lint:commit": "commitlint", 43 | "lint:editorconfig": "editorconfig-checker", 44 | "lint:markdown": "markdownlint-cli2", 45 | "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore", 46 | "lint:prettier": "prettier . --check", 47 | "lint:staged": "lint-staged", 48 | "test": "cross-env NODE_ENV=test node --enable-source-maps --test build/", 49 | "release": "semantic-release", 50 | "postinstall": "husky install", 51 | "prepublishOnly": "pinst --disable", 52 | "postpublish": "pinst --enable" 53 | }, 54 | "peerDependencies": { 55 | "socket.io": ">=3.0.0" 56 | }, 57 | "dependencies": { 58 | "jsonwebtoken": "9.0.2" 59 | }, 60 | "devDependencies": { 61 | "@commitlint/cli": "18.0.0", 62 | "@commitlint/config-conventional": "18.0.0", 63 | "@swc/cli": "0.1.62", 64 | "@swc/core": "1.3.94", 65 | "@tsconfig/strictest": "2.0.2", 66 | "@types/jsonwebtoken": "9.0.4", 67 | "@types/node": "20.8.7", 68 | "@typescript-eslint/eslint-plugin": "6.9.0", 69 | "@typescript-eslint/parser": "6.9.0", 70 | "axios": "1.5.1", 71 | "cross-env": "7.0.3", 72 | "editorconfig-checker": "5.1.1", 73 | "eslint": "8.52.0", 74 | "eslint-config-conventions": "12.0.0", 75 | "eslint-config-prettier": "9.0.0", 76 | "eslint-plugin-import": "2.29.0", 77 | "eslint-plugin-prettier": "5.0.1", 78 | "eslint-plugin-promise": "6.1.1", 79 | "eslint-plugin-unicorn": "48.0.1", 80 | "fastify": "4.24.3", 81 | "husky": "8.0.3", 82 | "lint-staged": "15.0.2", 83 | "markdownlint-cli2": "0.10.0", 84 | "markdownlint-rule-relative-links": "2.1.0", 85 | "pinst": "3.0.0", 86 | "prettier": "3.0.3", 87 | "rimraf": "5.0.5", 88 | "semantic-release": "22.0.5", 89 | "socket.io": "4.7.2", 90 | "socket.io-client": "4.7.2", 91 | "typescript": "5.2.2" 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/UnauthorizedError.ts: -------------------------------------------------------------------------------- 1 | export class UnauthorizedError extends Error { 2 | public inner: { message: string } 3 | public data: { message: string; code: string; type: "UnauthorizedError" } 4 | 5 | constructor(code: string, error: { message: string }) { 6 | super(error.message) 7 | this.name = "UnauthorizedError" 8 | this.inner = error 9 | this.data = { 10 | message: this.message, 11 | code, 12 | type: "UnauthorizedError", 13 | } 14 | Object.setPrototypeOf(this, UnauthorizedError.prototype) 15 | } 16 | } 17 | 18 | export const isUnauthorizedError = ( 19 | error: unknown, 20 | ): error is UnauthorizedError => { 21 | return ( 22 | typeof error === "object" && 23 | error != null && 24 | "data" in error && 25 | typeof error.data === "object" && 26 | error.data != null && 27 | "type" in error.data && 28 | error.data.type === "UnauthorizedError" 29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/__test__/authorize.test.ts: -------------------------------------------------------------------------------- 1 | import test from "node:test" 2 | import assert from "node:assert/strict" 3 | 4 | import axios from "axios" 5 | import type { Socket } from "socket.io-client" 6 | import { io } from "socket.io-client" 7 | 8 | import { isUnauthorizedError } from "../UnauthorizedError.js" 9 | import type { Profile } from "./fixture/index.js" 10 | import { 11 | API_URL, 12 | fixtureStart, 13 | fixtureStop, 14 | getSocket, 15 | basicProfile, 16 | } from "./fixture/index.js" 17 | 18 | export const api = axios.create({ 19 | baseURL: API_URL, 20 | headers: { 21 | "Content-Type": "application/json", 22 | }, 23 | }) 24 | 25 | const secretCallback = async (): Promise => { 26 | return "somesecret" 27 | } 28 | 29 | await test("authorize", async (t) => { 30 | await t.test("with secret as string in options", async (t) => { 31 | let token = "" 32 | let socket: Socket | null = null 33 | 34 | t.beforeEach(async () => { 35 | await fixtureStart() 36 | const response = await api.post("/login", {}) 37 | token = response.data.token 38 | }) 39 | 40 | t.afterEach(async () => { 41 | socket?.disconnect() 42 | await fixtureStop() 43 | }) 44 | 45 | await t.test("should emit error with no token provided", () => { 46 | socket = io(API_URL) 47 | socket.on("connect_error", async (error) => { 48 | assert.strictEqual(isUnauthorizedError(error), true) 49 | if (isUnauthorizedError(error)) { 50 | assert.strictEqual(error.data.message, "no token provided") 51 | assert.strictEqual(error.data.code, "credentials_required") 52 | assert.ok(true) 53 | } else { 54 | assert.fail("should be unauthorized error") 55 | } 56 | }) 57 | socket.on("connect", async () => { 58 | assert.fail("should not connect") 59 | }) 60 | }) 61 | 62 | await t.test("should emit error with bad token format", () => { 63 | socket = io(API_URL, { 64 | auth: { token: "testing" }, 65 | }) 66 | socket.on("connect_error", async (error) => { 67 | assert.strictEqual(isUnauthorizedError(error), true) 68 | if (isUnauthorizedError(error)) { 69 | assert.strictEqual( 70 | error.data.message, 71 | "Format is Authorization: Bearer [token]", 72 | ) 73 | assert.strictEqual(error.data.code, "credentials_bad_format") 74 | assert.ok(true) 75 | } else { 76 | assert.fail("should be unauthorized error") 77 | } 78 | }) 79 | socket.on("connect", async () => { 80 | assert.fail("should not connect") 81 | }) 82 | }) 83 | 84 | await t.test("should emit error with unauthorized handshake", () => { 85 | socket = io(API_URL, { 86 | auth: { token: "Bearer testing" }, 87 | }) 88 | socket.on("connect_error", async (error) => { 89 | assert.strictEqual(isUnauthorizedError(error), true) 90 | if (isUnauthorizedError(error)) { 91 | assert.strictEqual( 92 | error.data.message, 93 | "Unauthorized: Token is missing or invalid Bearer", 94 | ) 95 | assert.strictEqual(error.data.code, "invalid_token") 96 | assert.ok(true) 97 | } else { 98 | assert.fail("should be unauthorized error") 99 | } 100 | }) 101 | socket.on("connect", async () => { 102 | assert.fail("should not connect") 103 | }) 104 | }) 105 | 106 | await t.test("should connect the user", () => { 107 | socket = io(API_URL, { 108 | auth: { token: `Bearer ${token}` }, 109 | }) 110 | socket.on("connect", async () => { 111 | assert.ok(true) 112 | }) 113 | socket.on("connect_error", async (error) => { 114 | assert.fail(error.message) 115 | }) 116 | }) 117 | }) 118 | 119 | await t.test("with secret as callback in options", async (t) => { 120 | let token = "" 121 | let socket: Socket | null = null 122 | 123 | t.beforeEach(async () => { 124 | await fixtureStart({ secret: secretCallback }) 125 | const response = await api.post("/login", {}) 126 | token = response.data.token 127 | }) 128 | 129 | t.afterEach(async () => { 130 | socket?.disconnect() 131 | await fixtureStop() 132 | }) 133 | 134 | await t.test("should emit error with no token provided", () => { 135 | socket = io(API_URL) 136 | socket.on("connect_error", async (error) => { 137 | assert.strictEqual(isUnauthorizedError(error), true) 138 | if (isUnauthorizedError(error)) { 139 | assert.strictEqual(error.data.message, "no token provided") 140 | assert.strictEqual(error.data.code, "credentials_required") 141 | assert.ok(true) 142 | } else { 143 | assert.fail("should be unauthorized error") 144 | } 145 | }) 146 | socket.on("connect", async () => { 147 | assert.fail("should not connect") 148 | }) 149 | }) 150 | 151 | await t.test("should emit error with bad token format", () => { 152 | socket = io(API_URL, { 153 | auth: { token: "testing" }, 154 | }) 155 | socket.on("connect_error", async (error) => { 156 | assert.strictEqual(isUnauthorizedError(error), true) 157 | if (isUnauthorizedError(error)) { 158 | assert.strictEqual( 159 | error.data.message, 160 | "Format is Authorization: Bearer [token]", 161 | ) 162 | assert.strictEqual(error.data.code, "credentials_bad_format") 163 | assert.ok(true) 164 | } else { 165 | assert.fail("should be unauthorized error") 166 | } 167 | }) 168 | socket.on("connect", async () => { 169 | assert.fail("should not connect") 170 | }) 171 | }) 172 | 173 | await t.test("should emit error with unauthorized handshake", () => { 174 | socket = io(API_URL, { 175 | auth: { token: "Bearer testing" }, 176 | }) 177 | socket.on("connect_error", async (error) => { 178 | assert.strictEqual(isUnauthorizedError(error), true) 179 | if (isUnauthorizedError(error)) { 180 | assert.strictEqual( 181 | error.data.message, 182 | "Unauthorized: Token is missing or invalid Bearer", 183 | ) 184 | assert.strictEqual(error.data.code, "invalid_token") 185 | assert.ok(true) 186 | } else { 187 | assert.fail("should be unauthorized error") 188 | } 189 | }) 190 | socket.on("connect", async () => { 191 | assert.fail("should not connect") 192 | }) 193 | }) 194 | 195 | await t.test("should connect the user", () => { 196 | socket = io(API_URL, { 197 | auth: { token: `Bearer ${token}` }, 198 | }) 199 | socket.on("connect", async () => { 200 | assert.ok(true) 201 | }) 202 | socket.on("connect_error", async (error) => { 203 | assert.fail(error.message) 204 | }) 205 | }) 206 | }) 207 | 208 | await t.test("with onAuthentication callback in options", async (t) => { 209 | let token = "" 210 | let wrongToken = "" 211 | let socket: Socket | null = null 212 | 213 | t.beforeEach(async () => { 214 | await fixtureStart({ 215 | secret: secretCallback, 216 | onAuthentication: (decodedToken: Profile) => { 217 | if (!decodedToken.checkField) { 218 | throw new Error("Check Field validation failed") 219 | } 220 | return { 221 | email: decodedToken.email, 222 | } 223 | }, 224 | }) 225 | const response = await api.post("/login", {}) 226 | token = response.data.token 227 | const responseWrong = await api.post("/login-wrong", {}) 228 | wrongToken = responseWrong.data.token 229 | }) 230 | 231 | t.afterEach(async () => { 232 | socket?.disconnect() 233 | await fixtureStop() 234 | }) 235 | 236 | await t.test("should emit error with no token provided", () => { 237 | socket = io(API_URL) 238 | socket.on("connect_error", async (error) => { 239 | assert.strictEqual(isUnauthorizedError(error), true) 240 | if (isUnauthorizedError(error)) { 241 | assert.strictEqual(error.data.message, "no token provided") 242 | assert.strictEqual(error.data.code, "credentials_required") 243 | assert.ok(true) 244 | } else { 245 | assert.fail("should be unauthorized error") 246 | } 247 | }) 248 | socket.on("connect", async () => { 249 | assert.fail("should not connect") 250 | }) 251 | }) 252 | 253 | await t.test("should emit error with bad token format", () => { 254 | socket = io(API_URL, { 255 | auth: { token: "testing" }, 256 | }) 257 | socket.on("connect_error", async (error) => { 258 | assert.strictEqual(isUnauthorizedError(error), true) 259 | if (isUnauthorizedError(error)) { 260 | assert.strictEqual( 261 | error.data.message, 262 | "Format is Authorization: Bearer [token]", 263 | ) 264 | assert.strictEqual(error.data.code, "credentials_bad_format") 265 | assert.ok(true) 266 | } else { 267 | assert.fail("should be unauthorized error") 268 | } 269 | }) 270 | socket.on("connect", async () => { 271 | assert.fail("should not connect") 272 | }) 273 | }) 274 | 275 | await t.test("should emit error with unauthorized handshake", () => { 276 | socket = io(API_URL, { 277 | auth: { token: "Bearer testing" }, 278 | }) 279 | socket.on("connect_error", async (error) => { 280 | assert.strictEqual(isUnauthorizedError(error), true) 281 | if (isUnauthorizedError(error)) { 282 | assert.strictEqual( 283 | error.data.message, 284 | "Unauthorized: Token is missing or invalid Bearer", 285 | ) 286 | assert.strictEqual(error.data.code, "invalid_token") 287 | assert.ok(true) 288 | } else { 289 | assert.fail("should be unauthorized error") 290 | } 291 | }) 292 | socket.on("connect", async () => { 293 | assert.fail("should not connect") 294 | }) 295 | }) 296 | 297 | await t.test("should connect the user", () => { 298 | socket = io(API_URL, { 299 | auth: { token: `Bearer ${token}` }, 300 | }) 301 | socket.on("connect", async () => { 302 | assert.ok(true) 303 | }) 304 | socket.on("connect_error", async (error) => { 305 | assert.fail(error.message) 306 | }) 307 | }) 308 | 309 | await t.test("should contains user properties", () => { 310 | const socketServer = getSocket() 311 | socketServer?.on("connection", (client: any) => { 312 | assert.strictEqual(client.user.email, basicProfile.email) 313 | assert.ok(true) 314 | }) 315 | socket = io(API_URL, { 316 | auth: { token: `Bearer ${token}` }, 317 | }) 318 | socket.on("connect_error", async (error) => { 319 | assert.fail(error.message) 320 | }) 321 | }) 322 | 323 | await t.test("should emit error when user validation fails", () => { 324 | socket = io(API_URL, { 325 | auth: { token: `Bearer ${wrongToken}` }, 326 | }) 327 | socket.on("connect_error", async (error) => { 328 | try { 329 | assert.strictEqual(error.message, "Check Field validation failed") 330 | assert.ok(true) 331 | } catch { 332 | assert.fail(error.message) 333 | } 334 | }) 335 | socket.on("connect", async () => { 336 | assert.fail("should not connect") 337 | }) 338 | }) 339 | }) 340 | }) 341 | -------------------------------------------------------------------------------- /src/__test__/fixture/index.ts: -------------------------------------------------------------------------------- 1 | import jwt from "jsonwebtoken" 2 | import { Server as SocketIoServer } from "socket.io" 3 | import type { FastifyInstance } from "fastify" 4 | import fastify from "fastify" 5 | 6 | import type { AuthorizeOptions } from "../../index.js" 7 | import { authorize } from "../../index.js" 8 | 9 | interface FastifyIo { 10 | instance: SocketIoServer 11 | } 12 | 13 | declare module "fastify" { 14 | export interface FastifyInstance { 15 | io: FastifyIo 16 | } 17 | } 18 | 19 | export interface BasicProfile { 20 | email: string 21 | id: number 22 | } 23 | 24 | export interface Profile extends BasicProfile { 25 | checkField: boolean 26 | } 27 | 28 | export const PORT = 9000 29 | export const API_URL = `http://localhost:${PORT}` 30 | export const basicProfile: BasicProfile = { 31 | email: "john@doe.com", 32 | id: 123, 33 | } 34 | 35 | let application: FastifyInstance | null = null 36 | 37 | export const fixtureStart = async ( 38 | options: AuthorizeOptions = { secret: "super secret" }, 39 | ): Promise => { 40 | const profile: Profile = { ...basicProfile, checkField: true } 41 | let keySecret = "" 42 | if (typeof options.secret === "string") { 43 | keySecret = options.secret 44 | } else { 45 | keySecret = await options.secret({ 46 | header: { alg: "HS256" }, 47 | payload: profile, 48 | }) 49 | } 50 | application = fastify() 51 | application.post("/login", async (_request, reply) => { 52 | const token = jwt.sign(profile, keySecret, { 53 | expiresIn: 60 * 60 * 5, 54 | }) 55 | reply.statusCode = 201 56 | return { token } 57 | }) 58 | application.post("/login-wrong", async (_request, reply) => { 59 | profile.checkField = false 60 | const token = jwt.sign(profile, keySecret, { 61 | expiresIn: 60 * 60 * 5, 62 | }) 63 | reply.statusCode = 201 64 | return { token } 65 | }) 66 | const instance = new SocketIoServer(application.server) 67 | instance.use(authorize(options)) 68 | application.decorate("io", { instance }) 69 | application.addHook("onClose", (fastify) => { 70 | fastify.io.instance.close() 71 | }) 72 | await application.listen({ 73 | port: PORT, 74 | }) 75 | } 76 | 77 | export const fixtureStop = async (): Promise => { 78 | await application?.close() 79 | } 80 | 81 | export const getSocket = (): SocketIoServer | undefined => { 82 | return application?.io.instance 83 | } 84 | -------------------------------------------------------------------------------- /src/authorize.ts: -------------------------------------------------------------------------------- 1 | import type { Algorithm } from "jsonwebtoken" 2 | import jwt from "jsonwebtoken" 3 | import type { Socket } from "socket.io" 4 | 5 | import { UnauthorizedError } from "./UnauthorizedError.js" 6 | 7 | declare module "socket.io" { 8 | interface Socket extends ExtendedSocket {} 9 | } 10 | 11 | interface ExtendedSocket { 12 | encodedToken?: string 13 | decodedToken?: any 14 | user?: any 15 | } 16 | 17 | type SocketIOMiddleware = ( 18 | socket: Socket, 19 | next: (error?: UnauthorizedError) => void, 20 | ) => void 21 | 22 | interface CompleteDecodedToken { 23 | header: { 24 | alg: Algorithm 25 | [key: string]: any 26 | } 27 | payload: any 28 | } 29 | 30 | type SecretCallback = ( 31 | decodedToken: CompleteDecodedToken, 32 | ) => Promise | string 33 | 34 | export interface AuthorizeOptions { 35 | secret: string | SecretCallback 36 | algorithms?: Algorithm[] 37 | onAuthentication?: (decodedToken: any) => Promise | any 38 | } 39 | 40 | export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => { 41 | const { secret, algorithms = ["HS256"], onAuthentication } = options 42 | return async (socket, next) => { 43 | let encodedToken: string | null = null 44 | const { token } = socket.handshake.auth 45 | if (token != null) { 46 | const tokenSplitted = token.split(" ") 47 | if (tokenSplitted.length !== 2 || tokenSplitted[0] !== "Bearer") { 48 | return next( 49 | new UnauthorizedError("credentials_bad_format", { 50 | message: "Format is Authorization: Bearer [token]", 51 | }), 52 | ) 53 | } 54 | encodedToken = tokenSplitted[1] 55 | } 56 | if (encodedToken == null) { 57 | return next( 58 | new UnauthorizedError("credentials_required", { 59 | message: "no token provided", 60 | }), 61 | ) 62 | } 63 | socket.encodedToken = encodedToken 64 | let keySecret: string | null = null 65 | let decodedToken: any = null 66 | if (typeof secret === "string") { 67 | keySecret = secret 68 | } else { 69 | const completeDecodedToken = jwt.decode(encodedToken, { complete: true }) 70 | keySecret = await secret(completeDecodedToken as CompleteDecodedToken) 71 | } 72 | try { 73 | decodedToken = jwt.verify(encodedToken, keySecret, { algorithms }) 74 | } catch { 75 | return next( 76 | new UnauthorizedError("invalid_token", { 77 | message: "Unauthorized: Token is missing or invalid Bearer", 78 | }), 79 | ) 80 | } 81 | socket.decodedToken = decodedToken 82 | if (onAuthentication != null) { 83 | try { 84 | socket.user = await onAuthentication(decodedToken) 85 | } catch (error: any) { 86 | return next(error) 87 | } 88 | } 89 | return next() 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./authorize.js" 2 | export * from "./UnauthorizedError.js" 3 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/strictest/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "lib": ["ESNext"], 6 | "module": "NodeNext", 7 | "moduleResolution": "NodeNext", 8 | "outDir": "./build", 9 | "rootDir": "./src", 10 | "emitDeclarationOnly": true, 11 | "declaration": true 12 | } 13 | } 14 | --------------------------------------------------------------------------------