feat: add support for jwks-rsa (#1)

This commit is contained in:
divlo 2021-01-07 14:30:37 +01:00
parent ca83ad4ba2
commit 261e8d66e2
5 changed files with 101 additions and 20 deletions

View File

@ -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

View File

@ -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",

View File

@ -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()
})
})
})

View File

@ -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)

View File

@ -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()
} }
} }