feat: add support for jwks-rsa (#1)
This commit is contained in:
parent
ca83ad4ba2
commit
261e8d66e2
30
README.md
30
README.md
@ -19,7 +19,7 @@
|
|||||||
|
|
||||||
Authenticate socket.io incoming connections with JWTs.
|
Authenticate socket.io incoming connections with JWTs.
|
||||||
|
|
||||||
Compatible with `socket.io >= 3.0`.
|
Compatible with `socket.io >= 3.0.0`.
|
||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
@ -59,6 +59,34 @@ io.on('connection', async (socket) => {
|
|||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Server side with `jwks-rsa` (example)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import jwksClient from 'jwks-rsa'
|
||||||
|
import { Server } from 'socket.io'
|
||||||
|
import { authorize } from '@thream/socketio-jwt'
|
||||||
|
|
||||||
|
const client = jwksClient({
|
||||||
|
jwksUri: 'https://sandrino.auth0.com/.well-known/jwks.json'
|
||||||
|
})
|
||||||
|
|
||||||
|
const io = new Server(9000)
|
||||||
|
io.use(
|
||||||
|
authorize({
|
||||||
|
secret: async (decodedToken) => {
|
||||||
|
const key = await client.getSigningKeyAsync(decodedToken.header.kid)
|
||||||
|
return key.rsaPublicKey
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
io.on('connection', async (socket) => {
|
||||||
|
// jwt payload of the connected client
|
||||||
|
console.log(socket.decodedToken)
|
||||||
|
// You can do the same things of the previous example there...
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
### Client side
|
### Client side
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
|
@ -80,7 +80,7 @@
|
|||||||
"test:clearCache": "jest --clearCache"
|
"test:clearCache": "jest --clearCache"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"socket.io": "*"
|
"socket.io": ">=3.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsonwebtoken": "8.5.1"
|
"jsonwebtoken": "8.5.1"
|
||||||
@ -90,7 +90,7 @@
|
|||||||
"@commitlint/config-conventional": "11.0.0",
|
"@commitlint/config-conventional": "11.0.0",
|
||||||
"@release-it/conventional-changelog": "2.0.0",
|
"@release-it/conventional-changelog": "2.0.0",
|
||||||
"@types/express": "4.17.9",
|
"@types/express": "4.17.9",
|
||||||
"@types/jest": "26.0.19",
|
"@types/jest": "26.0.20",
|
||||||
"@types/jsonwebtoken": "8.5.0",
|
"@types/jsonwebtoken": "8.5.0",
|
||||||
"@types/node": "14.14.20",
|
"@types/node": "14.14.20",
|
||||||
"@types/server-destroy": "1.0.1",
|
"@types/server-destroy": "1.0.1",
|
||||||
|
@ -3,12 +3,12 @@ import { io } from 'socket.io-client'
|
|||||||
|
|
||||||
import { fixtureStart, fixtureStop } from './fixture'
|
import { fixtureStart, fixtureStop } from './fixture'
|
||||||
|
|
||||||
describe('authorize', () => {
|
describe('authorize - with secret as string in options', () => {
|
||||||
let token: string = ''
|
let token: string = ''
|
||||||
|
|
||||||
beforeEach((done) => {
|
beforeEach(async (done) => {
|
||||||
jest.setTimeout(15_000)
|
jest.setTimeout(15_000)
|
||||||
fixtureStart(async () => {
|
await fixtureStart(async () => {
|
||||||
const response = await axios.post('http://localhost:9000/login')
|
const response = await axios.post('http://localhost:9000/login')
|
||||||
token = response.data.token
|
token = response.data.token
|
||||||
done()
|
done()
|
||||||
@ -67,3 +67,37 @@ describe('authorize', () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const secretCallback = async (): Promise<string> => {
|
||||||
|
return 'somesecret'
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('authorize - with secret as callback in options', () => {
|
||||||
|
let token: string = ''
|
||||||
|
|
||||||
|
beforeEach(async (done) => {
|
||||||
|
jest.setTimeout(15_000)
|
||||||
|
await fixtureStart(
|
||||||
|
async () => {
|
||||||
|
const response = await axios.post('http://localhost:9000/login')
|
||||||
|
token = response.data.token
|
||||||
|
done()
|
||||||
|
},
|
||||||
|
{ secret: secretCallback }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach((done) => {
|
||||||
|
fixtureStop(done)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should connect the user', (done) => {
|
||||||
|
const socket = io('http://localhost:9000', {
|
||||||
|
extraHeaders: { Authorization: `Bearer ${token}` }
|
||||||
|
})
|
||||||
|
socket.on('connect', () => {
|
||||||
|
socket.close()
|
||||||
|
done()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
@ -5,7 +5,7 @@ import { Server as HttpsServer } from 'https'
|
|||||||
import { Server as SocketIoServer } from 'socket.io'
|
import { Server as SocketIoServer } from 'socket.io'
|
||||||
import enableDestroy from 'server-destroy'
|
import enableDestroy from 'server-destroy'
|
||||||
|
|
||||||
import { authorize } from '../../index'
|
import { authorize, AuthorizeOptions } from '../../index'
|
||||||
|
|
||||||
interface Socket {
|
interface Socket {
|
||||||
io: null | SocketIoServer
|
io: null | SocketIoServer
|
||||||
@ -21,16 +21,26 @@ const socket: Socket = {
|
|||||||
|
|
||||||
let server: HttpServer | null = null
|
let server: HttpServer | null = null
|
||||||
|
|
||||||
export const fixtureStart = (done: any): void => {
|
export const fixtureStart = async (
|
||||||
const options = { secret: 'aaafoo super sercret' }
|
done: any,
|
||||||
|
options: AuthorizeOptions = { secret: 'aaafoo super sercret' }
|
||||||
|
): Promise<void> => {
|
||||||
const app = express()
|
const app = express()
|
||||||
app.use(express.json())
|
app.use(express.json())
|
||||||
|
let keySecret = 'secret'
|
||||||
|
if (typeof options.secret === 'string') {
|
||||||
|
keySecret = options.secret
|
||||||
|
} else {
|
||||||
|
keySecret = await options.secret(() => {})
|
||||||
|
}
|
||||||
app.post('/login', (_req, res) => {
|
app.post('/login', (_req, res) => {
|
||||||
const profile = {
|
const profile = {
|
||||||
email: 'john@doe.com',
|
email: 'john@doe.com',
|
||||||
id: 123
|
id: 123
|
||||||
}
|
}
|
||||||
const token = jwt.sign(profile, options.secret, { expiresIn: 60 * 60 * 5 })
|
const token = jwt.sign(profile, keySecret, {
|
||||||
|
expiresIn: 60 * 60 * 5
|
||||||
|
})
|
||||||
return res.json({ token })
|
return res.json({ token })
|
||||||
})
|
})
|
||||||
server = app.listen(9000, done)
|
server = app.listen(9000, done)
|
||||||
|
@ -21,15 +21,17 @@ type SocketIOMiddleware = (
|
|||||||
next: (err?: ExtendedError) => void
|
next: (err?: ExtendedError) => void
|
||||||
) => void
|
) => void
|
||||||
|
|
||||||
interface AuthorizeOptions {
|
type SecretCallback = (decodedToken: null | { [key: string]: any } | string) => Promise<string>
|
||||||
secret: string
|
|
||||||
|
export interface AuthorizeOptions {
|
||||||
|
secret: string | SecretCallback
|
||||||
algorithms?: Algorithm[]
|
algorithms?: Algorithm[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
|
export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
|
||||||
const { secret, algorithms = ['HS256'] } = options
|
const { secret, algorithms = ['HS256'] } = options
|
||||||
return (socket, next) => {
|
return async (socket, next) => {
|
||||||
let token: string | null = null
|
let encodedToken: string | null = null
|
||||||
const authorizationHeader = socket.request.headers.authorization
|
const authorizationHeader = socket.request.headers.authorization
|
||||||
if (authorizationHeader != null) {
|
if (authorizationHeader != null) {
|
||||||
const tokenSplitted = authorizationHeader.split(' ')
|
const tokenSplitted = authorizationHeader.split(' ')
|
||||||
@ -40,9 +42,9 @@ export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
|
|||||||
})
|
})
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
token = tokenSplitted[1]
|
encodedToken = tokenSplitted[1]
|
||||||
}
|
}
|
||||||
if (token == null) {
|
if (encodedToken == null) {
|
||||||
return next(
|
return next(
|
||||||
new UnauthorizedError('credentials_required', {
|
new UnauthorizedError('credentials_required', {
|
||||||
message: 'no token provided'
|
message: 'no token provided'
|
||||||
@ -50,10 +52,17 @@ export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Store encoded JWT
|
// Store encoded JWT
|
||||||
socket.encodedToken = token
|
socket.encodedToken = encodedToken
|
||||||
let payload: any
|
let keySecret: string | null = null
|
||||||
|
let decodedToken: any
|
||||||
|
if (typeof secret === 'string') {
|
||||||
|
keySecret = secret
|
||||||
|
} else {
|
||||||
|
decodedToken = jwt.decode(encodedToken, { complete: true })
|
||||||
|
keySecret = await secret(decodedToken)
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
payload = jwt.verify(token, secret, { algorithms })
|
decodedToken = jwt.verify(encodedToken, keySecret, { algorithms })
|
||||||
} catch {
|
} catch {
|
||||||
return next(
|
return next(
|
||||||
new UnauthorizedError('invalid_token', {
|
new UnauthorizedError('invalid_token', {
|
||||||
@ -62,7 +71,7 @@ export const authorize = (options: AuthorizeOptions): SocketIOMiddleware => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
// Store decoded JWT
|
// Store decoded JWT
|
||||||
socket.decodedToken = payload
|
socket.decodedToken = decodedToken
|
||||||
return next()
|
return next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user