Compare commits

..

6 Commits

23 changed files with 3356 additions and 6623 deletions

View File

@ -1,6 +1,7 @@
{ {
"extends": ["conventions", "prettier"], "extends": ["conventions", "prettier"],
"plugins": ["prettier", "import", "unicorn"], "plugins": ["prettier", "import", "unicorn"],
"parser": "@typescript-eslint/parser",
"parserOptions": { "parserOptions": {
"project": "./tsconfig.json" "project": "./tsconfig.json"
}, },

View File

@ -1,8 +1,8 @@
--- ---
name: '🐛 Bug Report' name: "🐛 Bug Report"
about: 'Report an unexpected problem or unintended behavior.' about: "Report an unexpected problem or unintended behavior."
title: '[Bug]' title: "[Bug]"
labels: 'bug' labels: "bug"
--- ---
<!-- <!--

View File

@ -1,8 +1,8 @@
--- ---
name: '📜 Documentation' name: "📜 Documentation"
about: 'Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...).' about: "Correct spelling errors, improvements or additions to documentation files (README, CONTRIBUTING...)."
title: '[Documentation]' title: "[Documentation]"
labels: 'documentation' labels: "documentation"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '✨ Feature Request' name: "✨ Feature Request"
about: 'Suggest a new feature idea.' about: "Suggest a new feature idea."
title: '[Feature]' title: "[Feature]"
labels: 'feature request' labels: "feature request"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🔧 Improvement' name: "🔧 Improvement"
about: 'Improve structure/format/performance/refactor/tests of the code.' about: "Improve structure/format/performance/refactor/tests of the code."
title: '[Improvement]' title: "[Improvement]"
labels: 'improvement' labels: "improvement"
--- ---
<!-- Please make sure your issue has not already been fixed. --> <!-- Please make sure your issue has not already been fixed. -->

View File

@ -1,8 +1,8 @@
--- ---
name: '🙋 Question' name: "🙋 Question"
about: 'Further information is requested.' about: "Further information is requested."
title: '[Question]' title: "[Question]"
labels: 'question' labels: "question"
--- ---
### Question ### Question

View File

@ -1,4 +1,4 @@
name: 'Build' name: "Build"
on: on:
push: push:
@ -8,20 +8,20 @@ on:
jobs: jobs:
build: build:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: 'lts/*' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- run: 'npm run build:typescript' - run: "npm run build:typescript"

View File

@ -1,4 +1,4 @@
name: 'Lint' name: "Lint"
on: on:
push: push:
@ -8,21 +8,21 @@ on:
jobs: jobs:
lint: lint:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: 'lts/*' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- run: 'npm run lint:commit -- --to "${{ github.sha }}"' - run: 'npm run lint:commit -- --to "${{ github.sha }}"'
- run: 'npm run lint:editorconfig' - run: "npm run lint:editorconfig"
- run: 'npm run lint:markdown' - run: "npm run lint:markdown"
- run: 'npm run lint:eslint' - run: "npm run lint:eslint"
- run: 'npm run lint:prettier' - run: "npm run lint:prettier"

View File

@ -1,4 +1,4 @@
name: 'Release' name: "Release"
on: on:
push: push:
@ -6,34 +6,34 @@ on:
jobs: jobs:
release: release:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
permissions: permissions:
contents: 'write' contents: "write"
issues: 'write' issues: "write"
pull-requests: 'write' pull-requests: "write"
id-token: 'write' id-token: "write"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: 'lts/*' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build Package' - name: "Build Package"
run: 'npm run build' run: "npm run build"
- run: 'npm run build:typescript' - run: "npm run build:typescript"
- name: 'Verify the integrity of provenance attestations and registry signatures for installed dependencies' - name: "Verify the integrity of provenance attestations and registry signatures for installed dependencies"
run: 'npm audit signatures' run: "npm audit signatures"
- name: 'Release' - name: "Release"
run: 'npm run release' run: "npm run release"
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

@ -1,4 +1,4 @@
name: 'Test' name: "Test"
on: on:
push: push:
@ -8,21 +8,21 @@ on:
jobs: jobs:
test: test:
runs-on: 'ubuntu-latest' runs-on: "ubuntu-latest"
steps: steps:
- uses: 'actions/checkout@v3.5.3' - uses: "actions/checkout@v4.0.0"
- name: 'Setup Node.js' - name: "Setup Node.js"
uses: 'actions/setup-node@v3.6.0' uses: "actions/setup-node@v3.8.1"
with: with:
node-version: 'lts/*' node-version: "20.x"
cache: 'npm' cache: "npm"
- name: 'Install dependencies' - name: "Install dependencies"
run: 'npm clean-install' run: "npm clean-install"
- name: 'Build' - name: "Build"
run: 'npm run build' run: "npm run build"
- name: 'Test' - name: "Test"
run: 'npm run test' run: "npm run test"

View File

@ -1,5 +0,0 @@
{
"reporter": ["text", "cobertura"],
"src": "./build",
"all": true
}

View File

@ -1,6 +1,3 @@
{ {
"singleQuote": true, "semi": false
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

3
.swcrc
View File

@ -1,10 +1,11 @@
{ {
"sourceMaps": true,
"jsc": { "jsc": {
"parser": { "parser": {
"syntax": "typescript", "syntax": "typescript",
"dynamicImport": true "dynamicImport": true
}, },
"target": "es2022" "target": "esnext"
}, },
"module": { "module": {
"type": "es6" "type": "es6"

9
.taprc
View File

@ -1,9 +0,0 @@
ts: false
jsx: false
flow: false
check-coverage: false
coverage: false
timeout: 10000
files:
- 'build/**/*.test.js'

View File

@ -4,6 +4,10 @@
<strong>Authenticate socket.io incoming connections with JWTs.</strong> <strong>Authenticate socket.io incoming connections with JWTs.</strong>
</p> </p>
<p align="center">
<strong>⚠️ This project is not maintained anymore, you can still use the code as you wish and fork it to maintain it yourself.</strong>
</p>
<p align="center"> <p align="center">
<a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a> <a href="./CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" /></a>
<a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a> <a href="./LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="Licence MIT"/></a>
@ -22,13 +26,12 @@
Authenticate socket.io incoming connections with JWTs. Authenticate socket.io incoming connections with JWTs.
Compatible with `socket.io >= 3.0.0`. 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.
This repository was originally forked from [auth0-socketio-jwt](https://github.com/auth0-community/auth0-socketio-jwt) & it is not intended to take any credit but to improve the code from now on.
## Prerequisites ## Prerequisites
- [Node.js](https://nodejs.org/) >= 16.0.0 - [Node.js](https://nodejs.org/) >= 16.0.0
- [Socket.IO](https://socket.io/) >= 3.0.0
## 💾 Install ## 💾 Install
@ -43,24 +46,24 @@ npm install --save @thream/socketio-jwt
### Server side ### Server side
```ts ```ts
import { Server } from 'socket.io' import { Server } from "socket.io"
import { authorize } from '@thream/socketio-jwt' import { authorize } from "@thream/socketio-jwt"
const io = new Server(9000) const io = new Server(9000)
io.use( io.use(
authorize({ authorize({
secret: 'your secret or public key' secret: "your secret or public key",
}) }),
) )
io.on('connection', async (socket) => { io.on("connection", async (socket) => {
// jwt payload of the connected client // jwt payload of the connected client
console.log(socket.decodedToken) console.log(socket.decodedToken)
const clients = await io.sockets.allSockets() const clients = await io.sockets.allSockets()
if (clients != null) { if (clients != null) {
for (const clientId of clients) { for (const clientId of clients) {
const client = io.sockets.sockets.get(clientId) const client = io.sockets.sockets.get(clientId)
client?.emit('messages', { message: 'Success!' }) client?.emit("messages", { message: "Success!" })
// we can access the jwt payload of each connected client // we can access the jwt payload of each connected client
console.log(client?.decodedToken) console.log(client?.decodedToken)
} }
@ -71,12 +74,12 @@ io.on('connection', async (socket) => {
### Server side with `jwks-rsa` (example) ### Server side with `jwks-rsa` (example)
```ts ```ts
import jwksClient from 'jwks-rsa' import jwksClient from "jwks-rsa"
import { Server } from 'socket.io' import { Server } from "socket.io"
import { authorize } from '@thream/socketio-jwt' import { authorize } from "@thream/socketio-jwt"
const client = jwksClient({ const client = jwksClient({
jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json' jwksUri: "https://sandrino.auth0.com/.well-known/jwks.json",
}) })
const io = new Server(9000) const io = new Server(9000)
@ -85,11 +88,11 @@ io.use(
secret: async (decodedToken) => { secret: async (decodedToken) => {
const key = await client.getSigningKeyAsync(decodedToken.header.kid) const key = await client.getSigningKeyAsync(decodedToken.header.kid)
return key.getPublicKey() return key.getPublicKey()
} },
}) }),
) )
io.on('connection', async (socket) => { io.on("connection", async (socket) => {
// jwt payload of the connected client // jwt payload of the connected client
console.log(socket.decodedToken) console.log(socket.decodedToken)
// You can do the same things of the previous example there... // You can do the same things of the previous example there...
@ -99,21 +102,21 @@ io.on('connection', async (socket) => {
### Server side with `onAuthentication` (example) ### Server side with `onAuthentication` (example)
```ts ```ts
import { Server } from 'socket.io' import { Server } from "socket.io"
import { authorize } from '@thream/socketio-jwt' import { authorize } from "@thream/socketio-jwt"
const io = new Server(9000) const io = new Server(9000)
io.use( io.use(
authorize({ authorize({
secret: 'your secret or public key', secret: "your secret or public key",
onAuthentication: async (decodedToken) => { onAuthentication: async (decodedToken) => {
// return the object that you want to add to the user property // return the object that you want to add to the user property
// or throw an error if the token is unauthorized // or throw an error if the token is unauthorized
} },
}) }),
) )
io.on('connection', async (socket) => { io.on("connection", async (socket) => {
// jwt payload of the connected client // jwt payload of the connected client
console.log(socket.decodedToken) console.log(socket.decodedToken)
// You can do the same things of the previous example there... // You can do the same things of the previous example there...
@ -131,23 +134,23 @@ io.on('connection', async (socket) => {
### Client side ### Client side
```ts ```ts
import { io } from 'socket.io-client' import { io } from "socket.io-client"
import { isUnauthorizedError } from '@thream/socketio-jwt/build/UnauthorizedError.js' import { isUnauthorizedError } from "@thream/socketio-jwt/build/UnauthorizedError.js"
// Require Bearer Token // Require Bearer Token
const socket = io('http://localhost:9000', { const socket = io("http://localhost:9000", {
auth: { token: `Bearer ${yourJWT}` } auth: { token: `Bearer ${yourJWT}` },
}) })
// Handling token expiration // Handling token expiration
socket.on('connect_error', (error) => { socket.on("connect_error", (error) => {
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
console.log('User token has expired') console.log("User token has expired")
} }
}) })
// Listening to events // Listening to events
socket.on('messages', (data) => { socket.on("messages", (data) => {
console.log(data) console.log(data)
}) })
``` ```

9197
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,7 +10,8 @@
"files": [ "files": [
"build", "build",
"!**/*.test.js", "!**/*.test.js",
"!**/*.test.d.ts" "!**/*.test.d.ts",
"!**/*.map"
], ],
"engines": { "engines": {
"node": ">=16.0.0", "node": ">=16.0.0",
@ -41,10 +42,10 @@
"lint:commit": "commitlint", "lint:commit": "commitlint",
"lint:editorconfig": "editorconfig-checker", "lint:editorconfig": "editorconfig-checker",
"lint:markdown": "markdownlint-cli2", "lint:markdown": "markdownlint-cli2",
"lint:eslint": "eslint . --ignore-path .gitignore", "lint:eslint": "eslint . --max-warnings 0 --report-unused-disable-directives --ignore-path .gitignore",
"lint:prettier": "prettier . --check --ignore-path .gitignore", "lint:prettier": "prettier . --check",
"lint:staged": "lint-staged", "lint:staged": "lint-staged",
"test": "c8 tap", "test": "cross-env NODE_ENV=test node --enable-source-maps --test build/",
"release": "semantic-release", "release": "semantic-release",
"postinstall": "husky install", "postinstall": "husky install",
"prepublishOnly": "pinst --disable", "prepublishOnly": "pinst --disable",
@ -54,41 +55,39 @@
"socket.io": ">=3.0.0" "socket.io": ">=3.0.0"
}, },
"dependencies": { "dependencies": {
"jsonwebtoken": "9.0.0" "jsonwebtoken": "9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.6.6", "@commitlint/cli": "18.0.0",
"@commitlint/config-conventional": "17.6.6", "@commitlint/config-conventional": "18.0.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.67", "@swc/core": "1.3.94",
"@tsconfig/strictest": "2.0.1", "@tsconfig/strictest": "2.0.2",
"@types/jsonwebtoken": "9.0.2", "@types/jsonwebtoken": "9.0.4",
"@types/node": "20.3.3", "@types/node": "20.8.7",
"@types/tap": "15.0.8", "@typescript-eslint/eslint-plugin": "6.9.0",
"@typescript-eslint/eslint-plugin": "5.60.1", "@typescript-eslint/parser": "6.9.0",
"@typescript-eslint/parser": "5.60.1", "axios": "1.5.1",
"axios": "1.4.0", "cross-env": "7.0.3",
"c8": "8.0.0",
"editorconfig-checker": "5.1.1", "editorconfig-checker": "5.1.1",
"eslint": "8.44.0", "eslint": "8.52.0",
"eslint-config-conventions": "10.0.0", "eslint-config-conventions": "12.0.0",
"eslint-config-prettier": "8.8.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.29.0",
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "5.0.1",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "47.0.0", "eslint-plugin-unicorn": "48.0.1",
"fastify": "4.19.1", "fastify": "4.24.3",
"husky": "8.0.3", "husky": "8.0.3",
"lint-staged": "13.2.3", "lint-staged": "15.0.2",
"markdownlint-cli2": "0.8.1", "markdownlint-cli2": "0.10.0",
"markdownlint-rule-relative-links": "2.1.0", "markdownlint-rule-relative-links": "2.1.0",
"pinst": "3.0.0", "pinst": "3.0.0",
"prettier": "2.8.8", "prettier": "3.0.3",
"rimraf": "5.0.1", "rimraf": "5.0.5",
"semantic-release": "21.0.6", "semantic-release": "22.0.5",
"socket.io": "4.7.1", "socket.io": "4.7.2",
"socket.io-client": "4.7.1", "socket.io-client": "4.7.2",
"tap": "16.3.7", "typescript": "5.2.2"
"typescript": "5.0.4"
} }
} }

View File

@ -1,30 +1,30 @@
export class UnauthorizedError extends Error { export class UnauthorizedError extends Error {
public inner: { message: string } 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) super(error.message)
this.name = 'UnauthorizedError' this.name = "UnauthorizedError"
this.inner = error this.inner = error
this.data = { this.data = {
message: this.message, message: this.message,
code, code,
type: 'UnauthorizedError' type: "UnauthorizedError",
} }
Object.setPrototypeOf(this, UnauthorizedError.prototype) Object.setPrototypeOf(this, UnauthorizedError.prototype)
} }
} }
export const isUnauthorizedError = ( export const isUnauthorizedError = (
error: unknown error: unknown,
): error is UnauthorizedError => { ): error is UnauthorizedError => {
return ( return (
typeof error === 'object' && typeof error === "object" &&
error != null && error != null &&
'data' in error && "data" in error &&
typeof error.data === 'object' && typeof error.data === "object" &&
error.data != null && error.data != null &&
'type' in error.data && "type" in error.data &&
error.data.type === 'UnauthorizedError' error.data.type === "UnauthorizedError"
) )
} }

View File

@ -1,37 +1,39 @@
import tap from 'tap' import test from "node:test"
import axios from 'axios' import assert from "node:assert/strict"
import type { Socket } from 'socket.io-client'
import { io } from 'socket.io-client'
import { isUnauthorizedError } from '../UnauthorizedError.js' import axios from "axios"
import type { Profile } from './fixture/index.js' import type { Socket } from "socket.io-client"
import { io } from "socket.io-client"
import { isUnauthorizedError } from "../UnauthorizedError.js"
import type { Profile } from "./fixture/index.js"
import { import {
API_URL, API_URL,
fixtureStart, fixtureStart,
fixtureStop, fixtureStop,
getSocket, getSocket,
basicProfile basicProfile,
} from './fixture/index.js' } from "./fixture/index.js"
export const api = axios.create({ export const api = axios.create({
baseURL: API_URL, baseURL: API_URL,
headers: { headers: {
'Content-Type': 'application/json' "Content-Type": "application/json",
} },
}) })
const secretCallback = async (): Promise<string> => { const secretCallback = async (): Promise<string> => {
return 'somesecret' return "somesecret"
} }
await tap.test('authorize', async (t) => { await test("authorize", async (t) => {
await t.test('with secret as string in options', async (t) => { await t.test("with secret as string in options", async (t) => {
let token = '' let token = ""
let socket: Socket | null = null let socket: Socket | null = null
t.beforeEach(async () => { t.beforeEach(async () => {
await fixtureStart() await fixtureStart()
const response = await api.post('/login', {}) const response = await api.post("/login", {})
token = response.data.token token = response.data.token
}) })
@ -40,82 +42,87 @@ await tap.test('authorize', async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test('should emit error with no token provided', (t) => { await t.test("should emit error with no token provided", () => {
t.plan(4)
socket = io(API_URL) socket = io(API_URL)
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'no token provided') assert.strictEqual(error.data.message, "no token provided")
t.equal(error.data.code, 'credentials_required') assert.strictEqual(error.data.code, "credentials_required")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should emit error with bad token format', (t) => { await t.test("should emit error with bad token format", () => {
t.plan(4)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: 'testing' } auth: { token: "testing" },
}) })
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'Format is Authorization: Bearer [token]') assert.strictEqual(
t.equal(error.data.code, 'credentials_bad_format')
}
t.pass()
})
socket.on('connect', async () => {
t.fail()
})
})
await t.test('should emit error with unauthorized handshake', (t) => {
t.plan(4)
socket = io(API_URL, {
auth: { token: 'Bearer testing' }
})
socket.on('connect_error', async (error) => {
t.equal(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
t.equal(
error.data.message, error.data.message,
'Unauthorized: Token is missing or invalid Bearer' "Format is Authorization: Bearer [token]",
) )
t.equal(error.data.code, 'invalid_token') assert.strictEqual(error.data.code, "credentials_bad_format")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should connect the user', (t) => { await t.test("should emit error with unauthorized handshake", () => {
t.plan(1)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` } auth: { token: "Bearer testing" },
}) })
socket.on('connect', async () => { socket.on("connect_error", async (error) => {
t.pass() assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
assert.strictEqual(
error.data.message,
"Unauthorized: Token is missing or invalid Bearer",
)
assert.strictEqual(error.data.code, "invalid_token")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
}
}) })
socket.on('connect_error', async (error) => { socket.on("connect", async () => {
t.fail(error.message) assert.fail("should not connect")
})
})
await t.test("should connect the user", () => {
socket = io(API_URL, {
auth: { token: `Bearer ${token}` },
})
socket.on("connect", async () => {
assert.ok(true)
})
socket.on("connect_error", async (error) => {
assert.fail(error.message)
}) })
}) })
}) })
await t.test('with secret as callback in options', async (t) => { await t.test("with secret as callback in options", async (t) => {
let token = '' let token = ""
let socket: Socket | null = null let socket: Socket | null = null
t.beforeEach(async () => { t.beforeEach(async () => {
await fixtureStart({ secret: secretCallback }) await fixtureStart({ secret: secretCallback })
const response = await api.post('/login', {}) const response = await api.post("/login", {})
token = response.data.token token = response.data.token
}) })
@ -124,78 +131,83 @@ await tap.test('authorize', async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test('should emit error with no token provided', (t) => { await t.test("should emit error with no token provided", () => {
t.plan(4)
socket = io(API_URL) socket = io(API_URL)
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'no token provided') assert.strictEqual(error.data.message, "no token provided")
t.equal(error.data.code, 'credentials_required') assert.strictEqual(error.data.code, "credentials_required")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should emit error with bad token format', (t) => { await t.test("should emit error with bad token format", () => {
t.plan(4)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: 'testing' } auth: { token: "testing" },
}) })
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'Format is Authorization: Bearer [token]') assert.strictEqual(
t.equal(error.data.code, 'credentials_bad_format')
}
t.pass()
})
socket.on('connect', async () => {
t.fail()
})
})
await t.test('should emit error with unauthorized handshake', (t) => {
t.plan(4)
socket = io(API_URL, {
auth: { token: 'Bearer testing' }
})
socket.on('connect_error', async (error) => {
t.equal(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
t.equal(
error.data.message, error.data.message,
'Unauthorized: Token is missing or invalid Bearer' "Format is Authorization: Bearer [token]",
) )
t.equal(error.data.code, 'invalid_token') assert.strictEqual(error.data.code, "credentials_bad_format")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should connect the user', (t) => { await t.test("should emit error with unauthorized handshake", () => {
t.plan(1)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` } auth: { token: "Bearer testing" },
}) })
socket.on('connect', async () => { socket.on("connect_error", async (error) => {
t.pass() assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
assert.strictEqual(
error.data.message,
"Unauthorized: Token is missing or invalid Bearer",
)
assert.strictEqual(error.data.code, "invalid_token")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
}
}) })
socket.on('connect_error', async (error) => { socket.on("connect", async () => {
t.fail(error.message) assert.fail("should not connect")
})
})
await t.test("should connect the user", () => {
socket = io(API_URL, {
auth: { token: `Bearer ${token}` },
})
socket.on("connect", async () => {
assert.ok(true)
})
socket.on("connect_error", async (error) => {
assert.fail(error.message)
}) })
}) })
}) })
await t.test('with onAuthentication callback in options', async (t) => { await t.test("with onAuthentication callback in options", async (t) => {
let token = '' let token = ""
let wrongToken = '' let wrongToken = ""
let socket: Socket | null = null let socket: Socket | null = null
t.beforeEach(async () => { t.beforeEach(async () => {
@ -203,16 +215,16 @@ await tap.test('authorize', async (t) => {
secret: secretCallback, secret: secretCallback,
onAuthentication: (decodedToken: Profile) => { onAuthentication: (decodedToken: Profile) => {
if (!decodedToken.checkField) { if (!decodedToken.checkField) {
throw new Error('Check Field validation failed') throw new Error("Check Field validation failed")
} }
return { return {
email: decodedToken.email email: decodedToken.email,
} }
} },
}) })
const response = await api.post('/login', {}) const response = await api.post("/login", {})
token = response.data.token token = response.data.token
const responseWrong = await api.post('/login-wrong', {}) const responseWrong = await api.post("/login-wrong", {})
wrongToken = responseWrong.data.token wrongToken = responseWrong.data.token
}) })
@ -221,104 +233,107 @@ await tap.test('authorize', async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test('should emit error with no token provided', (t) => { await t.test("should emit error with no token provided", () => {
t.plan(4)
socket = io(API_URL) socket = io(API_URL)
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'no token provided') assert.strictEqual(error.data.message, "no token provided")
t.equal(error.data.code, 'credentials_required') assert.strictEqual(error.data.code, "credentials_required")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should emit error with bad token format', (t) => { await t.test("should emit error with bad token format", () => {
t.plan(4)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: 'testing' } auth: { token: "testing" },
}) })
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.equal(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
t.equal(error.data.message, 'Format is Authorization: Bearer [token]') assert.strictEqual(
t.equal(error.data.code, 'credentials_bad_format')
}
t.pass()
})
socket.on('connect', async () => {
t.fail()
})
})
await t.test('should emit error with unauthorized handshake', (t) => {
t.plan(4)
socket = io(API_URL, {
auth: { token: 'Bearer testing' }
})
socket.on('connect_error', async (error) => {
t.equal(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
t.equal(
error.data.message, error.data.message,
'Unauthorized: Token is missing or invalid Bearer' "Format is Authorization: Bearer [token]",
) )
t.equal(error.data.code, 'invalid_token') assert.strictEqual(error.data.code, "credentials_bad_format")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
} }
t.pass()
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
await t.test('should connect the user', (t) => { await t.test("should emit error with unauthorized handshake", () => {
t.plan(1)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` } auth: { token: "Bearer testing" },
}) })
socket.on('connect', async () => { socket.on("connect_error", async (error) => {
t.pass() assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) {
assert.strictEqual(
error.data.message,
"Unauthorized: Token is missing or invalid Bearer",
)
assert.strictEqual(error.data.code, "invalid_token")
assert.ok(true)
} else {
assert.fail("should be unauthorized error")
}
}) })
socket.on('connect_error', async (error) => { socket.on("connect", async () => {
t.fail(error.message) assert.fail("should not connect")
}) })
}) })
await t.test('should contains user properties', (t) => { await t.test("should connect the user", () => {
t.plan(2) socket = io(API_URL, {
auth: { token: `Bearer ${token}` },
})
socket.on("connect", async () => {
assert.ok(true)
})
socket.on("connect_error", async (error) => {
assert.fail(error.message)
})
})
await t.test("should contains user properties", () => {
const socketServer = getSocket() const socketServer = getSocket()
socketServer?.on('connection', (client: any) => { socketServer?.on("connection", (client: any) => {
t.equal(client.user.email, basicProfile.email) assert.strictEqual(client.user.email, basicProfile.email)
t.pass() assert.ok(true)
}) })
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` } auth: { token: `Bearer ${token}` },
}) })
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
t.fail(error.message) assert.fail(error.message)
}) })
}) })
await t.test('should emit error when user validation fails', (t) => { await t.test("should emit error when user validation fails", () => {
t.plan(2)
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${wrongToken}` } auth: { token: `Bearer ${wrongToken}` },
}) })
socket.on('connect_error', async (error) => { socket.on("connect_error", async (error) => {
try { try {
t.equal(error.message, 'Check Field validation failed') assert.strictEqual(error.message, "Check Field validation failed")
t.pass() assert.ok(true)
} catch { } catch {
t.fail() assert.fail(error.message)
} }
}) })
socket.on('connect', async () => { socket.on("connect", async () => {
t.fail() assert.fail("should not connect")
}) })
}) })
}) })

View File

@ -1,16 +1,16 @@
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken"
import { Server as SocketIoServer } from 'socket.io' import { Server as SocketIoServer } from "socket.io"
import type { FastifyInstance } from 'fastify' import type { FastifyInstance } from "fastify"
import fastify from 'fastify' import fastify from "fastify"
import type { AuthorizeOptions } from '../../index.js' import type { AuthorizeOptions } from "../../index.js"
import { authorize } from '../../index.js' import { authorize } from "../../index.js"
interface FastifyIo { interface FastifyIo {
instance: SocketIoServer instance: SocketIoServer
} }
declare module 'fastify' { declare module "fastify" {
export interface FastifyInstance { export interface FastifyInstance {
io: FastifyIo io: FastifyIo
} }
@ -28,49 +28,49 @@ export interface Profile extends BasicProfile {
export const PORT = 9000 export const PORT = 9000
export const API_URL = `http://localhost:${PORT}` export const API_URL = `http://localhost:${PORT}`
export const basicProfile: BasicProfile = { export const basicProfile: BasicProfile = {
email: 'john@doe.com', email: "john@doe.com",
id: 123 id: 123,
} }
let application: FastifyInstance | null = null let application: FastifyInstance | null = null
export const fixtureStart = async ( export const fixtureStart = async (
options: AuthorizeOptions = { secret: 'super secret' } options: AuthorizeOptions = { secret: "super secret" },
): Promise<void> => { ): Promise<void> => {
const profile: Profile = { ...basicProfile, checkField: true } const profile: Profile = { ...basicProfile, checkField: true }
let keySecret = '' let keySecret = ""
if (typeof options.secret === 'string') { if (typeof options.secret === "string") {
keySecret = options.secret keySecret = options.secret
} else { } else {
keySecret = await options.secret({ keySecret = await options.secret({
header: { alg: 'HS256' }, header: { alg: "HS256" },
payload: profile payload: profile,
}) })
} }
application = fastify() application = fastify()
application.post('/login', async (_request, reply) => { application.post("/login", async (_request, reply) => {
const token = jwt.sign(profile, keySecret, { const token = jwt.sign(profile, keySecret, {
expiresIn: 60 * 60 * 5 expiresIn: 60 * 60 * 5,
}) })
reply.statusCode = 201 reply.statusCode = 201
return { token } return { token }
}) })
application.post('/login-wrong', async (_request, reply) => { application.post("/login-wrong", async (_request, reply) => {
profile.checkField = false profile.checkField = false
const token = jwt.sign(profile, keySecret, { const token = jwt.sign(profile, keySecret, {
expiresIn: 60 * 60 * 5 expiresIn: 60 * 60 * 5,
}) })
reply.statusCode = 201 reply.statusCode = 201
return { token } return { token }
}) })
const instance = new SocketIoServer(application.server) const instance = new SocketIoServer(application.server)
instance.use(authorize(options)) instance.use(authorize(options))
application.decorate('io', { instance }) application.decorate("io", { instance })
application.addHook('onClose', (fastify) => { application.addHook("onClose", (fastify) => {
fastify.io.instance.close() fastify.io.instance.close()
}) })
await application.listen({ await application.listen({
port: PORT port: PORT,
}) })
} }

View File

@ -1,10 +1,10 @@
import type { Algorithm } from 'jsonwebtoken' import type { Algorithm } from "jsonwebtoken"
import jwt from 'jsonwebtoken' import jwt from "jsonwebtoken"
import type { Socket } from 'socket.io' import type { Socket } from "socket.io"
import { UnauthorizedError } from './UnauthorizedError.js' import { UnauthorizedError } from "./UnauthorizedError.js"
declare module 'socket.io' { declare module "socket.io" {
interface Socket extends ExtendedSocket {} interface Socket extends ExtendedSocket {}
} }
@ -16,7 +16,7 @@ interface ExtendedSocket {
type SocketIOMiddleware = ( type SocketIOMiddleware = (
socket: Socket, socket: Socket,
next: (error?: UnauthorizedError) => void next: (error?: UnauthorizedError) => void,
) => void ) => void
interface CompleteDecodedToken { interface CompleteDecodedToken {
@ -28,7 +28,7 @@ interface CompleteDecodedToken {
} }
type SecretCallback = ( type SecretCallback = (
decodedToken: CompleteDecodedToken decodedToken: CompleteDecodedToken,
) => Promise<string> | string ) => Promise<string> | string
export interface AuthorizeOptions { export interface AuthorizeOptions {
@ -38,32 +38,32 @@ export interface AuthorizeOptions {
} }
export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => { export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
const { secret, algorithms = ['HS256'], onAuthentication } = options const { secret, algorithms = ["HS256"], onAuthentication } = options
return async (socket, next) => { return async (socket, next) => {
let encodedToken: string | null = null let encodedToken: string | null = null
const { token } = socket.handshake.auth const { token } = socket.handshake.auth
if (token != null) { if (token != null) {
const tokenSplitted = token.split(' ') const tokenSplitted = token.split(" ")
if (tokenSplitted.length !== 2 || tokenSplitted[0] !== 'Bearer') { if (tokenSplitted.length !== 2 || tokenSplitted[0] !== "Bearer") {
return next( return next(
new UnauthorizedError('credentials_bad_format', { new UnauthorizedError("credentials_bad_format", {
message: 'Format is Authorization: Bearer [token]' message: "Format is Authorization: Bearer [token]",
}) }),
) )
} }
encodedToken = tokenSplitted[1] encodedToken = tokenSplitted[1]
} }
if (encodedToken == null) { if (encodedToken == null) {
return next( return next(
new UnauthorizedError('credentials_required', { new UnauthorizedError("credentials_required", {
message: 'no token provided' message: "no token provided",
}) }),
) )
} }
socket.encodedToken = encodedToken socket.encodedToken = encodedToken
let keySecret: string | null = null let keySecret: string | null = null
let decodedToken: any = null let decodedToken: any = null
if (typeof secret === 'string') { if (typeof secret === "string") {
keySecret = secret keySecret = secret
} else { } else {
const completeDecodedToken = jwt.decode(encodedToken, { complete: true }) const completeDecodedToken = jwt.decode(encodedToken, { complete: true })
@ -73,9 +73,9 @@ export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
decodedToken = jwt.verify(encodedToken, keySecret, { algorithms }) decodedToken = jwt.verify(encodedToken, keySecret, { algorithms })
} catch { } catch {
return next( return next(
new UnauthorizedError('invalid_token', { new UnauthorizedError("invalid_token", {
message: 'Unauthorized: Token is missing or invalid Bearer' message: "Unauthorized: Token is missing or invalid Bearer",
}) }),
) )
} }
socket.decodedToken = decodedToken socket.decodedToken = decodedToken

View File

@ -1,2 +1,2 @@
export * from './authorize.js' export * from "./authorize.js"
export * from './UnauthorizedError.js' export * from "./UnauthorizedError.js"

View File

@ -2,9 +2,9 @@
"extends": "@tsconfig/strictest/tsconfig.json", "extends": "@tsconfig/strictest/tsconfig.json",
"compilerOptions": { "compilerOptions": {
"target": "ESNext", "target": "ESNext",
"module": "ESNext",
"lib": ["ESNext"], "lib": ["ESNext"],
"moduleResolution": "node", "module": "NodeNext",
"moduleResolution": "NodeNext",
"outDir": "./build", "outDir": "./build",
"rootDir": "./src", "rootDir": "./src",
"emitDeclarationOnly": true, "emitDeclarationOnly": true,