Password Strategy
Install
npm i -S @bluelibs/security-bundle @bluelibs/password-bundle
import { SecurityBundle } from "@bluelibs/security-bundle";import { PasswordBundle } from "@bluelibs/password-bundle";const kernel = new Kernel({ bundles: [new SecurityBundle(), new PasswordBundle()],});
Purpose
This is an authentication strategy implemented for the SecurityBundle
. It does not expose any routes, nor does it send any emails and it doesn't care about your persistence layer (it is database-agnostic). It just focuses on the low-level handling of passwords for a user.
import { PasswordBundle } from "@bluelibs/password-bundle";
new PasswordBundle({ // All of these are optional, these are the defaults failedAuthenticationAttempts: { lockAfter: 10, cooldown: "10m", // After how much time of invalid passwords you can try again to login }, resetPassword: { cooldown: "5m", // After how much time you can request ANOTHER password reset request expiresAfter: "2h", // How much time do we allow for the token to exist },});
It all starts with a user:
import { SecurityService } from "@bluelibs/security-bundle";import { PasswordService } from "@bluelibs/password-bundle";
const securityService = container.get(SecurityService);const passwordService = container.get(PasswordService);
const userId = await this.securityService.createUser();
// Now that we have the user we attach options to itawait passwordService.attach(userId, { username: "USERNAME", // in most situation the username is the email, in fact. email: "USERNAME@MAIL.COM"; password: "PASSWORD", isEmailVerified: false;});
Finding a userId by username:
const userId = await passwordService.findUserIdByUsername("username");
Checking is password is valid:
const isValid = await passwordService.isPasswordValid(userId, "PASSWORD");
Note that password validation will also register invalid attempts, and depending on how you have configured the bundle it can temporarily suspend the user.
If you want to bypass this functionality you can pass a 3rd argument:
passwordService.isPasswordValid(userId, "PASSWORD", { failedAuthenticationAttemptsProcessing: false;})
Forgot Password
This contains the full flow of a forgot password process. First we get a token to reset the password and send it by email, then we check if the token is valid and we reset it with it.
const token = await passwordService.createTokenForPasswordReset(userId);
const isTokenValid = await passwordService.isResetPasswordTokenValid( userId, token);
await passwordService.resetPassword(userId, token, "NEW_PASSWORD");
Set Password
Overriding a password is as easy as:
await passwordService.setPassword(userId, "NEW_PASSWORD");
The passwords are hashed individually with the user's salt via sha512
Events
This events can be imported from the package. So you can listen to them.
- PasswordAuthenticationStrategyAttachedEvent
- A new strategy has been attached to the user
- PasswordResetRequestedEvent
- The user has requested a forgot password
- PasswordResetWithTokenEvent
- The user has reset his password
- PasswordInvalidEvent
- A user has tried to login but password was invalid
- PasswordValidatedEvent
- This is a successful password validation (this can happen in change password as well)
- This can be regarded as a user logged in, but you have this event at
Security
level.
- UserLockedAfterFailedAttemptsEvent
- We emit this after too many invalid password entries
Exceptions
- CooldownException
- This is triggered when he tries to login after many failed login attempts
- PasswordResetExpiredException
- Someone tried to reset his password with a token that expired. Look at
expiresAfter
in config.
- Someone tried to reset his password with a token that expired. Look at
- ResetPasswordInvalidTokenException
- Someone tried to reset password with an invalid token
Data Model
The data we store to manage everything in the strategy looks like this:
export interface IPasswordAuthenticationStrategy { username: string; email?: string;
isEmailVerified?: boolean; emailVerificationToken?: string;
// Unique salt per user salt: string; passwordHash: string; lastSuccessfulPasswordValidationAt: Date;
// Resetting the password resetPasswordVerificationToken: string; // optional when resetting the password resetPasswordRequestedAt: Date;
// Failed login attempts currentFailedLoginAttempts: number; lastFailedLoginAttemptAt: Date;}
You can update things such as username
and email
:
import { PasswordService } from "@bluelibs/password-bundle";
const passwordService = container.get(PasswordService);
await passwordService.updateData(userId, { username: "new-username", email: "new-email",});
const data = await passwordService.getData(userId);
Custom Hasher Service
@Service()class MyHasherService implements IHasherService { // generateSalt(userId?: any): string; // getHashedPassword(plainPassword, salt?: string): string; // generateToken(userId?: any): string;}
class AppBundle extends Bundle { async init() { this.container.set({ id: HASHER_SERVICE_TOKEN, type: MyHasherService, }); }}
Meta
Summary
This is the raw functionality of handling passwords. A complete integration for this is done inside XAuthBundle that is X-Framework compatible.
Boilerplates
- COMING SOON
Challenges
- What does
cooldown
insideresetPasword
represent? (1p) - Can I use this bundle in such a way that I prevent a password brute-force attack? How? (2p)
- Write an app, in which a user has had too many invalid authentication attempts is sent a warning email and suspend the user? (2p)