We use cookies to enhance your experience on the site
CodeWorlds

Authentication - identifying legionaries

Gate guard! Consul Caesar.js has noticed that unwanted guests are getting into our forts, and some legionaries are impersonating legion members. It's time to learn authentication - the magical art of distinguishing real legionaries from impostors!

What is Authentication in the world of legionaries?

Imagine you are the chief guard of the largest fortress in the empire. Every day, dozens of people try to enter the fort:

  • Real legion members - they have the right to access
  • Guests from allied forts - they need special permission
  • Merchants and suppliers - temporary access to specific areas
  • Spies and enemies - they absolutely must not get into the system!

Authentication is the process of verifying identity:

  • Who is trying to log in? (Identity)
  • Do they have access rights? (Authentication)
  • What can they access? (Authorization)
  • How long can they stay? (Session Management)

Authentication Basics in NestJS

Installing required packages

1# Basic authentication packages
2npm install @nestjs/passport passport passport-local passport-jwt
3npm install @nestjs/jwt bcrypt
4npm install --save-dev @types/passport-local @types/passport-jwt @types/bcrypt

User Entity - legion registry

1// entities/user.entity.ts
2import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
3import { Exclude } from 'class-transformer';
4
5@Entity('users')
6export class User {
7 @PrimaryGeneratedColumn()
8 id: number;
9
10 @Column({ unique: true })
11 username: string;
12
13 @Column({ unique: true })
14 email: string;
15
16 @Column()
17 @Exclude() // Never return the password in the response
18 password: string;
19
20 @Column({ default: 'LEGION_MEMBER' })
21 role: string; // CENTURION, OPTIO, LEGION_MEMBER, GUEST
22
23 @Column({ nullable: true })
24 firstName: string;
25
26 @Column({ nullable: true })
27 lastName: string;
28
29 @Column({ nullable: true })
30 cohortName: string;
31
32 @Column({ default: true })
33 isActive: boolean;
34
35 @CreateDateColumn()
36 joinedAt: Date;
37
38 @UpdateDateColumn()
39 lastLoginAt: Date;
40
41 @Column({ nullable: true })
42 lastLoginIp: string;
43
44 @Column({ default: 0 })
45 failedLoginAttempts: number;
46
47 @Column({ nullable: true })
48 lockedUntil: Date;
49}

DTO for login

1// dto/auth.dto.ts
2import { IsEmail, IsNotEmpty, MinLength, IsOptional } from 'class-validator';
3
4export class RegisterDto {
5 @IsNotEmpty({ message: 'Legionary name is required!' })
6 @MinLength(3, { message: 'Name must be at least 3 characters long' })
7 username: string;
8
9 @IsEmail({}, { message: 'Please provide a valid email address' })
10 email: string;
11
12 @IsNotEmpty({ message: 'Password is required!' })
13 @MinLength(6, { message: 'Password must be at least 6 characters long' })
14 password: string;
15
16 @IsOptional()
17 firstName?: string;
18
19 @IsOptional()
20 lastName?: string;
21
22 @IsOptional()
23 cohortName?: string;
24}
25
26export class LoginDto {
27 @IsNotEmpty({ message: 'Username is required!' })
28 username: string;
29
30 @IsNotEmpty({ message: 'Password is required!' })
31 password: string;
32}
33
34export class ChangePasswordDto {
35 @IsNotEmpty({ message: 'Current password is required!' })
36 currentPassword: string;
37
38 @IsNotEmpty({ message: 'New password is required!' })
39 @MinLength(6, { message: 'New password must be at least 6 characters long' })
40 newPassword: string;
41}

Password Hashing - encrypting access codes

