[ ] 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.
There is no information about auth in websockets microservice in the docs.
Can we use middlewares / interceptors / guards / nest-passport for it ?
@WebSocketGateway({
middlewares: [AuthenticationGatewayMiddleware]
})
But as I see it's no longer supported, isn't it?
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?handleConnection()
inside gateway.Can you explain plz the best practices how to do it inside Nestjs ecosystem? ;)
Thanks.
[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
handleConnection
level (it might be available in the future #882)client
instance so you can easily close the connection)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}`
}
},
});
@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.
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
}
@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!
Did you resolved the problem with dependencie injection?
@TristanMarion your solution does not work with WebSocket, and WsAdapter
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);
}
}
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?
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?
Found the issue, package conflict between "@nestjs/websockets": "^6.8.2" & "@nestjs/core": "^6.0.0",. I need to upgrade all @nestjs packages
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?
@mranon0007 yes
Most helpful comment
This is how I achieved it :
Guard :
Gateway :