fix(security): harden auth, upload, and API configuration
- Lock public registration to first-user bootstrap (403 afterwards) - Require JwtAuthGuard on upload + whitelist png/jpg/webp/gif (SVG/XSS blocked) - Add global throttling (login/register 5/min, upload 10/min) - Add helmet security headers; serve uploads with nosniff - Replace permissive CORS (origin:true+credentials) with CORS_ORIGINS whitelist - Disable Swagger outside development; sanitize 500 error responses - Enforce 32+ char JWT_SECRET; make token expiry configurable (TOKEN_EXPIRES_IN) - Re-check user in DB on every JWT validation (revocation on user delete) - Dummy bcrypt compare to prevent login user-enumeration via timing - Map malformed BigInt inputs to 400 instead of 500 - Widen .gitignore to .env* and add apps/api/.env.example - Disable Nuxt devtools and sourcemaps
This commit is contained in:
@@ -2,6 +2,7 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PrismaService } from '../../prisma/prisma.service';
|
||||
|
||||
/**
|
||||
* Shape of the JWT we issue.
|
||||
@@ -15,11 +16,17 @@ export interface JwtPayload {
|
||||
|
||||
@Injectable()
|
||||
export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
constructor(config: ConfigService) {
|
||||
constructor(
|
||||
config: ConfigService,
|
||||
private readonly prisma: PrismaService,
|
||||
) {
|
||||
const secret = config.get<string>('JWT_SECRET');
|
||||
if (!secret) {
|
||||
throw new Error('JWT_SECRET is not configured');
|
||||
}
|
||||
if (secret.length < 32) {
|
||||
throw new Error('JWT_SECRET must be at least 32 characters');
|
||||
}
|
||||
super({
|
||||
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
||||
ignoreExpiration: false,
|
||||
@@ -29,12 +36,19 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
|
||||
/**
|
||||
* Runs on every authenticated request. The returned object becomes
|
||||
* `request.user` for downstream controllers.
|
||||
* `request.user` for downstream controllers. The user is re-checked in
|
||||
* the database so tokens of deleted users stop working immediately.
|
||||
*/
|
||||
validate(payload: JwtPayload): { id: bigint; username: string } {
|
||||
async validate(payload: JwtPayload): Promise<{ id: bigint; username: string }> {
|
||||
if (!payload?.sub || !payload.username) {
|
||||
throw new UnauthorizedException('Invalid token payload');
|
||||
}
|
||||
return { id: BigInt(payload.sub), username: payload.username };
|
||||
const user = await this.prisma.user
|
||||
.findUnique({ where: { id: BigInt(payload.sub) } })
|
||||
.catch(() => null);
|
||||
if (!user || user.username !== payload.username) {
|
||||
throw new UnauthorizedException('Invalid token');
|
||||
}
|
||||
return { id: user.id, username: user.username };
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user