Skip to content

Commit

Permalink
Add authentication in webhook (#175)
Browse files Browse the repository at this point in the history
* Make verification asynchronous

* Add adapter for authentication

* Add jwt auth guard

* Implement authentication for gateway
  • Loading branch information
takumihara authored Dec 25, 2023
1 parent d576276 commit 07efc2d
Show file tree
Hide file tree
Showing 8 changed files with 117 additions and 5 deletions.
30 changes: 30 additions & 0 deletions backend/src/adapters/authentication-io.adapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { INestApplicationContext } from '@nestjs/common';
import { IoAdapter } from '@nestjs/platform-socket.io';
import { AuthService } from 'src/auth/auth.service';

// This adapter can be used to authenticate socket.io connections
// Not using this for now because we want to provide some events for non-authenticated users
// For implementation: https://github.com/nestjs/nest/issues/882#issuecomment-632698668
export class AuthenticationIoAdapter extends IoAdapter {
private readonly authService: AuthService;
constructor(private app: INestApplicationContext) {
super(app);
this.authService = this.app.get(AuthService);
}
createIOServer(port: number, options?: any): any {
options.allowRequest = async (request, allowFunction) => {
const token = request.headers.cookie
?.split('; ')
?.find((c) => c.startsWith('token='))
?.split('=')[1];

try {
await this.authService.verifyAccessToken(token);
return allowFunction(null, true);
} catch (error) {
return allowFunction('Unauthorized', false);
}
};
return super.createIOServer(port, options);
}
}
8 changes: 6 additions & 2 deletions backend/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,11 @@ import { PrismaModule } from 'src/prisma/prisma.module';
import { UserModule } from 'src/user/user.module';
import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { JwtStrategy, JwtWithout2FAStrategy } from './jwt.strategy';
import {
JwtStrategy,
WsJwtStrategy,
JwtWithout2FAStrategy,
} from './jwt.strategy';

export const jwtConstants = {
publicKey: process.env.JWT_PUBLIC_KEY,
Expand All @@ -29,7 +33,7 @@ export const jwtConstants = {
UserModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, JwtWithout2FAStrategy],
providers: [AuthService, JwtStrategy, WsJwtStrategy, JwtWithout2FAStrategy],
exports: [AuthService],
})
export class AuthModule {}
2 changes: 1 addition & 1 deletion backend/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class AuthService {
}

async verifyAccessToken(accessToken: string) {
const payload = this.jwtService.verify(accessToken, {
const payload = await this.jwtService.verifyAsync(accessToken, {
publicKey: jwtConstants.publicKey,
});
const user = await this.prisma.user.findUnique({
Expand Down
3 changes: 3 additions & 0 deletions backend/src/auth/jwt-auth.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export class JwtAuthGuard extends AuthGuard('jwt') {}

@Injectable()
export class JwtGuardWithout2FA extends AuthGuard('jwt-without-2fa') {}

@Injectable()
export class WsJwtAuthGuard extends AuthGuard('ws-jwt') {}
32 changes: 32 additions & 0 deletions backend/src/auth/jwt.strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,35 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
return user;
}
}

@Injectable()
export class WsJwtStrategy extends PassportStrategy(Strategy, 'ws-jwt') {
constructor(private userService: UserService) {
super({
jwtFromRequest: ExtractJwt.fromExtractors([
(request) => {
// Socket is provided even though it is not in the types
const socket = request as any;
return socket.request.headers.cookie?.split('token=')[1];
},
]),
secretOrKey: jwtConstants.publicKey,
});
}

async validate(payload: {
userId: number;
isTwoFactorAuthenticated: boolean;
}) {
const user = await this.userService.findOne(payload.userId);

if (!user) {
throw new UnauthorizedException();
}
if (user.twoFactorEnabled && !payload.isTwoFactorAuthenticated) {
throw new UnauthorizedException();
}

return user;
}
}
2 changes: 2 additions & 0 deletions backend/src/events/events.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Module } from '@nestjs/common';
import { EventsGateway } from './events.gateway';
import { PongMatchGateway } from './pong-match.gateway';
import { AuthModule } from 'src/auth/auth.module';

@Module({
providers: [EventsGateway, PongMatchGateway],
imports: [AuthModule],
})
export class EventsModule {}
15 changes: 13 additions & 2 deletions backend/src/events/pong-match.gateway.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Logger } from '@nestjs/common';
import { Logger, UseGuards } from '@nestjs/common';
import {
ConnectedSocket,
OnGatewayDisconnect,
Expand All @@ -7,19 +7,29 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Namespace, Socket } from 'socket.io';
import { AuthService } from 'src/auth/auth.service';
import { UserGuardWs } from 'src/user/user.guard-ws';
import { v4 } from 'uuid';

@WebSocketGateway({
namespace: '/pong-match',
})
export class PongMatchGateway implements OnGatewayDisconnect {
constructor(private readonly authService: AuthService) {}

@WebSocketServer()
private server: Namespace;
private logger: Logger = new Logger('PongMatchGateway');
private waitingClient: string | null = null;

handleConnection(client: Socket) {
async handleConnection(client: Socket) {
this.logger.log(`connect: ${client.id} `);
try {
const token = client.request.headers.cookie?.split('token=')[1];
if (!token) return;
const user = await this.authService.verifyAccessToken(token);
(client as any).user = user;
} catch {}
}

handleDisconnect(client: Socket) {
Expand All @@ -29,6 +39,7 @@ export class PongMatchGateway implements OnGatewayDisconnect {
}
}

@UseGuards(UserGuardWs)
@SubscribeMessage('request')
async request(@ConnectedSocket() client: Socket) {
this.logger.log(`request: ${client.id}`);
Expand Down
30 changes: 30 additions & 0 deletions backend/src/user/user.guard-ws.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { CanActivate, ExecutionContext } from '@nestjs/common';
// import { WsException } from '@nestjs/websockets';
import { AuthService } from 'src/auth/auth.service';

export class UserGuardWs implements CanActivate {
constructor(private authService: AuthService) {}

async canActivate(context: ExecutionContext) {
const client = context.switchToWs().getClient();

// This is cashing the user in the client object.
// This means that the user could be outdated.
if (client.user) return true;

// When handleConnection is called, the connection is already created.
// This means that clients can send messages before `client.user` is set.
// The code below makes sure that authorized users don't get unauthorized.
const token = client.request.headers.cookie?.split('token=')[1];
if (!token) return false;

try {
const user = await this.authService.verifyAccessToken(token);
if (!user) return false;
client.user = user;
} catch {
return false;
}
return true;
}
}

0 comments on commit 07efc2d

Please sign in to comment.