A man in Roman armour walks up to the camp gate. The armour proves nothing - it can be bought, or stripped from the fallen. Before the sentry lets him in, he must answer one question: who exactly are you?
We call that question authentication. Do not confuse it with the question that comes later, at the treasury door: what are you allowed to do? - that is authorization. The order cannot be reversed: first we establish identity, only then privileges. You cannot check the rank of someone you have not yet recognised.
In this module we will build the whole gate. This lesson is its foundation: the legion's register and the safe admission of recruits.
Let's start with the tools we will need throughout the module:
1npm install @nestjs/passport passport passport-jwt @nestjs/jwtpassport is the authentication library itself, @nestjs/passport wires it into NestJS. @nestjs/jwt issues passes, and passport-jwt teaches the sentry to read them. For now we are only installing them - you will meet each in turn in the coming lessons.Every legionary must have an entry in the register:
1@Entity('users')
2export class User {
3 @PrimaryGeneratedColumn()
4 id: number;
5
6 @Column({ unique: true })
7 username: string;
8
9 @Column({ unique: true })
10 email: string;
11
12 @Column()
13 @Exclude()
14 password: string;
15}You read entities comfortably by now - that is knowledge from the TypeORM module.
unique: true on username and email guarantees that two legionaries cannot enlist under the same name.Let's pause at
@Exclude(), because it is the only new element here and at the same time the most common security hole in beginner applications. This decorator does not remove the field from the database and encrypts nothing - the password still sits in its column. It acts on the way out: when NestJS turns an entity into a JSON response, a field marked @Exclude() is left out.Without it, a plain
GET /users/1 would send the password hash back to the client along with the rest of the data. Nobody notices in testing, because the application works correctly - and passwords leak on every request. Remember this rule, @name: a field that must never leave the server gets @Exclude() the moment you create it, not sometime later.Before data reaches the register, it has to be inspected. A DTO with validation rules does that:
1export class RegisterDto {
2 @IsNotEmpty()
3 username: string;
4
5 @IsNotEmpty()
6 @IsEmail()
7 email: string;
8
9 @IsNotEmpty()
10 @MinLength(8)
11 password: string;
12}Note the order of the checks - it runs from the most basic to the most specific. First
@IsNotEmpty() asks whether the field was filled in at all. Then @IsEmail() examines whether what was typed has the shape of an address. Finally @MinLength(8) imposes a requirement on the content.That order has a practical point: there is no sense examining the format of an empty field. And one more distinction, because it tends to confuse. All these decorators inspect the request alone - they look only at what arrived and know nothing about the database. The question "is this username already taken?" requires looking into the register, so no decorator can ask it. That check belongs to the service, and you are about to see it.
Registration is four steps in a fixed order:
1async register(dto: RegisterDto): Promise<User> {
2 const existing = await this.userRepository.findOne({
3 where: { username: dto.username },
4 });
5
6 if (existing) {
7 throw new ConflictException('A legionary by that name already serves');
8 }
9
10 const hashedPassword = await bcrypt.hash(dto.password, 10);
11
12 return this.userRepository.save({
13 ...dto,
14 password: hashedPassword,
15 });
16}Let's walk through them. Validation already happened, automatically - the DTO data arrived here checked. The uniqueness check is the database query no decorator could perform; on a collision we throw
ConflictException, that is a 409 response. Hashing the password - and only now the save.The order of the last two steps is non-negotiable: the password becomes a hash before the save, never after. Nothing readable ever reaches the database.
What actually is this
bcrypt.hash(dto.password, 10)? Hashing is a one-way transformation - a string emerges from the password, and there is no way back to the original. It is not encryption, because encryption can by definition be reversed with a key. So at login we decrypt nothing; we hash the supplied password again and compare the results with bcrypt.compare(). Even you, with full access to the database, cannot read your user's password - and that is exactly the point.The number
10 is the salt rounds, controlling the cost of the computation. We will return to it and to the rest of bcrypt's mechanics in a separate lesson on hashing.The gate stands, the register works, recruits enlist safely:
passport, @nestjs/passport, @nestjs/jwt, passport-jwt,@Exclude() does not touch the database - it only prevents a field from being sent in an API response; without it passwords leak on every GET,@IsNotEmpty, then @IsEmail, then @MinLength,bcrypt.compare().In the next lesson we will take hashing apart - you will learn what a salt is and why slow hashing can be a virtue. For now remember: authentication asks "who are you", and the password does not exist in the legion's register - only its one-way imprint does.