Compare commits

..

No commits in common. "develop" and "v3.1.3" have entirely different histories.

18 changed files with 1254 additions and 1258 deletions

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@v4.0.0" - uses: 'actions/checkout@v4.0.0'
- name: "Setup Node.js" - name: 'Setup Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.8.1'
with: with:
node-version: "20.x" 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@v4.0.0" - uses: 'actions/checkout@v4.0.0'
- name: "Setup Node.js" - name: 'Setup Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.8.1'
with: with:
node-version: "20.x" 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@v4.0.0" - uses: 'actions/checkout@v4.0.0'
- name: "Setup Node.js" - name: 'Setup Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.8.1'
with: with:
node-version: "20.x" 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@v4.0.0" - uses: 'actions/checkout@v4.0.0'
- name: "Setup Node.js" - name: 'Setup Node.js'
uses: "actions/setup-node@v3.8.1" uses: 'actions/setup-node@v3.8.1'
with: with:
node-version: "20.x" 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,3 +1,6 @@
{ {
"semi": false "singleQuote": true,
"jsxSingleQuote": true,
"semi": false,
"trailingComma": "none"
} }

View File

@ -4,10 +4,6 @@
<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>
@ -46,24 +42,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)
} }
@ -74,12 +70,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)
@ -88,11 +84,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...
@ -102,21 +98,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...
@ -134,23 +130,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)
}) })
``` ```

