We use cookies to enhance your experience on the site
CodeWorlds

Authentication - identifying the legionaries

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.

Packages - equipping the guardhouse

Let's start with the tools we will need throughout the module:

1npm install @nestjs/passport passport passport-jwt @nestjs/jwt

passport
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.

The legion's register - the User entity

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.

Checking papers at the entrance - the DTO

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.

Admitting a recruit

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.

Summary

The gate stands, the register works, recruits enlist safely:

  • authentication answers who the visitor is; authorization - what he may do; the first always precedes the second,
  • the module rests on four packages:
    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
    ,
  • DTO validation runs from general to specific:
    @IsNotEmpty
    , then
    @IsEmail
    , then
    @MinLength
    ,
  • decorators see the request's contents only - a uniqueness check needs a database query and belongs to the service,
  • registration has four steps: validate, check existence, hash, save,
  • a hash is one-way: we never decrypt passwords, we hash again and compare with
    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.

Go to CodeWorlds