-
-
Notifications
You must be signed in to change notification settings - Fork 7.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support adding guards on gateway’s handleConnection method. #882
Comments
So what about to add support guards for handleConnection? |
what happen on this?? is this implement or not?? |
Also I am wondering about this. |
just newly stumble on here, similar implementation by this |
Instead of using Example chat server// in gateway
async handleConnection(socket) {
const user: User = await this.jwtService.verify(
socket.handshake.query.token,
true
);
this.connectedUsers = [...this.connectedUsers, String(user._id)];
// Send list of connected users
this.server.emit('users', this.connectedUsers);
} // in jwtService
async verify(token: string, isWs: boolean = false): Promise<User | null> {
try {
const payload = <any>jwt.verify(token, APP_CONFIG.jwtSecret);
const user = await this.usersService.findById(payload.sub._id);
if (!user) {
if (isWs) {
throw new WsException('Unauthorized access');
} else {
throw new HttpException(
'Unauthorized access',
HttpStatus.BAD_REQUEST
);
}
}
return user;
} catch (err) {
if (isWs) {
throw new WsException(err.message);
} else {
throw new HttpException(err.message, HttpStatus.BAD_REQUEST);
}
}
} Reference |
I would love if this was an option for lifecycle hooks as well. |
What is the status of this? |
@Hwan-seok Thanks for providing your solution but I have one question. I don't see how this implementation prevents the server from crashing, similar to what is being reported in #2028 . Am I missing something? |
Hi @ChrisKatsaras,
So, my past solution seems to be wrong, I think it should be change as follows async handleConnection(socket) {
const user = whatEverFindOrVerifyUser();
if (!user) {
socket.disconnect(true); // you can omit "true"
}
} I found I hope it will help! |
Thanks for the quick response @Hwan-seok . This is very helpful! |
Circling back to the original question, are there any plans to add guards to the |
@Hwan-seok I've also noticed one |
As @ChrisKatsaras, I have suffered from this too. However, I don't think that that lapse of time is a minor issue 😅 |
I agree, @tonivj5 ! Here's to hoping someone can help us out 🤞 |
@ChrisKatsaras Because it does not make sense that jumps over |
Hey @Hwan-seok ! Thanks for the quick reply. Unfortunately, adding await doesn't solve the problem due to the fact that the WebSocket establishes the connection (either before or at the beginning of handleConnection). For this reason, it doesn't matter if you await or not as the connection has been established and can receive events emitted from the server (assuming they are scoped to that socket e.g global emission). The good news is I did find another solution to the problem! We can make use of NestJS Adapters https://docs.nestjs.com/websockets/adapter in order to determine if the connection is valid. The code below is a skeleton of how you can go about validating incoming connections. export class AuthenticatedWsIoAdapter extends IoAdapter {
createIOServer(port: number, options?: any): any {
options.allowRequest = async (request, allowFunction) => {
// Do your validation here
// return allowFunction(null, true); Success
// return allowFunction("FORBIDDEN", false); Failure
}
return super.createIOServer(port, options);
}
} By extending All you need to do then is use the adapter like so: app.useWebSocketAdapter(new AuthenticatedWsIoAdapter(app)); After this, connections will be validated through the Hope this helps you, @tonivj5 and let me know if I missed anything! |
Thanks @ChrisKatsaras! I didn't explore that solution 👏. The only problem I still see, it's that it's out of the DI (injector) 😢. I inject a service and depend on providers to check user authenticity. |
Hey @tonivj5 , I just used @ChrisKatsaras 's answer and it works great!
The IoAdapter's constructor has an I've used it like such: import { INestApplicationContext } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { extract, parse } from 'query-string';
export class AuthenticatedSocketIoAdapter extends IoAdapter {
private readonly jwtService: JwtService;
constructor(private app: INestApplicationContext) {
super(app);
this.jwtService = this.app.get(JwtService);
}
createIOServer(port: number, options?: SocketIO.ServerOptions): any {
options.allowRequest = async (request, allowFunction) => {
const token = parse(extract(request.url))?.token as string;
const verified = token && (await this.jwtService.verify(token));
if (verified) {
return allowFunction(null, true);
}
return allowFunction('Unauthorized', false);
};
return super.createIOServer(port, options);
}
} Hope this helps! |
Thanks for adding that information @xWiiLLz ! Glad I could help out 😄 |
Thank you very much @xWiiLLz and @ChrisKatsaras! 👏👍 I will test it with my use-case 😃 |
I've been playing around with @ChrisKatsaras & @xWiiLLz 's approach, but am struggling to figure out how you would be able to tell which user is authenticated within future messages after the handshake. In the past, with vanilla socket.io, I'd handle the handshake in a similar way but also store the user's ID (sometimes even a copy of the user) on the socket during the handshake, which could then be retrieved by accessing Is it possible to replicate this with Nest's approach to WebSockets? |
@Jamie452 I'm also looking into this; curious if you find something out. I'll report here / blog about it if I do :) |
Hey, I can share the workaround I'm using in my personal project later tonight. Will get back to you both! |
@dsebastien what I ended up doing was a combination of the two previously posted suggestions. It's far from perfect but lets me get on for the time being. I'm checking the JWT is valid in Here's what my code looks like, interested to see how you handled it @xWiiLLz; authenticated-socket-io.adapter.ts export class AuthenticatedSocketIoAdapter extends IoAdapter {
private httpStrategy: HttpStrategy
constructor(
private app: INestApplicationContext,
) {
super(app)
this.httpStrategy = this.app.get(HttpStrategy)
}
create(port: number, options?: any): any {
return this.createIOServer(port, options)
}
createIOServer(port: number, options?: ServerOptions): any {
options.allowRequest = async (request, allowFunction) => {
try {
// This is where I validate the user has a valid JWT token, but don't necessarily care who they are
await this.httpStrategy.validate(request.headers.authorization.replace("Bearer ", ""))
} catch (e) {
console.warn("Failed to authenticate user:", e)
return allowFunction("Unauthorized", false)
}
return allowFunction(null, true)
}
return server
}
} app.gateway.ts interface SocketWithUserData extends Socket {
userData: UserJWTData
}
async handleConnection(socket: SocketWithUserData, ...args: any[]) {
try {
socket.userData = await this.httpStrategy.validate(socket.handshake.headers.authorization.replace("Bearer ", ""))
} catch (e) {
this.logger.error("Socket disconnected within handleConnection() in AppGateway:", e)
socket.disconnect(true)
return
}
}
@SubscribeMessage("whoAmI")
onWhoAmI(socket: SocketWithUserData, data: any): void {
socket.emit("whoAmI", socket.userData)
} |
Hey, So I guess my code is doing something similar to @Jamie452 , but using guards and the JwtService. Here's the relevant parts. connected-socket.ts import * as SocketIO from 'socket.io';
export interface ConnectedSocket extends SocketIO.Socket {
conn: SocketIO.EngineSocket & {
token: string;
userId: number;
};
} base-gateway.ts @UsePipes(new ValidationPipe())
@UseInterceptors(ClassSerializerInterceptor)
@UseGuards(SocketSessionGuard)
@UseFilters(new SocketExceptionFilter())
export class BaseGateway implements OnGatewayConnection, OnGatewayDisconnect {
constructor(protected jwtService: JwtService) {}
@WebSocketServer()
protected server: Server;
async handleConnection(client: ConnectedSocket, ...args: any[]) {
const authorized = await SocketSessionGuard.verifyToken(
this.jwtService,
client,
client.handshake.query.token,
);
if (!authorized) {
throw new UnauthorizedException();
}
console.log(`${client.conn.userId} Connected to gateway`);
}
...
} socket-session.guard.ts @Injectable()
export class SocketSessionGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
console.log('SocketSession activated');
const client = context?.switchToWs()?.getClient<ConnectedSocket>();
return SocketSessionGuard.verifyToken(
this.jwtService,
client,
client.request['token'],
);
}
static async verifyToken(
jwtService: JwtService,
socket: ConnectedSocket,
token?: string,
) {
if (
socket.conn.userId &&
(await jwtService.verifyAsync(socket.conn.token))
) {
return true;
}
if (!token) return false;
socket.conn.token = token;
const { sub } = await jwtService.decode(token);
socket.conn.userId = sub;
console.log(`Setting connection userId to "${sub}"`);
return true;
}
} Hope this helps 😊 Sorry about not providing this sample earlier, was in the middle of moving and completely forgot about that thread! |
@xWiiLLz why do you have |
I assume the validateConnection is just called once (at the handshake). With the guard, you can verify the token (expiry, token refresh logic...) at every call, as you would with a REST controller. |
Is there a way we can get the namespace the socket is trying to connect, in the adapter? I want to run different verifications for different namespace connections and the request URL doesn't seem to hold the namespace in it. |
Possible implementation of authorization for native ws and WsAdapter with correct handshake response. Auth can be different for every gateway (as it required in my case). some.gateway.ts
P.S. |
Hey guys, @WebSocketGateway({
cookie: 'sid',
path: '/ws',
allowUpgrades: false,
transports: ['websocket'],
verifyClient: (info, cb) => {
console.log(info.req);
cb(true, 200, 'you are verified', {
cookie: 'sid: hello world',
});
},
}) the verifyClient option gets past down to the websocket-server.js. There is no need to change anything, except to provide the needed information, that this option exist. I am not sure, how this could work with Authgards and session etc. If you have an idea how to use it with express-session, let me know. For more information, I have submitted a feature request I hope this helps anybody. |
Thank you @xWiiLLz for the great examples! Especially the adapter and guard! const token = request.headers.authorization; // in adapter
const token = client.handshake.headers.authorization; // in guard And on the client side you just add the token when creating the const socket = io(environment.host, {
extraHeaders: {
authorization: userToken,
},
}); |
Maybe we should add some samples provided here in the docs |
@DmitrySergeyev Hi, your code is very interesting, but how do you access wss._server ? |
same happened in #9231 https://socket.io/docs/v4/middlewares/ |
It looks good but should be pass the JWT token in URL query parameter ? |
It also how i deal with auth in native websocket, by storing information i need into context when connected |
I don't know if it is a bug or not, |
Based on Manuelbaun's answer,
|
This issue has been open since 2018, and I think none of the solutions are too great, when NestJS itself should just allow attaching guards to handleConnection. Why was this marked as won't fix without at least an explanation given? |
@jackykwandesign has the best answer. Thank you. |
In my case fetching user data is asynchronous, so I found this workaround: import { ConnectedSocket, OnGatewayConnection, SubscribeMessage, WebSocketGateway, } from '@nestjs/websockets';
import { UseGuards } from '@nestjs/common';
import { WsAuthGuard } from './ws-auth.guard';
import { WsAuthService } from './ws-auth.service';
import { Socket } from 'socket.io';
@WebSocketGateway(3001, { cors: true, transports: ['websocket'], path: '/io' })
@UseGuards(WsAuthGuard)
export class SocketIoController implements OnGatewayConnection {
constructor(private readonly wsAuthService: WsAuthService) {}
handleConnection(@ConnectedSocket() client: Socket) {
this.wsAuthService.onClientConnect(client);
}
@SubscribeMessage('test')
async test(@ConnectedSocket() client: Socket) {
return JSON.stringify(client.data);
}
} import { Injectable } from '@nestjs/common';
import { Socket } from 'socket.io';
@Injectable()
export class WsAuthService {
private initializationMap = new Map<string, Promise<any>>();
onClientConnect(socket: Socket) {
this.initializationMap.set(socket.id, this.initialize(socket));
}
async finishInitialization(socket: Socket): Promise<any> {
await this.initializationMap.get(socket.id);
}
private async initialize(socket: Socket): Promise<any> {
// asynchronously get user data
const user = await new Promise((resolve) =>
setTimeout(() => resolve({ id: 1234, hasPower: true, foo: 'bar' }), 2000),
);
// store result to socket data
socket.data.user = user;
this.initializationMap.delete(socket.id);
}
} import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsAuthService } from './ws-auth.service';
import { Socket } from 'socket.io';
@Injectable()
export class WsAuthGuard implements CanActivate {
constructor(private readonly wsAuthService: WsAuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const client = context.switchToWs().getClient<Socket>();
// wait until client data initialization will be finished
await this.wsAuthService.finishInitialization(client);
// your canActivate logic
return client.data.user.hasPower;
}
} The logic:
Cheers 🍻 |
Strongly support you! The auth should be at handleConnection, to auth it in message handler is totally of no sense. If the connection is created securely, the auth check is useless here. We wrote our 1st version of code in Java, and now want to migrate to NestJS and hit this problem. |
I'm submitting a...
Current behavior
Currently gateway’s handleConnection method don’t support guards.
Expected behavior
It would be helpful set current user on socket after successfully connected.
Minimal reproduction of the problem with instructions
What is the motivation / use case for changing the behavior?
Environment
The text was updated successfully, but these errors were encountered: