Skip to content
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

Websockets auth #1254

Closed
ed-fruty opened this issue Nov 1, 2018 · 12 comments
Closed

Websockets auth #1254

ed-fruty opened this issue Nov 1, 2018 · 12 comments

Comments

@ed-fruty
Copy link

ed-fruty commented Nov 1, 2018

I'm submitting a...


[ ] Regression 
[ ] Bug report
[ ] Feature request
[ x] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead post your question on Stack Overflow.

Current behavior

There is no information about auth in websockets microservice in the docs.
Can we use middlewares / interceptors / guards / nest-passport for it ?


  1. Middlewares.
    As I see, it was issue Middleware for socket.io not working #637 . In the end @adrien2p suggest to use such one:
@WebSocketGateway({
    middlewares: [AuthenticationGatewayMiddleware]
})

But as I see it's no longer supported, isn't it?


  1. Guards
    From the docs I understood that guards is like access policy. It executes before every method call and check permission to handler. But auth logic might not be here, isn't it?

  1. Interceptors
    Interceptors is not called at connection time. Only on emiting, so I think it's not our case.

  1. handleConnection() inside gateway.
    Is this is the place where auth might be?

Can you explain plz the best practices how to do it inside Nestjs ecosystem? ;)

Thanks.

Environment


[System Information]
OS Version     : Linux 4.15
NodeJS Version : v8.9.1
NPM Version    : 6.1.0
[Nest Information]
websockets version : 5.4.0
common version     : 5.4.0
core version       : 5.4.0
cqrs version       : 5.1.1
@kamilmysliwiec
Copy link
Member

kamilmysliwiec commented Nov 1, 2018

  1. Middleware are no longer supported.
  2. Guards would fit here very well. However, Nest doesn't support guards at handleConnection level (it might be available in the future Support adding guards on gateway’s handleConnection method. #882)
  3. As you said
  4. For now, this is a place where the authentication logic could be potentially located (inside method you have a reference to the client instance so you can easily close the connection)

@jsdevtom
Copy link

Here is an example implementation:

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { bindNodeCallback, Observable, of } from 'rxjs';
import { JwtPayload } from './jwt-payload.interface';
import * as jwt from 'jsonwebtoken';
import { catchError, flatMap, map } from 'rxjs/operators';
import { User } from '../user/user.entity';
import { AuthService } from './auth.service';

@Injectable()
export class JwtWsGuard implements CanActivate {
  constructor(
    protected readonly authService: AuthService,
  ) {
  }

  canActivate(
    context: ExecutionContext,
  ): Observable<boolean> {
    const data = context.switchToWs().getData();
    const authHeader = data.headers.authorization;
    const authToken = authHeader.substring(7, authHeader.length);
    const verify: (...args: any[]) => Observable<JwtPayload> = bindNodeCallback(jwt.verify) as any;

    return verify(authToken, process.env.JWT_SECRET_KEY, null)
      .pipe(
        flatMap(jwtPayload => this.authService.validateUser(jwtPayload)),
        catchError(e => {
          console.error(e);
          return of(null);
        }),
        map((user: User | null) => {
          const isVerified = Boolean(user);

          if (!isVerified) {
            throw new UnauthorizedException();
          }

          return isVerified;
        }),
      );
  }
}

Where on the client you would authenticate by passing 'dummy' headers in the data object like so:

    const websocket = this.websocketService
      .createConnection(`ws://localhost:8080`);
    const jsonWebToken = getJwtSomehow();

    websocket.subscribe(
      (msg) => console.log('message received: ',  msg),
      (err) => console.log(err),
      () => console.log('complete')
    );

    websocket.next({
      event: 'YOUR_EVENT_NAME',
      data: {
        // ...
        headers: {
          authorization: `Bearer ${jsonWebToken}`
        }
      },
    });

@TristanMarion
Copy link

TristanMarion commented Jan 10, 2019

@jsdevtom Thanks for the inspiration 😄
I prefered to use context.switchToWs().getClient() and then access the handshake property (which contains the headers and http cookies automatically sent by the socket.io client on connection).
Thus, you don't need to get the jwt in your client JavaScript and you can avoid security issues.

@TristanMarion
Copy link

This is how I achieved it :

Guard :

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
/*
    Custom imports for AuthService, jwt secret, etc...
*/
import * as jwt from 'jsonwebtoken';

@Injectable()
export class WsJwtGuard implements CanActivate {
    constructor(private authService: AuthService) {}

    async canActivate(context: ExecutionContext) {
        const client = context.switchToWs().getClient();
        const cookies: string[] = client.handshake.headers.cookie.split('; ');
        const authToken = cookies.find(cookie => cookie.startsWith('jwt')).split('=')[1];
        const jwtPayload: JwtPayload = <JwtPayload> jwt.verify(authToken, yourSecret);
        const user: User = await this.authService.validateUser(jwtPayload);
        // Bonus if you need to access your user after the guard 
        context.switchToWs().getData().user = user;
        return Boolean(user);
    }
}

Gateway :

@UseGuards(WsJwtGuard)
@SubscribeMessage('events')
onEvent(client, data) {
    // data.user contains your user if you set it in the guard
}

@ruslanguns
Copy link

@TristanMarion it does not work to me...

Actually getting two kind of issues, one of them is the following when I am doing it:

  @UseGuards(JwtWsGuard)
  @SubscribeMessage('nuevoUsuario')
  onEvent(client, data) {
    this.logger.log(client);
    this.logger.log(data);
  }

I am not able to run start:dev since I get the following:

[nodemon] starting `node dist/main`
[Nest] 4292   - 2019-06-07 21:04   [NestFactory] Starting Nest application...
[Nest] 4292   - 2019-06-07 21:04   [InstanceLoader] AppModule dependencies initialized +40ms
[Nest] 4292   - 2019-06-07 21:04   [InstanceLoader] MongooseModule dependencies initialized +1ms
[Nest] 4292   - 2019-06-07 21:04   [ExceptionHandler] Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument at index [0] is available in the WssModule context. +1msError: Nest can't resolve dependencies of the UsersService (?). Please make sure that the argument at index [0] is available in the WssModule context.
    at Injector.lookupComponentInExports (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:180:19)
    at processTicksAndRejections (internal/process/task_queues.js:89:5)
    at async Injector.resolveComponentInstance (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:143:33)
    at async resolveParam (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:96:38)
    at async Promise.all (index 0)
    at async Injector.resolveConstructorParams (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:112:27)
    at async Injector.loadInstance (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:78:9)
    at async Injector.loadProvider (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\injector.js:35:9)
    at async Promise.all (index 3)
    at async InstanceLoader.createInstancesOfProviders (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\instance-loader.js:41:9)
    at async D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\instance-loader.js:27:13
    at async Promise.all (index 9)
    at async InstanceLoader.createInstances (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\instance-loader.js:26:9)
    at async InstanceLoader.createInstancesOfDependencies (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\injector\instance-loader.js:16:9)
    at async D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\nest-factory.js:75:17
    at async Function.asyncRun (D:\wamp\www\learn\nestjs\nestjs-restapi\node_modules\@nestjs\core\errors\exceptions-zone.js:17:13)
 1: 00007FF7D3CA674F napi_wrap+120927
 2: 00007FF7D3C4FE26 uv_loop_fork+44550
 3: 00007FF7D3BF93B9 v8::internal::StackGuard::ArchiveSpacePerThread+26393
 4: 00007FF7D4238A69 v8::internal::OFStreamBase::xsputn+5065
 5: 00007FF7D4237FA8 v8::internal::OFStreamBase::xsputn+2312
 6: 00007FF7D42382D8 v8::internal::OFStreamBase::xsputn+3128
 7: 00007FF7D42380FD v8::internal::OFStreamBase::xsputn+2653
 8: 00007FF7D47F2C16 v8::internal::NativesCollection<0>::GetScriptsSource+662454
