[ ] Regression
[ ] Bug report
[x] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.
Currently gateway鈥檚 handleConnection method don鈥檛 support guards.
It would be helpful set current user on socket after successfully connected.
Nest version: X.Y.Z
For Tooling issues:
- Node version: XX
- Platform:
Others:
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 authGuard
on handleConnection
, I think the following approach can solve this problem alternatively.
// 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);
}
}
}
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,
A lot of time has passed since my last nestjs gateway coding.. so maybe I cannot explain it accurately. Please be aware of it.
Websocket Exception Filters
and Guards
are not do their job on HandleConnection
and it is intended as kamilmysliwiec said.
Look what I found at https://github.com/nestjs/nest/issues/336
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 socket.disconnect
from https://socket.io/docs/server-api/#socket-disconnect-close
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 handleConnection
method in the future? @kamilmysliwiec
@Hwan-seok I've also noticed one minor issue with your solution.
In handleConnection
we verify and disconnect the connection if it's invalid. This works fine except there is a period of time between when the handleConnection
function starts and when the authentication returns the result where the connection can receive events without being properly authenticated.
Has anyone found a solution for this? Any help is greatly appreciated! 馃槂
As @ChrisKatsaras, I have suffered from this too. However, I don't think that that lapse of time is a minor issue :sweat_smile:
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
I forgot to attach await
statement on my above example.
You're using await
statement on const user = await whatEverFindOrVerifyUser();
right?
Because it does not make sense that jumps over Connection handshake
step on lifecycle
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 IoAdapter
, we can use createIOServer
and write custom validation for the allowRequest
function.
Here is a more detailed description of allowRequest
:
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 allowRequest
function before entering handleConnection
in our gateway.
Hope this helps you, @tonivj5 and let me know if I missed anything!
Hey @tonivj5 , I just used @ChrisKatsaras 's answer and it works great!
The only problem I still see, it's that it's out of the DI (injector) :cry:
The IoAdapter's constructor has an INestApplicationContext
, which means you can use DI! :smile:
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! :clap::+1:
I will test it with my use-case :smiley:
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 socket.user.id
(or whatever) during future messages.
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 :)
@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!
@Jamie452 I'm also looking into this; curious if you find something out. I'll report here / blog about it if I do :)
@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 options.allowRequest
and then storing the user's data on the socket in the gateways handleConnection
method, where you can get the handshake headers using socket.handshake.headers.authorization
.
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 @UseGuards(SocketSessionGuard)
that's not actually doing anything? Why not inject the guard in the constructor and make the method non-static? Also why are you putting the token in the query rather than the header?
@xWiiLLz why do you have
@UseGuards(SocketSessionGuard)
that's not actually doing anything? Why not inject the guard in the constructor and make the method non-static? Also why are you putting the token in the query rather than the header?
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.
Most helpful comment
So what about to add support guards for handleConnection?