Implement authentication guard with JWT

main
MiguelMLorente 2025-12-06 22:13:51 +01:00
parent 797a8bbfda
commit 8a590b42f2
9 changed files with 248 additions and 24 deletions

133
package-lock.json generated
View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
@ -2368,6 +2369,19 @@
} }
} }
}, },
"node_modules/@nestjs/jwt": {
"version": "11.0.2",
"resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-11.0.2.tgz",
"integrity": "sha512-rK8aE/3/Ma45gAWfCksAXUNbOoSOUudU0Kn3rT39htPF7wsYXtKfjALKeKKJbFrIWbLjsbqfXX5bIJNvgBugGA==",
"license": "MIT",
"dependencies": {
"@types/jsonwebtoken": "9.0.10",
"jsonwebtoken": "9.0.3"
},
"peerDependencies": {
"@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0"
}
},
"node_modules/@nestjs/platform-express": { "node_modules/@nestjs/platform-express": {
"version": "11.1.9", "version": "11.1.9",
"resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz", "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-11.1.9.tgz",
@ -2903,6 +2917,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/jsonwebtoken": {
"version": "9.0.10",
"resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz",
"integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==",
"license": "MIT",
"dependencies": {
"@types/ms": "*",
"@types/node": "*"
}
},
"node_modules/@types/methods": { "node_modules/@types/methods": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz", "resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
@ -2917,11 +2941,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ms": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
"license": "MIT"
},
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.19.1", "version": "22.19.1",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz",
"integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==",
"devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -4302,6 +4331,12 @@
"ieee754": "^1.1.13" "ieee754": "^1.1.13"
} }
}, },
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -5015,6 +5050,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -7428,6 +7472,49 @@
"graceful-fs": "^4.1.6" "graceful-fs": "^4.1.6"
} }
}, },
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/keyv": { "node_modules/keyv": {
"version": "4.5.4", "version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@ -7525,6 +7612,42 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.memoize": { "node_modules/lodash.memoize": {
"version": "4.1.2", "version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@ -7539,6 +7662,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/log-symbols": { "node_modules/log-symbols": {
"version": "4.1.0", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz",
@ -8897,7 +9026,6 @@
"version": "7.7.3", "version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
@ -10369,7 +10497,6 @@
"version": "6.21.0", "version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
"devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/universalify": { "node_modules/universalify": {

View File

@ -17,6 +17,7 @@
"dependencies": { "dependencies": {
"@nestjs/common": "^11.0.1", "@nestjs/common": "^11.0.1",
"@nestjs/core": "^11.0.1", "@nestjs/core": "^11.0.1",
"@nestjs/jwt": "^11.0.2",
"@nestjs/platform-express": "^11.0.1", "@nestjs/platform-express": "^11.0.1",
"@nestjs/typeorm": "^11.0.0", "@nestjs/typeorm": "^11.0.0",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",

View File

@ -13,6 +13,9 @@ import { Session } from './dto/session';
import { TokensController } from './controller/tokens.controller'; import { TokensController } from './controller/tokens.controller';
import { TokenService } from './service/token.service'; import { TokenService } from './service/token.service';
import { Token } from './dto/token'; import { Token } from './dto/token';
import { JwtModule } from '@nestjs/jwt';
import { AuthGuard } from './auth.guard';
import { AuthService } from './service/auth.service';
@Module({ @Module({
imports: [ imports: [
@ -28,6 +31,11 @@ import { Token } from './dto/token';
synchronize: true, synchronize: true,
}), }),
TypeOrmModule.forFeature([User, Purchase, Session, Token]), TypeOrmModule.forFeature([User, Purchase, Session, Token]),
JwtModule.register({
global: true,
secret: process.env.JWT_SECRET || 'jwt-key',
signOptions: { expiresIn: '4 WEEKS' },
}),
], ],
controllers: [ controllers: [
AccessController, AccessController,
@ -41,6 +49,11 @@ import { Token } from './dto/token';
StripeService, StripeService,
SessionService, SessionService,
TokenService, TokenService,
AuthService,
{
provide: 'APP_GUARD',
useClass: AuthGuard,
},
], ],
}) })
export class AppModule {} export class AppModule {}

55
src/auth.guard.ts Normal file
View File

@ -0,0 +1,55 @@
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { SetMetadata } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
@Injectable()
export class AuthGuard implements CanActivate {
private readonly jwtSecret = process.env.JWT_SECRET || 'jwt-key';
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true;
}
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.jwtSecret,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}

View File

@ -6,37 +6,36 @@ import {
UnauthorizedException, UnauthorizedException,
} from '@nestjs/common'; } from '@nestjs/common';
import { UserService } from '../service/user.service'; import { UserService } from '../service/user.service';
import { AuthService } from 'src/service/auth.service';
import { Public } from 'src/auth.guard';
@Controller('/access') @Controller('/access')
export class AccessController { export class AccessController {
private readonly logger = new Logger(AccessController.name); private readonly logger = new Logger(AccessController.name);
constructor(private readonly userService: UserService) {} constructor(
private readonly userService: UserService,
private readonly authService: AuthService,
) {}
@Post('/login') @Post('/login')
@Public()
public async login( public async login(
@Body('name') name: string, @Body('name') name: string,
@Body('password') password: string, @Body('password') password: string,
) { ) {
this.logger.debug('Received login request'); this.logger.debug('Received login request');
const user = await this.userService.getUserByName(name); return await this.authService.signIn(name, password);
if (user.password !== password) {
throw new UnauthorizedException();
}
return {
userId: user.id,
};
} }
@Post('/register') @Post('/register')
@Public()
public async new( public async new(
@Body('name') name: string, @Body('name') name: string,
@Body('password') password: string, @Body('password') password: string,
) { ) {
this.logger.debug('Received register request'); this.logger.debug('Received register request');
const user = await this.userService.createUser(name, password); await this.userService.createUser(name, password);
return { return await this.authService.signIn(name, password);
userId: user.id,
};
} }
} }

View File

@ -1,4 +1,4 @@
import { Body, Controller, Logger, Post } from '@nestjs/common'; import { Body, Controller, Logger, Post, Req } from '@nestjs/common';
import { PurchaseService } from '../service/purchase.service'; import { PurchaseService } from '../service/purchase.service';
import { UserService } from '../service/user.service'; import { UserService } from '../service/user.service';
import { PurchaseItem } from '../dto/purchase-item'; import { PurchaseItem } from '../dto/purchase-item';
@ -23,9 +23,10 @@ export class BuyController {
@Post('/start') @Post('/start')
public async startPurchaseFlow( public async startPurchaseFlow(
@Body('userId') userId: string, @Req() request,
@Body('quantity') quantity: number, @Body('quantity') quantity: number,
) { ) {
const userId = request.userId;
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
const purchase = await this.purchaseService.createPurchase( const purchase = await this.purchaseService.createPurchase(
user, user,
@ -41,7 +42,8 @@ export class BuyController {
} }
@Post('/complete') @Post('/complete')
public async complete(@Body('userId') userId: string) { public async complete(@Req() request) {
const userId = request.userId;
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
const purchases = await this.purchaseService.getPurchasesForUser(user); const purchases = await this.purchaseService.getPurchasesForUser(user);
purchases purchases

View File

@ -1,4 +1,4 @@
import { Body, Controller, Get, Post, Query } from '@nestjs/common'; import { Body, Controller, Get, Post, Query, Req } from '@nestjs/common';
import { SessionService } from 'src/service/session.service'; import { SessionService } from 'src/service/session.service';
import { UserService } from 'src/service/user.service'; import { UserService } from 'src/service/user.service';
@ -18,7 +18,8 @@ export class SessionController {
} }
@Get() @Get()
public async getAllSessions(@Query('userId') userId: string) { public async getAllSessions(@Req() request) {
const userId = request.userId;
const sessions = await this.sessionService.getAllSessions(); const sessions = await this.sessionService.getAllSessions();
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
return sessions.map((session) => ({ return sessions.map((session) => ({
@ -33,18 +34,20 @@ export class SessionController {
@Post('/join') @Post('/join')
public async joinSession( public async joinSession(
@Body('userId') userId: string, @Req() request,
@Body('sessionId') sessionId: string, @Body('sessionId') sessionId: string,
) { ) {
const userId = request.userId;
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
await this.sessionService.joinSession(user, sessionId); await this.sessionService.joinSession(user, sessionId);
} }
@Post('/leave') @Post('/leave')
public async leaveSession( public async leaveSession(
@Body('userId') userId: string, @Req() request,
@Body('sessionId') sessionId: string, @Body('sessionId') sessionId: string,
) { ) {
const userId = request.userId;
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
await this.sessionService.leaveSession(user, sessionId); await this.sessionService.leaveSession(user, sessionId);
} }

View File

@ -1,4 +1,4 @@
import { Controller, Get, Logger, Query } from '@nestjs/common'; import { Controller, Get, Logger, Query, Req } from '@nestjs/common';
import { TokenService } from 'src/service/token.service'; import { TokenService } from 'src/service/token.service';
import { UserService } from 'src/service/user.service'; import { UserService } from 'src/service/user.service';
@ -12,7 +12,8 @@ export class TokensController {
) {} ) {}
@Get() @Get()
public async getTokenCount(@Query('userId') userId: string) { public async getTokenCount(@Req() request) {
const userId = request.userId;
this.logger.debug(`Finding tokens for user: ${userId}`); this.logger.debug(`Finding tokens for user: ${userId}`);
const user = await this.userService.getUserById(userId); const user = await this.userService.getUserById(userId);
const token = await this.tokenService.getTokens(user); const token = await this.tokenService.getTokens(user);

View File

@ -0,0 +1,23 @@
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserService } from './user.service';
@Injectable()
export class AuthService {
constructor(
private usersService: UserService,
private jwtService: JwtService,
) {}
async signIn(name: string, password: string) {
const user = await this.usersService.getUserByName(name);
if (user?.password !== password) {
throw new UnauthorizedException();
}
const payload = { userId: user.id };
return {
jwtToken: await this.jwtService.signAsync(payload),
};
}
}