1// services/crypto.service.ts
2import { Injectable } from '@nestjs/common';
3import * as bcrypt from 'bcrypt';
4
5@Injectable()
6export class CryptoService {
7 private readonly saltRounds = 12;
8
9 async hashPassword(password: string): Promise<string> {
10 // Generate a salt for each password individually
11 const salt = await bcrypt.genSalt(this.saltRounds);
12 return await bcrypt.hash(password, salt);
13 }
14
15 async comparePasswords(plainPassword: string, hashedPassword: string): Promise<boolean> {
16 return await bcrypt.compare(plainPassword, hashedPassword);
17 }
18
19 generateRandomToken(length: number = 32): string {
20 const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
21 let result = '';
22 for (let i = 0; i < length; i++) {
23 result += chars.charAt(Math.floor(Math.random() * chars.length));
24 }
25 return result;
26 }
27
28 // Check password strength
29 checkPasswordStrength(password: string): {
30 score: number;
31 feedback: string[];
32 isStrong: boolean;
33 } {
34 const feedback: string[] = [];
35 let score = 0;
36
37 // Length
38 if (password.length >= 8) score += 1;
39 else feedback.push('Password should be at least 8 characters long');
40
41 // Lowercase letters
42 if (/[a-z]/.test(password)) score += 1;
43 else feedback.push('Add lowercase letters');
44
45 // Uppercase letters 
46 if (/[A-Z]/.test(password)) score += 1;
47 else feedback.push('Add uppercase letters');
48
49 // Digits
50 if (/d/.test(password)) score += 1;
51 else feedback.push('Add digits');
52
53 // Special characters
54 if (/[!@#$%^&*(),.?":{}|<>]/.test(password)) score += 1;
55 else feedback.push('Add special characters');
56
57 return {
58 score,
59 feedback,
60 isStrong: score >= 4,
61 };
62 }
63}

User Service - managing the soldiers

1// services/user.service.ts
2import { Injectable, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
3import { InjectRepository } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5import { User } from '../entities/user.entity';
6import { RegisterDto, ChangePasswordDto } from '../dto/auth.dto';
7import { CryptoService } from './crypto.service';
8
9@Injectable()
10export class UserService {
11 constructor(
12 @InjectRepository(User)
13 private userRepository: Repository<User>,
14 private cryptoService: CryptoService,
15 ) {}
16
17 async createUser(registerDto: RegisterDto): Promise<User> {
18 // Check if the user already exists
19 const existingUser = await this.userRepository.findOne({
20 where: [{ username: registerDto.username }, { email: registerDto.email }],
21 });
22
23 if (existingUser) {
24 if (existingUser.username === registerDto.username) {
25 throw new ConflictException('A legionary with this name already exists in the legion!');
26 }
27 if (existingUser.email === registerDto.email) {
28 throw new ConflictException('This email address is already in use!');
29 }
30 }
31
32 // Check password strength
33 const passwordStrength = this.cryptoService.checkPasswordStrength(registerDto.password);
34 if (!passwordStrength.isStrong) {
35 throw new BadRequestException(`Password is too weak: ${passwordStrength.feedback.join(', ')}`);
36 }
37
38 // Hash the password
39 const hashedPassword = await this.cryptoService.hashPassword(registerDto.password);
40
41 // Create a new user
42 const user = this.userRepository.create({
43 ...registerDto,
44 password: hashedPassword,
45 role: 'LEGION_MEMBER', // Default role
46 });
47
48 return await this.userRepository.save(user);
49 }
50
51 async findByUsername(username: string): Promise<User | null> {
52 return await this.userRepository.findOne({
53 where: { username },
54 });
55 }
56
57 async findByEmail(email: string): Promise<User | null> {
58 return await this.userRepository.findOne({
59 where: { email },
60 });
61 }
62
63 async findById(id: number): Promise<User | null> {
64 return await this.userRepository.findOne({
65 where: { id },
66 });
67 }
68
69 async validateUser(username: string, password: string): Promise<User | null> {
70 const user = await this.findByUsername(username);
71 
72 if (!user) {
73 return null;
74 }
75
76 // Check if the account is locked
77 if (user.lockedUntil && user.lockedUntil > new Date()) {
78 throw new BadRequestException('Account has been temporarily locked. Please try again later.');
79 }
80
81 // Check if the account is active
82 if (!user.isActive) {
83 throw new BadRequestException('Account has been deactivated.');
84 }
85
86 // Verify the password
87 const isPasswordValid = await this.cryptoService.comparePasswords(password, user.password);
88 
89 if (!isPasswordValid) {
90 // Increment the failed attempts counter
91 await this.incrementFailedLoginAttempts(user.id);
92 return null;
93 }
94
95 // Reset the failed attempts counter on successful login
96 await this.resetFailedLoginAttempts(user.id);
97 
98 return user;
99 }
100
101 async updateLastLogin(userId: number, ip: string): Promise<void> {
102 await this.userRepository.update(userId, {
103 lastLoginAt: new Date(),
104 lastLoginIp: ip,
105 });
106 }
107
108 async changePassword(userId: number, changePasswordDto: ChangePasswordDto): Promise<void> {
109 const user = await this.findById(userId);
110 
111 if (!user) {
112 throw new NotFoundException('User not found');
113 }
114
115 // Check the current password
116 const isCurrentPasswordValid = await this.cryptoService.comparePasswords(
117 changePasswordDto.currentPassword,
118 user.password
119 );
120
121 if (!isCurrentPasswordValid) {
122 throw new BadRequestException('Current password is incorrect!');
123 }
124
125 // Check the strength of the new password
126 const passwordStrength = this.cryptoService.checkPasswordStrength(changePasswordDto.newPassword);
127 if (!passwordStrength.isStrong) {
128 throw new BadRequestException(`New password is too weak: ${passwordStrength.feedback.join(', ')}`);
129 }
130
131 // Hash the new password
132 const hashedPassword = await this.cryptoService.hashPassword(changePasswordDto.newPassword);
133 
134 await this.userRepository.update(userId, {
135 password: hashedPassword,
136 });
137 }
138
139 private async incrementFailedLoginAttempts(userId: number): Promise<void> {
140 const user = await this.findById(userId);
141 const newAttempts = user.failedLoginAttempts + 1;
142 
143 const updateData: Partial<User> = {
144 failedLoginAttempts: newAttempts,
145 };
146
147 // Lock the account after 5 failed attempts for 15 minutes
148 if (newAttempts >= 5) {
149 const lockDuration = 15 * 60 * 1000; // 15 minutes
150 updateData.lockedUntil = new Date(Date.now() + lockDuration);
151 }
152
153 await this.userRepository.update(userId, updateData);
154 }
155
156 private async resetFailedLoginAttempts(userId: number): Promise<void> {
157 await this.userRepository.update(userId, {
158 failedLoginAttempts: 0,
159 lockedUntil: null,
160 });
161 }
162
163 async getUserProfile(userId: number): Promise<Partial<User>> {
164 const user = await this.findById(userId);
165 
166 if (!user) {
167 throw new NotFoundException('Legionary profile not found');
168 }
169
170 // Do not return the password and sensitive data
171 const { password, failedLoginAttempts, lockedUntil, ...profile } = user;
172 return profile;
173 }
174
175 async updateProfile(userId: number, updateData: Partial<User>): Promise<User> {
176 // Remove fields that the user cannot change
177 const { password, role, isActive, failedLoginAttempts, lockedUntil, ...allowedUpdates } = updateData;
178 
179 await this.userRepository.update(userId, allowedUpdates);
180 return await this.findById(userId);
181 }
182
183 async getAllUsers(): Promise<Partial<User>[]> {
184 const users = await this.userRepository.find({
185 order: { joinedAt: 'DESC' },
186 });
187
188 // Remove sensitive data
189 return users.map(({ password, ...user }) => user);
190 }
191
192 async deactivateUser(userId: number): Promise<void> {
193 await this.userRepository.update(userId, { isActive: false });
194 }
195
196 async activateUser(userId: number): Promise<void> {
197 await this.userRepository.update(userId, { 
198 isActive: true,
199 failedLoginAttempts: 0,
200 lockedUntil: null,
201 });
202 }
203}

Auth Controller - access gate

1// controllers/auth.controller.ts
2import { 
3 Controller, 
4 Post, 
5 Body, 
6 Get, 
7 Put, 
8 UseGuards, 
9 Request,
10 HttpCode,
11 HttpStatus,
12 Ip
13} from '@nestjs/common';
14import { UserService } from '../services/user.service';
15import { RegisterDto, LoginDto, ChangePasswordDto } from '../dto/auth.dto';
16import { User } from '../entities/user.entity';
17
18@Controller('auth')
19export class AuthController {
20 constructor(private userService: UserService) {}
21
22 @Post('register')
23 async register(@Body() registerDto: RegisterDto): Promise<{ message: string; user: Partial<User> }> {
24 const user = await this.userService.createUser(registerDto);
25 
26 // Do not return the password
27 const { password, ...userWithoutPassword } = user;
28 
29 return {
30 message: 'New legionary has been successfully registered in the legion!',
31 user: userWithoutPassword,
32 };
33 }
34
35 @Post('login')
36 @HttpCode(HttpStatus.OK)
37 async login(
38 @Body() loginDto: LoginDto,
39 @Ip() ip: string,
40 ): Promise<{ message: string; user: Partial<User> }> {
41 const user = await this.userService.validateUser(loginDto.username, loginDto.password);
42 
43 if (!user) {
44 throw new BadRequestException('Invalid login credentials!');
45 }
46
47 // Update last login information
48 await this.userService.updateLastLogin(user.id, ip);
49 
50 const { password, ...userWithoutPassword } = user;
51 
52 return {
53 message: `Welcome back to the system, ${user.firstName || user.username}!`,
54 user: userWithoutPassword,
55 };
56 }
57
58 @Get('profile')
59 // @UseGuards(JwtAuthGuard) // We will add this in the next exercise
60 async getProfile(@Request() req): Promise<Partial<User>> {
61 // Temporarily - in a real application user.id will come from JWT
62 const userId = req.user?.id || 1;
63 return await this.userService.getUserProfile(userId);
64 }
65
66 @Put('change-password')
67 // @UseGuards(JwtAuthGuard)
68 async changePassword(
69 @Request() req,
70 @Body() changePasswordDto: ChangePasswordDto,
71 ): Promise<{ message: string }> {
72 const userId = req.user?.id || 1;
73 await this.userService.changePassword(userId, changePasswordDto);
74 
75 return {
76 message: 'Password has been successfully changed!',
77 };
78 }
79
80 @Put('profile')
81 // @UseGuards(JwtAuthGuard)
82 async updateProfile(
83 @Request() req,
84 @Body() updateData: Partial<User>,
85 ): Promise<{ message: string; user: Partial<User> }> {
86 const userId = req.user?.id || 1;
87 const updatedUser = await this.userService.updateProfile(userId, updateData);
88 
89 const { password, ...userWithoutPassword } = updatedUser;
90 
91 return {
92 message: 'Profile has been updated!',
93 user: userWithoutPassword,
94 };
95 }
96
97 @Get('users')
98 // @UseGuards(JwtAuthGuard, RolesGuard)
99 // @Roles('CENTURION', 'OPTIO')
100 async getAllUsers(): Promise<Partial<User>[]> {
101 return await this.userService.getAllUsers();
102 }
103}

Logging middleware

1// middleware/auth-logger.middleware.ts
2import { Injectable, NestMiddleware, Logger } from '@nestjs/common';
3import { Request, Response, NextFunction } from 'express';
4
5@Injectable()
6export class AuthLoggerMiddleware implements NestMiddleware {
7 private readonly logger = new Logger('AuthLogger');
8
9 use(req: Request, res: Response, next: NextFunction) {
10 const { method, originalUrl, ip } = req;
11 const userAgent = req.get('User-Agent') || '';
12 
13 // Log only authentication-related requests
14 if (originalUrl.includes('/auth/')) {
15 this.logger.log(`${method} ${originalUrl} - IP: ${ip} - User-Agent: ${userAgent}`);
16 
17 // Log details after the request is completed
18 res.on('finish', () => {
19 const { statusCode } = res;
20 const contentLength = res.get('content-length');
21 
22 if (statusCode >= 400) {
23 this.logger.warn(`${method} ${originalUrl} ${statusCode} - IP: ${ip}`);
24 } else {
25 this.logger.log(`${method} ${originalUrl} ${statusCode} ${contentLength} - IP: ${ip}`);
26 }
27 });
28 }
29 
30 next();
31 }
32}

Module configuration

1// auth.module.ts
2import { Module, MiddlewareConsumer } from '@nestjs/common';
3import { TypeOrmModule } from '@nestjs/typeorm';
4import { User } from './entities/user.entity';
5import { UserService } from './services/user.service';
6import { CryptoService } from './services/crypto.service';
7import { AuthController } from './controllers/auth.controller';
8import { AuthLoggerMiddleware } from './middleware/auth-logger.middleware';
9
10@Module({
11 imports: [TypeOrmModule.forFeature([User])],
12 controllers: [AuthController],
13 providers: [UserService, CryptoService],
14 exports: [UserService, CryptoService],
15})
16export class AuthModule {
17 configure(consumer: MiddlewareConsumer) {
18 consumer
19 .apply(AuthLoggerMiddleware)
20 .forRoutes('auth/*');
21 }
22}

Testing Authentication

1// auth.service.spec.ts
2import { Test, TestingModule } from '@nestjs/testing';
3import { getRepositoryToken } from '@nestjs/typeorm';
4import { Repository } from 'typeorm';
5import { UserService } from './user.service';
6import { CryptoService } from './crypto.service';
7import { User } from '../entities/user.entity';
8
9describe('UserService', () => {
10 let service: UserService;
11 let mockRepository: Partial<Repository<User>>;
12 let cryptoService: CryptoService;
13
14 beforeEach(async () => {
15 mockRepository = {
16 findOne: jest.fn(),
17 create: jest.fn(),
18 save: jest.fn(),
19 update: jest.fn(),
20 };
21
22 const module: TestingModule = await Test.createTestingModule({
23 providers: [
24 UserService,
25 CryptoService,
26 {
27 provide: getRepositoryToken(User),
28 useValue: mockRepository,
29 },
30 ],
31 }).compile();
32
33 service = module.get<UserService>(UserService);
34 cryptoService = module.get<CryptoService>(CryptoService);
35 });
36
37 describe('validateUser', () => {
38 it('should return user when credentials are valid', async () => {
39 const mockUser = {
40 id: 1,
41 username: 'caesar',
42 password: await cryptoService.hashPassword('secretpassword'),
43 isActive: true,
44 failedLoginAttempts: 0,
45 lockedUntil: null,
46 };
47
48 mockRepository.findOne = jest.fn().mockResolvedValue(mockUser);
49
50 const result = await service.validateUser('caesar', 'secretpassword');
51 expect(result).toBeDefined();
52 expect(result.username).toBe('caesar');
53 });
54
55 it('should return null when password is invalid', async () => {
56 const mockUser = {
57 id: 1,
58 username: 'caesar',
59 password: await cryptoService.hashPassword('correctpassword'),
60 isActive: true,
61 failedLoginAttempts: 0,
62 lockedUntil: null,
63 };
64
65 mockRepository.findOne = jest.fn().mockResolvedValue(mockUser);
66
67 const result = await service.validateUser('caesar', 'wrongpassword');
68 expect(result).toBeNull();
69 });
70 });
71});

Practical exercise

Implement additional security features:

1// Your code here
2
3// 1. Email verification system
4// 2. Password reset functionality 
5// 3. Two-factor authentication (2FA)
6// 4. Account lockout after a specified time
7// 5. Rate limiting for login endpoints

Summary

Congratulations! You have been appointed chief gate guard! Now you can:

Create a registration system with data validation ✓ Hash passwords securely with bcrypt ✓ Validate users during login ✓ Manage sessions and account state ✓ Implement protections against brute-force attacks ✓ Log authentication activity

In the next lessons, you will learn about JWT tokens, Passport.js, and advanced authorization techniques!

Go to CodeWorlds