[nodemon] app crashed - waiting for file changes before starting...

Any work around? Thanks in advance!

@alonssro
Copy link

Did you resolved the problem with dependencie injection?

@vladborsh
Copy link

@TristanMarion your solution does not work with WebSocket, and WsAdapter

@prdatur
Copy link

prdatur commented Aug 10, 2019

I do not know if this works with WsAdapter, but here is my solution.
headers and cookies for me is no option, I use the data implementation.
Within the openid.guard.ts the request will only be processed if you return true, so any WsException will send back to the client a json object with the exception message. If it returns true, your action will be processed and as in the previous answer, the data object will have a property "authorizedUser" with the decoded id_token data.

Hope this helps someone.

My ws server is socket.io

openid.guard.ts

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { WsException } from '@nestjs/websockets';
import { AuthService } from './lib/auth-service/AuthService';

@Injectable()
export class OpenidGuard implements CanActivate {

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

    const { id_token } = context.switchToWs().getData();
    if (typeof id_token === 'undefined') {
      throw new WsException('Missing id_token');
    }

    return AuthService.verifyAccessToken(id_token)
      .then((decodedToken) => {
        context.switchToWs().getData().authorizedUser = decodedToken;
        return true;
      })
    .catch((error) => {
      throw new WsException(error.message);
    });
  }
}

AuthService.ts

import * as jwt from 'jsonwebtoken';
import * as ms from 'ms';
import * as jwksClient from 'jwks-rsa';

import config from '../../../config/config';

const APIUrl = config.openId.url;

export class AuthService {

  static async verifyAccessToken(accessToken) {
    if (typeof accessToken === 'undefined' || accessToken.trim() === '') {
      throw new Error('Missing access token.');
    }

    const client = jwksClient({
      cache: true,
      cacheMaxEntries: 5, // Default value
      cacheMaxAge: ms('10h'), // Default value
      jwksUri: `${APIUrl}/jwks`,
    });

    function getKey(header, callback) {
      client.getSigningKey(header.kid, (err, key) => {
        // @ts-ignore
        callback(null, key.publicKey || key.rsaPublicKey);
      });
    }

    return new Promise((resolve) => {

      jwt.verify(accessToken.trim(), getKey, {}, (err, decoded) => {
        if (typeof err !== 'undefined' && err !== null) {
          throw err;
        }
        resolve(decoded);
      });
    });
  }

}

gateway

import { SubscribeMessage, WebSocketGateway } from '@nestjs/websockets';
import { UseGuards } from '@nestjs/common';
import { OpenidGuard } from '../openid.guard';

@WebSocketGateway({
  path: '/user-service/gateway',
  origins: '*:*',
  transports: ['websocket'],
})
export class ItemsGateway {

  @UseGuards(OpenidGuard)
  @SubscribeMessage('get-items')
  async getItems(client: any, payload: any) {
    console.log('get items requested', payload);
  }
}

@fredtma
Copy link

fredtma commented Oct 10, 2019

Hi,

I am using the same strategy as @TristanMarion but getting a host.SetType is not a function error
has anyone encountered this and how to fix it?
image

@fredtma
Copy link

fredtma commented Oct 10, 2019

Hi,

I am using the same strategy as @TristanMarion but getting a host.SetType is not a function error
has anyone encountered this and how to fix it?
image

Found the issue, package conflict between "@nestjs/websockets": "^6.8.2" & "@nestjs/core": "^6.0.0",. I need to upgrade all @nestjs packages

@yusufkhan07
Copy link

const data = context.switchToWs().getData();

Is using switchToWs safe to use? Socket.io might be using WS or it might be using another protocol for communication. In the later case, is using switchToWs safe & would it work?

@kamilmysliwiec
Copy link
Member

@mranon0007 yes

@nestjs nestjs locked as resolved and limited conversation to collaborators Oct 16, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants