in my NestJS application I'm using the local passport strategy to secure the login route which then returns a jwt. This process is working properly.
Now I implemented logic in my local-strategy to prevent brute-force (in addition to the overall rate limit in main.ts) as described here. When I throw a TooManyRequests HttpException I also want to set the 'Retry-After' header to be able to give the user helpful information in the frontend. But I don't have access to the response object in the guard. I tried to implement an interceptor which did not help. In addition I only have access to the calculated value for 'Retry-after' in my local strategy, as the value is calculated there.
What's the right approach to set this header? Here's my code which is not working properly yet.
@Injectable()
export class LocalStrategy extends PassportStrategy(Strategy) {
private maxWrongAttemptsByIpPerDay = 100;
private maxConsecutiveFailsByUsernameAndIp = 5;
private limiterSlowBruteByIp = new RateLimiterMongo({
storeClient: this.connection,
keyPrefix: 'login_fail_ip_per_day',
points: this.maxWrongAttemptsByIpPerDay,
duration: 60 * 60 * 24,
blockDuration: 60 * 60 * 24, // Block for 1 day, if 100 wrong attempts per day
})
public limiterConsecutiveFailsByUsernameAndIp = new RateLimiterMongo({
storeClient: this.connection,
keyPrefix: 'login_fail_consecutive_username_and_ip',
points: this.maxConsecutiveFailsByUsernameAndIp,
duration: 60 * 60 * 24 * 90, // Store number for 90 days since first fail
blockDuration: 60 * 60 // Block for 1 hour
})
constructor(private authService: AuthService, @InjectConnection() private connection: Connection) {
super({passReqToCallback: true});
}
async validate(req: Request, username: string, password: string): Promise<any> {
const usernameIpKey = this.authService.getUsernameIPkey(username, req['ip'])
const [resUsernameAndIp, resSlowByIp] = await Promise.all([
this.limiterConsecutiveFailsByUsernameAndIp.get(usernameIpKey),
this.limiterSlowBruteByIp.get(req['ip'])
]);
let retrySecs = 0;
// Check if IP or Username + IP is already blocked
if (resSlowByIp !== null && resSlowByIp.consumedPoints > this.maxWrongAttemptsByIpPerDay) {
retrySecs = Math.round(resSlowByIp.msBeforeNext / 1000) || 1;
} else if (resUsernameAndIp !== null && resUsernameAndIp.consumedPoints > this.maxConsecutiveFailsByUsernameAndIp) {
retrySecs = Math.round(resUsernameAndIp.msBeforeNext / 1000) || 1;
}
if (retrySecs > 0) {
// res.set('Retry-After', String(retrySecs));
throw new HttpException('Too many requests', HttpStatus.TOO_MANY_REQUESTS);
} else {
const user = await this.authService.validateUser(username, password);
if (!user) {
// Consume 1 point from limiters on wrong attempt and block if limits reached
try {
const promises = [this.limiterSlowBruteByIp.consume(req['ip'])];
if (!user) {
// Count failed attempts by Username + IP only for registered users
promises.push(this.limiterConsecutiveFailsByUsernameAndIp.consume(usernameIpKey));
}
await promises;
throw new UnauthorizedException();
} catch (rlRejected) {
if (rlRejected instanceof Error) {
throw rlRejected;
} else {
// res.set('Retry-After', String(Math.round(rlRejected.msBeforeNext / 1000) || 1));
throw new HttpException('Too many requests', HttpStatus.TOO_MANY_REQUESTS);
}
}
} else {
if (resUsernameAndIp !== null && resUsernameAndIp.consumedPoints > 0) {
// Reset on successful authorisation
await this.limiterConsecutiveFailsByUsernameAndIp.delete(usernameIpKey);
}
return user;
}
}
}
}
Thank you very much in advance!
There's a couple of options you have instead of using the strategy as you're currently doing.
like you said, in a guard throw the exception where you have access to the ExecutionContext
so you can context.switchToHttp().getResponse()
for the response object and be able to set headers as needed (you're currently trying to do this in a strategy file)
Use a package like nestjs-throttler and use its decorators to help with setting up rate limiting