1923
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -58,34 +58,34 @@
"jsonwebtoken": "9.0.2" "jsonwebtoken": "9.0.2"
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "18.0.0", "@commitlint/cli": "17.7.1",
"@commitlint/config-conventional": "18.0.0", "@commitlint/config-conventional": "17.7.0",
"@swc/cli": "0.1.62", "@swc/cli": "0.1.62",
"@swc/core": "1.3.94", "@swc/core": "1.3.85",
"@tsconfig/strictest": "2.0.2", "@tsconfig/strictest": "2.0.2",
"@types/jsonwebtoken": "9.0.4", "@types/jsonwebtoken": "9.0.3",
"@types/node": "20.8.7", "@types/node": "20.6.2",
"@typescript-eslint/eslint-plugin": "6.9.0", "@typescript-eslint/eslint-plugin": "6.7.2",
"@typescript-eslint/parser": "6.9.0", "@typescript-eslint/parser": "6.7.2",
"axios": "1.5.1", "axios": "1.5.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"editorconfig-checker": "5.1.1", "editorconfig-checker": "5.1.1",
"eslint": "8.52.0", "eslint": "8.49.0",
"eslint-config-conventions": "12.0.0", "eslint-config-conventions": "11.0.1",
"eslint-config-prettier": "9.0.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-import": "2.29.0", "eslint-plugin-import": "2.28.1",
"eslint-plugin-prettier": "5.0.1", "eslint-plugin-prettier": "5.0.0",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-unicorn": "48.0.1", "eslint-plugin-unicorn": "48.0.1",
"fastify": "4.24.3", "fastify": "4.23.2",
"husky": "8.0.3", "husky": "8.0.3",
"lint-staged": "15.0.2", "lint-staged": "14.0.1",
"markdownlint-cli2": "0.10.0", "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": "3.0.3", "prettier": "3.0.3",
"rimraf": "5.0.5", "rimraf": "5.0.1",
"semantic-release": "22.0.5", "semantic-release": "22.0.0",
"socket.io": "4.7.2", "socket.io": "4.7.2",
"socket.io-client": "4.7.2", "socket.io-client": "4.7.2",
"typescript": "5.2.2" "typescript": "5.2.2"

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,39 +1,39 @@
import test from "node:test" import test from 'node:test'
import assert from "node:assert/strict" import assert from 'node:assert/strict'
import axios from "axios" import axios from 'axios'
import type { Socket } from "socket.io-client" import type { Socket } from 'socket.io-client'
import { io } from "socket.io-client" import { io } from 'socket.io-client'
import { isUnauthorizedError } from "../UnauthorizedError.js" import { isUnauthorizedError } from '../UnauthorizedError.js'
import type { Profile } from "./fixture/index.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 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
}) })
@ -42,87 +42,87 @@ await test("authorize", async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test("should emit error with no token provided", () => { await t.test('should emit error with no token provided', () => {
socket = io(API_URL) socket = io(API_URL)
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual(error.data.message, "no token provided") assert.strictEqual(error.data.message, 'no token provided')
assert.strictEqual(error.data.code, "credentials_required") assert.strictEqual(error.data.code, 'credentials_required')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with bad token format", () => { await t.test('should emit error with bad token format', () => {
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) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Format is Authorization: Bearer [token]", 'Format is Authorization: Bearer [token]'
) )
assert.strictEqual(error.data.code, "credentials_bad_format") assert.strictEqual(error.data.code, 'credentials_bad_format')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with unauthorized handshake", () => { await t.test('should emit error with unauthorized handshake', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: "Bearer testing" }, auth: { token: 'Bearer testing' }
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Unauthorized: Token is missing or invalid Bearer", 'Unauthorized: Token is missing or invalid Bearer'
) )
assert.strictEqual(error.data.code, "invalid_token") assert.strictEqual(error.data.code, 'invalid_token')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should connect the user", () => { await t.test('should connect the user', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` }, auth: { token: `Bearer ${token}` }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.ok(true) assert.ok(true)
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.fail(error.message) 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
}) })
@ -131,83 +131,83 @@ await test("authorize", async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test("should emit error with no token provided", () => { await t.test('should emit error with no token provided', () => {
socket = io(API_URL) socket = io(API_URL)
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual(error.data.message, "no token provided") assert.strictEqual(error.data.message, 'no token provided')
assert.strictEqual(error.data.code, "credentials_required") assert.strictEqual(error.data.code, 'credentials_required')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with bad token format", () => { await t.test('should emit error with bad token format', () => {
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) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Format is Authorization: Bearer [token]", 'Format is Authorization: Bearer [token]'
) )
assert.strictEqual(error.data.code, "credentials_bad_format") assert.strictEqual(error.data.code, 'credentials_bad_format')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with unauthorized handshake", () => { await t.test('should emit error with unauthorized handshake', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: "Bearer testing" }, auth: { token: 'Bearer testing' }
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Unauthorized: Token is missing or invalid Bearer", 'Unauthorized: Token is missing or invalid Bearer'
) )
assert.strictEqual(error.data.code, "invalid_token") assert.strictEqual(error.data.code, 'invalid_token')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should connect the user", () => { await t.test('should connect the user', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` }, auth: { token: `Bearer ${token}` }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.ok(true) assert.ok(true)
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.fail(error.message) 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 () => {
@ -215,16 +215,16 @@ await 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
}) })
@ -233,107 +233,107 @@ await test("authorize", async (t) => {
await fixtureStop() await fixtureStop()
}) })
await t.test("should emit error with no token provided", () => { await t.test('should emit error with no token provided', () => {
socket = io(API_URL) socket = io(API_URL)
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual(error.data.message, "no token provided") assert.strictEqual(error.data.message, 'no token provided')
assert.strictEqual(error.data.code, "credentials_required") assert.strictEqual(error.data.code, 'credentials_required')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with bad token format", () => { await t.test('should emit error with bad token format', () => {
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) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Format is Authorization: Bearer [token]", 'Format is Authorization: Bearer [token]'
) )
assert.strictEqual(error.data.code, "credentials_bad_format") assert.strictEqual(error.data.code, 'credentials_bad_format')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should emit error with unauthorized handshake", () => { await t.test('should emit error with unauthorized handshake', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: "Bearer testing" }, auth: { token: 'Bearer testing' }
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.strictEqual(isUnauthorizedError(error), true) assert.strictEqual(isUnauthorizedError(error), true)
if (isUnauthorizedError(error)) { if (isUnauthorizedError(error)) {
assert.strictEqual( assert.strictEqual(
error.data.message, error.data.message,
"Unauthorized: Token is missing or invalid Bearer", 'Unauthorized: Token is missing or invalid Bearer'
) )
assert.strictEqual(error.data.code, "invalid_token") assert.strictEqual(error.data.code, 'invalid_token')
assert.ok(true) assert.ok(true)
} else { } else {
assert.fail("should be unauthorized error") assert.fail('should be unauthorized error')
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") assert.fail('should not connect')
}) })
}) })
await t.test("should connect the user", () => { await t.test('should connect the user', () => {
socket = io(API_URL, { socket = io(API_URL, {
auth: { token: `Bearer ${token}` }, auth: { token: `Bearer ${token}` }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.ok(true) assert.ok(true)
}) })
socket.on("connect_error", async (error) => { socket.on('connect_error', async (error) => {
assert.fail(error.message) assert.fail(error.message)
}) })
}) })
await t.test("should contains user properties", () => { await t.test('should contains user properties', () => {
const socketServer = getSocket() const socketServer = getSocket()
socketServer?.on("connection", (client: any) => { socketServer?.on('connection', (client: any) => {
assert.strictEqual(client.user.email, basicProfile.email) assert.strictEqual(client.user.email, basicProfile.email)
assert.ok(true) 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) => {
assert.fail(error.message) assert.fail(error.message)
}) })
}) })
await t.test("should emit error when user validation fails", () => { await t.test('should emit error when user validation fails', () => {
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 {
assert.strictEqual(error.message, "Check Field validation failed") assert.strictEqual(error.message, 'Check Field validation failed')
assert.ok(true) assert.ok(true)
} catch { } catch {
assert.fail(error.message) assert.fail(error.message)
} }
}) })
socket.on("connect", async () => { socket.on('connect', async () => {
assert.fail("should not connect") 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'