34

I am using the default passport jwt AuthGuard for my project. That works for my post & get routes fine when setting the authentication header.

Now I want to use Nestjs Gateways as well with socket.io on the client-side, but I don't know how to send the access_token to the gateway?

That is basically my Gateway:

@WebSocketGateway()
export class UserGateway {

  entityManager = getManager();

  @UseGuards(AuthGuard('jwt'))
  @SubscribeMessage('getUserList')
  async handleMessage(client: any, payload: any) {
    const results = await this.entityManager.find(UserEntity);
    console.log(results);
    return this.entityToClientUser(results);
  }

And on the client I'm sending like this:

this.socket.emit('getUserList', users => {
    console.log(users);
    this.userListSub.next(users);
});

How and where do I add the jwt access_token? The documentation of nestjs misses that point completely for Websockets. All they say is, that the Guards work exactly the same for websockets as they do for post / get etc. See here

5 Answers 5

39

While the question is answered, I want to point out the Guard is not usable to prevent unauthorized users from establishing a connection.

It's only usable to guard specific events.

The handleConnection method of a class annotated with @WebSocketGateway is called before canActivate of your Guard.

I end up using something like this in my Gateway class:

  async handleConnection(client: Socket) {
    const payload = this.authService.verify(
      client.handshake.headers.authorization,
    );
    const user = await this.usersService.findOne(payload.userId);
    
    !user && client.disconnect();
  }
Sign up to request clarification or add additional context in comments.

3 Comments

I will just add that if you only verify token on handleConnection, if the token is invalid there is a short amount of time the client is connected and able to send other events before failed verification disconnects him.
You can implement OnGatewayConnection to force yourself to implement handleConnection method.
@TGPerson, either way the client would be able to connect before it will get rejected. If you're going with the other implementation, having a Guard service implementing canActivate, you'll have to put that guard on specific event messages, which means the user will be able to connect, anyway.
35

For anyone looking for a solution. Here it is:

@UseGuards(WsGuard)
@SubscribeMessage('yourRoute')
async saveUser(socket: Socket, data: any) {
    let auth_token = socket.handshake.headers.authorization;
    // get the token itself without "Bearer"
    auth_token = auth_token.split(' ')[1];
}

On the client side you add the authorization header like this:

this.socketOptions = {
    transportOptions: {
        polling: {
            extraHeaders: {
                Authorization: 'your token', // 'Bearer h93t4293t49jt34j9rferek...'
            }
        }
    }
};
// ...
this.socket = io.connect('http://localhost:4200/', this.socketOptions);
// ...

Afterwards you have access to the token on every request serverside like in the example.

Here also the WsGuard I implemented.

@Injectable()
export class WsGuard implements CanActivate {

    constructor(private userService: UserService) {
    }

    canActivate(
        context: any,
    ): boolean | any | Promise<boolean | any> | Observable<boolean | any> {
        const bearerToken = context.args[0].handshake.headers.authorization.split(' ')[1];
        try {
            const decoded = jwt.verify(bearerToken, jwtConstants.secret) as any;
            return new Promise((resolve, reject) => {
                return this.userService.findByUsername(decoded.username).then(user => {
                    if (user) {
                        resolve(user);
                    } else {
                        reject(false);
                    }
                });

             });
        } catch (ex) {
            console.log(ex);
            return false;
        }
    }
}

I simply check if I can find a user with the username from the decoded token in my database with my user service. I am sure you could make this implementation cleaner, but it works.

8 Comments

Hi, i'm having the same issue, can you please add some more code?
You have to add the token to the socket connection on the client side when you initialize the socket itself. There you can set headers. I edited my answer for the client side.
Hi, no problem! The WsGuard is my implementation with CanActivate. I Add my implementation of the guard in the answer aswell :)
If the token is decoded correctly, it means it has been signed by the server already. Isn't it overkill to re-check if the user is actually in the DB ? Since the server signed it in the first place.
The code says polling. I think if it's polling then it's not actual web-sockets but their emulation through polling. Am I wrong?
|
17

Thanks! At the end i implemented a Guard that like the jwt guard puts the user inside the request. At the end I'm using the query string method from the socket client to pass the auth token This is my implementation:

import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { Socket } from 'socket.io';
import { AuthService } from '../auth/auth.service';
import { User } from '../auth/entity/user.entity';

@Injectable()
export class WsJwtGuard implements CanActivate {
    private logger: Logger = new Logger(WsJwtGuard.name);

    constructor(private authService: AuthService) { }

    async canActivate(context: ExecutionContext): Promise<boolean> {

        try {
            const client: Socket = context.switchToWs().getClient<Socket>();
            const authToken: string = client.handshake?.query?.token;
            const user: User = await this.authService.verifyUser(authToken);
            client.join(`house_${user?.house?.id}`);
            context.switchToHttp().getRequest().user = user
    
            return Boolean(user);
        } catch (err) {
            throw new WsException(err.message);
        }
    }
}

4 Comments

Isn't it terrible to pass the token in the query string (= the URL) ? This is very likely to leak.
@YohjiNakamoto Where would it be better to pass it otherwise ?
I think handling authorization and disconnection client is guard is much cleaner and comply to Nest-js recommendation.
Also, this line context.switchToHttp().getRequest().user = user which is used for switching to httpRequest to attach user data is redundant and not necessary. You can directly add user data to socket variable by modifying socket type as Socket & { [ k:string ]: any}
4

@arminfro answer works, but it has some drawbacks as pointed out by @tg-person

I will just add that if you only verify token on handleConnection, if the token is invalid there is a short amount of time the client is connected and able to send other events before failed verification disconnects him

To be able to address this issue, you can instead implement afterInit() method and intercept the request headers. After that, you can handle the token authorization accordingly. This will directly close the connection if the token is not valid.

afterInit(server: Server) {
  server.use((socket: Socket, next) => {
    socket.handshake.headers.authorization
    const [type, token] = socket.handshake.headers.authorization?.split(' ') ?? [];
    const bearerToken = type === 'Bearer' ? token : undefined;

    if (bearerToken) { // handle token validation
      next()
    } else {
      next(new Error("Empty Token!"));
    }
  })
}

1 Comment

Just to add to this, the afterInit method is inside IGatewayInit interface that can be implemented by the gateway
0

I had the same issue. I did just as the official docs said. I used the same logic for HTTP request but changed my code to gain access to the request handshake.

I noticed you have to use a separate Guard and Interceptor. Like, make them specific to your WebSocket module, class or endpoints.

See my implementation below.

    async canActivate(context: ExecutionContext): Promise<boolean> {

    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    const request = context.switchToHttp().getRequest();
    //access the request handshake to get the header 
    const token = request?.handshake?.headers?.authorization?.split('Bearer ')[1]

    if (isPublic) return true;

    if (!token) throw new UnauthorizedException('no token provided');

    try {
      const user = (await this.jwtService.decode(
        token,
      )) as JwtAuthResponseInterface;
      request.user = user;
      return true;
    } catch (err) {
      throw new UnauthorizedException(err);
    }
  }

I hope this helps.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.